6

I've read this and it doesn't solve my problem.

I have a space-separated string, let's say $MyString = "arg1 arg2". Suppose I have a command line program called MyProgram, which accepts an arbitrary number of positional arguments, so it can be run like MyProgram arg1 arg2. However doing MyProgram $MyString doesn't work, and neither does MyProgram ($MyString -split ' ') nor MyProgram $($MyString -split ' '). I get the same error which basically says that it doesn't recognise the argument "arg1 arg2", which I guess is because it still thinks it's one argument containing a space rather than two arguments. In practice, $MyString may be quite huge and is read from a file. How do I make this work?

mklement0
  • 382,024
  • 64
  • 607
  • 775
Ray
  • 7,833
  • 13
  • 57
  • 91
  • There are other long and good-looking answers that I haven't made time to read yet and I feel bad because they've clearly put in a lot of effort and I felt conceited to accept my own answer without reading theirs LOL. – Ray Sep 23 '21 at 11:36
  • Well, you can still take the time, and you can change the accepted answer anytime. Either way, I encourage you to improve your own answer based on the feedback given. – mklement0 Sep 23 '21 at 13:12

3 Answers3

8

Oh I just found out how LOL. I should have thought of this sooner; basically, just use splatting The following worked for me:

$MyArray = $($MyString -split " ")
MyProgram @MyArray

Explanation: The first line converts the string into an array of strings split by space (" "); The $(...) notation around a command captures the output of the command, which I then assign to $MyArray. Then, instead of using $MyArray with a dollar sign $, I use it with @ to splat the array of strings into arguments for MyProgram.

mklement0
  • 382,024
  • 64
  • 607
  • 775
Ray
  • 7,833
  • 13
  • 57
  • 91
  • 1
    Kudos on coming back to update with your solution, many users don't do this :) You might find [this answer](https://stackoverflow.com/a/56485284/584676) I wrote on another question regarding how to splat useful as well, it explains how to splat both named and positional parameters, and how to use them together. – codewario Sep 13 '21 at 16:50
  • 1
    When calling _PowerShell_ commands you do indeed need splatting to pass an array's elements as individual, positional arguments. By contrast, you don't when calling an _external program_, as in the post you link to. Note that, unlike in POSIX-compatible shells such as `bash`, `$(...)` in PowerShell is rarely needed outside of `"..."` strings; `(...)` is usually sufficient and preferable, but here you need neither: `$MyArray = $MyString -split " "`, or, for more flexibility, `$MyArray = -split $MyString` – mklement0 Sep 14 '21 at 01:36
  • @mklement0 Thank you for the clarification. Why is `(...)` preferred over `$(...)`? I would have thought that the explicitness of `$(...)` might make it preferable. – Ray Sep 14 '21 at 11:15
  • Pipeline logic is applied to the output from `$(...)`, which means that array expressions are enumerated and rebuilt, and single-element arrays are unwrapped - see [this answer](https://stackoverflow.com/a/58248195/45375) for details. As for readability: While`$(...)` is familiar from POSIX-compatible shells, to me `(...)` is more readable (less noisy). – mklement0 Sep 14 '21 at 13:15
2

tl;dr

For calling PowerShell commands you indeed need splatting in order to pass the elements of an array as individual, positional arguments; this requires defining the array in an auxiliary variable that can then be passed with sigil @ in lieu of $ to request splatting:

$myArray = -split $myString   # See below for limitations, bottom section for fix
MyPowerShellCommand @myArray  # Array elements are passed as indiv. arguments.

While this technique also works with external programs, it isn't strictly necessary there, and you can pass an array directly to achieve the same effect:

MyExternalProgram (-split $myString) # Array elements are passed as indiv. args.

Note that (...) rather than $(...) is used to pass the expression as an argument. (...) is usually sufficient and generally preferable, because $(...) can have side effects - see this answer for details.


Just to bring the post you link to in your question and your answer here together:

First, to be clear: neither answer, due to splitting by spaces only will deal properly with arguments inside the argument-list string that have embedded spaces (and therefore, of necessity use embedded quoting), e.g., $myString = "arg1 `"arg2 with spaces`" arg3" would not work as expected - see the bottom section for a solution.

Leaving that aside, the difference is:

  • When calling an external program, as in the linked post, passing an array causes each element to become its own argument.

    • That is, myExternalProgram (-split $MyString) would work.
    • Note that I'm using the unary form of the -split operator for more flexible tokenization, which splits by any non-empty run of whitespace while ignoring leading and trailing whitespace (same as awk's default behavior).
  • When calling a PowerShell command, as in your case, an array is by default passed as-is, as a whole, as a single argument.

    • To achieve the same effect as with external programs, i.e. to pass the array's elements as individual, positional arguments, you indeed have to use splatting, i.e. you have to:

      • save the array in a variable first: $myArray = -split $myString,
      • which you can then pass as as a splatted argument by using @ instead of $ as the sigil: MyPowerShellCommand @myArray
    • Do note that when calling PowerShell commands it is more common - and more robust - to use hashtable- rather than an array-based splatting, as it allows you to explicitly bind to parameters by name rather than by position - and PowerShell commands often have parameters that can only be bound by name.

      • E.g., if MyPowerShellCommand accepts parameters -Foo and -Bar, you could use:
        $myArgs = @{ Foo='foo value'; Bar='bar value '}; MyPowerShellCommand @myArgs

If you do want to handle argument-list strings that have arguments with embedded quoting:

$myString = 'arg1 "arg2 with spaces" arg3'

$myArray = (Invoke-Expression ('Write-Output -- ' + $myString -replace '\$', "`0")) -replace "`0", '$$'

Note: Invoke-Expression (iex) should generally be avoided, but the extra precautions taken in this particular command make its use safe.

$myArray is then a 3-element array with verbatim elements arg1, arg2 with spaces and arg3, which can again be used as shown above.

See this answer for an explanation of the technique.

mklement0
  • 382,024
  • 64
  • 607
  • 775
0

These work for me ($args is reserved). -split on the left side splits on whitespace. Or you can get-content from a file where each argument is on a seperate line. You might run into a limit with how long a commandline can be. Piping that list in or loading it from a file might be a better approach.

echo hi > file.txt

$args2 = 'hi','file.txt'
findstr $args2
# hi

$args2 = 'hi','file.txt'
& findstr $args2
# hi

$args2 = 'hi file.txt'
findstr (-split $args2)
# hi

findstr ($args2 -split ' ')
# hi
js2010
  • 23,033
  • 6
  • 64
  • 66