Invoke a second script with arguments from a script
Asked Answered
P

7

85

I have a script that reads a configuration file that results in a set of name value pairs that I would like to pass as arguments to a function in a second PowerShell script.

I do not know what parameters will be placed in this configuration file at design time, so right at the point where I need to invoke this second PowerShell script, I basically just have one variable that has the path to this second script, and a second variable that is an array of arguments to pass to the script identified in the path variable.

So the variable containing the path to the second script ($scriptPath), might have a value like:

"c:\the\path\to\the\second\script.ps1"

The variable containing the arguments ($argumentList) might look something like:

-ConfigFilename "doohickey.txt" -RootDirectory "c:\some\kind\of\path" -Max 11

How do I get from this state of affairs to the execution of script.ps1 with all of the arguments from $argumentList?

I'd like any write-host commands from this second script to be visible to the console from which this first script is invoked.

I have tried dot-sourcing, Invoke-Command, Invoke-Expression, and Start-Job, but I haven't found an approach that doesn't produce errors.

For example, I thought the easiest first route was to try Start-Job called as follows:

Start-Job -FilePath $scriptPath -ArgumentList $argumentList

...but this fails with this error:

System.Management.Automation.ValidationMetadataException:
Attribute cannot be added because it would cause the variable
ConfigFilename with value -ConfigFilename to become invalid.

...in this case, "ConfigFilename" is the first parameter in the param list defined by the second script, and my invocation is apparently trying to set its value to "-ConfigFilename", which is obviously intended to identify the parameter by name, not set its value.

What am I missing?

EDIT:

Ok, here is a mock-up of the to-be-called script, in a file named invokee.ps1

Param(
[parameter(Mandatory=$true)]
[alias("rc")]
[string]
[ValidateScript( {Test-Path $_ -PathType Leaf} )]
$ConfigurationFilename,
[alias("e")]
[switch]
$Evaluate,
[array]
[Parameter(ValueFromRemainingArguments=$true)]
$remaining)

function sayHelloWorld()
{
    Write-Host "Hello, everybody, the config file is <$ConfigurationFilename>."
    if ($ExitOnErrors)
    {
        Write-Host "I should mention that I was told to evaluate things."
    }
    Write-Host "I currently live here: $gScriptDirectory"
    Write-Host "My remaining arguments are: $remaining"
    Set-Content .\hello.world.txt "It worked"
}

$gScriptPath = $MyInvocation.MyCommand.Path
$gScriptDirectory = (Split-Path $gScriptPath -Parent)
sayHelloWorld

...and here is a mock-up of the calling script, in a file named invoker.ps1:

function pokeTheInvokee()
{
    $scriptPath = (Join-Path -Path "." -ChildPath "invokee.ps1")
    $scriptPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($scriptPath)
    
    $configPath = (Join-Path -Path "." -ChildPath "invoker.ps1")
    $configPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($configPath)
    
    $argumentList = @()
    $argumentList += ("-ConfigurationFilename", "`"$configPath`"")
    $argumentList += , "-Evaluate"

    Write-Host "Attempting to invoke-expression with: `"$scriptPath`" $argumentList"
    Invoke-Expression "`"$scriptPath`" $argumentList"
    Invoke-Expression ".\invokee.ps1 -ConfigurationFilename `".\invoker.ps1`" -Evaluate
    Write-Host "Invokee invoked."
}

pokeTheInvokee

When I run invoker.ps1, this is the error I'm currently getting on the first call to Invoke-Expression:

Invoke-Expression : You must provide a value expression on
the right-hand side of the '-' operator.

The second call works just fine, but one significant difference is that the first version is using arguments whose paths have spaces in them, and the second does not. Am I mishandling the presence of spaces in these paths?

Proclus answered 12/10, 2012 at 0:12 Comment(2)
It would also help if you provided source for each of your scripts. For example, create POC (proof of concept) project illustrating your problem. We can then test and play with it, and maybe somebody will find a solution, or a workaround.Mangosteen
Yes - I'm trying to gin up a mini version to illustrate. Will post soon...Proclus
P
72

Aha. This turned out to be a simple problem of there being spaces in the path to the script.

Changing the Invoke-Expression line to:

Invoke-Expression "& `"$scriptPath`" $argumentList"

...was enough to get it to kick off. Thanks to Neolisk for your help and feedback!

Proclus answered 12/10, 2012 at 3:25 Comment(3)
To add to your answer. Having spaces in the path means that you have to use quotes. Quotes get treated as a string, as opposed to a command, by Invoke-Expression. Therefore, if you want to execute the string, you need to use the "call operator", which is the ampersand symbol at the beginning of your example. Here is a definition for the call operator.Giraldo
When I try my script this way, it has a syntax error with missing end quote. Method 2 below works for meFloatation
I had a problem with a semicolon character in one of the string arguments in my scripts argument list. Invoke-Expression kept interpreting it as statement separator and only passed the beginning of string as argument to the script. When I applied this answer in my code it worked just fine.Sarraute
M
51

Invoke-Expression should work perfectly, just make sure you are using it correctly. For your case it should look like this:

Invoke-Expression "$scriptPath $argumentList"

I tested this approach with Get-Service and seems to be working as expected.

Mangosteen answered 12/10, 2012 at 0:52 Comment(5)
Invoke-Expression "$scriptPath $argumentList" returns immediately and doesn't appear to run the script. If I change it to Invoke-Expression "powershell $scriptPath $argumentList", PowerGui just appears to hang.Proclus
@Hoobajoob: Invoke-Expression does not return immediately, it patiently waits until the invoked script ends - I just tested on a POC project. There must be something wrong with the script you are targeting.Mangosteen
Hm - I've run this second script many different ways from batch files, the visual studio IDE, DOS command lines, and powershell console sessions, but this is the first time I've tried to run it from another powershell script. What kind of things could there be in the script itself that would keep it from successfully being launched via Invoke-Expression?Proclus
this will only work if the objects in the argument list are all of type string though right?Allmon
@Backwards_Dave: $scriptPath and $argumentList here are strings. A string can contain a non-string argument (written as a string). Hope it answers your question.Mangosteen
A
41

Much simpler actually:

Method 1:

Invoke-Expression $scriptPath $argumentList

Method 2:

& $scriptPath $argumentList

Method 3:

$scriptPath $argumentList

If you have spaces in your scriptPath, don't forget to escape them `"$scriptPath`"

Advocate answered 23/1, 2014 at 3:42 Comment(1)
Thanks for the options, I really like the additional option provided by this answer on another question. https://mcmap.net/q/246469/-how-to-pass-arguments-to-an-invoke-expression-in-powershellProtuberancy
G
18

We can use splatting for this:

& $command @args

where @args (automatic variable $args) is splatted into array of parameters.

Under PS, 5.1

Greensand answered 19/8, 2017 at 14:6 Comment(0)
A
16

Here's an answer covering the more general question of calling another PS script from a PS script, as you may do if you were composing your scripts of many little, narrow-purpose scripts.

I found it was simply a case of using dot-sourcing. That is, you just do:

# This is Script-A.ps1

. ./Script-B.ps1 -SomeObject $variableFromScriptA -SomeOtherParam 1234;

I found all the Q/A very confusing and complicated and eventually landed upon the simple method above, which is really just like calling another script as if it was a function in the original script, which I seem to find more intuitive.

Dot-sourcing can "import" the other script in its entirety, using:

. ./Script-B.ps1

It's now as if the two files are merged.

Ultimately, what I was really missing is the notion that I should be building a module of reusable functions.

Angrist answered 26/11, 2015 at 15:32 Comment(2)
& "./Script-B.ps1" -SomeObject $variableFromScriptA -SomeOtherParam 1234Anamorphoscope
What is the advantage of a module if I don't need to export it to other systems?Fidler
C
4

I tried the accepted solution of using the Invoke-Expression cmdlet but it didn't work for me because my arguments had spaces on them. I tried to parse the arguments and escape the spaces but I couldn't properly make it work and also it was really a dirty work around in my opinion. So after some experimenting, my take on the problem is this:

function Invoke-Script
{
    param
    (
        [Parameter(Mandatory = $true)]
        [string]
        $Script,

        [Parameter(Mandatory = $false)]
        [object[]]
        $ArgumentList
    )

    $ScriptBlock = [Scriptblock]::Create((Get-Content $Script -Raw))
    Invoke-Command -NoNewScope -ArgumentList $ArgumentList -ScriptBlock $ScriptBlock -Verbose
}

# example usage
Invoke-Script $scriptPath $argumentList

The only drawback of this solution is that you need to make sure that your script doesn't have a "Script" or "ArgumentList" parameter.

Clog answered 21/11, 2016 at 11:17 Comment(0)
P
2

You can execute it same as SQL query. first, build your command/Expression and store in a variable and execute/invoke.

$command =  ".\yourExternalScriptFile.ps1" + " -param1 '$paramValue'"

It is pretty forward, I don't think it needs explanations. So all set to execute your command now,

Invoke-Expression $command

I would recommend catching the exception here

Pignut answered 3/8, 2017 at 10:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.