Escaping in Powershell for a git log - powershell

I know the escape character is (`), the backtick, but even if I try to use it toe escape the < characters I get error...
git log ORIG_HEAD --no-merges --date=short --pretty="format:<tr><td>%h</td><td>%ad</td><td>%an</td><td>%s</td></tr>" > test.txt
< was unexpected at this time.
How can I go about formatting my git log as above?

If on PowerShell v3 try this:
$out = git log ORIG_HEAD --% --no-merges --date=short --pretty="format:<tr><td>%h</td><td>%ad</td><td>%an</td><td>%s</td></tr>"
$out > test.txt
The --% puts PowerShell into a different parsing mode more suitable for native executables. See this blog post for more details.
If you're not using PowerShell v3, I suggest you use echoargs from the PowerShell Community Extensions to see the arguments as git.exe receives them from PowerShell e.g.:
PS> echoargs log ORIG_HEAD --no-merges --date=short --pretty="format:<tr><td>%h</td><td>%ad</td><td>%an</td><td>%s</td></tr>"
Arg 0 is <log>
Arg 1 is <ORIG_HEAD>
Arg 2 is <--no-merges>
Arg 3 is <--date=short>
Arg 4 is <--pretty=format:<tr><td>%h</td><td>%ad</td><td>%an</td><td>%s</td></tr>>
Command line:
"C:\Program Files (x86)\PowerShell Community Extensions\Pscx3\Pscx\Apps\EchoArgs.exe" log ORIG_HEAD --no-merges --date=short --pretty=format:<tr><td>%h</td><td>%ad</td><td>%an</td><td>%s</td></tr>
If you can see how PowerShell is passing the arguments to the exe you have a fighting chance to figure out how to massage the arguments which usually involves using extra quotes.

Related

Avoid Line break at end of cmd output?

when I use this command pwsh -c echo hello in cmd I get the following output:
C:\>pwsh -c echo hello
hello
C:\>
I do not get that line break at the end
when I run it on powershell:
PS C:\> pwsh -c echo hello
hello
PS C:\>
So I think the problem is in cmd. I know this is not such a problem and have an easy fix but I have some programs uses cmd to access powershell and removing that line break is not that fun.
So is there any fix to prevent cmd to add that line ?
Mofi has provided the crucial pointers in comments:
When executing a command interactively, cmd.exe unconditionally appends a a newline (line break) to the command's output, presumably for readability and perhaps also to ensure that the next prompt always starts on a new line.
This applies irrespective of what that command is. In other words: It doesn't matter that your command happens to be a PowerShell command.
However, that trailing newline does not become part of the command's output, therefore programmatic processing of a command's output is not affected, such as when you redirect > to a file or process the output lines one by one with for /f.
In other words: for programmatic processing you need not remove the trailing newline, because it isn't part of the actual command output.
Conversely, if you really need to in effect suppress the trailing newline for display, you'll have to modify the command's output - if that is even an option - so that the output itself doesn't end in a newline, as shown in this SuperUser answer for cmd.exe's own echo command; for PowerShell, you could do pwsh -c Write-Host -NoNewLine hello.
Edge case:
When capturing output from a batch file that is running without #echo off (or with echo on) - in which case the trailing newlines do become part of the output - you can filter out empty lines by piping to findstr /r /v /c:"^$" (as also shown in the linked answer); e.g.
foo.cmd | findstr /r /v /c:"^$"
However, note that all empty lines are filtered out this way - potentially including actual empty lines in the output from commands executed by the batch file.
If preventing that is required, a more sophisticated approach is required, which, however (a) relies on the standard prompt string (e.g., C:\>) being used and (b) can still yield false positives:
foo.cmd | powershell -nop -c "#($Input) -join \"`n\" -replace '\n(?=[a-z]:\\.*?>)'"
Finally note that if you execute the above commands without capturing or redirecting their output, their overall output in the cmd.exe console will again have a trailing newline.

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 "
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.

Cannot pass asterisk character to external command via powershell

I cannot pass an asterisk character to external command using powershell
I use following a line like
& .\args.bat #("-arg1", "-arg2", "*.test.com")
where args.bat just dumps passed arguments
#ECHO off
ECHO The %~nx0 script args are...
for %%I IN (%*) DO ECHO %%I
And instead of passing "*.test.com" it lookups up current directory for files matching pattern "*.test.com" and sends list of them to args.bat
That can be seen if you use "*" instead of "*.test.com"
I've also tried back tick character to escape asterisk, but it didn't help
In my case using asterisk is required as I'm passing it to makecert.exe to create wildcard domain certificate
Please help
I'm not seeing that. As a test I created args.bat like so:
'pause' > args.bat
Then executed it:
& .\args.bat #("-arg1", "-arg2", "*.ps1")
When I look in task manager at the command line for that cmd.exe, I see:
It must be something the batch file is doing because PowerShell is doing nothing to your *. BTW I used *.ps1 because the directory I executed from had a ton of ps1 files in it.
If you are PS 3.0 or higher you can use "stop parsing operator" --%. See help about_parsing for details.

Compressing to tar.xz using 7-zip through a pipe on windows

My command line is this (powershell):
$7z ="`"c:\Program Files\7-Zip\7z.exe`""
&$7z a -r -ttar -bd -so . | &$7z a -r -txz -bd $archive -si
The produced archive file indeed contains a tar file, but that tar file is corrupt.
Note, that breaking the pipe into two commands works correctly:
&$7z a -r -ttar -bd ${archive}.tmp .
&$7z a -r -txz -bd $archive ${archive}.tmp
The produced archive is perfectly valid.
So, what is wrong with my pipeline?
(I am using Powershell)
Nothing is wrong with your pipeline it is the way that the pipeline works that's causing the error.
PowerShell pipe works in an asynchronous way. Meaning that output of the first command is available to the second command immediately one object at the time even if the first one has not finished executing, See here.
Both Unix and PowerShell pipes operate in the same way. The reason why you might be seeing a difference from Unix to PowerShell is the way in which they go about it is different.
Unix passes Strings between the commands. Where as a Powershell pipe will pass full-fledged .net object between commands. This difference in the data type being past between command will be why it works on unix and not in PowerShell. If 7z.exe can not huddle these .net objects correctly the files will be come corrupt, See here.
Try adding | %{ "$_" } in between the pipes like
&$7z a -r -ttar -bd -so . | %{ "$_" } | &$7z a -r -txz -bd $archive -si
The point is that the second call to 7z expects unmodified data on STDIN, but PowerShell is converting the output from the first call to 7z to (multiple) (string) objects. % is an alias for foreach-object, so what the additional command does is to loop over each object and convert it to a plain string before passing it on to the second call to 7z.
Edit: Reading through PowerShell’s Object Pipeline Corrupts Piped Binary Data it looks to me now as if my suggestion would not work, and there's also no way to fix it. Well, other than wrapping the whole pipeline into a cmd /c "..." call to make cmd and not PowerShell handle the pipeline.
Edit2: I also was trying this solution from the PowerShell Cookbook, but it was very slow.
In the end, I created a .cmd script with the 7z pipes that I'm calling from my PowerShell script.

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