How can a Windows PowerShell script pass its parameters through to another script invocation?

12,844

Solution 1

This is probably going to look a little odd, but:

param(
[String] $stringArg,
[Int32] $int32Arg
)

if ([IntPtr]::Size -gt 4)
{
[String] $scriptPath = $MyInvocation.MyCommand.Path;

$params = @()
$psboundparameters.keys |
  foreach {
      $params += "-$($_)"
      $params +=  $psboundparameters.$_
      }


$sb = [scriptblock]::Create(@"
&'$scriptpath' $params
"@)

[Object] $job = Start-Job -scriptblock $sb -RunAs32 
$job | Wait-Job | Receive-Job;
}
else
{
Get-Date
}

Solution 2

You could re-launch your script programmatically as a 32 bit process like this:

[cmdletbinding()]
param(
    [parameter(Mandatory=$true)]
    [String] $stringArg,
    [parameter(Mandatory=$true)]
    [Int32] $int32Arg
)

if ([IntPtr]::Size -ne 4) {
    $p = $PSBoundParameters
    $32bitPs = 'C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe'
    $myArgs = @($MyInvocation.MyCommand.Path)
    $myArgs += ( $p.Keys | % { ('-' + $_), $p.Item($_) } )
    Start-Process -NoNewWindow -FilePath $32bitPs -ArgumentList $myArgs
    exit
}

Write-Host "Hi I'm a 32 bit process, my args are:"
$PSBoundParameters
# Do 32 bit stuff here...
Read-Host "Press enter to quit"
Share:
12,844
Lance U. Matthews
Author by

Lance U. Matthews

Formerly @BACON. The 'U' is undefined.

Updated on June 04, 2022

Comments

  • Lance U. Matthews
    Lance U. Matthews almost 2 years

    In short, I'm just looking for the PowerShell equivalent of how a batch file can call a script with the same parameters...

    "%~dpnx0" %*
    

    ...where "%~dpnx0" expands to the absolute path of the script and %* is expanded to the list of parameters. Is there an easy way to replicate %*? Or, at least, a way that works?

    I have a PowerShell script that uses the System.Data.OleDb namespace to read data from an Excel workbook. Because there is no 64-bit implementation of the Microsoft Jet provider, the script needs to be run from a 32-bit PowerShell session. Rather than simply having the script fail with an error message if it's run from a 64-bit session, I'd like to have the 64-bit session invoke the script in and retrieve the results from a 32-bit session. I found that this can be done using the Start-Job cmdlet with the -RunAs32 switch, however I'm having trouble providing a value for the -ArgumentList parameter.

    I came up with the following to search for whichever script parameters have values and build a command line out of them:

    function GetValueText([Object] $value)
    {
        [String] $text = if ($value -eq $null) {
            '$null';
        } elseif ($value -is [String]) {
            "'$value'";
        } elseif ($value -is [Array]) {
            '@({0})' -f (($value | ForEach-Object { GetValueText $_ }) -join ', ');
        } else {
            "$value";
        }
    
        return $text;
    }
    
    if ([IntPtr]::Size -gt 4)
    {
        [String] $scriptPath = $MyInvocation.MyCommand.Path;
        [String[]] $parameters = @(
            $MyInvocation.MyCommand.Parameters.Keys `
            | ForEach-Object {
                [Object] $parameterValue = Get-Variable -Name $_ -ValueOnly;
    
                if ($parameterValue -ne $null)
                {
                    [String] $parameterValueText = GetValueText $parameterValue;
    
                    '-{0}' -f $_;
                    $parameterValueText;
                }
            }
        );
        [Object] $job = Start-Job -FilePath $scriptPath -RunAs32 -ArgumentList $parameters;
        [Object[]] $data = $job | Wait-Job | Receive-Job;
    
        $data;
    }
    else
    {
        # Retrieve data...
    }
    

    When it gets to the Start-Job line it generates an error with message "Cannot convert value "-Argument1" to type "System.Int32[]"". -Argument1 is the script's first parameter and is of type [Int32[]], so does this mean that -ArgumentList only works with positional and not named parameters?

    I've also tried simplifying it to...

    param(
        [String] $stringArg,
        [Int32] $int32Arg
    )
    
    $PSBoundParameters;
    
    if ([IntPtr]::Size -gt 4)
    {
        [String] $scriptPath = $MyInvocation.MyCommand.Path;
        [Object] $job = Start-Job -FilePath $scriptPath -RunAs32 -ArgumentList @PSBoundParameters;
    
        $job | Wait-Job | Receive-Job;
    }
    else
    {
        Get-Date;
    }
    

    ...but when I run .\Test.ps1 'string' 12345 from a 64-bit session, it displays...

    Key                                                         Value
    ---                                                         -----
    stringArg                                                   string
    int32Arg                                                    12345
    Start-Job : Missing an argument for parameter 'ArgumentList'. Specify a parameter of type 'System.Object[]' and try again.
    At X:\Test.ps1:11 char:72
    +     [Object] $job = Start-Job -FilePath $scriptPath -RunAs32 -ArgumentList <<<<  @PSBoundParameters;
        + CategoryInfo          : InvalidArgument: (:) [Start-Job], ParameterBindingException
        + FullyQualifiedErrorId : MissingArgument,Microsoft.PowerShell.Commands.StartJobCommand
    

    ...so @PSBoundParameters seems to evaluate to $null. I'm not sure why this isn't working or what else to try.

  • Lance U. Matthews
    Lance U. Matthews about 12 years
    Thanks for the response. That fails if the script path has spaces in it, but I was able to fix it by passing "& '$scriptpath' $(&{$args} @psboundparameters)" to [scriptblock]::Create(). After making that change, the script works when called with no parameters, but if I run .\Test.ps1 'string' 12345, it fails at Receive-Job with Cannot process argument transformation on parameter 'int32Arg'. Cannot convert value "string" to type "System.Int32". Error: "Input string was not in a correct format." Can you explain what this is doing? &{$args}, in particular, is confusing me.
  • mjolinor
    mjolinor about 12 years
    That will allow you to pass named parameters. Splatting a hash table produces a set of "-parameter value" strings from the key/value pairs of the hash table. But you can't evaluate a splat by itself. &{$args} provides a script block to accept the splat that will echo back the "-parameter value" strings and then they will be included in the script block that is passed to the job.
  • mjolinor
    mjolinor about 12 years
    I cannot reproduce your results. On my system (W7 x64) it returns the date, and running get-job shows that the created job is still there, with a status of "Completed'.
  • Lance U. Matthews
    Lance U. Matthews about 12 years
    I am also on 64-bit Windows 7. I tried your updated script and .\Test.ps1, .\Test.ps1 123, .\Test.ps1 123 456, .\Test.ps1 123 456 foo, and .\Test.ps1 -int32Arg 123 all work. .\Test.ps1 foo, .\Test.ps1 foo 123, .\Test.ps1 -stringArg foo and anything else where the first two parameters cannot be converted to an integer fail with the error Cannot process argument transformation on parameter 'int32Arg'. Cannot convert value "foo" to type "System.Int32". I don't understand why it's trying to match those tokens up with the second positional parameter.
  • mjolinor
    mjolinor about 12 years
    I do not understand that either. Running that script as: PS C:\scripts\test\test scripts> ./test.ps1 'string' 12345 or PS C:\scripts\test\test scripts> ./test.ps1 -stringarg 'string' -int32arg 12345 both return the date, as expected on my system. Note - that's from a ps console, not ISE.
  • Lance U. Matthews
    Lance U. Matthews about 12 years
    I did have to edit your script because @" and "@ need to appear at the very beginning of the lines, but I can't imagine that'd introduce any problems. I write my scripts with PowerGUI, but have been testing this from a console session just because PowerGUI sometimes introduces some little differences. Starting the session with powershell.exe -NoProfile doesn't make any difference. I'm using PowerShell 2.0, not the 3.0 preview or anything. Hmmm.
  • mjolinor
    mjolinor about 12 years
    I am runnin the 3.0 preview. Let me do some more testing.
  • mjolinor
    mjolinor about 12 years
    OK. There is a difference between the way &{$args} @hash evaluates between V2 and V3. In V3 the keys will be prepended with a '-' (which I would expect). I made a change to the code that does work, but I don't really like it because it's now broken in V3. I may need to go back and look at using an array instead.
  • mjolinor
    mjolinor about 12 years
    OK, changed it to an array splat, and that seems to work in V2 and in V3. Sorry for leading you down that rabbit hole. Maybe somebody else will have a better solution.
  • Lance U. Matthews
    Lance U. Matthews about 12 years
    This is the way I originally thought of doing it and would work fine for a script that is purely writing to the console, files, etc. without yielding any data to the pipeline, but I realized that any data that is retrieved will be stuck in the 32-bit instance of the script with no (easy?) way to return it to the parent 64-bit instance. If you add Get-Date to the end of the script you'll see it in the pipeline when run directly from a 32-bit session, but from a 64-bit session it gets lost. I'd like the 64-bit handling to be transparent such that I can still pipe this script to cmdlets.
  • Andy Arismendi
    Andy Arismendi about 12 years
    If you have to mix the two then relaunching the whole process won't work so we'll and creating a background job would be better. If everything works as a 32 bit script you could just run all the code that way.
  • Lance U. Matthews
    Lance U. Matthews about 12 years
    No problem. I did notice one time that the command line that was being built didn't have any hyphens before the parameter names, so I guess that explains why it wasn't matching up the parameters to the correct positions. Your updated code works perfectly, though. I just needed to change the second line of the foreach to $params += GetValueText $psboundparameters.$_ (using the function defined in my question) so it works properly with my array parameters. Thanks for your help and for sticking with this.