There's a few commands I need to run repetitively in Powershell, with some variable arguments in content and length, but some arguments must always be there. I don't want to forget those arguments, so is there a way to create a function that does this?
I have tried using things like Invoke-Expression, but when I use brackets in a command, Powershell thinks it's a type and tells me my cast is not valid. If I escape the argument list and provide an argument in the form Key=Value, PowerShell parses it into a System.Object[] and the command fails. I figured it might be better not to ask about how to fix that, but how to solve my root problem.
You can consider this a somewhat duplicate of How to always append an ampersand for certain commands? (MacOS / bash) but for PowerShell.
Just write your own version of the command as a Function with mandatory parameters:
Function RunMyCommand {
Param(
[Parameter(Position=0,mandatory=$true)][string]$Argument1,
[Parameter(Position=1,mandatory=$true)][int]$Argument2,
[Parameter(Position=2,mandatory=$true)]$Argument3
)
# Amend this according to the syntax and string manipulation required for your command
ThisIsMyCommand $Argument1 $Argument2 $Argument3
}
RunMyCommand -Argument1 ThisString -Argument2 ThisNumber -Argument3 ThisAnything
Related
Here is the command that works in command prompt.
C:\Temp\Agent.exe CustomerId={9c0-4ab1-123-102423a} ActivationId={9c0-4ab1-123-102423a} WebServiceUri=https://Agent/
Here is the error. (I have tried invoke-command and arguments but I think the { is causing issues.
Error:
Agent.exe: The command parameter was already specified.
You are certainly not required to use Start-Process (although it may "work," with some limitations, in some scenarios). The simplest and most straightforward answer is to quote the arguments:
C:\Temp\Agent.exe 'CustomerId={9c0-4ab1-123-102423a}' 'ActivationId={9c0-4ab1-123-102423a}' 'WebServiceUri=https://Agent/'
If the executable you want to run is in a path that contains spaces (or the executable filename itself contains spaces), quote the command and use the & (call/invocation) operator; e.g.:
& 'C:\Temp Dir\Agent.exe' 'CustomerId={9c0-4ab1-123-102423a}' 'ActivationId={9c0-4ab1-123-102423a}' 'WebServiceUri=https://Agent/'
Remarks:
If you need string interpolation (i.e., automatic expansion of $variable names inside strings), then use " instead of ' as your quote character. Use ' instead of " (as in the examples above) to prevent string interpolation.
Parameter quoting in this case is required because the { and } symbols have special meaning in PowerShell.
The proper way to run external programs is to use Start-Process. It gives you a couple of additional options like a separate ArgumentList parameter, running-as another user, or redirecting outputs:
Start-Process -FilePath 'C:\Temp\Agent.exe' -ArgumentList #(
# Arguments are space-separated when run. You could also just use one big string.
'CustomerId={9c0-4ab1-123-102423a}',
'ActivationId={9c0-4ab1-123-102423a}',
'WebServiceUri=https://Agent/'
)
I know I can dot source a file:
. .\MyFunctions.ps1
But, I would like to dot source the commands in a string variable:
. $myFuctions
I see that this is possible:
.{$x=2}
And $x equals 2 after the script block is sourced.
But... .{$myFunctions} does not work.
I tried $myFunctions | Invoke-Expression, but it doesn't keep the source function in the current scope. The closest I have been able to come up with is to write the variable to a temporary file, dot source the file, and then remove the file.
Inevitably, someone will ask: "What are you trying to do?" So here is my use case:
I want to obfuscate some functions I intend to call from another script. I don't want to obfuscate the master script, just my additional functions. I have a user base that will need to adjust the master script to their network, directory structure and other local factors, but I don't want certain functions modified. I would also like to protect the source code. So, an alternate question would be: What are some good ways to protect PowerShell script code?
I started with the idea that PowerShell will execute a Base64-encoded string, but only when passed on the command line with -EncodedCommand.
I first wanted to dot source an encoded command, but I couldn't figure that out. I then decided that it would be "obfuscated" enough for my purposes if I converted by Base64 file into a decode string and dot sourced the value of the string variable. However, without writing the decoded source to a file, I cannot figure out how to dot source it.
It would satisfy my needs if I could Import-Module -EncodedCommand .\MyEncodedFile.dat
Actually, there is a way to achieve that and you were almost there.
First, as you already stated, the source or dot operator works either by providing a path (as string) or a script block. See also: . (source or dot operator).
So, when trying to dot-source a string variable, PowerShell thinks it is a path. But, thanks to the possibility of dot-sourcing script blocks, you could do the following:
# Make sure everything is properly escaped.
$MyFunctions = "function Test-DotSourcing { Write-Host `"Worked`" }"
. { Invoke-Expression $MyFunctions }
Test-DotSourcing
And you successfully dot-sourced your functions from a string variable!
Explanation:
With Invoke-Expression the string is evaluated and run in the child scope (script block).
Then with . the evaluated expressions are added to the current scope.
See also:
Invoke-Expression
About scopes
While #dwettstein's answer is a viable approach using Invoke-Expression to handle the fact that the function is stored as a string, there are other approaches that seem to achieve the same result below.
One thing I'm not crystal clear on is the scoping itself, Invoke-Expression doesn't create a new scope so there isn't exactly a need to dot source at that point...
#Define your function as a string
PS> $MyUselessFunction = "function Test-WriteSomething { 'It works!' }"
#Invoke-Expression would let you use the function
PS> Invoke-Expression $MyUselessFunction
PS> Test-WriteSomething
It works!
#Dot sourcing works fine if you use a script block
PS> $ScriptBlock = [ScriptBlock]::Create($MyUselessFunction)
PS> . $ScriptBlock
PS> Test-WriteSomething
It works!
#Or just create the function as a script block initially
PS> $MyUselessFunction = {function Test-WriteSomething { 'It works!' }}
PS> . $MyUselessFunction
PS> Test-WriteSomething
It works!
In other words, there are probably a myriad of ways to get something similar to what you want - some of them documented, and some of them divined from the existing documentation. If your functions are defined as strings, then Invoke-Expression might be needed, or you can convert them into script blocks and dot source them.
At this time it is not possible to dot source a string variable.
I stand corrected! . { Invoke-Expression $MyFunctions } definitely works!
I downloaded the npm package for merge junit reports - https://www.npmjs.com/package/junit-merge.
The problem is that I have multiple files to merge and I am trying to use string variable to hold file names to merge.
When I write the script myslef like:
junit-merge a.xml b.xml c.xml
This works, the merged file is being created, but when I do it like
$command = "a.xml b.xml c.xml"
junit-merge $command
This does not work. The error is
Error: File not found
Has anyone faced similar issues?
# WRONG
$command = "a.xml b.xml c.xml"; junit-merge $command
results in command line junit-merge "a.xml b.xml c.xml"[1], i.e. it passes a string with verbatim value a.xml b.xml c.xml as a single argument to junit-merge, which is not the intent.
PowerShell does not act like POSIX-like shells such as bash do in this regard: In bash, the value of variable $command - due to being referenced unquoted - would be subject to word splitting (one of the so-called shell expansions) and would indeed result in 3 distinct arguments (though even there an array-based invocation would be preferable).
PowerShell supports no bash-like shell expansions[2]; it has different, generally more flexible constructs, such as the splatting technique discussed below.
Instead, define your arguments as individual elements of an array, as justnotme advises:
# Define the *array* of *individual* arguments.
$command = "a.xml", "b.xml", "c.xml"
# Pass the array to junit-merge, which causes PowerShell
# to pass its elements as *individual arguments*; it is the equivalent of:
# junit-merge a.xml b.xml c.xml
junit-merge $command
This is an application of a PowerShell technique called splatting, where you specify arguments to pass to a command via a variable:
Either (typically only used for external programs, as in your case):
As an array of arguments to pass individually as positional arguments, as shown above.
Or (more typically when calling PowerShell commands):
As a hashtable to pass named parameter values, in which you must replace the $ sigil in the variable reference with #; e.g., in your case #command; e.g., the following is the equivalent of calling Get-ChildItem C:\ -Directory:
$paramVals = #{ LiteralPath = 'C:\'; Directory = $true }; Get-ChildItem #paramVals
Caveat re array-based splatting:
Due to a bug detailed in GitHub issue #6280, PowerShell doesn't pass empty arguments through to external programs (applies to all Windows PowerShell versions / and as of PowerShell (Core) 7.2.x; a fix may be coming in 7.3, via the $PSNativeCommandArgumentPassing preference variable, which in 7.2.x relies on an explicitly activated experimental feature).
E.g., foo.exe "" unexpectedly results in just foo.exe being called.
This problem equally affects array-based splatting, so that
$cmdArgs = "", "other"; foo.exe $cmdArgs results in foo.exe other rather than the expected foo.exe "" other.
Optional use of # with array-based splatting:
You can use the # sigil also with arrays, so this would work too:
junit-merge #command
There is a subtle distinction, however.
While it will rarely matter in practice,
the safer choice is to use $, because it guards against (the however hypothetical) accidental misinterpretation of a --% array element you intend to be a literal.
Only the # syntax recognizes an array element --% as the special stop-parsing symbol, --%
Said symbol tells PowerShell not to parse the remaining arguments as it normally would and instead pass them through as-is - unexpanded, except for expanding cmd.exe-style variable references such as %USERNAME%.
This is normally only useful when not using splatting, typically in the context of being able to use command lines that were written for cmd.exe from PowerShell as-is, without having to account for PowerShell's syntactical differences.
In the context of splatting, however, the behavior resulting from --% is non-obvious and best avoided:
As in direct argument passing, the --% is removed from the resulting command line.
Argument boundaries are lost, so that a single array element foo bar, which normally gets placed as "foo bar" on the command line, is placed as foo bar, i.e. effectively as 2 arguments.
[1] Your call implies the intent to pass the value of variable $command as a single argument, so when PowerShell builds the command line behind the scenes, it double-quotes the verbatim a.xml b.xml c.xml string contained in $command to ensure that. Note that these double quotes are unrelated to how you originally assigned a value to $command.
Unfortunately, this automatic quoting is broken for values with embedded " chars. - see this answer, for instance.
[2] As a nod to POSIX-like shells, PowerShell does perform one kind of shell expansion, but (a) only on Unix-like platforms (macOS, Linux) and (b) only when calling external programs: Unquoted wildcard patterns such as *.txt are indeed expanded to their matching filenames when you call an external program (e.g., /bin/echo *.txt), which is feature that PowerShell calls native globbing.
I had a similar problem. This technique from powershell worked for me:
Invoke-Expression "junit-merge $command"
I also tried the following (from a powershell script) and it works:
cmd / c "junit-merge $command"
Recently I've started fiddling around with PowerShell, and I bumped into an issue with running .jar files. Simply put, I'm using plantuml and I'd like to simply have a "plantuml" command to run it. Normally, running the program would be done by typing. java -jar C:\Users\Name\Documents\WindowsPowerShell\Commands\plantuml.jar. This is of course quite a handful, and I'd like to shorten this to simply plantuml.
My current work-around is the following function:
function plantuml($UmlPath, $ImgPath) {
java -jar C:\Users\Name\Documents\WindowsPowerShell\Commands\plantuml.jar $UmlPath $ImgPath
}
However, I cannot pass any parameters to the .jar file like this, because Powershell intercepts them and interprets them as function parameters. A current workaround for this is by wrapping them in quotation marks, but I find this ugly and I often forget.
Is there any way to simply be able to type plantuml so that PowerShell expands it to java -jar C:\Users\Name\Documents\WindowsPowerShell\Commands\plantuml.jar? The only similar question I found was this one, but it doesn't appear to have an actual answer.
I don't have plantuml or anything similar to test with, but you can get all the parameters passed to a function with the $args variable, so this approach might work:
function plantuml {
# Array of your default arguments to Java.exe to start plantuml
$arguments = #('-jar',
'C:\Users\Name\Documents\WindowsPowerShell\Commands\plantuml.jar')
# All arguments passed to this function, umlpath, imgpath, and anything else
# are in the special variable $args, add those into the array as well.
$arguments += $args
Start-Process Java -ArgumentList $arguments
}
I want to call an executable from a PowerShell script that requires quotes at specific positions in the argument list. Although I found similar questions I did not find a solution at all.
This is what the command must look like on the command line:
reptool.exe --profile="C:\My profile"
The parameter value ("C:\Profiles...") is supposed to be generated dynamically using a variable:
$repToolProfile = "C:\My profile"
This is what I have already tried:
&"reptool.exe" --profile=$repToolProfile
Fails as the argument is given as "--profile=C:\My profile" (quotes around the whole argument).
&"reptool.exe" --profile="$repToolProfile"
Fails as the argument is given as "--profile=C:\My profile" (quotes around the whole argument, same as above).
&"reptool.exe" "--profile=`"$repToolProfile`"
Fails as the argument is given as "--profile="C:\My profile"" (quotes around the whole argument and the value).
I cannot use single quotes or the "verbatim operator" (--%) as I have to use a PowerShell variable, neither I can use Start-Process as it is called asynchroneously (even when I use the -Wait parameter. Also I want to check the exit code. I don't want to convert my arguments to Base64.
This worked for me:
$command = '& "reptool.exe" --% ' + "--profile=`"$repToolProfile`"
Invoke-Expression $command