Search code examples
windows-10escapingwhitespacestart-processpowershell-5.1

PowerShell removes multiple consecutive whitespaces when I pass arguments to a nested Start-Process command


This powershell code works fine:

powershell -NoProfile -Command {Start-Process -FilePath wscript.exe -Verb RunAs -ArgumentList '"C:\Users\TestAccount\Desktop\cartella  con  spazi\test.vbs" "/CurrentDirectory:C:\Users\TestAccount\Desktop\Cartella  con  spazi" "/AppData:C:\Users\TestAccount\AppData\Roaming"'}

But when I enter the complete command, powershell leaves only one space between the words but in my path there are two consecutive ones:

Start-Process -FilePath powershell.exe -WorkingDirectory "$env:ALLUSERSPROFILE" -Credential $credential -WindowStyle Hidden -ArgumentList "-NoProfile -Command & {Start-Process -FilePath wscript.exe -Verb RunAs -ArgumentList '\`"C:\Users\TestAccount\Desktop\cartella  con  spazi\test.vbs\`" \`"/CurrentDirectory:C:\Users\TestAccount\Desktop\Cartella  con  spazi\`" \`"/AppData:C:\Users\TestAccount\AppData\Roaming\`"'}"

Same error with this:

Start-Process -FilePath powershell.exe -WorkingDirectory "$env:ALLUSERSPROFILE" -Credential $credential -WindowStyle Hidden -ArgumentList "-NoProfile -Command Start-Process -FilePath wscript.exe -Verb RunAs -ArgumentList '\`"C:\Users\TestAccount\Desktop\cartella  con  spazi\test.vbs\`" \`"/CurrentDirectory:C:\Users\TestAccount\Desktop\Cartella  con  spazi\`" \`"/AppData:C:\Users\TestAccount\AppData\Roaming\`"'"

This is the error message:

enter image description here

As you can see, in the error message the path contains only one space between words, while in the code I have correctly entered two consecutive ones.

This is the code to enter the credentials (just before the line that gives an error):

$username = 'Username'
$password = 'Password'
$securePassword = ConvertTo-SecureString $password -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential $username, $securePassword

I've been getting a single line of code to work properly for days but without success.
Those example paths I entered are actually variables (with an indefinite number of consecutive spaces) and therefore by enclosing -ArgumentList in single quotes, the variables are not expanded.
You can adopt the strategy you think is most appropriate to help me.
Windows 10 Pro 64-bit
Powershell Version: 5.1.19041.1237 (Integrated in Windows 10).

UPDATE:

@mklement0 There is the problem that in reality the line in question contains an expandable variable:

Start-Process -FilePath powershell.exe -WorkingDirectory "$env:ALLUSERSPROFILE" -Credential $credential -WindowStyle Hidden -ArgumentList @"
-NoProfile -Command "Start-Process -FilePath wscript.exe -Verb RunAs -ArgumentList '\"$PWD\test.vbs\" \"/CurrentDirectory:$PWD\" \"/AppData:$env:APPDATA\"'"
"@

so if the path contains single quotes, the line goes in error.

UPDATE 2:

@mklement0 Your solution has stopped working since I put it in a script, which is what I need, with the following error message:

enter image description here


Solution

  • Note: This answer addresses the Start-Process problem; for the unrelated problem with .ps1 files whose paths have spaces in them failing to run when they are double-clicked in File Explorer, see this answer.


    Try the following, with the arguments passed verbatim:

    Start-Process `
      -FilePath powershell.exe `
      -WorkingDirectory "$env:ALLUSERSPROFILE" `
      -Credential $credential `
      -WindowStyle Hidden `
      -ArgumentList @'
        -NoProfile -Command "Start-Process -FilePath wscript.exe -Verb RunAs -ArgumentList '\"C:\Users\TestAccount\Desktop\cartella  con  spazi\test.vbs\" \"/CurrentDirectory:C:\Users\TestAccount\Desktop\Cartella  con  spazi\" \"/AppData:C:\Users\TestAccount\AppData\Roaming\"'"
    '@
    
    • The -Command argument is enclosed in "..." in order to preserve the command-internal spaces as-is.

    • The embedded " characters in the command string are escaped as \"

    • The outer -ArgumentList uses a here-string to simplify the embedded quoting.

      • The verbatim form (@'<newline>...<newline>'@) is used here; use the _expandable form, @"<newline>...<newline>"@, if you need string interpolation (the ability to embed variable references and expressions) - see next section.
    • While PowerShell-internally it is `, the backtick, that serves as the escape character, PowerShell's CLI expects " characters to be \-escaped on the command line, so as to conform to the most widely used escaping convention.

      • The cross-platform PowerShell (Core) 7+ edition now also accepts "" as an alternative to \", another common convention on Windows.

      • While you may situationally have to combine the two escaping approaches, i.e. to use `\" in order to quote a " first for the CLI and then for PowerShell-internal use, in the context of interpreting a -Command argument, this isn't needed in the command above, because the inner -ArgumentList argument uses single-quoting, inside of which " can be be used as-is.


    Generalized solution, with arguments provided via variables:

    The assumption is that the variable whose value are to be used are named $dir1, $dir2, and $dir3 (and that their values have no embedded " chars., but " aren't supported in file-system paths on Windows anyway).

    Start-Process `
      -FilePath powershell.exe `
      -WorkingDirectory "$env:ALLUSERSPROFILE" `
      -Credential $credential `
      -WindowStyle Hidden `
      -ArgumentList @"
        -NoProfile -Command "Start-Process -FilePath wscript.exe -Verb RunAs -ArgumentList '\"$($dir1 -replace "'", "''")\" \"/CurrentDirectory:$($dir2 -replace "'", "''")\" \"/AppData:$($dir3 -replace "'", "''")\"'"
    "@
    
    • The interpolating form of a here-string (@"<newline>...<newline>"@) must now be used for the outer -ArgumentList argument.

    • Since the variable values are interpolated inside what powershell.exe will see as a single-quoted string ('...'), any ' characters embedded in the variable values must be escaped as '', which is what -replace "'", "''" in the embedded $(...) subexpressions does.


    Optional reading: Troubleshooting calls to external programs:

    It can be difficult to diagnose problems with calls to external programs, especially in a nested call such as this. The following is a Windows solution; see the bottom for a Unix solution.

    Preparation: Create helper executable echoArgsPause.exe:

    The following creates a helper executable, echoArgsPause.exe, which echoes the full command line it was called with, as well as the individual arguments it has parsed the command line into, and then waits for a keystroke.

    Note:

    • As a .NET-based *.exe, it uses .NET's rules for command-line parsing, which in turn is based on Microsoft's C/C++ conventions. Unfortunately, not all executables are guaranteed to use the same conventions. And, indeed, the WSH CLIs (cscript.exe and wscript.exe) do not support embedded " characters in arguments, for instance.

    • Run the code from Windows PowerShell, because the executable must be compiled via .NET Framework, not .NET (Core), because only the former reports the true, raw command line via [Environment]::CommandLine.

    • For later reuse, place the resulting .\echoArgsPause.exe file in a directory in your PATH (in a directory listed in $env:PATH).

    • To create a variant that doesn't wait for a keystroke, remove the Console.Write("Press a key to exit: "); and Console.ReadKey(true); lines and change -OutputAssembly .\echoArgsPause.exe to, say, -OutputAssembly ./echoArgs.exe

    Add-Type -OutputType ConsoleApplication -OutputAssembly .\echoArgsPause.exe -TypeDefinition @'
      using System;
      static class ConsoleApp {
        static int Main(string[] args) {
          Console.WriteLine("\nraw: [{0}]\n", Environment.CommandLine);
          for (int i = 0; i < args.Length; ++i) {
              Console.WriteLine("arg #{0}: [{1}]", i, args[i]);
          }
          Console.Write("Press a key to exit: ");
          Console.ReadKey(true);
          return 0;
        }
      }
    '@
    

    Use of echoArgsPause.exe to troubleshoot a call

    • The following, self-contained sample code uses a simplified version of your command, and uses the echoArgsPause.exe in lieu of your target executable (wscript.exe) to print the command line and the parsed arguments.
    # Specify the full path to the helper executable.
    # Here I'm assuming it is in the current dir.
    $echoArgsPausePath = "$($PWD.ProviderPath)\echoArgsPause.exe"
    
    # Sample variable values.
    $dir1='c:\temp''o  1'
    $dir2='c:\temp   2'
    $dir3='c:\temp    3'
    
    # Call your command, with echoArgsPause.exe in lieu
    # of the target executable (wscript.exe)
    Start-Process `
      -FilePath powershell.exe `
      -WorkingDirectory "$env:ALLUSERSPROFILE" `
      -WindowStyle Hidden `
      -ArgumentList @"
      -NoProfile -Command "Start-Process -FilePath "$echoArgsPausePath" -Verb RunAs -ArgumentList '\"$($dir1 -replace "'", "''")\" \"/CurrentDirectory:$($dir2 -replace "'", "''")\" \"/AppData:$($dir3 -replace "'", "''")\"'"
    "@
    

    On Unix-like platforms:

    Unix-like platforms have no process-level command line; instead, they are launched via an array of verbatim arguments.

    Therefore, a simple solution is to use the standard printf utility to list each argument received on its own line; e.g.:

    printf %s\n foo 'c:\temp''o  1' '3" of snow'
    

    Output up to PowerShell (Core) 7.2.x (broken! note the missing "):

    foo
    c:\temp'o  1
    3 of snow
    

    Output in v7.3.0+:

    foo
    c:\temp'o  1
    3" of snow
    

    The 7.2.x behavior reveals a long-standing bug with respect to passing arguments with embedded double quotes, which was fixed in 7.3.0; it is in effect by default on Unix-like platforms, but will be opt-in on Windows, at some point post-v7.3.1 - see this answer.