2

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.

2 Answers 2

3

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
}
Sign up to request clarification or add additional context in comments.

10 Comments

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.
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.
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'.
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.
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.
|
1

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"

2 Comments

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.
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.

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.