Very new to coding in general, so I fear I am missing something completely obvious. I want my program to check for a file. If it is there, just continue the code. If it has not arrived, continue cheking for a given amount of time, or untill the file shows up. My loop works on its own, so when i only select the do-part in Powershell ISE, it works. But when i try running it inside the if statement, nothing happens. The loops doesnt begin.
$exists= Test-Path $resultFile
$a = 1
if ($exists -eq "False")
{
do
{
$a++
log "Now `$a is $a "
start-sleep -s ($a)
$exists= Test-Path $resultFile
write-host "exists = $exists"
}
while (($a -le 5) -and ($exists -ne "True"))
}
Another way of doing this is using a while loop:
$VerbosePreference = 'Continue'
$file = 'S:\myFile.txt'
$maxRetries = 5; $retryCount = 0; $completed = $false
while (-not $completed) {
if (Test-Path -LiteralPath $file) {
Write-Verbose "File '$file' found"
$completed = $true
# Do actions with your file here
}
else {
if ($retryCount -ge $maxRetries) {
throw "Failed finding the file within '$maxRetries' retries"
} else {
Write-Verbose "File not found, retrying in 5 seconds."
Start-Sleep '5'
$retryCount++
}
}
}
Some tips:
Try to avoid Write-Host as it kills puppies and the pipeline (Don Jones). Better would be, if it's meant for viewing the script's progress, to use Write-Verbose.
Try to be consistent in spacing. The longer and more complex your scripts become, the more difficult it will be to read and understand them. Especially when others need to help you. For this reason, proper spacing helps all of us.
Try to use Tab completion in the PowerShell ISE. When you type start and press the TAB-key, it will automatically propose the options available. When you select what you want with the arrow down/up and press enter, it will nicely format the CmdLet to Start-Sleep.
The most important tip of all: keep exploring! The more you try and play with PowerShell, the better you'll get at it.
As pointed out in comments, your problem is that you're comparing a boolean value with the string "False":
$exists -eq "False"
In PowerShell, comparison operators evaluate arguments from left-to-right, and the type of the left-hand argument determines the type of comparison being made.
Since the left-hand argument ($exists) has the type [bool] (a boolean value, it can be $true or $false), PowerShell tries to convert the right-hand argument to a [bool] as well.
PowerShell interprets any non-empty string as $true, so the statement:
$exists -eq "False"
is equivalent to
$exists -eq $true
Which is probably not what you intended.
Related
I want to create the "Select-Multiple" function.
The function takes some parameters, but the most important one is the list of options.
Let's say
#("First Option", "Second Option")
Then the function will display something like:
a All
b First Option
c Second Option
d Exit
Choose your option: > ...
The "Choose your option: > ..." text, will be repeated as long as:
User choose "All" or "Exit" option
User will choose all possible options (other than "All" and "Exit")
At the end the function returns the List of options chosen by the user.
Simple. But... I'd like to highlight the options already chosen by the user.
So if the user chose "b", then "b First Option" gets green colour.
Is it possible to do something like that, without using Clear-Host, as I don't want to clear previous steps?
I attach you my "Select-Multiple" function in powershell, sorry if that's ugly written, but I don't use powershell that often.
function Select-Multiple {
Param(
[Parameter(Mandatory=$false)]
[string] $title,
[Parameter(Mandatory=$false)]
[string] $description,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
$options,
[Parameter(Mandatory=$true)]
[string] $question
)
if ($title) {
Write-Host -ForegroundColor Yellow $title
Write-Host -ForegroundColor Yellow ("-"*$title.Length)
}
if ($description) {
Write-Host $description
Write-Host
}
$chosen = #()
$values = #()
$offset = 0
$all = "All"
$values += #($all)
Write-Host -ForegroundColor Yellow "$([char]($offset+97)) " -NoNewline
Write-Host $all
$offset++
$options.GetEnumerator() | ForEach-Object {
Write-Host -ForegroundColor Yellow "$([char]($offset+97)) " -NoNewline
$values += #($_)
Write-Host $_
$offset++
}
$exit = "Exit"
$values += #($exit)
Write-Host -ForegroundColor Yellow "$([char]($offset+97)) " -NoNewline
Write-Host $exit
$answer = -1
while($chosen.Count -ne $options.Count) {
Write-Host "$question " -NoNewline
$selection = (Read-Host).ToLowerInvariant()
if (($selection.Length -ne 1) -or (([int][char]($selection)) -lt 97 -or ([int][char]($selection)) -gt (97+$offset))) {
Write-Host -ForegroundColor Red "Illegal answer. " -NoNewline
}
else {
$answer = ([int][char]($selection))-97
$value = $($values)[$answer]
if ($value -eq $exit) {
return $chosen
}
if ($value -eq $all) {
return $options
}
else {
if ($chosen.Contains($value)) {
Write-Host -ForegroundColor Red "The value $value was already chosen."
}
else {
$chosen += ($value)
}
}
}
if ($answer -eq -1) {
Write-Host -ForegroundColor Red "Please answer one letter, from a to $([char]($offset+97))"
}
$answer = -1;
}
return $chosen
}
Because of how the console window works, you can't just recolor an existing line. Once you've written something to the console, the only way you can modify it is by overwriting it. This is no different when applying colors.
To understand why this is the case, let's go over how text is colored in PowerShell. Let's use the following command as an example:
Write-Host "Test" -ForegroundColor Green
Here is a (simplified) step by step overview of what this command will do:
Set the ForegroundColor property of the console to green
Write "Test" to the console
Set the ForegroundColor property of the console to whatever it was previously
This explains why we are unable to change the color of text that has already been written to the console. If you want to color a piece of text, you are required to set the console color before writing the text to the console.
However, there are a couple ways to create the same visual effect. In fact there are exactly two ways. One of them is clearing the console and re-writing everything which you mentioned you don't want to do, so let's talk about the other way first.
Overwriting Individual Lines
Let me preface this by saying that this does not work very well with the PowerShell ISE. If you decide to use this, you will have to debug it by either using the normal PowerShell console, or an IDE that supports this. This is also the more complicated option, so if you don't want to deal with the complexity of it, the second option would be the way to go.
The console window allows you to retrieve and set the cursor position by using Console.CursorLeft and Console.CursorTop. You can also use Console.SetCursorPosition() if you need to set both of them at the same time. There is also $Host.UI.RawUI.CursorPosition, but it's long and has some strange side effects when paired with Read-Host, so I would not recommend using it. When you write output to the console, it will write the output to wherever the cursor happens to be. We can use this to our advantage by setting the cursor's position to the beginning of the line we want to change the color of, then overwriting the normal text with colored text or vice versa.
In order to do this, all we need to do is keep track of which option is on which line. This is pretty simple, especially if you have an array of options that is in the same order that you printed them to the console in.
Here is a simple script I made that does exactly this:
$options = $("Option 1", "Option 2", "Option 3")
$initialCursorTop = [Console]::CursorTop
# An array to keep track of which options are selected.
# All entries are initially set to $false.
$selectedOptionArr = New-Object bool[] $options.Length
for($i = 0; $i -lt $options.Length; $i++)
{
Write-Host "$($i + 1). $($options[$i])"
}
Write-Host # Add an extra line break to make it look pretty
while($true)
{
Write-Host "Choose an option>" -NoNewline
$input = Read-Host
$number = $input -as [int]
if($number -ne $null -and
$number -le $options.Length -and
$number -gt 0)
{
# Input is a valid number that corresponds to an option.
$oldCursorTop = [Console]::CursorTop
$oldCursorLeft = [Console]::CursorLeft
# Set the cursor to the beginning of the line corresponding to the selected option.
$index = $number - 1
[Console]::SetCursorPosition(0, $index + $initialCursorTop)
$choice = $options[$index]
$isSelected = $selectedOptionArr[$index]
$choiceText = "$($number). $($choice)"
if($isSelected)
{
Write-Host $choiceText -NoNewline
}
else
{
Write-Host $choiceText -ForegroundColor Green -NoNewline
}
$selectedOptionArr[$index] = !$isSelected
[Console]::SetCursorPosition($oldCursorLeft, $oldCursorTop)
}
# Subtract 1 from Y to compensate for the new line created when providing input.
[Console]::SetCursorPosition(0, [Console]::CursorTop - 1)
# Clear the input line.
Write-Host (' ' * $Host.UI.RawUI.WindowSize.Width) -NoNewline
[Console]::CursorLeft = 0
}
The main advantage of this approach is that it doesn't need to clear the entire console in order to update text. This means you can display whatever you want above it without worrying about it being cleared every time the user inputs something. Another advantage is that this performs a minimal number of operations in order to accomplish the task.
The main disadvantage is that this is relatively volatile. This requires you to use exact line numbers, so if something happens that offsets some of the lines (such as one option being multiple lines), it will more than likely cause some major issues.
However, these disadvantages can be overcome. Since you have access to $Host.UI.RawUI.WindowSize.Width which tells you how many characters you can put on a single line, we know that any string with a length greater than this will be wrapped onto multiple lines. Another option is just to keep track of which line the cursor starts on, then you can clear all text between the starting position and where the cursor currently is.
Clearing the Console
This approach is much simpler because you don't have to worry about what is on which line or where the cursor is at all. The idea is that you simply clear the entire console, then re-write everything with the changes you want to make. This is the nuclear approach, but it's also the most reliable.
Here is the same example as above using this approach instead:
$options = $("Option 1", "Option 2", "Option 3")
# An array to keep track of which options are selected.
# All entries are initially set to $false.
$selectedOptionArr = New-Object bool[] $options.Length
while($true)
{
Clear-Host
for($i = 0; $i -lt $options.Length; $i++)
{
if($selectedOptionArr[$i])
{
Write-Host "$($i + 1). $($options[$i])" -ForegroundColor Green
}
else
{
Write-Host "$($i + 1). $($options[$i])"
}
}
Write-Host # Add an extra line break to make it look pretty
Write-Host "Choose an option>" -NoNewline
$input = Read-Host
$number = $input -as [int]
if($number -ne $null -and
$number -le $options.Length -and
$number -gt 0)
{
# Input is a valid number that corresponds to an option.
$index = $number - 1
$choice = $options[$index]
$selectedOptionArr[$index] = !$selectedOptionArr[$index]
}
}
The main advantage of this approach is that it's super simple and easy to understand.
The main disadvantage is that this clears the entire console every time the user inputs something. In most cases this isn't a huge problem, but it can be with large data sets.
I agree with Trevor Winge, I it is most likely not possible to change earlier console outputs appearance, but it is for certain not worth your while. There are certain limitations of the console, and that is ok since that is what GUIs are for. I hope you feel encouraged by this situation to look into Windows.Froms or WPF. Make use of the controls! checkboxes would be interesting for your scenario. I know that is not exactly what you asked for, but it is garanteed a journey that is worth your time. When i started my first GUI with powershell, i was astonished how much i could accomplish with little afford. Stackoverflow is full of examples.
I am having an issue with my If/Else statement where it will successfully prompt the user until either a "Y/y" or an "N/n" are entered and will store the proper response in the $input variable outside of the Do/Until loop but will execute the first block of code in the following If Statement whether $input is equal to "Y/y" or "N/n"
How can I make it so the If Statement will only execute when $input is equal to "Y/y" and otherwise if it's "N/n" just execute the empty else and move on to the next $program in $InstallList?
I've tried using an ElseIf and checking for "N/n" but it still only executes the first If Statement.
I've also put Write-Host for $input as a check after it leaves the Do/Until loop and it is the correct input but all falls apart when it moves on to executing the If/Else Statement.
Please help.
foreach($program in $InstallList){
if($program -notin $Installed){
$input = ""
do {
$input = Read-Host -Prompt "$($Program) is not installed would you like to install now? (y/n)"
}
until(($input -eq "y") -or ($input -eq "n"))
if($input -eq "y")
{
Write-ProgressHelper -Message "Installing $($Program)" -StepNumber ($stepCounter++)
Start-Sleep -Seconds 3
Start-Process $Software.$program -Wait
Write-Host "$($Software) installed`n"
}
else {}
else{}
}
Abraham Zinala correctly states that use of $input as a custom variable should be avoided, because it is an automatic variable whose value is managed by PowerShell itself, namely to reflect pipeline input.
It is unfortunate that PowerShell even lets you assign a value, which is a problem that affects other automatic variables too - see this answer.
Technically, as long as you stay within the same scope, you can get away with using $input for custom purposes - though it should still be avoided.
Since this constraint is fulfilled in your code, use of $input is not the source of your problem; there is no obvious problem with your code.
Here's version of your code that avoids use of $input and streamlines ensuring that the user input is valid via the -in operator:
foreach ($program in $InstallList) {
if ($program -notin $Installed) {
do {
$response = Read-Host -Prompt "$($Program) is not installed would you like to install now? (y/n)"
} until ($response -in 'y', 'n')
if ($response -eq 'y') {
'yes' # ... install
}
}
}
Like I said, this code works in PowerShell version 2, but not in PowerShell version 5.
function wait
{
$compte = 0
Write-Host "To continue installation and ignore configuration warnings type [y], type any key to abort"
While(-not $Host.UI.RawUI.KeyAvailable -and ($compte -le 20))
{
$compte++
Start-Sleep -s 1
}
if ($compte -ge 20)
{
Write-Host "Installation aborted..."
break
}
else
{
$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyup")
}
if ($key.character -eq "y")
{Write-Host "Ignoring configuration warnings..."}
else
{Write-Host "Installation aborted..."
}}
The official documentation or Read-Host -? will tell that it's not possible to use Read-Host in that manner. There is no possible parameter to tell it to run with some kind of timeout.
But there are various other questions detailing how to do this in PowerShell (usually utilizing C#).
The idea seems to be to check whenever the user pressed a key using $Host.UI.RawUI.KeyAvailable and check that for the duration of your timeout.
A simple working example could be the following:
$secondsRunning = 0;
Write-Output "Press any key to abort the following wait time."
while( (-not $Host.UI.RawUI.KeyAvailable) -and ($secondsRunning -lt 5) ){
Write-Host ("Waiting for: " + (5-$secondsRunning))
Start-Sleep -Seconds 1
$secondsRunning++
}
You could use $host.UI.RawUI.ReadKey to get the key that was pressed. This solution probably would not be acceptable if you need more complex input than a simple button press. See also:
Windows PowerShell Tip of the Week - Pausing a Script Until the User Presses a Key
PowerTip: Use PowerShell to Wait for a Key Press (Hey, Scripting Guy!)
Seth, thank you for your solution. I expanded on the example you provided and wanted to give that back to the community.
The use case is a bit different here - I have a loop checking if an array of VMs can be migrated and if there are any failures to that check the operator can either remediate those until the checks clear or they can opt to "GO" and have those failing VMs excluded from the operation. If something other than GO is typed state remains within the loop.
One downside to this is if the operator inadvertently presses a key the script will be blocked by Read-Host and may not be immediately noticed. If that's a problem for anyone I'm sure they can hack around that ;-)
Write-Host "Verifying all VMs have RelocateVM_Task enabled..."
Do {
$vms_pivoting = $ph_vms | Where-Object{'RelocateVM_Task' -in $_.ExtensionData.DisabledMethod}
if ($vms_pivoting){
Write-Host -ForegroundColor:Red ("Some VMs in phase have method RelocateVM_Task disabled.")
$vms_pivoting | Select-Object Name, PowerState | Format-Table -AutoSize
Write-Host -ForegroundColor:Yellow "Waiting until this is resolved -or- type GO to continue without these VMs:" -NoNewline
$secs = 0
While ((-not $Host.UI.RawUI.KeyAvailable) -and ($secs -lt 15)){
Start-Sleep -Seconds 1
$secs++
}
if ($Host.UI.RawUI.KeyAvailable){
$input = Read-Host
Write-Host ""
if ($input -eq 'GO'){
Write-Host -ForegroundColor:Yellow "NOTICE: User prompted to continue migration without the blocked VM(s)"
Write-Host -ForegroundColor:Yellow "Removing the following VMs from the migration list"
$ph_vms = $ph_vms | ?{$_ -notin $vms_pivoting} | Sort-Object -Property Name
}
}
} else {
Write-Host -ForegroundColor:Green "Verified all VMs have RelocateVM_Task method enabled."
}
} Until(($vms_pivoting).Count -eq 0)
Also note that all this $Host.UI stuff doesn't work from the Powershell ISE.
To find out from within a script you could test for $Host.Name -eq "ConsoleHost". When true you can use the code from this topic. Otherwise you could use $Host.UI.PromptForChoice or any other way of showing a dialog box. With System.Windows.Forms.Timer you can then set a timer, and code to close the dialog box or form can be run when it expires.
How can I script "Does String -contains -not _" / "Does the string contain anything other than _"?
I'm not stuck as I've found a good enough work around. More curiosity than anything else.
Example:
$String = 1,1,1,2,5
$String -contains !(1)
This always comes up False
My solution at the moments is to remove the 1's and see if it's null like so:
$String2 = $String -ne 1
if ([String]::IsNullOrEmpty($String2)) {
Write-Host "True"
} else {
Write-Host "False"
}
Real World Example:
My script is designed to try a certain action until it works. In this case get-msoluser.
At the end of my script I want to count any errors (and list them later) but there will always be an error listed for "get-msoluser" as it fails until it works. So I'm trying to not include that certain error in the count.
$Errors = $Error.InvocationInfo.MyCommand.Name
if ($Errors -contains !("get-msoluser")) {
Write-Host "There was an error I actually care about"
}
INSTEAD I have to do this:
$Errors = $Error.InvocationInfo.MyCommand.Name
$ErrorsICareAbout = $Errors -ne "get-msoluser"
if ([String]::IsNullOrEmpty($ErrorsICareAbout)) {
Write-Host "$ErrorsICareAbout.Count"
} else {
Write-Host "There were errors you actually cared about"
}
Am I missing something that's right under my nose?
You simply need to use -notcontains or add the not operator around then entire -contains comparison like this:
If ($Errors -notcontains ("get-msoluser"))
or
If (!($Errors -contains ("get-msoluser")))
Rather than filtering out the error, try not producing an error in the first place. To suppress errors from a particular command, you can set the error action to SilentlyContinue.
Write-Error 'fail' -ErrorAction SilentlyContinue
So in the case of retrying until Get-MsOlUser works, you could use something like
while($msolUser -eq $null) {
$msolUser = Get-MsOlUser ... -ErrorAction SilentlyContinue
#Wait a second before retrying.
Start-Sleep -Seconds 1
}
#Now work with $msolUser
(You probably also want to put an upper limit on the number of retries)
Very new to coding in general, so I fear I am missing something completely obvious. I want my program to check for a file. If it is there, just continue the code. If it has not arrived, continue cheking for a given amount of time, or untill the file shows up. My loop works on its own, so when i only select the do-part in Powershell ISE, it works. But when i try running it inside the if statement, nothing happens. The loops doesnt begin.
$exists= Test-Path $resultFile
$a = 1
if ($exists -eq "False")
{
do
{
$a++
log "Now `$a is $a "
start-sleep -s ($a)
$exists= Test-Path $resultFile
write-host "exists = $exists"
}
while (($a -le 5) -and ($exists -ne "True"))
}
Another way of doing this is using a while loop:
$VerbosePreference = 'Continue'
$file = 'S:\myFile.txt'
$maxRetries = 5; $retryCount = 0; $completed = $false
while (-not $completed) {
if (Test-Path -LiteralPath $file) {
Write-Verbose "File '$file' found"
$completed = $true
# Do actions with your file here
}
else {
if ($retryCount -ge $maxRetries) {
throw "Failed finding the file within '$maxRetries' retries"
} else {
Write-Verbose "File not found, retrying in 5 seconds."
Start-Sleep '5'
$retryCount++
}
}
}
Some tips:
Try to avoid Write-Host as it kills puppies and the pipeline (Don Jones). Better would be, if it's meant for viewing the script's progress, to use Write-Verbose.
Try to be consistent in spacing. The longer and more complex your scripts become, the more difficult it will be to read and understand them. Especially when others need to help you. For this reason, proper spacing helps all of us.
Try to use Tab completion in the PowerShell ISE. When you type start and press the TAB-key, it will automatically propose the options available. When you select what you want with the arrow down/up and press enter, it will nicely format the CmdLet to Start-Sleep.
The most important tip of all: keep exploring! The more you try and play with PowerShell, the better you'll get at it.
As pointed out in comments, your problem is that you're comparing a boolean value with the string "False":
$exists -eq "False"
In PowerShell, comparison operators evaluate arguments from left-to-right, and the type of the left-hand argument determines the type of comparison being made.
Since the left-hand argument ($exists) has the type [bool] (a boolean value, it can be $true or $false), PowerShell tries to convert the right-hand argument to a [bool] as well.
PowerShell interprets any non-empty string as $true, so the statement:
$exists -eq "False"
is equivalent to
$exists -eq $true
Which is probably not what you intended.