On 'nix based systems, this is a fundamental shell limitation, as others have said here. So, just suck it up. That said, it's really not that important because you don't often need backslashes in arguments on those platforms.
On Windows, however, backslashes are of critical value! A path ending in one would explicitly denote a directory vs a file. I have seen the documentation for MS C (see: https://learn.microsoft.com/en-us/previous-versions/17w5ykft(v=vs.85) ), and within the Python source (e.g. in subprocess.list2cmd
https://github.com/python/cpython/blob/master/Lib/subprocess.py), explaining this problem with quoting a process argument and have it not able to end with a backslash. So, I forgive the Python developers for keeping the logic the same - but not the MS C ones! This is not a cmd.exe shell issue or a universal limitation for arguments in Windows! (The caret ^
is the equivalent escape character in that natural shell.)
Batch Example (test.bat):
@echo off
echo 0: %0
echo 1: %1
echo 2: %2
echo 3: %3
Now execute it (via cmd.exe):
test.bat -t "C:\test\this path\" -v
Yields:
0: test.bat
1: -t
2: "C:\test\this path\"
3: -v
As you can see - a simple batch file implicitly understands what we want!
But... let's see what happens in Python, when using the standard argparse
module (https://docs.python.org/3/library/argparse.html), which is intertwined with sys.argv
initial parsing by default:
broken_args.py
import os
import argparse # pip install argparse
parser = argparse.ArgumentParser( epilog="DEMO HELP EPILOG" )
parser.add_argument( '-v', '--verbose', default=False, action='store_true',
help='enable verbose output' )
parser.add_argument( '-t', '--target', default=None,
help='target directory' )
args = parser.parse_args()
print( "verbose: %s" % (args.verbose,) )
print( "target: %s" % (os.path.normpath( args.target ),) )
Test that:
python broken_args.py -t "C:\test\this path\" -v
Yields these bad results:
verbose: False
target: C:\test\this path" -v
And so, here's how I solved this. The key "trick" is first fetching the full, raw command line for the process via the Windows api:
fixed_args.py
import sys, os, shlex
import argparse # pip install argparse
IS_WINDOWS = sys.platform.startswith( 'win' )
IS_FROZEN = getattr( sys, 'frozen', False )
class CustomArgumentParser( argparse.ArgumentParser ):
if IS_WINDOWS:
# override
def parse_args( self ):
def rawCommandLine():
from ctypes.wintypes import LPWSTR
from ctypes import windll
Kernel32 = windll.Kernel32
GetCommandLineW = Kernel32.GetCommandLineW
GetCommandLineW.argtypes = ()
GetCommandLineW.restype = LPWSTR
return GetCommandLineW()
NIX_PATH_SEP = '/'
commandLine = rawCommandLine().replace( os.sep, NIX_PATH_SEP )
skipArgCount = 1 if IS_FROZEN else 2
args = shlex.split( commandLine )[skipArgCount:]
return argparse.ArgumentParser.parse_args( self, args )
parser = CustomArgumentParser( epilog="DEMO HELP EPILOG" )
parser.add_argument( '-v', '--verbose', default=False, action='store_true',
help='enable verbose output' )
parser.add_argument( '-t', '--target', default=None,
help='target directory' )
args = parser.parse_args()
print( "verbose: %s" % (args.verbose,) )
print( "target: %s" % (os.path.normpath( args.target ),) )
Confirm the fix:
python fixed_args.py -t "C:\test\this path\" -v
Yields these good results:
verbose: True
target: C:\test\this path