I'm trying to send just the result I want from a called PowerShell script back to the calling script.
So the script testcaller.ps1
$Result = Invoke-Expression "$PSScriptRoot\testcalled"
$LogMessage = "TestCalled ended with Result $Result"
Write-Output $LogMessage
Calls the script testcalled.ps1
$TestMessage = "this is a test"
Write-Output $TestMessage
$Level = 1
exit $Level
When run it produces this...
TestCalled ended with Result this is a test
0
I have two problems. I get the testmessage passed back to my calling script and the level past back is 0 when it should be 1. What I want to see is...
TestCalled ended with Result 1
Given testcalled.ps1
'hello world'
function Say-Hello { 'hello!' }
exit 1
If you want to run this external script from another script you can either dot source it:
The functions, variables, aliases, and drives
that the script creates are created in the scope in which you are
working. After the script runs, you can use the created items and
access their values in your session.
$Result = . "$PSScriptRoot\testcalled.ps1"
$Result # => hello world
Say-Hello # => 'hello!'
$LASTEXITCODE # => 1
Or use the call operator &:
The call operator executes in a child scope.
Meaning that, functions, variables, aliases, and drives will not be available on the current scope.
$Result = & "$PSScriptRoot\testcalled.ps1"
$Result # => hello world
Say-Hello # => SayHello: The term 'SayHello' is not recognized as a name of a cmdlet, function....
$LASTEXITCODE # => 1
As you can see, on both cases, you can use the automatic variable $LASTEXITCODE.
Last, but not least, it is not recommended to use Invoke-Expression. See https://devblogs.microsoft.com/powershell/invoke-expression-considered-harmful/
Related
Writing a powershell script with a function that invokes an executable, and then attempts to return a bool. Two errors occur:
The output from the .exe doesn't show in the console window while running
the returned value is contains the output + the return value..
I'm doing something stupid, but what?
Script is
function HelloWorld () {
$cmd = "./HelloWorld.exe"
& $cmd
return $true}
$ret = HelloWorld
The $ret = "Hello World
True"
UPDATED:
Ok, I found a script online to create the helloworld.exe
Add-Type -outputtype consoleapplication -outputassembly helloworld.exe 'public class helloworld{public static void Main(){System.Console.WriteLine("hello world");}}'
Now We can run this:
function HelloWorld {
$cmd = ".\HelloWorld.exe"
& $cmd | Write-Host
return $true
}
$ret = HelloWorld
After run:
hello world
PS> $ret
True
PS> $ret | gm
TypeName: System.Boolean
Seems to work here at least.
HTH
The output is being captured by the assignment to $ret. Important to understand PowerShell functions return all output. when you ran & $cmd the output is returned, then you ran an explicit albeit unnecessary Return $true. Both pieces of data get returned and you see no screen output because it was consumed by the assignment.
In order to get the output of HelloWorld.exe to the console while only returning the Boolean from the function you can use either Write-Host or Out-Host. The difference being the latter traverses PowerShell's for-display formatting system, which may not be necessary in this case.
function HelloWorld ()
{
$cmd = "./HelloWorld.exe"
& $cmd | Write-Host
return $true
}
$return = HelloWorld
In this case the screen should show the output from HellowWorld.exe, but $return should only contain $true.
Note: Because of the aforementioned behavior Return isn't technically necessary. The only necessary use case for Return is to explicitly exit a function usually early.
Also note: this assumes HelloWorld.exe is a typical console application for which PowerShell will capture output into the success stream.
So in the function, there is a call to an executable which had a console.writeline statement (console.writelne("Hello world"))
calling the function works fine, but there no output that appears. Instead it got returned to the caller along with the return true/false. How can I get just the bool return without disrupting the output from the exe which we need to see on the console real time...
I want to use start-job to run a .ps1 script requiring a parameter. Here's the script file:
#Test-Job.ps1
Param (
[Parameter(Mandatory=$True)][String]$input
)
$output = "$input to output"
return $output
and here is how I am running it:
$input = "input"
Start-Job -FilePath 'C:\PowerShell\test_job.ps1' -ArgumentList $input -Name "TestJob"
Get-Job -name "TestJob" | Wait-Job | Receive-Job
Get-Job -name "TestJob" | Remove-Job
Run like this, it returns " to output", so $input is null in the script run by the job.
I've seen other questions similar to this, but they mostly use -Scriptblock in place of -FilePath. Is there a different method for passing parameters to files through Start-Job?
tl;dr
$input is an automatic variable (value supplied by PowerShell) and shouldn't be used as a custom variable.
Simply renaming $input to, say, $InputObject solves your problem.
As Lee_Dailey notes, $input is an automatic variable and shouldn't be assigned to (it is automatically managed by PowerShell to provide an enumerator of pipeline input in non-advanced scripts and functions).
Regrettably and unexpectedly, several automatic variables, including $input, can be assigned to: see this answer.
$input is a particularly insidious example, because if you use it as a parameter variable, any value you pass to it is quietly discarded, because in the context of a function or script $input invariably is an enumerator for any pipeline input.
Here's a simple example to demonstrate the problem:
PS> & { param($input) "[$input]" } 'hi'
# !! No output - the argument was quietly discarded.
That the built-in definition of $input takes precedence can be demonstrated as follows:
PS> 'ho' | & { param($input) "[$input]" } 'hi'
ho # !! pipeline input took precedence
While you can technically get away with using $input as a regular variable (rather than a parameter variable) as long as you don't cross scope boundaries, custom use of $input should still be avoided:
& {
$input = 'foo' # TO BE AVOIDED
"[$input]" # Technically works: -> '[foo]'
& { "[$input]" } # FAILS, due to child scope: -> '[]'
}
I have a function that executes a script block. For convenience, the script block does not need to have explicitly defined parameters, but instead can use $_ and $A to refer to the inputs.
In the code, this is done as such:
$_ = $Value
$A = $Value2
& $ScriptBlock
This whole thing is wrapped in a function. Minimal example:
function F {
param(
[ScriptBlock]$ScriptBlock,
[Object]$Value
[Object]$Value2
)
$_ = $Value
$A = $Value2
& $ScriptBlock
}
If this function is written in a PowerShell script file (.ps1), but imported using Import-Module, the behaviour of F is as expected:
PS> F -Value 7 -Value2 1 -ScriptBlock {$_ * 2 + $A}
15
PS>
However, when the function is written in a PowerShell module file (.psm1) and imported using Import-Module, the behaviour is unexpected:
PS> F -Value 7 -Value2 1 -ScriptBlock {$_ * 2 + $A}
PS>
Using {$_ + 1} instead gives 1. It seems that $_ has a value of $null instead. Presumably, some security measure restricts the scope of the $_ variable or otherwise protects it. Or, possibly, the $_ variable is assigned by some automatic process. Regardless, if only the $_ variable was affected, the first unsuccessful example would return 1.
Ideally, the solution would involve the ability to explicitly specify the environment in which a script block is run. Something like:
Invoke-ScriptBlock -Variables #{"_" = $Value; "A" = $Value2} -InputObject $ScriptBlock
In conclusion, the questions are:
Why can't script blocks in module files access variables defined in functions from which they were called?
Is there a method for explicitly specifying the variables accessible by a script block when invoking it?
Is there some other way of solving this that does not involve including an explicit parameter declaration in the script block?
Out of order:
Is there some other way of solving this that does not involve including an explicit parameter declaration in the script block?
Yes, if you just want to populate $_, use ForEach-Object!
ForEach-Object executes in the caller's local scope, which helps you work around the issue - except you won't have to, because it also automatically binds input to $_/$PSItem:
# this will work both in module-exported commands and standalone functions
function F {
param(
[ScriptBlock]$ScriptBlock,
[Object]$Value
)
ForEach-Object -InputObject $Value -Process $ScriptBlock
}
Now F will work as expected:
PS C:\> F -Value 7 -ScriptBlock {$_ * 2}
Ideally, the solution would involve the ability to explicitly specify the environment in which a script block is run. Something like:
Invoke-ScriptBlock -Variables #{"_" = $Value; "A" = $Value2} -InputObject $ScriptBlock
Execute the scripblock using ScriptBlock.InvokeWithContext():
$functionsToDefine = #{
'Do-Stuff' = {
param($a,$b)
Write-Host "$a - $b"
}
}
$variablesToDefine = #(
[PSVariable]::new("var1", "one")
[PSVariable]::new("var2", "two")
)
$argumentList = #()
{Do-Stuff -a $var1 -b two}.InvokeWithContext($functionsToDefine, $variablesToDefine, $argumentList)
Or, wrapped in a function like your original example:
function F
{
param(
[scriptblock]$ScriptBlock
[object]$Value
)
$ScriptBlock.InvokeWithContext(#{},#([PSVariable]::new('_',$Value)),#())
}
Now you know how to solve your problem, let's get back to the question(s) about module scoping.
At first, it's worth noting that you could actually achieve the above using modules, but sort of in reverse.
(In the following, I use in-memory modules defined with New-Module, but the module scope resolution behavior describe is the same as when you import a script module from disk)
While module scoping "bypasses" normal scope resolution rules (see below for explanation), PowerShell actually supports the inverse - explicit execution in a specific module's scope.
Simply pass a module reference as the first argument to the & call operator, and PowerShell will treat the subsequent arguments as a command to be invoked in said module:
# Our non-module test function
$twoPlusTwo = { return $two + $two }
$two = 2
& $twoPlusTwo # yields 4
# let's try it with explicit module-scoped execution
$myEnv = New-Module {
$two = 2.5
}
& $myEnv $twoPlusTwo # Hell froze over, 2+2=5 (returns 5)
Why can't script blocks in module files access variables defined in functions from which they were called?
If they can, why can't the $_ automatic variable?
Because loaded modules maintain state, and the implementers of PowerShell wanted to isolate module state from the caller's environment.
Why might that be useful, and why might one preclude the other, you ask?
Consider the following example, a non-module function to test for odd numbers:
$two = 2
function Test-IsOdd
{
param([int]$n)
return $n % $two -ne 0
}
If we run the above statements in a script or an interactive prompt, subsequently invocating Test-IsOdd should yield the expected result:
PS C:\> Test-IsOdd 123
True
So far, so great, but relying on the non-local $two variable bears a flaw in this scenario - if, somewhere in our script or in the shell we accidentally reassign the local variable $two, we might break Test-IsOdd completely:
PS C:\> $two = 1 # oops!
PS C:\> Test-IsOdd 123
False
This is expected since, by default, variable scope resolution just wanders up the call stack until it reaches the global scope.
But sometimes you might require state to be kept across executions of one or more functions, like in our example above.
Modules solve this by following slightly different scope resolution rules - module-exported functions defer to something we call module scope (before reaching the global scope).
To illustrate how this solves our problem from before, considering this module-exported version of the same function:
$oddModule = New-Module {
function Test-IsOdd
{
param([int]$n)
return $n % $two -ne 0
}
$two = 2
}
Now, if we invoke our new module-exported Test-IsOdd, we predictably get the expected result, regardless of "contamination" in the callers scope:
PS C:\> Test-IsOdd 123
True
PS C:\> $two = 1
PS C:\> Test-IsOdd 123 # still works
True
This behavior, while maybe surprising, basicly serves to solidify the implicit contract between the module author and the user - the module author doesn't need to worry too much about what's "out there" (the callers session state), and the user can expect whatever going on "in there" (the loaded module's state) to work correctly without worrying about what they assign to variables in the local scope.
Module scoping behavior poorly documented in the help files, but is explained in some depth in chapter 8 of Bruce Payette's "PowerShell In Action" (ISBN:9781633430297)
I am porting a script from bash to PowerShell, and I would like to keep the same support for argument parsing in both. In the bash, one of the possible arguments is -- and I want to also detect that argument in PowerShell. However, nothing I've tried so far has worked. I cannot define it as an argument like param($-) as that causes a compile error. Also, if I decide to completely forego PowerShell argument processing, and just use $args everything appears good, but when I run the function, the -- argument is missing.
Function Test-Function {
Write-Host $args
}
Test-Function -- -args go -here # Prints "-args go -here"
I know about $PSBoundParameters as well, but the value isn't there, because I can't bind a parameter named $-. Are there any other mechanisms here that I can try, or any solution?
For a bit more context, note that me using PowerShell is a side effect. This isn't expected to be used as a normal PowerShell command, I have also written a batch wrapper around this, but the logic of the wrapper is more complex than I wanted to write in batch, so the batch wrapper just calls the PowerShell function, which then does the more complex processing.
I found a way to do so, but instead of double-hyphen you have to pass 3 of them.
This is a simple function, you can change the code as you want:
function Test-Hyphen {
param(
${-}
)
if (${-}) {
write-host "You used triple-hyphen"
} else {
write-host "You didn't use triple-hyphen"
}
}
Sample 1
Test-Hyphen
Output
You didn't use triple-hyphen
Sample 2
Test-Hyphen ---
Output
You used triple-hyphen
As an aside: PowerShell allows a surprising range of variable names, but you have to enclose them in {...} in order for them to be recognized; that is, ${-} technically works, but it doesn't solve your problem.
The challenge is that PowerShell quietly strips -- from the list of arguments - and the only way to preserve that token is you precede it with the PSv3+ stop-parsing symbol, --%, which, however, fundamentally changes how the arguments are passed and is obviously an extra requirement, which is what you're trying to avoid.
Your best bet is to try - suboptimal - workarounds:
Option A: In your batch-file wrapper, translate -- to a special argument that PowerShell does preserve and pass it instead; the PowerShell script will then have to re-translate that special argument to --.
Option B: Perform custom argument parsing in PowerShell:
You can analyze $MyInvocation.Line, which contains the raw command line that invoked your script, and look for the presence of -- there.
Getting this right and making it robust is nontrivial, however.
Here's a reasonably robust approach:
# Don't use `param()` or `$args` - instead, do your own argument parsing:
# Extract the argument list from the invocation command line.
$argList = ($MyInvocation.Line -replace ('^.*' + [regex]::Escape($MyInvocation.InvocationName)) -split '[;|]')[0].Trim()
# Use Invoke-Expression with a Write-Output call to parse the raw argument list,
# performing evaluation and splitting it into an array:
$customArgs = if ($argList) { #(Invoke-Expression "Write-Output -- $argList") } else { #() }
# Print the resulting arguments array for verification:
$i = 0
$customArgs | % { "Arg #$((++$i)): [$_]" }
Note:
There are undoubtedly edge cases where the argument list may not be correctly extracted or where the re-evaluation of the raw arguments causes side effect, but for the majority of cases - especially when called from outside PowerShell - this should do.
While useful here, Invoke-Expression should generally be avoided.
If your script is named foo.ps1 and you invoked it as ./foo.ps1 -- -args go -here, you'd see the following output:
Arg #1: [--]
Arg #2: [-args]
Arg #3: [go]
Arg #4: [-here]
I came up with the following solution, which works well also inside pipelines multi-line expressions. I am using the PowerShell Parser to parse the invocation expression string (while ignoring any incomplete tokens, which might be present at the end of $MyInfocation.Line value) and then Invoke-Expression with Write-Output to get the actual argument values:
# Parse the whole invocation line
$code = [System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.Line.Substring($MyInvocation.OffsetInLine - 1), [ref]$null, [ref]$null)
# Find our invocation expression without redirections
$myline = $code.Find({$args[0].CommandElements}, $true).CommandElements | % { $_.ToString() } | Join-String -Separator ' '
# Get the argument values
$command, $arguments = Invoke-Expression ('Write-Output -- ' + $myline)
# Fine-tune arguments to be always an array
if ( $arguments -is [string] ) { $arguments = #($arguments) }
if ( $arguments -eq $null ) { $arguments = #() }
Please be aware that the original values in the function call are reevaluated in Invoke-Expression, so any local variables might shadow values of the actual arguments. Because of that, you can also use this (almost) one-liner at the top of your function, which prevents the pollution of local variables:
# Parse arguments
$command, $arguments = Invoke-Expression ('Write-Output -- ' + ([System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.Line.Substring($MyInvocation.OffsetInLine - 1), [ref]$null, [ref]$null).Find({$args[0].CommandElements}, $true).CommandElements | % { $_.ToString() } | Join-String -Separator ' '))
# Fine-tune arguments to be always an array
if ( $arguments -is [string] ) { $arguments = #($arguments) }
if ( $arguments -eq $null ) { $arguments = #() }
I have one powershell script, like this one:
$ErrorActionPreference = "Stop"
$WORKDIR=$args[0]
Write-Host "Num of arguments given: " $args.Count
$AllArguments = ""
for ($i = 1; $i -lt $args.Count; $i += 2) {
$AllArguments = '$AllArguments $args[$i] "$($args[$i+1])"'
}
Write-Host "AllArguments: $($AllArguments)"
Write-Host "Starting .\someotherscript.ps1 in directory $($WORKDIR)"
pushd "$WORKDIR"
& Powershell -File .\someotherscript.ps1 $AllArguments
popd
Basically, this powershell script shoud start another powershell script, but without the first argument. So, e.g. when this script is started with .\firstscript.ps1 C:\some\dir -GiveMeParameter param1, then it should call the other script with the following parameters .\someotherscript.ps1 -GivMeParameter param1.
How to achieve that? At the moment I do not know how to solve this problem.
You can use multiple assignment to extract the first and remaining arguments by doing
$WORKDIR, $RemainingArgs = $args
I'm not sure why you're calling PowerShell again to run the script. You can just run the script directly and use splatting to pass the remaining args to the child script.
.\someotherscript.ps1 #RemainingArgs