Git Config Double Quotes in PowerShell - 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 "

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.

Related

PowerShell command only works without spaces between arguments

I try certain source codes using PowerShell to extract an password protected archive using 7zip:
This command doesn' work (7zip is an alias for $7zipPath):
& 7zip x "$zipFile" -o "$output" -p $zipFilePassword
I get the this error:
Command Line Error:
Too short switch:
But when I remove the spaces between the variables -o and -p, the archive can be extracted. This behaviour confuses me with other command line tools like git etc.? Why is it so?
The behavior is specific to 7-Zip (7z.exe) and applies to whatever program (shell) you invoke it from:
Deviating from widely used conventions observed by CLIs such as git, 7z requires that even switches (options) that have mandatory arguments, such as -o and -p, have the argument directly attached to the switch name - no spaces are allowed:
& 7zip x $zipFile -o"$output" -p"$zipFilePassword"
Note that you normally need not enclose variable references in PowerShell in "..." (note how $zipFile isn't), even if they contain spaces. However, in order to attach them directly to switch names, you do.
Alternatively, you could enclose the entire token - switch name and argument - in double quotes:
& 7zip x $zipFile "-o$output" "-p$zipFilePassword"

Environment variables in Compose using Powershell with only 1 line [duplicate]

This question already has answers here:
PowerShell: Setting an environment variable for a single command only
(10 answers)
Closed 4 years ago.
I know that you can pass environment variables to docker-compose.
docker-compose.yml
. . .
mysql:
image: mariadb:10.2
ports:
- "${DB_PORT}:3306"
. . .
$ DB_PORT=3396 docker-compose up
However this only works using bash. I am using PowerShell and am trying to find an equivalent that is only a one line command.
PS> $env:DB_PORT:3306 docker-compose up does not work. Neither does
multiline
$env:DB_PORT=3396 `
>> docker-compose -up
The error I get is
Unexpected token 'docker-compose' in expression or statement.
If I do it one at a time it does work...
PS> $env:DB_PORT=3396
PS> docker-compose -up
Is there not way to do this in PowerShell when the equivalent in bash is ridiculously simple?
POSIX-like shells such as bash offer a way to set environment variables in a command-scoped way, simply by prepending <varName>=<value> pairs directly to a command, as the following example demonstrates:
$ foo=bar bash -c 'echo "[$foo]"'; echo "[$foo]"
[bar]
[]
foo=bar defines environment variable foo for the bash -c '...' child process only; the next command - echo ... - does not see this variable.
PowerShell has NO equivalent construct.
The best you can do is to define the environment variable of interest first, in a separate statement, using ;, PowerShell's statement separator. Any external utility you invoke thereafter - which invariably runs in a child process - will see it, but note that the environment variable will remain in effect in the current PowerShell session, unless you manually remove it:
# Set the env. variable, call the command that should see it,
# remove it afterwards.
PS> $env:foo = 'bar'; bash -c 'echo "[$foo]"'; $env:foo = $null
[bar]
Note how $env:foo = $null i.e., setting the environment variable to $null is the same as removing it; alternatively, you could all Remove-Item env:foo
If you also want to restore a pre-existing value afterwards:
$env:foo = 'original'
# Temporarily change $env:foo to a different value, invoke the
# program that should see it, then restore the previous value.
& { $org, $env:foo = $env:foo, 'bar'; bash -c 'echo "[$foo]"'; $env:foo = $org }
$env:foo
The above yields:
[bar]
original
showing that while the bash process saw the temporary value, bar, the original value of $env:foo was restored afterwards.
Also note another important difference:
In POSIX-like shells, environment variables are implicitly surfaced as shell variables - they share the one and only namespace the shell has for variables.
By contrast, PowerShell surfaces environment variables only via the $env:<varName> namespace (e.g., $env:foo), which is distinct from the (prefix-less) namespace for PowerShell's own variables (e.g., $foo).

Mercurial - How to configure multiline hook on Windows

Running Windows 7, configuring a commit hook in my .hgrc file.
The hook calls an external powershell script and passes it a few parameters. When I put all the parameters on one long line, the script receives them just fine. However, when I try to put each parameter on its own line, the hook can't figure out that they are all part of the same call to my external script.
[hooks]
commit.working_one_liner = PowerShell.exe -ExecutionPolicy Bypass -File .\MyScript.ps1 -hg %HG% -updatedToChangeset %HG_NODE% -dbName 'Test'
commit.multi_line_hook_not_working = PowerShell.exe
-ExecutionPolicy Bypass
-File .\MyScript.ps1
-hg %HG%
-updatedToChangeset %HG_NODE%
-dbName 'Test'
According to the Hg documentation, I should be able to do this:
"A configuration file consists of sections, led by a [section] header and followed by name = value entries (sometimes called configuration keys):
[spam]
eggs=ham
green=
eggs
Each line contains one entry. If the lines that follow are indented, they are treated as continuations of that entry. Leading whitespace is removed from values. Empty lines are skipped. Lines beginning with # or ; are ignored and may be used to provide comments."
Despite indenting, the multi_line_hook seems to ignore everything after the first line. I've tried various escape characters (`, ^, \, etc.) after each new line. Any ideas what I'm missing here?
The example from the documentation only works for the first line to follow and not the rest.
I don't have access to windows at the moment, but on linux I need to use a backslash:
[hooks]
commit.working = echo \
test

How can I ensure my autocompleted spaces are fed into my function properly?

I'm using zsh, and am trying to write a function to operate on a URL and a pathname:
function my-function
{
somecommand --url $1 $(readlink -f $2)
}
(to complicate things somewhat, the function actually uses sh syntax, as it is sourced from my ~/.zshrc using a trick like this). The readlink is there to expand symlinks and ensure directories such as . are evaluated correctly (the directory name is stored for later use by somecommand).
When I type a command from the command-line like this:
my-function http://example.org/example /tmp/myexampledirectory
... it works fine, even if I autocomplete the directory name. However, if the directory name contains spaces, zsh completes it like this:
my-function http://example.org/example /tmp/My\ Example\ Directory
For most "normal" commands (cp, mv, etc.) that never seems to cause a problem. However, in my case, somecommand sees $2 as only being /tmp/My - presumably the rest is seen as another argument.
How can I avoid this situation? I would prefer not to alter the standard zsh autocompletion, but rather find a way for my function to handle this.
The zsh completion system works very well here, and the solution is very simple, just put double-quotes around the readlink argument in the script:
somecommand --url $1 $(readlink -f "$2")
The point is that without quotes readlink removes backslashes which escape whitespaces. Compare three results:
1. Without backslashes and quotes readlink -f assumes that there are three different files/directories (with default path in current directory) and produces
$ readlink -f /tmp/My Example Directory
/tmp/My
/home/jimmij/Example
/home/jimmij/Directory
2. With escaping backslashes but without quotes readlink -f understands that there is only one directory, but removes backslashes from output, so that somecommand takes three separate arguments
$ readlink -f /tmp/My\ Example\ Directory
/tmp/My Example Directory
3. With backslashes and with double-quotes readlink -f gives the output with backslashes what is (most probably) expected by somecommand
$ readlink -f "/tmp/My\ Example\ Directory"
/tmp/My\ Example\ Directory
BTW, as a rule of thumb: if there are any problems with whitespaces in the shell-like scripts (bash, zsh, whatever) the first thing to play with is different quotation marks around variables.

Pipe output to environment variable export command

I'm trying to set a git hash value into an environment variable, i thought it would be as simple as doing this:
git log --oneline -1 | export GIT_HASH=$1
But the $1 doesn't contain anything. What am I doing wrong?
$1 is used to access the first argument in a script or a function. It is not used to access output from an earlier command in a pipeline.
You can use command substitution to get the output of the git command into an environment variable like this:
GIT_HASH=`git log --oneline -1` && export GIT_HASH
However...
This answer is specially in response to the question regarding the Bourne Shell and it is the most widely supported. Your shell (e.g. GNU Bash) will most likely support the $() syntax and so you should also consider Michael Rush's answer.
But some shells, like tcsh, do not support the $() syntax and so if you're writing a shell script to be as bulletproof as possible for the greatest number of systems then you should use the `` syntax despite the limitations.
Or, you can also do it using $(). (see What is the benefit of using $() instead of backticks shell scripts?)
For example:
export FOO_BACKWARDS=$(echo 'foo' | rev)
You can use an external file as a temporary variable:
TMPFILE=/var/tmp/mark-unsworth-bashvar-1
git log --oneline -1 >$TMPFILE; export GIT_HASH=$(cat $TMPFILE); rm $TMPFILE
echo GIT_HASH is $GIT_HASH