powershelldouble-quotesgit-config

Git Config Double Quotes in PowerShell


This was asked here, but no solution was provided for PowerShell. The solution given does not work in PowerShell (specifically, PowerShell 5.1 within VSCode).

I have tried

git config --global mergetool.vscode.cmd '"code --wait $MERGED"'

but I lose the double-quotes (i.e. there are no double-quotes in the corresponding .gitconfig file). FYI, the single-quotes are necessary to pass $MERGED as a string literal and not have PowerShell attempt to expand it.

I've also tried

echo '"code --wait $MERGED"' | git config --global mergetool.vscode.cmd
git config --global mergetool.vscode.cmd '`"code --wait $MERGED`"'
git config --global mergetool.vscode.cmd @'
code --wait $MERGED
'@

but nothing works. Is there no way to do this from within PowerShell?

Additionally, I've researched this question, and the solution of

git config --global mergetool.vscode.cmd '\"code --wait $MERGED\"'

nor

git config --global mergetool.vscode.cmd "\"code --wait $MERGED\""

does not work either.

The closest I get is using

git config --global mergetool.vscode.cmd '" code --wait $MERGED "'

but this outputs the spaces in the string as such:

[mergetool "vscode"]
    cmd = " code --wait $MERGED "

Solution

  • Unfortunately, an obscure workaround is required (see the bottom section for a generic solution via an installable module):

    git config --global mergetool.vscode.cmd --% "\"code --wait $MERGED\""
    

    Note: This assumes that you want to end up with the following in ~/.gitconfig, which probably isn't your intent:

    [mergetool "vscode"]
        cmd = \"code --wait $MERGED\"
    

    Instead, as one of the answers you link to suggests, only the $MERGED part should be double-quoted with embedded double quotes, in which case you should use (see the alternative at the bottom):

    git config --global mergetool.vscode.cmd --% "code --wait \"$MERGED\""
    

    That would give you:

    [mergetool "vscode"]
        cmd = code --wait \"$MERGED\"
    

    That is, only those individual arguments in the shell command that your value constitutes that need double-quoting for the shell's sake must be enclosed in "...".

    Note that it is git that re-escapes verbatim " chars. in the given configuration value as \" in the config file. Unescaped " in the config file - which have syntactic function for git itself - are only used if you pass a value with leading and/or trailing spaces, so as to delimit the value as a whole; values with (interior) spaces do not themselves require manual double-quoting: git stores them as-is in the config file, except for escaping embedded \ as \\ and " as \".

    Passing --% "code --wait \"$MERGED\"" from PowerShell signals the intent to pass verbatim value code --wait "$MERGED" to git config --global mergetool.vscode.cmd, which is the proper formulation of a sh command line[1] (sh is a POSIX-compatible shell that git uses even on Windows, via a bash implementation that ships with it) in which the "..." around (environment) variable reference $MERGED ensure that the expanded value is passed as-is to the target binary, code, even if it contains spaces, for instance.

    You can verify this by querying the value afterwards:

    # After running the command above:
    PS> git config --global mergetool.vscode.cmd
    code --wait "$MERGED"
    

    git config and the configuration file format are documented here, also available locally by running git help config or, alternatively, on Unix-like platforms only, man git-config


    Explanation of the workaround:

    Use of --%, the stop-parsing symbol, allows you to control the exact quoting of the subsequent arguments passed to external programs (while also suppressing expansion of PowerShell variables, so that $MERGED is left as-is), whereas PowerShell by default performs re-quoting behind the scenes, after having performed its own parsing.

    Leaving aside that double-quoting the entire command line is ultimately not the right approach, purely from a PowerShell syntax perspective what you tried, '"code --wait $MERGED"', should work - PowerShell should automatically translate that to "\"code --wait $MERGED\"" behind the scenes - but as of PowerShell 7.1 doesn't, due to a long-standing bug, described in detail in this answer.

    While explicit \-escaping of embedded " chars.(!) is typically a better workaround and works reliably in PowerShell (Core) 7+, there are edge cases in Windows PowerShell where it doesn't, and you've hit one of them:

    # Alternative workaround for *PowerShell (Core) v7+ only*
    git config --global mergetool.vscode.cmd '\"code --wait $MERGED\"'
    

    The reason this doesn't work in Windows PowerShell is that it mistakenly concludes that the \"...\" constitutes syntactic double-quoting and therefore doesn't enclose the argument in "..."; that is, instead of passing "\"...\"", it passes just \"...\" as part of the target process' command line.

    Note that quoting arguments individually avoids the edge case discussed, so that explicit \-escaping works even in Windows PowerShell:

    # Also works in Windows PowerShell, because the edge case is avoided.
    git config --global mergetool.vscode.cmd 'code --wait \"$MERGED\"'
    

    Generic solution via module Native:

    If you want to solve quoting problems for most[2] (on Windows) / all (on Unix) calls to external programs, consider my Native module's ie function, which encapsulates all required workarounds:

    # Install the module in the current user's scope.
    Install-Module Native
    
    # Simply prepend `ie` to your external-program calls, which correctly
    # handles all behind-the-scenes re-quoting, allowing you to focus on 
    # PowerShell syntax only.
    ie git config --global mergetool.vscode.cmd 'code --wait "$MERGED"'
    

    [1] git appears to be invoking sh as follows (using sh syntax with placeholders for illustration):
    sh -c '<config-value> "$@"' '<config-value>' <git-supplied args>
    That is, the command line stored in the configuration value is invoked with git-supplied pass-through arguments appended. Note that the first first post -c argument is again the configuration value, which binds to $0, i.e. sets the invocation name, and is therefore not part of the array of pass-through arguments, "$@". An example of a git-supplied pass-through argument is the path of a file to edit that is passed to the command line stored in the core.editor configuration value.

    [2] A solution that works with all external programs is fundamentally impossible on Windows, because each program can decide for itself how it parses the string that encodes the arguments being passed. However, there are a few widespread patterns that ie is aware of and applies appropriately, which notably makes it work robustly with batch files and msiexec-like executables.