10

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.

Charlie
  • 44,214
  • 4
  • 43
  • 69
  • [Here](https://stackoverflow.com/a/74757686/1147688) I provide a few different methods, to expand *commandlet* arguments, using variables. – not2qubit Dec 11 '22 at 01:10
  • @not2qubit I like your Solution-3! A little sneaky but definitely good for some style points. – Charlie Dec 12 '22 at 03:38

4 Answers4

18

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
JamesThomasMoon
  • 6,169
  • 7
  • 37
  • 63
Bruce Payette
  • 291
  • 1
  • 2
  • 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 Jan 24 '09 at 01:04
  • 1
    @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 Apr 27 '17 at 09:29
  • @Artyom hmm good question. In what way does it not work? Like what errors are you seeing? – Charlie May 03 '17 at 15:53
  • @Charlie The error was the argument passed was "System.Object[]" not my flattened parameters. – Artyom May 03 '17 at 17:16
0

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

inner --flag [string]::Join(" ", $args)
EBGreen
  • 36,735
  • 12
  • 65
  • 85
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.

Charlie
  • 44,214
  • 4
  • 43
  • 69
  • 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. – EBGreen Jan 16 '09 at 16:38
  • 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). – JasonMArcher Feb 16 '09 at 03:03
  • Yup, you're correct about that. The separator is controlled by the $OFS variable. – Charlie Feb 16 '09 at 23:13
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
        }
    }
}
Santhos
  • 3,348
  • 5
  • 30
  • 48