Pass a variable to another script - powershell

I have two PowerShell scripts.
The first script has the following code:
$var = "abc"
$DIR = "C:\"
$SCRIPT_NAME = "abc.ps1"
&"${DIR}\${SCRIPT_NAME}" #execute the second script
If I want to pass the variable $var to the second script, how do I achieve that? What code do I need to put in both the first and the second script?

Parameters (Recommended): Use parameters to pass values to the second script.
Step2.ps1:
param ($myparameter)
write-host $myparameter
Step1.ps1:
$var = "abc"
$DIR = "C:\"
$SCRIPT_NAME = "step2.ps1"
&"${DIR}\${SCRIPT_NAME}" -myparameter $var
Alternative: You could also have used arguments $args (extra values not linked to a parameter). You can specify the first argument using $args[0]. I would however always recommend parameters as arguments needs to be in a specific order (if multiple arguments are passed) etc.
Step2.ps1:
write-host $args[0]
Step1.ps1:
$var = "abc"
$DIR = "C:\"
$SCRIPT_NAME = "step2.ps1"
&"${DIR}\${SCRIPT_NAME}" $var

There are several ways to do what you want, two of which have already been suggested by #FrodeF..
Pass the variable as a (named) parameter:
# script1.ps1
$var = 'foo'
$dir = 'C:\some\folder'
$scriptname = "script2.ps1"
& "${dir}\${scriptname}" -Foo $var
# script2.ps1
Param($foo)
Write-Output $foo
This is the cleanest solution. You have a well-defined interface and pass the variable in a clear-cut way from one script to another.
Parameter definitions will also allow you to make a parameter mandatory (so that the script will ask the user to provide input if the parameter was omitted), require a particular data type, easily incorporate validation routines, or add comment-based help.
# script2.ps1
<#
.SYNOPSIS
Short description of the script or function.
.DESCRIPTION
Longer description of what the script or function actually does.
.PARAMETER Foo
Description of the parameter Foo.
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
[ValidateRange(2,42)]
[int]$foo
)
Write-Output $foo
See Get-Help about_Function_Advanced_Parameters for more information.
Pass the variable as an unnamed argument:
# script1.ps1
$var = 'foo'
$dir = 'C:\some\folder'
$scriptname = "script2.ps1"
& "${dir}\${scriptname}" $var
# script2.ps1
Write-Output $args[0]
This is the second best approach, because you still pass the variable in a clear-cut way, but the interface isn't as well defined as before.
Define the variable as an environment variable:
# script1.ps1
$env:var = 'foo'
$dir = 'C:\some\folder'
$scriptname = "script2.ps1"
& "${dir}\${scriptname}"
# script2.ps1
Write-Output $env:var
This is a less clean approach than the argument-based ones, as the variable is passed using a "side-channel" (the process environment, which is inherited by child processes).
Just define the variable in the first script and use it in the second one:
# script1.ps1
$var = 'foo'
$dir = 'C:\some\folder'
$scriptname = "script2.ps1"
& "${dir}\${scriptname}"
# script2.ps1
Write-Output $var
This will work as well, because by using the call operator (&) the second script is run in the same context as the first script and thus has access to the same variables. However, "passing" a variable like this will easily break if someone runs the second script in a different context/scope or modies it without being aware of the implicit dependency.
If you want to go this route it's usually better to use the first script for variable (and function) definitions only, and dot-source it in the second script, so that the definitions are imported into the scope of the second script:
# script1.ps1
$var = 'foo'
# script2.ps1
. 'C:\path\to\script1.ps1'
Write-Output $var
Technically, passing values via a file would be another option. However, I would recommend against using this approach for several reasons:
it's prone to errors due to improper permissions (could be mitigated by creating the file in the $env:TEMP folder),
it's prone to littering the filesystem if you don't clean up the file afterwards,
it needlessly generates disk I/O when simple in-memory operations provided by the language would suffice.

Related

How to escape the following regex, so its usable in PowerShell?

As far as I know, there is no way in PowerShell to execute an exe with parameters specified in a variable and direct the return of the exe into a variable. Therefore I am currently writing a small function to make this possible. But now I am stuck at the point that the parameters have to be passed individually when calling with &. (This is not necessary for all programs, but some programs cause problems if you pass all parameters as a string in a variable) Therefore I want to use a split to write the parameters passed to my function into an array. And then pass the array with the parameters in my exe call.
For this I have the following regex:
[^\s"']+|"([^"]*)"|'([^']*)'
This regex allows that single and double quotes are taken into account when passing parameters and that a text with spaces inside them is not split.
But unfortunately I don't have the slightest idea how to best escape this regex so that it doesn't cause any problems in the PowerShell script.
Here then still my function to make it a little easier to understand:
The function executes the file passed in the $Path parameter with the parameters from the $Arguments. Before the execution i try to split the $Arguments with the regex. As return of the function, you get an object with the ExitCode and the output of the executed file. Here you can see my attempt, but the quotes cause problems with the following code.
function Invoke-Process ($Path,$Arguments){
[PsObject[]] $ReturnValue = #()
$Params=$Arguments -split([regex]::escape([^\s"']+|"([^"]*)"|'([^']*)'))
$ExCode = 0
try {
$ProcOutput = & $Path $Params | out-string
}catch{
$ProcOutput = "Failed: $($_.Exception.Message)"
$ExCode = 1
}
$ReturnValue += [PsObject]#{ ExitCode = $ExCode; Output = $ProcOutput}
Return $ReturnValue
}
The function is called as follows:
$ExePath = "C:\arcconf\arcconf.exe"
$ExeParams = "getconfig 1"
$Output = Invoke-Process $ExePath $ExeParams
I hope you can understand my problem. I am also open to other ways of writing the function.
Greetings
There's nothing you need to escape - the pattern is perfectly valid.
All you need is a string literal type that won't treat the quotation marks as special. For this, I would suggest a verbatim here-string:
#'
This is a single-line string ' that " can have all sorts of verbatim quotation marks
'#
The qualifiers for a here-string is #' as the last token on the preceding line and '# as the first token on the following line (for an expandable here-string, use #" and "#).
Try running it with valid sample input:
$pattern = #'
[^\s"']+|"([^"]*)"|'([^']*)'
'#
'getconfig 1 "some string" unescaped stuff 123' |Select-String $pattern -AllMatches |ForEach-Object {$_.Matches.Value}
Which, as expected, returns:
gesture
1
"some string"
unescaped
stuff
123
As an alternative to a here-string, the best alternative is a regular verbatim string literal. The only character you need to escape is ', and the escape sequence is simply two in a row '', so your source code becomes:
$pattern = '[^\s"'']+|"([^"]*)"|''([^'']*)'''
Mathias R. Jessen's helpful answer answers your immediate question well.
Taking a step back:
there is no way in PowerShell to execute an exe with parameters specified in a variable and direct the return of the exe into a variable
No: PowerShell does support both of those things:
$output = someExternalProgram ... captures the stdout output from an external program in variable $output; use redirection 2>&1 to also capture stderr output; use >$null or 2>$null to selectively discard stdout or stderr output - see this answer for details.
someExternalProgram $someVariable ... passes the value of variable $someVariable as an argument to an external program; if $someVariable is an array (collection) of values, each element is passed as a separate argument. Instead of a variable reference, you may also use the output from a command or expression, via (...); e.g., someExternalProgram (1 + 2) passes 3 as the argument - see this answer for details.
Note: An array's elements getting passed as individual arguments only happens for external programs by default; to PowerShell-native commands, arrays are passed as a whole, as a single argument - unless you explicitly use splatting, in which case you must pass #someVariable rather than $someVariable. For external programs, #someVariable is effectively the same as $someVariable. While array-based splatting also works for PowerShell-native commands, for passing positional arguments only, the typical and more robust and complete approach is to use hashtable-based splatting, where the target parameters are explicitly identified.
A general caveat that applies whether or not you use variables or literal values as arguments: Up to at least PowerShell 7.2.x, passing empty-string arguments or arguments with embedded " chars. to external programs is broken - see this answer.
With the above in mind you can rewrite your function as follows, which obviates the need for - ultimately brittle - regex parsing:
function Invoke-Process {
# Split the arguments into the executable name / path and all
# remaining arguments.
$exe, $passThruArgs = $args
try {
# Call the executable with the pass-through arguments.
# and capture its *stdout* output.
$output = & $exe #passThruArgs
$exitCode = $LASTEXITCODE # Save the process' exit code.
} catch {
# Note: Only If $exe is empty or isn't a valid executable name or path
# does a trappable statement-terminating error occur.
# By contrast, if the executable can be called in principle, NO
# trappable error occurs if the process exit code is nonzero.
$exitCode = 127 # Use a special exit code
$output = $_.ToString() # Use the exception message
}
# Construct and output a custom object containing
# the captured stdout output and the process' exit code.
[pscustomobject] #{
ExitCode = $exitCode
Output = $output
}
}
Sample calls:
Invoke-Process cmd /c 'echo hi!'
# Equivalent, using an array variable for the pass-through arguments.
$argsForCmd = '/c', 'echo hi!'
Invoke-Process cmd #argsForCmd
Note:
Since Invoke-Process is a PowerShell command,
splatting (#argsForCmd) is necessary here in order to pass the array
elements as individual arguments, which inside the function are then reflected in the automatic $args variable variable.
The automatic $args variable is only available in simple functions, as opposed to advanced ones, which behave like cmdlets and therefore automatically support additional features, such as common parameters; to make your function and advanced one, replace the line $exe, $passThruArgs = $args at the top of the function with the following:
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $exe,
[Parameter(ValueFromRemainingArguments)]
[string[]] $passThruArgs
)

start powershell wont accept variable as parameter

issue
the called powershell script will accept parameters but not all of them:
Current Set-Up and code:
I have a common folder where two .ps1 scripts are located:
DoWork.ps1
Workmanager.ps1
Workmanager.ps1 calls the Dowork.ps1:
$targetPath="M:\target"
echo "target path: $targetPath"
start powershell {.\DoWork.ps1 -target $targetPath -tempdrive D:\}
output (as expected):
target path: M:\target
DoWork.ps1 contains some start code:
param
(
[string]$tempdrive,
[string]$target,
[int] $threads = 8,
[int] $queuelength = -1
)
echo "variables:"
echo "temp drive: $tempdrive"
echo "target path: $target"
Unexpectedly, the $target is not beeing assigned. Previously I had the variable named $targetpath, which did not work either.
variables:
temp drive: D:\
target path:
Findings
It appears that the issue relies in Workmanager.ps1. Spcifying the parameter as fixed string rather than as variable will load the parameter. Any solution for this?
start powershell {.\DoWork.ps1 -target "foo" -tempdrive D:\}
When you use a ScriptBlock as an argument to powershell.exe, variables aren't going to be evaluated until after the new session starts. $targetPath has not been set in the child PowerShell process called by Workmanager.ps1 and so it has no value. This is actually an expected behavior of a ScriptBlock in general and behaves this way in other contexts too.
The solution is mentioned in the help text for powershell -?:
[-Command { - | <script-block> [-args <arg-array>] <========== THIS GUY
| <string> [<CommandParameters>] } ]
You must provide the -args parameter which will be passed to the ScriptBlock on execution (separate multiple arguments with a ,). Passed arguments are passed positionally, and must be referenced as though you were processing the arguments to a function manually using the $args array. For example:
$name = 'Bender'
& powershell { Write-Output "Hello, $($args[0])" } -args $name
However, especially with more complicated ScriptBlock bodies, having to remember which index of $args[i] contains the value you want at a given time is a pain in the butt. Luckily, we can use a little trick with defining parameters within the ScriptBlock to help:
$name = 'Bender'
& powershell { param($name) Write-Output "Hello, $name" } -args $name
This will print Hello, Bender as expected.
Some additional pointers:
The ScriptBlock can be multiline as though you were defining a function. way. The examples above are single line due to their simplicity.
A ScriptBlock is just an unnamed function, which is why defining parameters and referencing arguments within one works the same way.
To exemplify this behavior outside of powershell.exe -Command, Invoke-Command requires you to pass variables to its ScriptBlock in a similar fashion. Note however that answer uses an already-defined function body as the ScriptBlock (which is totally valid to do)
You don't need to use Start-Process here (start is its alias), at least as demonstrated in your example. You can simply use the call operator & unless you need to do something more complex than "run the program and wait for it to finish". See this answer of mine for more information.
If you opt to pass a string to powershell.exe instead, you don't need to provide arguments and your variables will get rendered in the current PowerShell process. However, so will any other unescaped variables that might be intended to set within the child process, so be careful with this approach. Personally, I prefer using ScriptBlock regardless, and just deal with the extra parameter definition and arguments.
Using the call & operator is optional when you are not executing a path rendered as a string. It can be omitted in the examples above, but is more useful like so:
& "C:\The\Program Path\Contains\spaces.exe"
& $programPathAsAVariable

Powershell script parameter as variable name

I'm quite new to Powershell scripting and I have hit a bump where I obviously don't know how to ask Google the right question.
I am writing a script to be called from a system that only allows me to add a single parameter to the command line, but I in fact need more values to execute the script.
My idea is to build a variable for each possible parameter, and then use the variable going forward (Simplified):
$name1= "value1","value2","value3"
$name2= "value4","value5","value6"
$name3= "value7","vlaue8","value9"
foreach ($value in $nameX) { }
and then call the script like: script.ps1 nameX
But how to convert the parameter into the name of the corresponding variable?
Or are there easier ways...?
You should be able to solve your problem with the Get-Variable cmdlet:
# The one and only argument passed: the name of a variable defined inside
# the script; e.g., 'name1'
$variableName = $args[0]
# Define the variables that the argument can refer to:
$name1= "value1","value2","value3"
$name2= "value4","value5","value6"
$name3= "value7","vlaue8","value9"
# Use Get-Variable to get a variable's value by name.
# (Error handling omitted for brevity.)
foreach ($value in (Get-Variable $variableName -ValueOnly)) {
# ...
}

Why does PowerShell code behave different when called from a function?

Experienced C# developer learning PowerShell here so I am sure I am just missing something silly. What I am trying to do is write a function that simply writes it's input to a temporary file in JSON format. I have code that works fine if I run it 'inline', but that same code writes an empty file when called in a function.
Here is the code:
function Dump-File {
param (
[Parameter(Mandatory=$true)]
$Input
)
$tmp = New-TemporaryFile
$Input | ConvertTo-Json | Out-File $tmp.FullName
Write-Output "Dump file written: $($tmp.FullName)"
}
$args = #{}
$args.Add('a', 1)
$args.Add('b', 2)
$args.Add('c', 3)
$args.Add('d', 4)
# results in json written to temp file
$tmp = New-TemporaryFile
$args | ConvertTo-Json | Out-File $tmp.FullName
Write-Output "args dumped: $($tmp.FullName)"
# results in empty temp file
Dump-File $args
Can anyone help me understand why the code called inline works but the same code does not work when I wrap it up as a function?
$Input is an automatic variable.
Changing the name of your Dump-File parameter to $somethingelse will resolve your problem. Never use $input as a parameter or variable name.
Automatic variables are to be considered read-only.
About Automatic Variables
SHORT DESCRIPTION
Describes variables that store state information for
PowerShell. These variables are created and maintained by PowerShell.
LONG DESCRIPTION
Conceptually, these variables are considered to be
read-only. Even though they can be written to, for backward
compatibility they should not be written to.
Here is a list of the automatic variables in PowerShell:
...
$INPUT
Contains an enumerator that enumerates all input that is passed to a function. The $input variable is available only to functions and script blocks (which are unnamed functions). In the Process block of a function, the $input variable enumerates the object that is currently in the pipeline. When the Process block completes, there are no objects left in the pipeline, so the $input variable enumerates an empty collection. If the function does not have a Process block, then in the End block, the $input variable enumerates the collection of all input to the function.
Source: About_Automatic_Variables
This information is also avaible through Get-help command
Get-Help about_Automatic_Variables

Wrapper function in PowerShell: Pass remaining parameters

I’m trying to write a wrapper function in PowerShell that basically evaluates the first parameter and based on that runs a program on the computer. All the remaining parameters to the wrapper function should then be passed to the program that is ran as well.
So it should look something like this:
function test ( [string] $option )
{
if ( $option -eq 'A' )
{
Write-Host $args
}
elseif ( $option -eq 'B' )
{
. 'C:\Program Files\some\program.exe' $args
}
}
Now just adding $args does not work, so what do I have to do to make it work? Another option would probably be using Invoke-Expression, but it feels a bit like eval so I want to avoid if possible, and in addition I think doing it like that would limit me to string-only parameters right? If possible I would want to have the full support for the wrapped program/cmdlet - basically like a dynamic alias. Is that even possible?
This sort of does what you ask. You may run into trouble if you need to pass dash-prefixed options to the executable that conflict or cause ambiguity with the PowerShell common parameters. But this may get you started.
function Invoke-MyProgram
{
[CmdletBinding()]
Param
(
[parameter(mandatory=$true, position=0)][string]$Option,
[parameter(mandatory=$false, position=1, ValueFromRemainingArguments=$true)]$Remaining
)
if ($Option -eq 'A')
{
Write-Host $Remaining
}
elseif ($Option -eq 'B')
{
& 'C:\Program Files\some\program.exe' #Remaining # NOTE: # not $ (splatting)
}
}
What you have written does work. Note that what is there is $args is the unnamed arguments that are over and above the parameters expected by the function.
So if you call test as
test -option "A" 1 2 3
$args will have 1,2,3
Note that if you call test as
test -option "A" -other "B" 1 2 3
$args will have -other,B,1,2,3
Your solution works as-is for external programs (such as your C:\Program Files\some\program.exe example): you can always pass an array of values (which is what $args is) to an external program, and its elements will be passed as individual arguments (stringified, if necessary).
You can make your solution work with any command if you change $args to #args[1], to take advantage of a PowerShell parameter-passing technique called splatting:
function test ( [string] $option )
{
if ( $option -eq 'A' )
{
Write-Host $args
}
elseif ( $option -eq 'B' )
{
# Use #args to also support passing *named* arguments
# through to *PowerShell* commands.
& $someCommand #args
}
}
Caveats:
The automatic $args variable, which collects all arguments for which no parameter was declared, is only available in simple (non-advanced) functions and scripts; advanced functions and scripts - those that use the [CmdletBinding()] attribute and/or [Parameter()] attributes - require that all potential parameters be declared.
PowerShell has built-in magic that makes the automatic array variable $args also support passing named parameters through via splatting, which no custom array or collection supports.
By contrast, custom arrays and collections only support splatting positional (unnamed) arguments, which, however, covers all calls to external programs.
When calling PowerShell commands, this limitation is problematic, however: For instance, if you wanted to pass the named argument -Path C:\ through to the Set-Location cmdlet via splatting, using a custom collection parameter declared via ValueFromRemaining Arguments, as shown in OldFart's answer (Set-Location #Remaining), would not work; to support passing through named arguments (other than via #args, if available), you must use hashtable-based splatting.
Therefore, if your function is an advanced one and you need to support passing named arguments through to other PowerShell commands, a different approach is required: this answer shows two alternatives.
[1] With external programs, there is a corner case where #args behaves differently from $args, namely if the $args array contains --%, the stop-parsing symbol: #args recognizes it, $args treats it as a literal.