11

I have two PowerShell functions, the first of which invokes the second. They both take N arguments, and one of them is defined to simply add a flag and invoke the other. Here are example definitions:

function inner
{
  foreach( $arg in $args )
    {
      # do some stuff
    }
}

function outer
{
  inner --flag $args
}

Usage would look something like this:

inner foo bar baz

or this

outer wibble wobble wubble

The goal is for the latter example to be equivalent to

inner --flag wibble wobble wubble

The Problem: As defined here, the latter actually results in two arguments being passed to inner: the first is "--flag", and the second is an array containing "wibble", "wobble", and "wubble". What I want is for inner to receive four arguments: the flag and the three original arguments.

So what I'm wondering is how to convince powershell to expand the $args array before passing it to inner, passing it as N elements rather than a single array. I believe you can do this in Ruby with the splatting operator (the * character), and I'm pretty sure PowerShell can do it, but I don't recall how.

2
  • Here I provide a few different methods, to expand commandlet arguments, using variables. Commented Dec 11, 2022 at 1:10
  • @not2qubit I like your Solution-3! A little sneaky but definitely good for some style points. Commented Dec 12, 2022 at 3:38

5 Answers 5

22

There isn't a good solution to this problem in PowerShell Version 1. In Version 2 we added splatting (though for various reasons, we use @ instead of * for this purpose).

Here's what it looks like:

PS> function foo ($x,$y,$z) { "x:$x y:$y z:$z" }
PS> $a = 1,2,3
PS> foo $a # passed as single arg
x:1 2 3 y: z:
PS> foo @a # splatted
x:1 y:2 z:3
Sign up to request clarification or add additional context in comments.

4 Comments

What an honor to get an answer from one of the PowerShell gurus! I have PowerShell 2 CTP2 installed, so I'll give that a try.
@Charlie @BrucePayette I'm having System.Object[] in & someexefile.exe $args under Powershell v5.1. When I do & someexefile.exe @args it doesn't work for Unicode characters (chcp 65001 done). How to pass args between ps1 scripts?
@Artyom hmm good question. In what way does it not work? Like what errors are you seeing?
@Charlie The error was the argument passed was "System.Object[]" not my flattened parameters.
1

You can splat the arguments in outer:

function outer
{
  inner --flag @args
}

Or, in any situation (not just when providing the arguments to a command call), you can use the subexpression operator which will automatically spread arrays by 1 level:

function outer
{
  $flattened = $("--flag"; $args)
  inner @flattened 
}

Comments

0

Well, there may be a better way, but see if this works:

inner --flag [string]::Join(" ", $args)

2 Comments

It looks like this doesn't work directly, because the result of string::Join is passed as a single argument, in this case "wibble wobble wubble".
hmmm...let me poke around some more
0

Building on @EBGreen's idea, and a related question I noticed in the sidebar, a possible solution is this:

function outer
{
    invoke-expression "inner --flag $($args -join ' ')"
}

Note: This example makes use of the Powershell 2.0 CTP's new -join operator.

However, I'd still like to find a better method, since this feels like a hack and is horrible security-wise.

3 Comments

I'm not a fan of invoke expression either, but if you control of the args it isn't terrible. I think CTP2 or CTP3 exposes the tokenizer too in which case you could sanitize the args before the invoke.
You can do this with PowerShell v1.0 only stuff as well: invoke-expression "inner --flag $args" The array will be joined with spaces by default (there is also a way to change the default character, but I can't remember it).
Yup, you're correct about that. The separator is controlled by the $OFS variable.
0

If you want a quick ready-made solution, you can copy paste mine:

<#
    .SYNOPSIS
    Asks a question and waits for user's answer

    .EXAMPLE
    Usage with shortcuts and without ReturnValue
    Invoke-Question -Question "What would you like" -Answers "&Eggs", "&Toasts", "&Steak"

    Shows the quesiton and waits for input. Let's assume user input is 'S', the return value would be 2 (index of "&Steak")

    .EXAMPLE
    Usage without shortcuts and with ReturnValue
    Invoke-Question -Question "What would you like" -Answers "Eggs", "Toasts", "Steak" -ReturnValue

    Shows the quesiton and waits for input. The answers are prefixed with numbers 1, 2 and 3 as shortcuts.
    Let's assume user input is 2, the return value would be "Toasts" (prefixed numbers are "index + 1")

    .EXAMPLE
    Usage from pipeline with default value
    @("Eggs", "Toasts", "Steak") | Invoke-Question -Question "What would you like" -ReturnValue -Default 2

    Shows the quesiton and waits for input. The answers are taken from pipeline and prefixed with numbers 1, 2 and 3 as shortcuts.
    Steak is marked as default. If user simply continues without a choice, Steak is chosen for her.
    However, let's assume user input is 1, the return value would be "Eggs" (prefixed numbers are "index + 1")
#>
function Invoke-Question {
    [CmdletBinding()]
    param(
        # Main question text
        [Parameter(Mandatory = $true)]
        [string] $Question,

        # Question description, e.g. explanation or more information
        [Parameter(Mandatory = $false)]
        [string] $Description = "",

        # Default answer as index in the array, no answer is selected by default (value -1)
        [Parameter(Mandatory = $false)]
        [int] $Default = -1,

        # Set of answers, if the label is given with & sign, the prefixed letter is used as shortcut, e.g. "&Yes" -> Y,
        # otherwise the answer is prefixed with "index + 1" number as a shortcut
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string[]] $Answers,

        # If set, returns a value of selected answer, otherwise returns its index in the Answer array
        [switch] $ReturnValue
    )

    begin {
        # init choices
        $choices = New-Object Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]
        $answerNumber = 1
        $rememberAnswers = @()
    }

    process {
        #init answers
        foreach ($answer in $answers) {
            $rememberAnswers += $answer
            if ($answer -notmatch "&") {
                # add number if shortcut not specified
                $answer = "&$answerNumber $answer"
            }

            $choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList $answer))
            $answerNumber++
        }
    }

    end {
        # ask question and return either value or index
        $index = $Host.UI.PromptForChoice($Question, $Description, $choices, $Default)
        if ($ReturnValue) {
            $rememberAnswers[$index]
        } else {
            $index
        }
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.