Unable to properly pass arguments with spaces to a PowerShell script from Jenkins - powershell

I have a stage in a Jenkins pipeline where I call a PowerShell script which I do within a container and I call it like this:
stage('Processing') {
container('remote') {
sh "pwsh -file script.ps1 ${params.NAME} ${params.DESCRIPTION} ${params.PEOPLE}
}
}
Within the script I do some preparation for a remote session and I call it using this command
Invoke-Command -Session $RemoteSession -ArgumentList $parameters -ScriptBlock $ScriptBlock
The mentioned preparation is basically me adding another parameter to args which I do like this
$parameters = #()
$parameters = $parameters + $args
$parameters += $var
Within the scriptblock I reference the args by their index like $args[1]. These are primarily strings, and everything works when an argument passed from Jenkins has no spaces in it. But when let's say ${params.NAME} has a space in it, but the indexing does not work correctly as spaces separate the original string into multiple arguments, hence the if the original index was $args[1], instead of taking in the value of ${params.DESCRIPTION}, it takes in a part of ${params.NAME}.
Do you know how to avoid this issue and take in the parameters with the original indexing even if it has spaces in it?

To make PowerShell see the expanded values such as ${params.NAME} as a single argument each, enclose them in ", which in the context of the "..." Groovy string must be escaped as \"
Therefore:
sh "pwsh -File script.ps1 \"${params.NAME}\" \"${params.DESCRIPTION}\" \"${params.PEOPLE}\""

Related

Is there a dynamic variable for passed arguments in powershell?

In batch, passed arguments can be used with %1, and onward counting.
Lets say I have the following "batch.bat" script:
# echo off
echo %1
pause>nul
If i call this from cmd like: call batch.bat hello it would output "hello" in the console.
Is there any variable in ps which does the same thing?
EDIT
I've found the folliwing, but it seems kind of unnatural.
$CommandLine = "-File `"" + $MyInvocation.MyCommand.Path + "`" " + $MyInvocation.UnboundArguments
Start-Process -FilePath PowerShell.exe -Verb Runas -ArgumentList $CommandLine
Exit
}
Is there something more elegant perhaps?
PowerShell has an automatic variable $args that stores all arguments passed to a script (unless parameters were defined for the script). The individual arguments can be accessed by index ($args[0] for the first argument, $args[1] for the second, etc.).
However, in general it's advisable to define parameters to control what arguments a script should accept, e.g.
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]$First,
[Parameter(Mandatory=$false)]
[integer]$Second = 42
)
There are numerous advantages to this, including (but not limited to):
arguments are parsed automatically and the values are stored in the respective variables
scripts automatically prompt for mandatory parameters
scripts throw an error if incorrect arguments are passed
you can define default values for optional parameters
you can have your script or function accept pipeline input
you can validate parameter values
you can use comment-based help for documenting the parameters and their usage

Run Powershell commands sequentially in their own windows and prevent them from exiting

I want to run multiple Powershell commands sequentially in their own Powershell windows and do not want those windows to be closed after running.
Example:
Start-Process powershell {Write-Host "hello"}; Start-Process powershell
{Write-Host "hello"}; Start-Process powershell {Write-Host "hello"}
Powershell windows get closed right after running. I want them to remain open.
Edit: Multiple commands are not always same and they may vary in number.
# Asynchronously starts 3 new PowerShell windows that
# print "hello #<n>" to the console and stay open.
1..3 | ForEach-Object {
Start-Process powershell -Args '-noexit', '-command', "Write-Host 'hello #$_'"
}
-noexit is required to keep a PowerShell session open after executing a command with -command (run powershell.exe -? to see all CLI parameters)
Note how the arguments are specified individually, as ,-separated elements of an array that is passed to
-Args (short for -ArgumentList, though the parameter name can be omitted altogether in this case).
Note how the Write-Host command is passed as a string - script blocks aren't supported as such in this scenario; you can pass one, as you tried, but it will be quietly converted to a string, which simply means that its literal content is used (everything between { and }).
In other words: passing {Write-Host "hello"} is the same as 'Write-Host "hello"', but to avoid confusion you should pass a string.
You can only pass a script block as such if you invoke powershell.exe directly, not via Start-Process; you need Start-Process, however, to run the new session in a new window and to start it asynchronously.
Also, the string was changed to a double-quoted string ("...") with embedded single-quoting ('...') to ensure that the reference to $_ - the automatic variable representing the pipeline object at hand (1, 2, or 3) - is expanded (interpolated).
Using the pipeline (|) with an array of inputs (1..3, which evaluates to array 1, 2, 3) with the ForEach-Object cmdlet is just an example - you can still invoke the individual commands individually, one after the other, on individual lines, or separated with ; - thanks to Start-Process they'll still launch asynchronously.
However, if the individual commands share logic, the pipeline approach can simplify matters; you can put the shared logic in the body of the ForEach-Object call and pass the variable parts as input via the pipeline.
Put a read-host at the end of the command sequence - it will wait for you to input something before continuing execution (and presumably exiting?). To copy/paste the example in this link, you could anything like this which will pause execution until you enter something: $Age = Read-Host "Please enter your age" -> Ref: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/read-host?view=powershell-6

Can't pass a script block as a parameter to powershell.exe via -Command

I'm trying this
$Global:commandBlock={
Start-Transcript -path $projectFolder\gruntLog.txt;
grunt $argList;
Stop-Transcript
}
$cmdProc=start-process powershell -ArgumentList ('-command `$Global:commandBlock') -WorkingDirectory $fwd -PassThru -NoNewWindow:$NoNewWindow
And keep getting $commandBlock : The term '$Global:commandBlock' is not recognized as the name of a cmdlet, function, script file, or operable program.
My guess was it has to do with scope. But making variable global didn't help. Adding -args $commandBlock like that:
-ArgumentList ('-command `$Global:commandBlock -args "-commandBlock:$commandBlock"')
-ArgumentList ('-command `$Global:commandBlock -args $commandBlock"')
didn't help
And I'm not sure that I escape variables correctly in the block, read this, but not sure how to apply to my script.
There's a few things which I think are keeping this from working. First, when you're using single quotes, ' you're instructing PowerShell to operate literally. This means that it won't expand variables. Not what you're looking for.
A better way to do this is to do it with an subexpression like this.
$Global:commandBlock={
'ham' >> C:\temp\test.txt
}
$cmdProc=start-process powershell -ArgumentList ("-command $($Global:commandBlock)") -PassThru -NoNewWindow:$NoNewWindow
This will give you the desired results.
Subexpressions are pretty sweet. It lets you embed a mini-scriptblock within a string, and it's then expanded out in the parent string.
"today's date is $(get-date), on system: $($env:COMPUTERNAME)"
today's date is 02/14/2017 11:50:49, on system: BEHEMOTH
There are two major issues (leaving the obvious mistake of attempting to reference a variable inside a single-quoted string aside):
Any argument you want to pass to a new powershell instance via -Command must be escaped in non-obvious ways if it contains " and/or \ chars, which is especially likely if you're passing a piece of PowerShell source code.
The escaping issue can generally be solved by Base64-encoding the source-code string and passing it via the -EncodedCommand parameter - see this answer of mine to a related question for how to do that, but a more concise alternative is presented below.
If the source code being passed references any variables that only exist in the calling session, the new instance won't see them.
The solution is not to reference session-specific variables in the source code being passed, but to pass their values as parameter values instead.
To solve the local-variable-not-seen-by-the-new-instance problem, we must rewrite the script block to accept parameters:
$scriptBlock={
param($projectFolder, $argList)
# For demonstration, simply *output* the parameter values.
"folder: [$projectFolder]; arguments: [$argList]"
}
Now we can apply the necessary escaping, using PetSerAl's sophisticated -replace expression from his comment on the question.
We can then invoke the resulting string with & {...} while passing it parameter values (I'm omitting the -WorkingDirectory and -PassThru parameters for brevity):
# Parameter values to pass.
$projectFolder = 'c:\temp'
$argList='-v -f'
Start-Process -NoNewWindow powershell -ArgumentList '-noprofile', '-command',
(('& {' + $scriptBlock.ToString() + '}') -replace '\"|\\(?=\\*("|$))', '\$&'),
"'$projectFolder'",
"'$argList'"
For an explanation of the regular expression, again see this answer.
Note how the variable values passed as parameters to the script block are enclosed in '...' inside a "..."-enclosed string in order to:
pass the values as a single parameter value.
protect them from another round of interpretation by PowerShell.
Note: If your variable values have embedded ' instances, you'll have to escape them as ''.
The above yields:
folder: [c:\temp]; arguments: [-v -f]
Alternative with a temporary, self-deleting script file:
Using -File with a script file has the advantage of being able to pass parameter values as literals, with no concern over additional interpretation of their contents.
Caveat: As of PowerShell Core v6-beta.3, there is a problem when passing parameter values that start with -: they are not bound as expected; see this GitHub issue.
To work around this problem, the sample script block below accesses only the first parameter by name, and relies on all remaining ones binding via the automatic $Args variable.
# Define the script block to be executed by the new PowerShell instance.
$scriptBlock={
param($projectFolder)
# For demonstration, simply *output* the parameter values.
"folder: [$projectFolder]; arguments: [$Args]"
}
# Parameter values to pass.
$projectFolder = 'c:\temp'
$argList='-v -f'
# Determine the temporary script path.
$tempScript = "$env:TEMP\temp-$PID.ps1"
# Create the script from the script block and append the self-removal command.
# Note that simply referencing the script-block variable inside `"..."`
# expands to the script block's *literal* content (excluding the enclosing {...})
"$scriptBlock; Remove-Item `$PSCommandPath" > $tempScript
# Now invoke the temporary script file, passing the arguments as literals.
Start-Process -NoNewWindow powershell -ArgumentList '-NoProfile', '-File', $tempScript,
$projectFolder,
$argList
Again, the above yields:
folder: [c:\temp]; arguments: [-v -f]
I've messed around with the syntax for passing args to a new powershell instance and have found the following works. So many variations fail without a good error message. Maybe it would work in your case?
$arg = "HAM"
$command = {param($ham) write-host $ham}
#please not its important to wrap your command
#in a further script block to stop it being processed to a string at execution
#The following would normally suffice "& $command $arg"
Start-Process powershell -ArgumentList "-noexit -command & {$command} $arg"
Also simply using the Invoke-Command gives you the -ArgumentList parameter to opperate against the given Command that you are missing with the standard powershell.exe parameters. This is probably a bit cleaner looking.
Start-Process powershell -ArgumentList "-noexit -command invoke-command -scriptblock {$command} -argumentlist $arg"
No need for any extra complex escaping or unwanted persisted variables. Just keep the script block in curly braces so it remains a script block on arrival in the new session. At least in this simple case...
If you have several string parameters that contain spaces. I found popping the string in a single parenthesis and separating with commas works well. You could also probably pass a predefined array as a single argument.
Start-Process powershell -ArgumentList "-noexit -command invoke-command -scriptblock {$command} -argumentlist '$arg1', '$arg2', '$arg3'"
Will this work:
$Global:commandBlock={
Start-Transcript -path $projectFolder\gruntLog.txt;
grunt $argList;
Stop-Transcript
}
& $Global:commandBlock

Pass parameter from a batch file to a PowerShell script

In my batch file, I call the PowerShell script like this:
powershell.exe "& "G:\Karan\PowerShell_Scripts\START_DEV.ps1"
Now, I want to pass a string parameter to START_DEV.ps1. Let's say the parameter is w=Dev.
How can I do this?
Let's say you would like to pass the string Dev as a parameter, from your batch file:
powershell -command "G:\Karan\PowerShell_Scripts\START_DEV.ps1 Dev"
put inside your powershell script head:
$w = $args[0] # $w would be set to "Dev"
This if you want to use the built-in variable $args. Otherwise:
powershell -command "G:\Karan\PowerShell_Scripts\START_DEV.ps1 -Environment \"Dev\""
and inside your powershell script head:
param([string]$Environment)
This if you want a named parameter.
You might also be interested in returning the error level:
powershell -command "G:\Karan\PowerShell_Scripts\START_DEV.ps1 Dev; exit $LASTEXITCODE"
The error level will be available inside the batch file as %errorlevel%.
Assuming your script is something like the below snippet and named testargs.ps1
param ([string]$w)
Write-Output $w
You can call this at the commandline as:
PowerShell.Exe -File C:\scripts\testargs.ps1 "Test String"
This will print "Test String" (w/o quotes) at the console. "Test String" becomes the value of $w in the script.
When a script is loaded, any parameters that are passed are automatically loaded into a special variables $args. You can reference that in your script without first declaring it.
As an example, create a file called test.ps1 and simply have the variable $args on a line by itself. Invoking the script like this, generates the following output:
PowerShell.exe -File test.ps1 a b c "Easy as one, two, three"
a
b
c
Easy as one, two, three
As a general recommendation, when invoking a script by calling PowerShell directly I would suggest using the -File option rather than implicitly invoking it with the & - it can make the command line a bit cleaner, particularly if you need to deal with nested quotes.
Add the parameter declaration at the top of ps1 file
test.ps1
param(
# Our preferred encoding
[parameter(Mandatory=$false)]
[ValidateSet("UTF8","Unicode","UTF7","ASCII","UTF32","BigEndianUnicode")]
[string]$Encoding = "UTF8"
)
write ("Encoding : {0}" -f $Encoding)
Result
C:\temp> .\test.ps1 -Encoding ASCII
Encoding : ASCII
The answer from #Emiliano is excellent. You can also pass named parameters like so:
powershell.exe -Command 'G:\Karan\PowerShell_Scripts\START_DEV.ps1' -NamedParam1 "SomeDataA" -NamedParam2 "SomeData2"
Note the parameters are outside the command call, and you'll use:
[parameter(Mandatory=$false)]
[string]$NamedParam1,
[parameter(Mandatory=$false)]
[string]$NamedParam2

Executing powershell.exe from powershell script (run in ISE but not in script)

I'm new to these awesome Power shell world. I have a problem with script and really apreciate your help.
I have a script "cmd4.ps1" that needs to run another script "Transfer.ps1" that needs to receive 3 strings params and it needs to be run as other process thead different to "cmd4.ps1".
cmd4.ps1:
$Script="-File """+$LocalDir+"\Remote\Transfer.ps1"" http://"+$ServerIP+"/Upload/"+$FileName+" "+$ServerIP+" "+$LocalIP
Start-Process powershell.exe -ArgumentList $Script
After ejecution, the $Script cointain a value similar to
-File "c:\temp re\Remote\Transfer.ps1" http://10.1.1.1/Upload/file.txt 10.1.1.1 10.1.1.10
containing the syntax to use -File parameter to run a script of Powershell.exe, and three parameters that Transfer.ps1 needs ("http://10.1.1.1/Upload/file.txt", 10.1.1.1, 10.1.1.10).
When I write these instructions in PowerShell ISE I can see every values are right and PowerShell.exe is executed with right values, everything work fine!, but if I put these instructions inside "cmd4.ps1" script it doesn't work, I mean something is not right with parameters because I can see it start powershell but it never ends.
-ArgumentList is expecting an array of string arguments instead of a single string. Try this instead:
$ScriptArgs = #(
'-File'
"$LocalDir\Remote\Transfer.ps1"
"http://$ServerIP/Upload/$FileName $ServerIP $LocalIP"
)
Start-Process powershell.exe -ArgumentList $ScriptArgs
Note that you can simplify the string construction of the args as shown above.
Why don't you put this in cmd4.ps1?
& "c:\temp re\Remote\Transfer.ps1" "http://10.1.1.1/Upload/file.txt" "10.1.1.1" "10.1.1.10"