I want to execute an external program like so
& $exe $arguments
$arguments is string with spaces, lets say its a b c d
What happens is that the argument passed to the exe is
"a b c d"
with double quotes around the whole thing. It does not happen when $arguments contains no spaces.
How can I prevent powershell from trying to be smart and stop it from wrapping my string in double quotes? This is a ridiculous to assume that everything with spaces must be a path.
EDIT: Since a fix apparantly does not exist, I did work around by converting every bit into an array, and it does work in PS 5. However PS4 is being ****...
What I want is to have a command line argument which looks like
-filter:"+[RE*]* +[RR*]*"
PS 4 puts double quotes around the whole thing too:
$filter = "+[RE*]*", "+[RR*]*"
& $opencover -target:"$xunit" "-targetargs:$allPaths $traitsCmd -quiet -nunit xunit-$results -noshadow" -register:user -filter:"$filter" -output:opencover-$results
If I replace -filter: with '-filter:' I end up with a space between -filter: and the contents of the $filter array. Whatever I do I can't get rid of it, without wrapping the whole thing in doublequotes like "-filter:..."
You should make an array instead of trying.
So in your case:
$exe = "ping.exe"
$arguments = "-t","8.8.8.8"
& $exe $arguments
Please note that $args is a special value and cannot be assigned to anything. That is why I'm using $arguments in my example.
The answer to your question is then this:
$arguments = "a","b","c","d"
& $exe $arguments
I strongly recommend using Start-Process instead of using the . and & call operators, for the purpose of calling external executables from PowerShell.
The Start-Process command is much more predictable, because you are feeding in the path to the executable and command line arguments separately. There isn't any special interpretation (aka. "magic") going on that way.
$MyProcess = Start-Process -FilePath $exe -ArgumentList $arguments -Wait
I've shared similar advice on several other StackOverflow threads.
Powershell "Start-process" not running as scheduled task
Accessing output from Start-Process with -Credential parameter
Split your space-separated string into an array:
$arguments = "a b c d"
$arguments = $arguments -split '\.'
& $exe $arguments
When passing complex arguments to an exe, consider using the stop parsing operator: --%. From that operator to the end of the line, PowerShell parsing "features" go away and the text is parsed very similarly to how CMD would parse the text. In fact, the only way to reference variables is the CMD way %var% e.g:
77> $env:arguments = 'a b c d'
78> echoargs --% %arguments%
Arg 0 is <a>
Arg 1 is <b>
Arg 2 is <c>
Arg 3 is <d>
Command line:
"C:\Users\Keith\Documents\WindowsPowerShell\Modules\Pscx\3.2.2\Apps\EchoArgs.exe" a b c d
Related
I just asked about a problem and the suggested code has some obscure points for me.
Specifically on this line of code
powershell.exe -c "Start-Process -Verb RunAs cmd /k, ('%~f0' -replace '[ &]', '^$0')"
I would like to know what the comma after the cmd /k represents.
What's this? How does it work? Is there a link to the documentation explaining this ?
What's is the $0 in the last part. '^$0')"
The Start-Process call embedded in your Windows PowerShell CLI (powershell.exe) call uses positional parameter binding, for brevity:
cmd, as the first positional argument, binds to the -FilePath parameter, i.e. the name or path of the executable to launch.
/k, (...), as the second positional argument, binds to the -ArgumentList parameter, which accepts an array of arguments to pass the target executable, whose elements are separated with ,
Start-Process then builds a command line behind the scenes, by joining the -FilePath argument and the -ArgumentList array elements with spaces, and launches it.
As an aside:
The way Start-Process builds the command line is actually broken, because no on-demand quoting and escaping is performed - see this answer for background information; in your specific case, however, the problem doesn't surface.
To work around the bug it is actually preferable in general to pass a single string to -ArgumentList that contains all arguments to pass; however, in this case that would have complicated quoting (you'd need an embedded "..." string or an explicit string-concatenation expression), so the simpler solution of enumerating the arguments with , was chosen.
See this answer for how to discover a given cmdlet's positional parameters; in short: invoke it with -? (Start-Process -?) and, in the relevant parameter set, look for the parameters whose names are enclosed in [...]; e.g.
Start-Process [-FilePath] <System.String> [[-ArgumentList] <System.String[]>] ...
I am trying to start an installation via msiexec.
The following parameters are available and if I use this on that way, the installation will be correct.
This is the example from the vendor:
msiexec.exe /i ApexOne.msi MyServer="win2016-x64:8080|4343" MyDomain="Workgroup\Subdomain" /Lv+ "c:\temp\MSI.log"
This is my code:
Start-Process -FilePath "$env:systemroot\system32\msiexec.exe" -ArgumentList #('/i ".\agent.msi" MyServer="servername:8080|4344" MyDomain="999-Test"') -wait
But for automation I have to use the variable $domain to generate the correct domain.
I tried different codes, but nothing helped:
This is the code for my variable $domain
$domain='999-Test'
Example:
Start-Process -FilePath "$env:systemroot\system32\msiexec.exe" -ArgumentList #('/i ".\agent.msi" MyServer="servername:8080|4344" MyDomain=**"$domain"'**) -wait
I tried that also, but I don't know how to fix this failure.
I think the interpration of the variable is false, but I can not fix it.
-ArgumentList #('/i ".\agent.msi"','MyServer="servername:8080|4344"','MyDomain='$domain'')
Maybe someone can help me to fix this error?
Due to a long-standing bug in Start-Process, it is actually preferable to always pass all arguments for the target process encoded in a single string passed to -ArgumentList (-Args), rather than individually, as elements of an array - see this answer.
Doing so requires two things:
To incorporate variable values, you must use an expandable string, i.e. a double-quoted string, "...".
Any embedded quoting inside this overall "..." string must itself use double-quoting only, as CLIs on Windows generally only understand " (double quotes) and not also ' (single quotes) to have syntactic function. To embed " characters in a "..." string, escape them as `" (or as ""; inside a '...' string, you needn't escape them at all).
While this is somewhat cumbersome, it does have the advantage of allowing you to directly control the exact command line that the target process will see - and that enables you satisfy msiexec's picky syntax requirements where PROP="value with spaces" must be passed exactly with this value-only double-quoting (whereas PowerShell - justifiably - turns this argument into "PROP=value with spaces" behind the scenes, when it - of necessity - re-quotes arguments to synthesize the actual command line to use).
Therefore, use the following (using positional argument binding to -FilePath and -ArgumentList, respectively):
Start-Process -Wait $env:systemroot\system32\msiexec.exe "/i .\agent.msi MyServer=`"servername:8080|4344`" MyDomain=`"999-Test`""
Note: Strictly speaking, you do not need the embedded `"...`" quoting in your case, given that your property values have no embedded spaces.
The msiexec.exe program seems to have some peculiarities in its command-line parser. I recommend an approach like the following:
$msiFileName = ".\agent.msi"
$myServer = "servername:8080|4344"
$myDomain = "999-Test"
$argumentList = #(
'/i'
'"{0}"' -f $msiFileName
'MyServer="{0}"' -f $myServer
'MyDomain="{0}"' -f $myDomain
)
$startArgs = #{
"FilePath" = "msiexec.exe"
"ArgumentList" = $argumentList
"Wait" = $true
}
Start-Process #startArgs
This example creates an array of string arguments for the -ArgumentList parameter using single quotes (') and string formatting (-f). In the above example, each element in the $argumentList array is a separate string and contains the following elements:
/i
".\agent.msi"
MyServer="servername:8080|4344"
MyDomain="999-Test"
(Note that in the content of the array we are preserving the " characters.)
Next, we create a hashtable for the Start-Process cmdlet and call it using the # operator. (Creating a hashtable for a cmdlet's parameters is known as splatting and is a convenient way to build a large number of parameters for a cmdlet in a more readable and maintainable way.)
I have this bit of powershell code that gets the FQDN and saves it to a variable...works great:
$FQDN=(Get-WmiObject win32_computersystem).DNSHostName.ToLower()+"."+(Get-WmiObject win32_computersystem).Domain
LogWrite "[+] The system fully qualified hostname has been detected as: $FQDN"
However, when I go to insert the string of the FQDN variable, I can not get it to work correctly. I have tried back ticks, double and single quotes with no success:
Start-Process -Wait $OUTPATH64 -ArgumentList '/s /v"/qn INSTALLDIR=\"C:\Program Files\IBM\WinCollect\" LOG_SOURCE_AUTO_CREATION_ENABLED=TRUE LOG_SOURCE_AUTO_CREATION_PARAMETERS=""Component1.AgentDevice=DeviceWindowsLog&Component1.Action=create&Component1.LogSourceName=REPLACEME&Component1.LogSourceIdentifier=REPLACEME&Component1.Dest.Name=test.domain.com&Component1.Dest.Hostname=test.domain.com&Component1.Dest.Port=514&Component1.Dest.Protocol=TCP&Component1.Log.Security=true&Component1.Log.System=true&Component1.Log.Application=true&Component1.Log.DNS+Server=false&Component1.Log.File+Replication+Service=false&Component1.Log.Directory+Service=false&Component1.RemoteMachinePollInterval=3000&Component1.EventRateTuningProfile=Default+(Endpoint)&Component1.MinLogsToProcessPerPass=100&Component1.MaxLogsToProcessPerPass=150"""'
Where you see the "REPLACEME" string, I need that replaced with the $FQDN variable string. Whenever I use backticks $FQDN or with double quotes"$FQDN" or even single quotes '$FQDN' I get the literal string of "$FQDN" and the snippet of code does not actually do the variable replacement.
What am I missing here? I have also even tried backticks double quotes "$FQDN". I want the REPLACEME string to be replaced with something like the actual hostname + domain like hostname.domain.com or what is set in the $FQDN variable. The ArgumentList quotes need to stay the same as I can only get my command to work correctly by quoting the ArgumentList the way it is starting with a ' and ending with """'. Any help would be greatly appreciated.
This is how it should work using back ticks:
Start-Process -Wait $OUTPATH64 -ArgumentList "/s /v /qn INSTALLDIR=`"C:\Program Files\IBM\WinCollect\`" LOG_SOURCE_AUTO_CREATION_ENABLED=TRUE LOG_SOURCE_AUTO_CREATION_PARAMETERS=`"Component1.AgentDevice=DeviceWindowsLog&Component1.Action=create&Component1.LogSourceName=$FQDN&Component1.LogSourceIdentifier=$FQDN&Component1.Dest.Name=test.domain.com&Component1.Dest.Hostname=test.domain.com&Component1.Dest.Port=514&Component1.Dest.Protocol=TCP&Component1.Log.Security=true&Component1.Log.System=true&Component1.Log.Application=true&Component1.Log.DNS+Server=false&Component1.Log.File+Replication+Service=false&Component1.Log.Directory+Service=false&Component1.RemoteMachinePollInterval=3000&Component1.EventRateTuningProfile=Default+(Endpoint)&Component1.MinLogsToProcessPerPass=100&Component1.MaxLogsToProcessPerPass=150`""
Single-quoted strings ('...') in PowerShell are verbatim (literal) strings in PowerShell - by design, no expansion (interpolation) of embedded variable references or expression is performed.
To get the latter, you must use an expandable string, which is double-quoted ("..."); e.g.,
"...SourceName=$FQDN&Component1... "
Since your string contains embedded " characters, it is simplest to us the here-string variant of an expandable string (#"<newline>...<newline>"#):
Start-Process -Wait $OUTPATH64 -ArgumentList #"
/s /v"/qn INSTALLDIR=\"C:\Program Files\IBM\WinCollect\" LOG_SOURCE_AUTO_CREATION_ENABLED=TRUE LOG_SOURCE_AUTO_CREATION_PARAMETERS=""Component1.AgentDevice=DeviceWindowsLog&Component1.Action=create&Component1.LogSourceName=$FQDN&Component1.LogSourceIdentifier=$FQDN&Component1.Dest.Name=test.domain.com&Component1.Dest.Hostname=test.domain.com&Component1.Dest.Port=514&Component1.Dest.Protocol=TCP&Component1.Log.Security=true&Component1.Log.System=true&Component1.Log.Application=true&Component1.Log.DNS+Server=false&Component1.Log.File+Replication+Service=false&Component1.Log.Directory+Service=false&Component1.RemoteMachinePollInterval=3000&Component1.EventRateTuningProfile=Default+(Endpoint)&Component1.MinLogsToProcessPerPass=100&Component1.MaxLogsToProcessPerPass=150"""
"#
If you used a regular double-quoted string ("..."), you'd have to escape the embedded " characters as `" (or "").
See the bottom section of this answer for more information about PowerShell's string literals; the official help topic is about_Quoting_Rules.
Function Check-PC
{
$PC = Read-Host "PC Name"
If($PC -eq "exit"){EXIT}
Else{
Write-Host "Pinging $PC to confirm status..."
PING -n 1 $PC
}
This is a snippet of a function I have written into a PowerShell script. I would like the function to run in a new instance of PowerShell, not in the main window.
Is it possible to run this in a separate process of PowerShell without writing it as a separate script and calling the script? Something like this:
$x= Start-Process powershell | Check-PC
I need to keep everything in one script if possible.
Note: It is the involvement of Start-Process that complicates the solution significantly - see below. If powershell were invoked directly from PowerShell, you could safely pass a script block as follows:
powershell ${function:Check-PC} # !! Does NOT work with Start-Process
${function:Check-PC} is an instance of variable namespace notation: it returns the function's body as a script block ([scriptblock] instance); it is the more concise and faster equivalent of Get-Content Function:Check-PC.
If you needed to pass (positional-only) arguments to the script block, you'd have to append -Args, followed by the arguments as an array (,-separated).
Start-Process solution with an auxiliary self-deleting temporary file:
See the bottom half of this answer to a related question.
Start-Process solution with -EncodedCommand and Base64 encoding:
Start-Process powershell -args '-noprofile', '-noexit', '-EncodedCommand',
([Convert]::ToBase64String(
[Text.Encoding]::Unicode.GetBytes(
(Get-Command -Type Function Check-PC).Definition
)
))
The new powershell instance will not see your current session's definitions (unless they're defined in your profiles), so you must pass the body of your function to it (the source code to execute).
(Get-Command -Type Function Check-PC).Definition returns the body of your function definition as a string.
The string needs escaping, however, in order to be passed to the new Powershell process unmodified.
" instances embedded in the string are stripped, unless they are either represented as \" or tripled (""").
(\" rather than the usual `" is needed to escape double quotes in this case, because PowerShell expects \" when passing a command to the powershell.exe executable.)
Similarly, if the string as a whole or a double-quoted string inside the function body ends in (a non-empty run of) \, that \ would be interpreted as an escape character, so the \ must be doubled.Thanks, PetSerAl.
The most robust way to bypass these quoting (escaping) headaches is to use a Base64-encoded string in combination with the -EncodedCommand parameter:
[Convert]::ToBase64String() creates a Base64-encoded string from a [byte[]] array.
[Text.Encoding]::Unicode.GetBytes() converts the (internally UTF-16 -
"Unicode") string to a [byte[]] array.
Note: To also pass arguments, you have two options:
You can "bake" them into the -EncodedCommand argument, assuming you can call a command to pass them to there - see below, which shows how to define your function as such in the new session, so you can call it by name with arguments.Thanks, Abraham Zinala
The advantage of this approach is that you can pass named arguments this way. The disadvantage is that you are limited to arguments that have string-literal representations.
You can use the (currently undocumented) -EncodedArguments parameter, to which you must similarly pass a Base64-encoded string, albeit based on the CLIXML representation of the array of arguments to pass
The advantage of this approach is that you can pass a wider array of data types, within the limits of the type fidelity that CLIXML serialization can provide - see this answer; the disadvantage is that only positional arguments are supported this way (although you could work around that by passing a hashtable that the target code then uses for splatting with the ultimate target command).
Here's a simplified, self-contained example, which uses Write-Output to echo the (invariably positional) arguments received:
$command = 'Write-Output $args'
$argList = 'foo', 42
Start-Process powershell -args '-noprofile', '-noexit',
'-EncodedCommand',
([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command))),
'-EncodedArguments',
([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes(
[System.Management.Automation.PSSerializer]::Serialize($argList)
)))
In case you want to pass the complete function, so it can be called by name in order to pass arguments as part of the command string, a little more work is needed.
# Simply wrapping the body in `function <name> { ... }` is enough.
$func = (Get-Command -Type Function Check-PC)
$wholeFuncDef = 'Function ' + $func.Name + " {`n" + $func.Definition + "`n}"
Start-Process powershell -args '-noprofile', '-noexit', '-EncodedCommand', `
([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes("$wholeFuncDef; Check-PC")))
As stated above, you can "bake" any arguments to pass to your function - assuming they can be represented as string literals - into the -EncodedCommand argument, simply by appending them inside the "$wholeFuncDef; Check-PC" string above; e.g.,
"$wholeFuncDef; Check-PC -Foo Bar -Baz Quux"
Start-Process solution with regex-based escaping of the source code to pass:
PetSerAl suggests the following alternative, which uses a regex to perform the escaping.
The solution is more concise, but somewhat mind-bending:
Start-Process powershell -args '-noprofile', '-noexit', '-Command', `
('"' +
((Get-Command -Type Function Check-PC).Definition -replace '"|\\(?=\\*("|$))', '\$&') +
'"')
"|\\(?=\\*("|$)) matches every " instance and every nonempty run of \ chars. - character by character - that directly precedes a " char. or the very end of the string.
\\ is needed in the context of a regex to escape a single, literal \.
(?=\\*("|$)) is a positive look-ahead assertion that matches \ only if followed by " or the end of the string ($), either directly, or with further \ instances in between (\\*). Note that since assertions do not contribute to the match, the \ chars., if there are multiple ones, are still matched one by one.
\$& replaces each matched character with a \ followed by the character itself ($&) - see this answer for the constructs you can use in the replacement string of a -replace expression.
Enclosing the result in "..." ('"' + ... + '"') is needed to prevent whitespace normalization; without it, any run of more than one space char. and/or tab char. would be normalized to a single space, because the entire string wouldn't be recognized as a single argument.
Note that if you were to invoke powershell directly, PowerShell would generally automatically enclose the string in "..." behind the scenes, because it does so for arguments that contain whitespace when calling an external utility (a native command-line application), which is what powershell.exe is - unlike the Start-Process cmdlet.
PetSerAl points out that the automatic double-quoting mechanism is not quite that simple, however (the specific content of the string matters with respect to whether automatic double-quoting is applied), and that the specific behavior changed in v5, and again (slightly) in v6.
My Powershell script needs to invoke an EXE with a very complicated set of arguments. I'm using Powershell 3.0, and must stick with that version. Alas, even the "magic" escaping operator (--%) isn't helping me. For example, using the Call operator, consider this:
& other.exe --% action /mode fast /path:"location with spaces" /fancyparam { /dothis /dothat:"arg with spaces" } /verbose
Now, if it were that simple, my script could easily work fine. But it isn't that simple. The arguments for "other.exe" can be different, depending on user selections earlier in my script. So instead, I need to build up those parameters ahead of time, perhaps like this:
$commandArgs = 'action /mode ' + $userMode + ' /path:"location with spaces" /fancyparam { /dothis /dothat:"' + $userArgs + " } /verbose'
Thus I would invoke this way:
& other.exe --% $commandArgs
...well, expect that --% means it just passes a raw string of $commandArgs instead. But without the --%, powershell auto-quotes the contents of $commandArgs, which really messes up the internal quotes (not to mention breaking the 'action' argument at the front that other.exe needs first). In other words, I've already tried embedding the --% inside my $commandArgs string, but the damage is already done by the time it would be parsed (and I don't think it even works that way).
NOTE that this example is only about 1/4 of my actual command I need to execute -- which includes many more user args, quotes and other funny characters that would drive me into escaping-hell in a hurry! I've also already been using the echoargs.exe tool, which is how I'm seeing the troubles I'm having. Oh, and I need all the spaces in my example, too (i.e. need spaces around the brace characters).
So after much searching for an answer, I turn to you for help. Thanks in advance.
Ok, probably weird to be answering my own question, but after spending another day on this problem yesterday, I might have realized the answer myself. At least, this is what I found that works. But I post here to get further feedback, in case I'm really doing something that isn't recommended ...using Invoke-Expression :-)
I had sorta realized early on, and some of you confirmed this in your responses, that the --% prevents all further expansions (including my $variables). And my problem is that I'm still needing to expand lots of things when trying to use the Call operator (&). My problem would be solved if my command line was all ready to go before using --%, so that's what I did.
I created a new string, composed of:
$fullCommand = '& "other.exe" --% ' + $commandArgs
(The EXE path actually has spaces in it, hence the quotes.) Then, with it all built up (including the --% where it needs to be), I invoke it as a new piece of script:
Invoke-Expression $fullCommand
So far, I'm having very good results. But I know in my search up to this point that Invoke-Expression sounded like this bad thing people shouldn't use. Thoughts, everyone?
The purpose of --% is to suppress argument processing, so there's no expansion of variables after that parameter on behalf of PowerShell. You can work around this by using environment variables, though:
$env:UserMode = 'foo'
$env:UserArgs = 'bar baz'
& other.exe --% action /mode %UserMode% /path:"location with spaces" /fancyparam { /dothis /dothat:"%userArgs%" } /verbose
I always recommend that people build their command line arguments into a variable, and then pass that variable into the -ArgumentList parameter of the Start-Process cmdlet.
$Program = '{0}\other.exe' -f $PSScriptRoot;
$UserMode = 'fast';
$Path = 'c:\location with\spaces';
$ArgWithSpaces = 'arg with spaces';
$ArgumentList = 'action /mode {0} /path:"{1}" /fancyparam { /dothis /dothat:"{2}" } /verbose' -f $UserMode, $Path, $ArgWithSpaces;
$Output = '{0}\{1}.log' -f $env:Temp, [System.Guid]::NewGuid().ToString();
Start-Process -Wait -FilePath $Program -ArgumentList $ArgumentList -RedirectStandardOutput $Output;