silicontrip
silicontrip

Reputation: 1026

Nested Quoted Strings in Powershell

I'm writing a service installer script in Powershell where the service requires a complicated quoted command line.
So I tried to simplify it and broke each option down to individual variables, however when creating the final command line string the strings don't escape their quotes. Thus the command line doesn't work.

I'd like to keep all the options separate so that other admins can configure and install the service without needing to worry about escaping quotes.

I'm thinking I need to perform a search and replace or use a shell specific safe/escape string command to operate on the individual strings first.

I don't know how the command line of a service is parsed, so not sure which shell escape method to use.

I've done a search on quotes in strings but they never seem to deal with nesting of strings with quotes inside strings with quotes.

This is my install script and I do have control over the applicationservice, so if you know of a better method to get arguments into a service that would also be appreciated.

$installpath = (get-location)
$name="landingZone"
$displayName="LandingZone Starter"
$description="Sony CI automated download client"

$sessionuser="Engineering"
$processname="explorer"
$logname="Landing Zone"
$programpath="C:\Program Files (x86)\Common Files\Oracle\Java\javapath\java.exe"
$programarguments='"C:\Program Files (x86)\Common Files\Oracle\Java\javapath\java.exe -jar C:\Program Files (x86)\Sony CI\Sony-Ci-LZ-1.4.22\Sony-Ci-LZ-1.4.22.jar"'
$WorkDirectory="C:\Program Files (x86)\Sony CI\Sony-Ci-LZ-1.4.22" 
$Visible=1
$TerminateTimeout=1000

# Arg! help me!

$binpath = $installpath.toString() + "\applicationservice.exe ""SessionUser=$sessionuser"" ""ProcessName=$processname"" ""LogName=$logname"" ""ProgramPath=$programpath"" ""ProgramArguments=$programarguments"" ""WorkDirectory=$workdirectory"" Visible=$visible TerminateTimeout=$terminatetimeout"


New-Service -Name $name -BinaryPathName $binPath -DisplayName $displayname -Description $description -StartupType Manual

Thanks

Upvotes: 1

Views: 2231

Answers (3)

mklement0
mklement0

Reputation: 438153

Note:

  • This answers addresses the question as asked, with respect to the quoting and escaping required to construct a command line via a single string.

  • For a robust programmatic alternative that constructs the command line via a hashtable and a loop, see Mark's own answer.


Potential problem: If $installpath.toString() returns a path with spaces, you'll have to use embedded quoting for the executable path as well.

Definite problem:

The following argument itself has embedded ":

$programarguments='"C:\Program Files (x86)\Common Files\Oracle\Java\javapath\java.exe -jar C:\Program Files (x86)\Sony CI\Sony-Ci-LZ-1.4.22\Sony-Ci-LZ-1.4.22.jar"'

However, this embedded quoting isn't placed correctly: the executable path and the -jar argument individually need enclosing in "...", because both have embedded spaces:

$programarguments='"C:\Program Files (x86)\Common Files\Oracle\Java\javapath\java.exe" -jar "C:\Program Files (x86)\Sony CI\Sony-Ci-LZ-1.4.22\Sony-Ci-LZ-1.4.22.jar"'

In order to embed this value inside another double-quoted string for command-line use, you must escape its embedded " as either "" or \" (see below for when to use which), which you can do with -replace '"', '""' or -replace '"', '\"'.

The following uses an expandable here-string (@"<newline>...<newline>"@) to simplify the embedded quoting and spreads the command across several lines for readability (the resulting newlines are removed afterwards with -replace '\r?\n'):

$binpath = $installpath.toString() + @"
\applicationservice.exe 
  SessionUser="$sessionuser" 
  ProcessName="$processname"
  LogName="$logname"
  ProgramPath="$programpath"
  ProgramArguments="$($programarguments -replace '"', '""')"
  WorkDirectory="$workdirectory"
  Visible=$visible
  TerminateTimeout=$terminatetimeout
"@ -replace '\r?\n'

Note:

  • The above only uses embedded double-quoting for the value part of the <property>=<value> pairs (e.g., foo="bar none" rather than "foo=bar none"), which, unfortunately, is not an uncommon requirement on Windows (notably with msiexec), and it also seems to be necessary here, judging from your own answer,

  • " embedded inside the $programarguments value are escaped as "" rather than as \":

    • Either form of escaping typically works, but "" has the advantage that you needn't worry about values ending in \, which with \"-escaping would additionally require you to escape that trailing \ as \\.

    • The caveat is that while most CLIs on Windows recognize both "" and \" as an escaped ", some recognize only \, such as Ruby, Perl, and notably also applications that use the CommandLineToArgv WinAPI function.

See also:


As an aside:

  • The backtick ` is PowerShell's general-purpose escape character.

  • Inside "..." strings (only), you can alternatively escape an embedded " char. as "".

For instance, both " `"hi`" "` and " ""hi"" " return verbatim  "hi" .

The exception is that when PowerShell is called from the outside, via its CLI, only \" is recognized as an escaped " in Windows PowerShell, so as to be consistent with other CLIs, whereas PowerShell [Core] v6+ also accepts "".

Upvotes: 1

silicontrip
silicontrip

Reputation: 1026

Thanks to everyone who took time to comment and answer.

This is the single line PS script string which ended up working (look 6 double quotes in a row);

"$($inspath)\applicationservice.exe SessionUser=Engineering ProcessName=explorer 
LogName=""Landing Zone"" ProgramPath=""C:\Program Files (x86)\Common Files\Oracle\Java\javapath\java.exe"" 
ProgramArguments=""""""C:\Program Files (x86)\Common Files\Oracle\Java\javapath\java.exe"""" -jar """"c:\users\Engineering\Desktop\Sony CI\Sony-Ci-LZ-1.4.22\Sony-Ci-LZ-1.4.22.jar"""""" 
WorkDirectory=""c:\users\Engineering\Desktop\Sony CI\Sony-Ci-LZ-1.4.22"" Visible=1 
TerminateTimeout=1000"

Which ends up like this command line, as shown in the PathName property of the WMI win32_service.

C:\Users\mheath\Documents\20201106-MakeMeAService\ServiceScripts\applicationservice.exe
SessionUser=Engineering ProcessName=explorer LogName="Landing Zone" ProgramPath="C:\Program Files
(x86)\Common Files\Oracle\Java\javapath\java.exe" ProgramArguments="""C:\Program Files (x86)\Common
Files\Oracle\Java\javapath\java.exe"" -jar ""c:\users\Engineering\Desktop\Sony
CI\Sony-Ci-LZ-1.4.22\Sony-Ci-LZ-1.4.22.jar""" WorkDirectory="c:\users\Engineering\Desktop\Sony
CI\Sony-Ci-LZ-1.4.22" Visible=1 TerminateTimeout=1000

And finally the command line as read by the service, using Environment.GetCommandLineArgs(); in the OnStart() method (comma separated)

C:\Users\mheath\Documents\20201106-MakeMeAService\ServiceScripts\applicationservice.exe,SessionUser=mheath,ProcessName=explorer,LogName=Landing Zone test,ProgramPath=C:\Program Files (x86)\Common Files\Oracle\Java\javapath\java.exe,ProgramArguments="C:\Program Files (x86)\Common Files\Oracle\Java\javapath\java.exe" -jar "c:\users\Engineering\Desktop\Sony CI\Sony-Ci-LZ-1.4.22\Sony-Ci-LZ-1.4.22.jar",WorkDirectory=c:\users\Engineering\Desktop\Sony CI\Sony-Ci-LZ-1.4.22,Visible=1,TerminateTimeout=1000

I made some significant changes to the script, putting the config in a HashTable and using the -f string formatter.

$installpath = (get-location)
$name="testlz"
$displayName="Testlz"
$description="Testlz"

$config = @{
    SessionUser='mheath';
    ProcessName='explorer';
    LogName='Landing Zone test';
    ProgramPath= 'C:\Program Files (x86)\Common Files\Oracle\Java\javapath\java.exe';
    ProgramArguments='"C:\Program Files (x86)\Common Files\Oracle\Java\javapath\java.exe" -jar "c:\users\Engineering\Desktop\Sony CI\Sony-Ci-LZ-1.4.22\Sony-Ci-LZ-1.4.22.jar"';
    WorkDirectory='c:\users\Engineering\Desktop\Sony CI\Sony-Ci-LZ-1.4.22';
    Visible=1;
    TerminateTimeout="1000";
}

$escconfig = @{}

$escconfig.Add('ServicePath', $installpath.toString() + '\applicationservice.exe')

foreach ($it in $config.Keys)
{
    $escconfig[$it] = '"{0}"' -f ($config[$it] -replace '"','""')
}

$binpath = '{0} SessionUser={1} ProcessName={2} LogName={3} ProgramPath={4} ProgramArguments={5} WorkDirectory={6} Visible={7} TerminateTimeout={8}' `
    -f $escconfig['ServicePath'], $escconfig['SessionUser'], $escconfig['ProcessName'], $escconfig['logname'], `
    $escconfig['ProgramPath'], $escconfig['ProgramArguments'], $escconfig['WorkDirectory'], $escconfig['Visible'], $escconfig['TerminateTimeout']

New-Service -Name $name -BinaryPathName $binPath -DisplayName $displayname -Description $description -StartupType Manual

This hopefully protects any other paths that contain spaces.

PS. Any discrepancies you see from one text code block to the next, are just because I copied and pasted it as I was testing.

Upvotes: 1

Bassie
Bassie

Reputation: 10390

You should escape your quotes with a backtick:

$test = "`"test`""

Write-Host $test

Or you can use a here string like this

$test = @'
"test"
'@

Write-Host $test

Both write "test" to the console

Upvotes: 0

Related Questions