Run a command with a space in it

Introduction

It's an unfortunate fact of life that the standard place for applications between Win95 and WinXP is a folder called "program files" (in English-locale versions, anyway). It's unfortunate because Windows doesn't always handle spaces in filenames particularly well. So if, for example, you wanted to run the Adobe Acrobat Reader from its standard location and read in a test pdf from the same area, how do you get around the fact that the executable and the document filenames both have embedded spaces?

Old-Style: os.system

The os.system call simply calls the underlying system function from the MS C runtime. This doesn't handle quotes terribly gracefully, and you have to double-double quote to be sure of getting it right. Thanks to joep for highlighting this in a post to the python-list.

import os, sys
import shutil
import win32api

#
# Create a folder in the temp area with a space
# in it; copy the notepad executable there and
# create a text file which includes the location.
#
temp = os.environ['TEMP']
folder_name = os.path.join (temp, "abc def")
os.mkdir (folder_name)
doc_name = os.path.join (folder_name, "with-space.txt")
executable_name = os.path.join (folder_name, "with-space.exe")

open (doc_name, "w").write ("This is in %s" % doc_name)
_, executable = win32api.FindExecutable (doc_name)
#
# The doubling up seems necessary on Win2K where the
# first call doesn't return the full path name of,
# say, notepad.exe.
#
_, executable = win32api.FindExecutable (executable)
shutil.copyfile (executable, executable_name)

#
# This won't work, despite the fact that both the
# executable and document paths are both double-quoted.
#
print
cmd = '"%s" "%s"' % (executable_name, doc_name)
print "Running:", cmd
os.system (cmd)

#
# This will work, since the executable and document paths
# are both double-quoted and the whole string is double-quoted
# again.
#
print
cmd = '""%s" "%s""' % (executable_name, doc_name)
print "Running:", cmd
os.system (cmd)

shutil.rmtree (folder_name)

New-style: subprocess.call

Fortunately, there is sanity at hand. If you simply use the subprocess module, it takes care of the various special cases needed here. The module-level convenience function .call is subprocess' answer to os.system and takes either a single string or a list of strings as its parameter.

At present the approach below will still fail if you pass shell=True to the .call function. You probably don't need shell=True unless you're calling a builtin command such as dir or copy. Try it without. (See issue 2304 for details).

import subprocess
import os, sys
import shutil
import win32api

#
# Create a folder in the temp area with a space
# in it; copy the notepad executable there and
# create a text file which includes the location.
#
temp = os.environ['TEMP']
folder_name = os.path.join (temp, "abc def")
os.mkdir (folder_name)
doc_name = os.path.join (folder_name, "with-space.txt")
executable_name = os.path.join (folder_name, "with-space.exe")

open (doc_name, "w").write ("This is in %s" % doc_name)
_, executable = win32api.FindExecutable (doc_name)
#
# The doubling up seems necessary on Win2K where the
# first call doesn't return the full path name of,
# say, notepad.exe.
#
_, executable = win32api.FindExecutable (executable)
shutil.copyfile (executable, executable_name)

print
#
# This will work...
# (remember to close Notepad to let the next test proceed)
#
subprocess.call ("%s %s" % (executable_name, doc_name))

print
#
# ... and so will this
#
subprocess.call ([executable_name, doc_name])

shutil.rmtree (folder_name)