Is there a way to prevent percent expansion of env variable in Windows command line?
Asked Answered
L

2

4

I'm using the following git command in git bash on Windows:

git log --format="%C(cyan)%cd%Creset %s" --date=short -5

It displays commit date (%cd) followed by commit message (%s). Commit date is wrapped with color markers: %C(cyan) to start colored output and %Creset to stop colored output.

While it works fine in git bash, it doesn't do well with cmd: %cd% is expanded by Windows shell into current working directory (equivalent of $PWD in bash).

Hence when that command is run via cmd, I see current working directory displayed instead of commit date in the first column! git bash:

2015-10-08 commit msg
2015-10-08 commit msg
2015-10-07 commit msg
2015-10-06 commit msg
2015-10-06 commit msg

cmd:

D:\git\someFolderCreset commit msg
D:\git\someFolderCreset commit msg
D:\git\someFolderCreset commit msg
D:\git\someFolderCreset commit msg
D:\git\someFolderCreset commit msg

Actually, I never use cmd directly myself, I found this behavior while writing a nodejs (0.12) script in which I had

require('child_process').execSync('git log --format=...', {stdio: 'inherit'})

which gets executed by node using the cmd when on Windows).

A simple workaround could be to introduce a space to prevent %cd% being found, i.e. change

git log --format="%C(cyan)%cd%Creset %s" --date=short -5

to

git log --format="%C(cyan)%cd %Creset%s" --date=short -5

However this introduced a redundant space (I deleted another space before %s but it's still a hack, and requires manual intervention).

Is there a way to prevent expansion by the Windows shell?

I've found information about using %% or ^% to escape the % but they are not the solution here:

# produces superfluous ^ characters
git log --format="%C(cyan)^%cd^%Creset %s" --date=short -5

^2015-10-08^ commit msg
^2015-10-08^ commit msg
^2015-10-07^ commit msg
^2015-10-06^ commit msg
^2015-10-06^ commit msg

# seems the expansion is done at command parse time
git log --format="%C(cyan)%%cd%%Creset %s" --date=short -5

%D:\git\someFolder commit msg
%D:\git\someFolder commit msg
%D:\git\someFolder commit msg
%D:\git\someFolder commit msg
%D:\git\someFolder commit msg

The ideal solution should be either compatible with bash and cmd, without producing redundant characters, or an escape function in javascript to escape the generic UNIX-y command for Windows to prevent the expansions (if such an escape function can be created).

Lamoreaux answered 8/10, 2015 at 12:40 Comment(3)
According to this %% works in BAT files but not in the command lineLamoreaux
ECHO git log --format=^"^%C^(cyan^)^%cd^%Creset ^%s^" --date=short -5 Escaped all CMD poisonous characters for ECHO command.Rene
@JosefZ: That works great on Windows, but not on Unix-like systems, where the ^ chars. are retained.Kennard
K
7

To provide an alternative to MC ND's helpful answer:

If you really need to get the shell involved (which is unlikely, because you state that you want the command to work both with Windows' cmd.exe and Bash), consider the solution immediately below; for a shell-less alternative that bypasses the problem, see the solution at the bottom.

Tip of the hat to MC ND for improving my original approach by suggesting placing double-quotes around % instances rather than the potential variable names between % instances, and for suggesting a clarification re execFileSync.


"Escaping" % chars. for cmd.exe

As stated in MC ND's answer, you cannot technically escape % at the Windows command prompt (inside batch files you can use %%, but that doesn't work when invoking shell commands from other environments such as Node.js, and generally wouldn't work across platforms).

However, the workaround is to place double-quotes around each % instance:

// Input shell command.
var shellCmd = 'git log --format="%C(cyan)%cd%Creset %s" --date=short -5'

// Place a double-quote on either end of each '%'
// This yields (not pretty, but it works):
//   git log --format=""%"C(cyan)"%"cd"%"Creset "%"s" --date=short -5
var escapedShellCmd = shellCmd.replace(/%/g, '"%"')

// Should work on both Windows and Unix-like platforms:
console.log(require('child_process').execSync(escapedShellCmd).toString())

The inserted double-quotes prevent cmd.exe from recognizing tokens such as %cd% as variable references ("%"cd"%" won't get expanded).

This works, because the extra double-quotes are ultimately stripped from the string when processed by the target program:

  • Windows: git.exe (presumably via the C runtime) then takes care of stripping the extra double-quotes from the combined string.

  • Unix-like (POSIX-like shells such as Bash): the shell itself takes care of removing the double-quotes before passing them to the target program.

    • Caveat: Using double-quotes in your command in general means that you need to watch out for POSIX-like shells performing potentially unwanted expansions on $-prefixed tokens (not an issue here); however, in order to remain Windows-compatible you must use double-quotes.

Technically, applying this technique to a double-quoted string breaks it into a sequence of double-quoted substrings interspersed with unquoted % instances. POSIX-like shells still recognize this as a single string - the substrings are double-quoted and directly abut the % instances. (If you apply the technique to an unquoted string, the logic is reversed: you're effectively splicing in double-quoted % instances.) The double-quotes around the substrings, which are considered syntactical elements rather than part of the string, are then removed when the substrings are joined together to form the single literal to pass to the target program.


Bypassing the problem by avoiding the shell altogether

Note: The following builds on execFile[Sync], which only works for calling external executables (which is true in the OP's case: git.exe) - by contrast, for calling shell builtins (internal commands) or Windows batch files, you cannot avoid exec[Sync] and thus interpretation by cmd.exe (on Windows).[1]

If you use execFileSync rather than execSync, the shell (cmd.exe on Windows) will NOT be involved and thus you needn't worry about escaping % chars. or any other shell metacharacters, for that matter:

require('child_process').execFileSync('git', 
   [ 'log', 
     '--format=%C(cyan)%cd%Creset %s',
     '--date=short',
     '-5' ], {stdio: 'inherit'})

Note how the arguments must be supplied individually as elements of an array, and without embedded quoting.


[1] On Windows, script files (e.g., Python scripts) cannot be called directly with execFile[Sync], but you can instead pass the interpreter executable (e.g., python) as the file to execute, and the script file as an argument. On Unix-like platforms you can call scripts directly, as long as they have a shebang line and are marked as executable.
execFile[Sync] can be used to invoke Windows batch files, but cmd.exe invariably interpolates the arguments, as with exec[Sync].

Kennard answered 8/10, 2015 at 21:35 Comment(0)
U
4

In short, you can not do it or at least there is not a general method

note I was wrong. mklement0's answer pointed a way for a general solution.

Of course, you can use ^, not to escape the percent signs (you can not escape the percent signs at command line level) but to separate the percent sign from the name of the variable so it is not detected as a variable to be expanded. That is, for your case, it is enough to use

git log --format="%C(cyan)%cd^%Creset %s" --date=short -5

but as you point the caret is not consumed by the parser and is included in the output string.

So, any "escape" characters will be included in the output both in cmd and bash, but it can be solved for this particular (or similar) case using

process.env.cd = '%cd%'
require('child_process').execSync(
    'git log --format="%C(cyan)%cd%Creset %s" --date=short -5'
    , {stdio: 'inherit'}
)

Basically, what the code does is assign a fake value to the cd environment variable, changing it to the literal %cd%, so, when the cmd parser executes the variable expansion, the correct value ends in the command line.

But, maybe (I have not checked how/what the git code does) changing the cd variable could interfere. So, also for this case, instead of changing the value of the cd variable you can use

process.env['C(cyan)'] = '%C(cyan)%';

Why? When the cmd parser processes the command line, it will match the start of the format string against the existing variable, do the expansion and in the same process consume the starting percent sign in the %cd% variable, so it is not expanded.

Undressed answered 8/10, 2015 at 18:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.