Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
848 views
in Technique[技术] by (71.8m points)

foreach - How to properly use PowerShell $args[]

Sometimes a PowerShell script gets called with arguments containing spaces and enclosed by double-quotes, to ensure the string is interpreted as one single argument. A problem arises when the argument is empty and the double-quotes are passed containing nothing. PS sees the command line as non-empty, and tries to form an argument value from it. Except the double-quotes are stripped away, leaving an empty string as an argument value. Of course PS cannot actually use the empty value, so we want to validate command line arguments to disallow empty strings.

See the calling Cmd script, and the PS source, both commented. Tests 4 and 5 show the benefit of the function that scans the $args[] array and returns the first valid/non-empty argument.

Question: why can the function not be coded with just one ForEach loop?

Args.CMD:

@Echo Off
CD /D "%~dp0" & Rem Change to the folder where this script lives
CLS
Echo.

Rem PS script name is the same as this Cmd script
Set Script=%~n0.PS1

Rem Parameters of each call here are TestNumber, ExpectedExitCode, Argument1, Argument2

Rem Test 0: Try with zero       arguments;                 expected exit code is 1
Call :TryMe 0 1

Rem Test 1: Try with non-useful arguments;                 expected exit code is 2
Call :TryMe 1 2 ""        ""

Rem Test 2: Try with valid      arguments, single   words; expected exit code is 0
Call :TryMe 2 0 ArgOne     ArgTwo

Rem Test 3: Try with valid      arguments, multiple words; expected exit code is 0
Call :TryMe 3 0 "Arg One" "Arg Two"

Rem Test 4: Try with mixed      arguments, single   words; expected exit code is 0
Call :TryMe 4 0 ""         ArgTwo

Rem Test 5: Try with mixed      arguments, multiple words; expected exit code is 0
Call :TryMe 5 0 ""        "Arg Two"

Echo All Done.
Pause
GoTo :EOF

:TryMe
Set TstNbr=%1
Set ExpCod=%2
Set Args12=%3 %4
Echo Test %TstNbr% of 5: PowerShell.EXE -F %Script% %Args12%
                         PowerShell.EXE -F %Script% %Args12%
Echo ErrorLevel is [%ErrorLevel%], expected was [%ExpCod%]
Pause
Echo.
GoTo :EOF

Args.ps1:

Set-StrictMode -Version 3.0                     # disallow implicit variables

function ArgNonEmtFstGet( $arrArgs ) {          # get the first non-empty argument
  ForEach( $strArgs In $arrArgs ) {             # <=== why is this double-indirection needed?
    ForEach( $strArg In $strArgs ) {            # scan all argument strings
      $intArgLen = $strArg.Length               # get length of current argument-string
      If( $intArgLen -gt 0 ) { Return $strArg } # first time length is greater than zero, quit scanning and pass that argument back to caller
    }                                           # done scanning
  }                                             # <=== why is this double-indirection needed?
  ""                                            # when we finished scanning and did not return early, return a non-crashing value
} # ArgNonEmtFstGet

# Step 0: show we are alive
"Arg count: " + $args.Length                    # emit how many strings exist that *could be* arguments

# Step 1: look for any argument at all
If( $args.Length -eq 0 ) {                      # when there are zero strings that look like arguments
  Write-Output "Putative argument required."    # emit error message for 'no strings found'
  Exit( 1 )                                     # abort with exit code
}                                               # done with no strings found that *could be* arguments
"First putative argument: [" + $args[0] + "]"   # emit the first argument-looking string

# Step 2: look for an argument that is actually meaningful
$strArg = ArgNonEmtFstGet( $args )              # get the first non-empty argument; that is our input
If( $strArg -eq "" ) {                          # when it is not a usable argument
  Write-Output "Non-empty argument required."   # emit error message for 'no real arguments found'
  Exit( 2 )                                     # abort with exit code
}                                               # done with no usable arguments found
"First non-empty argument: [$strArg]"           # emit the first valid argument; this is our input

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I can't reproduce your issue with your nested foreach-statement. When I remove the inner loop I get the same result.

But I would suggest that you should avoid using the $args variable, because it accepts any input and doesn't allow named binding (only positional).

Just declare a param block in your PowerShell Script like below, then you don't need to loop over your arguments and additionally, you have named parameters you can use in your batch script. (Positional binding is still possible).

Sample script with param block and "ValidateScript" attribute, which validates the input to avoid null, empty or whitespace input.

param(
    # first mandatory parameter. Default Position = 1 (if using positional binding)
    [Parameter(Mandatory=$true)]
    [ValidateScript(
        {
            if([String]::IsNullOrWhiteSpace($_)) {
                throw [System.ArgumentException]'Your string is null, empty or whitespace!'
            } else {
                $true
            }
        }
    )]
    [string]
    $varA,

    # second mandatory parameter. Default Position = 2 (if using positional binding)
    [Parameter(Mandatory=$true)]
    [ValidateScript(
        {
            if([String]::IsNullOrWhiteSpace($_)) {
                throw [System.ArgumentException]'Your string is null, empty or whitespace!'
            } else {
                $true
            }
        }
    )]
    [string]
    $varB,

    # You can just use this parameter if you want your arguments being absolutely dynamic
    [Parameter(ValueFromRemainingArguments)]
    [ValidateScript(
        {
            if([String]::IsNullOrWhiteSpace($_)) {
                throw [System.ArgumentException]'Your string is null, empty or whitespace!'
            } else {
                $true
            }
        }
    )]
    [string[]]
    $remainingArgs
)


# create ordered dictionary of mandatory arguments used for returning object
$properties= [ordered]@{
    'Arg0' = "'$varA'"
    'Arg1' = "'$varB'"
}


# add any optional remaining argument (if any)
$index = 1 # start index
foreach ($remainingArg in $remainingArgs) {
    $index++
    $properties.Add("Arg$index", "'$remainingArg'")
}

# return as object
[pscustomobject]$properties

As mentioned in the script, you can also just use the third parameter if you want your script being absolutely dynamic in terms of parameters.

More infos about attribute "ValidateScript": https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-5.1#validatescript-validation-attribute

PS: Sample command when using named parameters in batch:

PowerShell.EXE -F %Script% -varA "%3" -varB "%4"

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...