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
Related
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'
$cmdlet="Disable-RemoteMailBox"
$arguments = #{Identity=$identity;DomainController=$domaincontroller;Archive=""}
$command_args=""
$arguments.keys | ForEach-Object{
$message = '-{0} {1} ' -f $_, $arguments[$_]
$command_args+= $message
}
$result=& $cmdlet #arguments 2>&1
In the end this is executed:
Disable-RemoteMailBox -Identity abc#corp.com -DomainController dc.corp.local -Archive
but i need to add a confirm:$false
Disable-RemoteMailBox -Identity abc#corp.com -DomainController dc.corp.local -Archive -Confirm:$false
How to add this $false in the Hashtable?
Change the $arguments hashtable from:
$arguments = #{Identity=$identity;DomainController=$domaincontroller;Archive=""}
to
$arguments = #{Identity=$identity;DomainController=$domaincontroller;Archive="";Confirm=$false}
Adding to Mathias's concise answer
Excepting for the confirm functionality's integration with the $ConfirmPreference preference variable, the -Confirm common parameter can be looked at as a simple switch parameter. It is either present or not present. However, PowerShell's internal type conversion engine will evaluate a [Switch] more like a [Boolean] You can see this if you cast a [Bool] to a [Switch].
[Switch]$true or [Switch]$false will return IsPresent True/False respectively.
If you specify Confirm = $false in a splatting hash table, the type coercion (casting) that occurs during the parameter binding will handle it correctly. This is also true for any other switch parameter, even custom ones you define in your custom functions. This type conversion is also noticiable when you need to evaluate a switch parameter internal to a function.
If I specify a switch parameter named $Delete
Param( [Switch]$Delete )
Then internally I can execute logic like:
If( $Delete -eq $true ) {
# Delete the file or whatever...
}
Of course, you can shorten to:
If( $Delete ) {
# Delete the file or whatever...
}
However, you don't need a deep understanding of PowerShell's type conversion system to use Boolean or Switch parameters in splatting hash tables. It's documented in about_Splatting. The first few lines will explain hash table splatting of switch parameters.
I'm trying to write a server build script that includes Boolean parameters for various tasks, e.g. whether to install IIS. If the user does not specify this parameter one way or the other, I want the script to prompt for a decision, but for convenience and unattended execution I want the user to be able to explicitly choose to install IIS or NOT install IIS by setting the value to True or False on the command line and therefore avoid being prompted. My issue is that when I create a Boolean parameter, PowerShell automatically sets it to False, rather than leaving it null, if it wasn't specified on the command line. Here is the design that I THOUGHT would've worked:
param(
[bool]$IIS
)
if ($IIS -eq $null) {
$InstallIIS = Read-Host "Do you want to install IIS? (Y/N)"
if ($InstallIIS -eq "Y") {$IIS = $true}
}
if ($IIS) {Do stuff here}
Any suggestions for how to achieve my desired outcome would be most appreciated. Then if this changes anything, what I'd REALLY like to do is leverage PSRemoting to accept these build decision parameters on the user's system host and then pass them to the targets as an ArgumentList, and I'm wondering if that will affect how these Booleans are handled. For example:
param (
[string[]]$Computers
[bool]$IIS
)
$Computers | Foreach-Object {
Invoke-Command -ComputerName $_ -ArgumentList $IIS -ScriptBlock {
param(
[bool]$IIS
)
if ($IIS -eq $null) {
$InstallIIS = Read-Host "Do you want to install IIS? (Y/N)"
if ($InstallIIS -eq "Y") {$IIS = $true}
}
if ($IIS) {Do stuff here}
Ideas?
The way to accomplish this is with Parameter Sets:
[CmdletBinding()]
param (
[Parameter()]
[string[]]$Computers ,
[Parameter(ParameterSetName = 'DoSomethingWithIIS', Mandatory = $true)]
[bool]$IIS
)
$Computers | Foreach-Object {
Invoke-Command -ArgumentList $IIS -ScriptBlock {
param(
[bool]$IIS
)
if ($PSCmdlet.ParameterSetName -ne 'DoSomethingWithIIS') {
$InstallIIS = Read-Host "Do you want to install IIS? (Y/N)"
if ($InstallIIS -eq "Y") {$IIS = $true}
}
if ($IIS) {Do stuff here}
Well of course even though I Googled about this quite a bit before posting here, including discovering the [AllowNull()] parameter and finding that it did NOT help in my use case, I ended up finding the answer in the first Google search AFTER posting. This is what worked:
[nullable[bool]]$IIS
My only gripe with that syntax is that running Get-Help against the script now returns shows this for the IIS parameter:
-IIS <Nullable`1>
instead of:
-IIS <Boolean>
But unless there's a more elegant way to achieve what I need, I think I can live with that by adding a useful description for that parameter as well as Examples.
Even though boolean operators handle $null, $False, '', "", and 0 the same, you can do an equality comparison to see which is which.
If ($Value -eq $Null) {}
ElseIf ($Value -eq $False) {}
..etc..
In your situation, you want to use [Switch]$IIS. This will be $False by default, or $True if entered with the command a la Command -IIS, then you can handle it in your code like:
If ($IIS) {}
Which will only be $True if entered at the command line with -IIS
Instead of using an equality test that's going to try to coerce the value to make the test work:
if ($IIS -eq $Null)
Use -is to check the type directly:
PS C:\> $iis = $null
PS C:\> $iis -is [bool]
False
Here's a sample powershell script:
$in = read-host -prompt "input"
write-host $in
Here's a sample 'test.txt' file:
hello
And we want to pass piped input to it from powershell. Here's some I have tried:
.\test.ps1 < test.txt
.\test.ps1 < .\test.txt
.\test.ps1 | test.txt
.\test.ps1 | .\test.txt
test.txt | .\test.ps1
.\test.txt | .\test.ps1
get-content .\test.txt | .\test.ps1
even just trying to echo input doesn't work either:
echo hi | \.test.ps1
Every example above that doesn't produce an error always prompts the user instead of accepting the piped input.
Note: My powershell version table says 4.0.-1.-1
Thanks
Edit/Result: To those looking for a solution, you cannot pipe input to a powershell script. You have to update your PS file. See the snippets below.
The issue is that your script \.test.ps1 is not expecting the value.
Try this:
param(
[parameter(ValueFromPipeline)]$string
)
# Edit: added if statement
if($string){
$in = "$string"
}else{
$in = read-host -prompt "input"
}
Write-Host $in
Alternatively, you can use the magic variable $input without a param part (I don't fully understand this so can't really recommend it):
Write-Host $input
You can't pipe input to Read-Host, but there should be no need to do so.
PowerShell doesn't support input redirection (<) yet. In practice this is not a (significant) limitation because a < b can be rewritten as b | a (i.e., send output of b as input to a).
PowerShell can prompt for input for a parameter if the parameter's value is missing and it is set as a mandatory attribute. For example:
function test {
param(
[parameter(Mandatory=$true)] [String] $TheValue
)
"You entered: $TheValue"
}
If you don't provide input for the $TheValue parameter, PowerShell will prompt for it.
In addition, you can specify that a parameter accepts pipeline input. Example:
function test {
param(
[parameter(ValueFromPipeline=$true)] [String] $TheValue
)
process {
foreach ( $item in $TheValue ) {
"Input: $item"
}
}
}
So if you write
"A","B","C" | test
The function will output the following:
Input: A
Input: B
Input: C
All of this is spelled out pretty concisely in the PowerShell documentation.
Yes; in Powershell 5.1 "<" is not implemented (which sucks)
so, this won't work: tenkeyf < c:\users\marcus\work\data.num
but,
this will: type c:\users\marcus\work\data.num | tenkeyf
...
PowerShell doesn’t have a redirection mechanism, but.NET have.
you can use [System.Diagnostics.Process] implements the purpose of redirecting input.
The relevant Microsoft documents are as follows.
Process Class
This is a sample program that works perfectly on my windows 10 computer
function RunAndInput{
$pi = [System.Diagnostics.ProcessStartInfo]::new()
$pi.FileName ="powershell"
$pi.Arguments = """$PSScriptRoot\receiver.ps1"""
$pi.UseShellExecute = $false
$pi.RedirectStandardInput = $true
$pi.RedirectStandardOutput = $true
$p = [System.Diagnostics.Process]::Start($pi)
$p.StandardInput.WriteLine("abc"+ "`r`n");
$p.StandardOutput.ReadToEnd()
$p.Kill()
}
RunAndInput
# OutPut
Please enter the information: abc
Received:abc
# receiver.ps1
('Received:' + (Read-Host -Prompt 'Please enter the information'))
Hope to help you!
Hi I am very new to powershell and I am writing a script that accepts multiple parameters. These parameters are being accessed in a for loop inside the file.
It looks something like this
$numOfArgs = args.Length
for ($i=3; $i -le $numOfArgs; $i++)
{
write-host "folder: $args[$i]"
# does something with the arguments
}
However, the output gives me all the parameters as a whole instead of just one parameter specified in the array as an array element? Can someone tell me where is the mistake here? Thanks!
EDIT: Thanks Duncan to point this out, missing a $ in a variable.
Try this:
$numOfArgs = $args.Length
for ($i=3; $i -lt $numOfArgs; $i++)
{
write-host "folder: $($args[$i])"
# does something with the arguments
}
When placing a variable in a string, the variable is evaluated, not the entire expression. So by surrounding it with $() Powershell will evaluate the whole expression.
In other words, only $args was evaluated instead of $args[$i]
The preferred Powershell way is to use a bind parameter, like this;
param(
[Parameter(Mandatory=$true)][string[]]$Paths
)
# not sure why we're skipping some elements
$Paths | foreach-object { write-host "folder: $_" }
Which you can specify an array or arguments, like this;
.\myScript.ps1 -Paths c:\,c:\Users\,'c:\Program Files\'
This way it will work with -argument TAB completion and will even give you a brief usage using the get-help cmdlet.
get-help .\myscript.ps1
myScript.ps1 [-Paths] <string[]> [<CommonParameters>]