I have a PowerShell script that has a y/n question asked in the script. I cannot change the script as it is downloaded from a url, but I would like to run it such that my choice is passed to the script and processing continues unattended.
I found this question, which is on a similar topic, but more related to cmdlets (and I tried everything here, but no luck).
Here is the relevant code (say this is in a script test.ps1)
function Confirm-Choice {
param ( [string]$Message )
$yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Yes";
$no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "No";
$choices = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no);
$caption = "" # Did not need this before, but now getting odd errors without it.
$answer = $host.ui.PromptForChoice($caption, $message, $choices, 1) # Set to 0 to default to "yes" and 1 to default to "no"
switch ($answer) {
0 {return 'yes'; break} # 0 is position 0, i.e. "yes"
1 {return 'no'; break} # 1 is position 1, i.e. "no"
}
}
$unattended = $false # default condition is to ask user for input
if ($(Confirm-Choice "Prompt all main action steps during setup?`nSelect 'n' to make all actions unattended.") -eq "no") { $unattended = $true }
i.e. Without altering the script, I would like to pass 'n' to this so that it will continue processing. Something like test.ps1 | echo 'n' (though, as before, this specific syntax does not work unfortunately, and I'm looking for a way to do this).
PromptForChoice appears to read input directly from the console host, so it can't be supplied with input from stdin.
You may override the function Confirm-Choice instead, by defining an alias that points to your own function which always outputs 'n'. This works because aliases take precedence over functions.
function MyConfirm-Choice {'no'}
New-Alias -Name 'Confirm-Choice' -Value 'MyConfirm-Choice' -Scope Global
.\test.ps1 # Now uses MyConfirm-Choice instead of its own Confirm-Choice
# Remove the alias again
Remove-Item 'alias:\Confirm-Choice'
Related
just wondering if its possible to escape a read-host in a while loop by pressing escape.
I've tried doing an do-else loop but it will only recognize button presses outside of the read-host.
This is basically what I have
#Import Active Directory Module
Import-Module ActiveDirectory
#Get standard variables
$_Date=Get-Date -Format "MM/dd/yyyy"
$_Server=Read-Host "Enter the domain you want to search"
#Request credentials
$_Creds=Get-Credential
while($true){
#Requests user input username
$_Name=Read-Host "Enter account name you wish to disable"
#rest of code
}
I want to be able to escape it if I want to change the domain
Using Read-Host you cannot do this, but you might consider using a graphical input dialog instead of prompting in the console. After all, the Get-Credential cmdlet also displays a GUI.
If that is an option for you, it can be done using something like this:
function Show-InputBox {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$Message,
[string]$Title = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.PSCommandPath),
[string]$defaultText = ''
)
Add-Type -AssemblyName 'Microsoft.VisualBasic'
return [Microsoft.VisualBasic.Interaction]::InputBox($Message, $Title, $defaultText)
}
while($true) {
$_Name = Show-InputBox "Enter account name you wish to disable"
if ([string]::IsNullOrWhiteSpace($_Name)) {
# the box was cancelled, so exit the loop
break
}
# proceed with the rest of the code
}
If the user presses the Esc key, clicks Cancel, or leaves the input blank, you can exit the while loop, otherwise proceed with the code.
You cannot do it with Read-Host, but you can do it via the PSReadLine module (ships with Windows PowerShell version 5 or higher on Windows 10 / Windows Server 2016) and PowerShell Core) and its PSConsoleHostReadline function:
Important:
As of PSReadLine v2.0.0-beta3, the solution below is a hack, because the PSConsoleHostReadline only supports prompting for PowerShell statements, not open-ended user input.
This GitHub feature suggestion asks for the function to be optionally usable for general-purpose user input as well, which would allow for a greatly customizable end-user prompting experience. Make your voice heard there, if you'd like see this suggestion implemented.
The hack should work in your case, since the usernames you're prompting for should be syntactically valid PowerShell statements.
However, supporting arbitrary input is problematic for two reasons:
Inapplicable syntax coloring will be applied - you could, however, temporarily set all configurable colors to the same color, but that would be cumbersome.
More importantly, if an input happens to be something that amounts to a syntactically incomplete PowerShell statement, PSConsoleHostReadline won't accept the input and instead continue to prompt (on a new line); for instance, input a| would cause this problem.
Also:
Whatever input is submitted is invariably added to the command history.
While you can currently remove a temporarily installed keyboard handler on exiting a script, there is no robust way to restore a previously active one - see this GitHub issue.
# Set up a key handler that cancels the prompt on pressing ESC (as Ctrl-C would).
Set-PSReadLineKeyHandler -Key Escape -Function CancelLine
try {
# Prompt the user:
Write-Host -NoNewline 'Enter account name you wish to disable: '
$_Name = PSConsoleHostReadLine
# If ESC was pressed, $_Name will contain the empty string.
# Note that you won't be able to distinguish that from the user
# just pressing ENTER.
$canceled = $_Name -eq ''
# ... act on potential cancellation
} finally {
# Remove the custom Escape key handler.
Remove-PSReadlineKeyHandler -Key Escape
}
I wrote this function that works for me.
# Returns the string "x" when Escape key is pressed, or whatever is indicated with -CancelString
# Pass -MaxLen n to define max string length
function Read-HostPlus()
{
param
(
$CancelString = "x",
$MaxLen = 60
)
$result = ""
$cursor = New-Object System.Management.Automation.Host.Coordinates
while ($true)
{
While (!$host.UI.RawUI.KeyAvailable ){}
$key = $host.UI.RawUI.ReadKey("NoEcho, IncludeKeyDown")
switch ($key.virtualkeycode)
{
27 { While (!$host.UI.RawUI.KeyAvailable ){}; return $CancelString }
13 { While (!$host.UI.RawUI.KeyAvailable ){}; return $result }
8
{
if ($result.length -gt 0)
{
$cursor = $host.UI.RawUI.CursorPosition
$width = $host.UI.RawUI.MaxWindowSize.Width
if ( $cursor.x -gt 0) { $cursor.x-- }
else { $cursor.x = $width -1; $cursor.y-- }
$Host.UI.RawUI.CursorPosition = $cursor ; write-host " " ; $Host.UI.RawUI.CursorPosition = $cursor
$result = $result.substring(0,$result.length - 1 )
}
}
Default
{
$key_char = $key.character
if( [byte][char]$key_char -ne 0 -and [byte][char]$key_char -gt 31 -and ($result + $key_char).Length -le $MaxLen )
{
$result += $key_char
$cursor.x = $host.UI.RawUI.CursorPosition.X
Write-Host $key_char -NoNewline
if ($cursor.X -eq $host.UI.RawUI.MaxWindowSize.Width-1 ) {write-host " `b" -NoNewline }
}
}
}
}
}
In some of my scripts I have a fall-back approach for getting a PSCredential object via the Get-Credential cmdlet. This is very useful when running those scripts interactively, esp. during testing etc.
If & when I run these scripts via a task scheduler, I'd like to ensure that they don't get stuck waiting for interactive input and simply fail if they don't have the necessary credentials. Note that this should never happen if the task is set to run under the correct application account.
Does anyone know of an approach that would allow me to prompt for input with a timeout (and presumably a NULL credential object) so the script doesn't get stuck?
Happy to consider a more general case with Read-Host instead of Get-Credential.
I use something similar in some of my scripts based around a timeout on read-host, code here :
Function Read-HostTimeout {
# Description: Mimics the built-in "read-host" cmdlet but adds an expiration timer for
# receiving the input. Does not support -assecurestring
# Set parameters. Keeping the prompt mandatory
# just like the original
param(
[Parameter(Mandatory=$true,Position=1)]
[string]$prompt,
[Parameter(Mandatory=$false,Position=2)]
[int]$delayInSeconds=5,
[Parameter(Mandatory=$false,Position=3)]
[string]$defaultValue = 'n'
)
# Do the math to convert the delay given into milliseconds
# and divide by the sleep value so that the correct delay
# timer value can be set
$sleep = 250
$delay = ($delayInSeconds*1000)/$sleep
$count = 0
$charArray = New-Object System.Collections.ArrayList
Write-host -nonewline "$($prompt): "
# While loop waits for the first key to be pressed for input and
# then exits. If the timer expires it returns null
While ( (!$host.ui.rawui.KeyAvailable) -and ($count -lt $delay) ){
start-sleep -m $sleep
$count++
If ($count -eq $delay) { "`n"; return $defaultValue}
}
# Retrieve the key pressed, add it to the char array that is storing
# all keys pressed and then write it to the same line as the prompt
$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyUp").Character
$charArray.Add($key) | out-null
# Comment this out if you want "no echo" of what the user types
Write-host -nonewline $key
# This block is where the script keeps reading for a key. Every time
# a key is pressed, it checks if it's a carriage return. If so, it exits the
# loop and returns the string. If not it stores the key pressed and
# then checks if it's a backspace and does the necessary cursor
# moving and blanking out of the backspaced character, then resumes
# writing.
$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyUp")
While ($key.virtualKeyCode -ne 13) {
If ($key.virtualKeycode -eq 8) {
$charArray.Add($key.Character) | out-null
Write-host -nonewline $key.Character
$cursor = $host.ui.rawui.get_cursorPosition()
write-host -nonewline " "
$host.ui.rawui.set_cursorPosition($cursor)
$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyUp")
}
Else {
$charArray.Add($key.Character) | out-null
Write-host -nonewline $key.Character
$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyUp")
}
}
Write-Host ""
$finalString = -join $charArray
return $finalString
}
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.
I'm making a menu system for a script I have where I can change the values I need to by using the menu or by passing them as arguments to the script. One of the annoyances I have at the moment is after entering a new value when the menu refreshes, the variable in the menu text does not update to the new values.
$global:drive="C:"
$title = "Setup
"
$message = "The default variables are:
VARIABLES TO CHANGE
1. The Drive Location: $global:drive <<<--- This is the variable that does not update after I change it when I run the script.
"
$one = New-Object System.Management.Automation.Host.ChoiceDescription "&1 Drive", "The Drive Location:"
$options = [System.Management.Automation.Host.ChoiceDescription[]]($one)
:OuterLoop do
{
for ($i = 1; )
{
$result = $host.ui.PromptForChoice($title, $message, $options, 1)
switch ($result)
{
0 {$global:drive = Read-Host "Drive is $global:drive .Set the Drive Location";
"The Drive is now: $global:drive";
break;}
}
}
}
while ($y -ne 100)
Initially I did not set the variable to to global but read on here that that might help. It did not but it did not hurt either. I also tried setting it to script too. The variable does change, so this is cosmetic more than anything.
Thanks
I ran your code, but mine changes in the menu. The only thing I did was comment out your first $global:drive="C:". If this is always at the top of the script, then $global:drive will always display C:.
You can use the following code to check for the existance of a variable, then assign the value if it doesn't already exist:
if $(!(Get-Variable -Name Drive -Scope global -ErrorAction SilentlyContinue)) { $global:drive="C:" }
If the global variable Drive exists, it will do nothing. If it doesn't exist, $global:drive will be set to C:. Hope this helps.
Edit after #Norm comment:
The reason your message isn't updating, is because the $title is set outside of the loop. Because $Title is already defined, it doesn't need to change every time the loop runs. Simply move the declaration for $Title inside the loop before the line $result = $host.ui.PromptForChoice($title, $message, $options, 1). This should fix the problem you are having.
Edit2: I'm sorry, it's $message that needs moved, not $title
What is the "best" way to handle command-line arguments?
It seems like there are several answers on what the "best" way is and as a result I am stuck on how to handle something as simple as:
script.ps1 /n name /d domain
AND
script.ps1 /d domain /n name.
Is there a plugin that can handle this better? I know I am reinventing the wheel here.
Obviously what I have already isn't pretty and surely isn't the "best", but it works.. and it is UGLY.
for ( $i = 0; $i -lt $args.count; $i++ ) {
if ($args[ $i ] -eq "/n"){ $strName=$args[ $i+1 ]}
if ($args[ $i ] -eq "-n"){ $strName=$args[ $i+1 ]}
if ($args[ $i ] -eq "/d"){ $strDomain=$args[ $i+1 ]}
if ($args[ $i ] -eq "-d"){ $strDomain=$args[ $i+1 ]}
}
Write-Host $strName
Write-Host $strDomain
You are reinventing the wheel. Normal PowerShell scripts have parameters starting with -, like script.ps1 -server http://devserver
Then you handle them in a param section (note that this must begin at the first non-commented line in your script).
You can also assign default values to your params, read them from console if not available or stop script execution:
param (
[string]$server = "http://defaultserver",
[Parameter(Mandatory=$true)][string]$username,
[string]$password = $( Read-Host "Input password, please" )
)
Inside the script you can simply
write-output $server
since all parameters become variables available in script scope.
In this example, the $server gets a default value if the script is called without it, script stops if you omit the -username parameter and asks for terminal input if -password is omitted.
Update:
You might also want to pass a "flag" (a boolean true/false parameter) to a PowerShell script. For instance, your script may accept a "force" where the script runs in a more careful mode when force is not used.
The keyword for that is [switch] parameter type:
param (
[string]$server = "http://defaultserver",
[string]$password = $( Read-Host "Input password, please" ),
[switch]$force = $false
)
Inside the script then you would work with it like this:
if ($force) {
//deletes a file or does something "bad"
}
Now, when calling the script you'd set the switch/flag parameter like this:
.\yourscript.ps1 -server "http://otherserver" -force
If you explicitly want to state that the flag is not set, there is a special syntax for that
.\yourscript.ps1 -server "http://otherserver" -force:$false
Links to relevant Microsoft documentation (for PowerShell 5.0; tho versions 3.0 and 4.0 are also available at the links):
about_Scripts
about_Functions
about_Functions_Advanced_Parameters