Commits

Anonymous committed 590238d

Added quoting of script arguments and extended the quoting logic to
handle embedded quotes.

Added support for passing a single argument on the shebang line to
pass things like -O and -i.

Fixed bug in handling trailing whitespace in Python command.

  • Participants
  • Parent commits 35fb3f0
  • Branches setuptools-0.6

Comments (0)

Files changed (5)

     fprintf(stderr, format, data);
     return 2;
 }
+
 char *quoted(char *data) {
-    char *result = calloc(strlen(data)+3,sizeof(char));
-    strcat(result,"\""); strcat(result,data); strcat(result,"\"");
+    int i, l = strlen(data), nb;
+    /* We allocate twice as much space as needed to deal with worse-case
+       of having to escape everything. */
+    char *result = calloc(l*2+3, sizeof(char));
+    char *presult = result;
+
+    *presult++ = '"';
+    for (nb=0, i=0; i < l; i++)
+      {
+        if (data[i] == '\\')
+          nb += 1;
+        else if (data[i] == '"')
+          {
+            for (; nb > 0; nb--)
+              *presult++ = '\\';
+            *presult++ = '\\';
+          }
+        else
+          nb = 0;
+        *presult++ = data[i];
+      }
+    for (; nb > 0; nb--)        /* Deal w trailing slashes */
+      *presult++ = '\\';
+
+    *presult++ = '"';
+    *presult++ = 0;
     return result;
 }
 
+char *getpyopt(char *python)
+{
+  /* Search a Python command string, read from a #! line for an
+     option.  An option must be separated from an executable name by
+     one or more spaces.  An option consistes of a hyphen followed by
+     one or more letters.
+   */
+  static char *letters = 
+    "abcdefghijklmnopqrstuvwxyz"
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+    ;
+  char *p = python + strlen(python) - 1;
+  if (strchr(letters, *p) == NULL)
+    return NULL; /* Path doen't end with a letter. Odd. */
+  while (p > python && strchr(letters, *p) != NULL)
+    p--;
+  if (p == python || *p != '-')
+    return NULL;                /* Can't be an option */
+  p--;
+  if (p > python && isspace(*p))
+    { /* BINGO, we have an option */
+      char *pyopt = p+1;
+      /* strip trailing spaces from remainder of python command */
+      while (p > python && isspace(*p))
+        *p-- = '\0';
+      return pyopt;
+    }
+  else
+    return NULL;
+}
+
 int run(int argc, char **argv, int is_gui) {
 
     char python[256];   /* python executable's filename*/
+    char *pyopt;        /* Python option */
     char script[256];   /* the script's filename */
 
     HINSTANCE hPython;  /* DLL handle for python executable */
     int scriptf;        /* file descriptor for script file */
 
-    char **newargs;     /* argument array for exec */
+    char **newargs, **newargsp; /* argument array for exec */
     char *ptr, *end;    /* working pointers for string manipulation */
+    int i;              /* loop counter */
 
     /* compute script name from our .exe name*/
     GetModuleFileName(NULL, script, sizeof(script));
             *ptr='\\';  /* convert slashes to avoid LoadLibrary crashes... */
     }
 
-    *ptr = '\0';
+    *ptr-- = '\0';
     while (ptr>python && isspace(*ptr)) *ptr-- = '\0';  /* strip trailing sp */
     if (strncmp(python, "#!", 2)) {
         /* default to python.exe if no #! header */
         strcpy(python, "#!python.exe");
     }
 
+    /* Check for Python options */
+    pyopt = getpyopt(python);
+
     /* At this point, the python buffer contains "#!pythonfilename" */
 
     /* Using spawnv() can fail strangely if you e.g. find the Cygwin
 
     /* printf("Python executable: %s\n", python); */
 
-    /* Argument array needs to be argc+1 for args, plus 1 for null sentinel */
-    newargs = (char **)calloc(argc+2, sizeof(char *));
-    newargs[0] = quoted(python);
-    newargs[1] = quoted(script);
-    memcpy(newargs+2, argv+1, (argc-1)*sizeof(char *));
-    newargs[argc+1] = NULL;
+    /* Argument array needs to be 
+       argc+1 for python executable,
+       plus 1 for possible python opts,
+       plus 1 for null sentinel */
+    newargs = (char **)calloc(argc+3, sizeof(char *));
+    newargsp = newargs;
+    *newargsp++ = quoted(python);
+    if (pyopt)
+      *newargsp++ = pyopt;
+    *newargsp++ = quoted(script);
+    for (i = 1; i < argc; i++)
+      *newargsp++ = quoted(argv[i]);
+    *newargsp++ = NULL;
 
     /* printf("args 0: %s\nargs 1: %s\n", newargs[0], newargs[1]); */
     if (is_gui) {

setuptools/cli.exe

Binary file modified.

setuptools/gui.exe

Binary file modified.

setuptools/tests/__init__.py

 import sys, os.path
 
 def additional_tests():
-    import doctest
-    return doctest.DocFileSuite(
-        'api_tests.txt', optionflags=doctest.ELLIPSIS, package='pkg_resources',
-    )
-
+    import doctest, unittest
+    suite = unittest.TestSuite((
+        doctest.DocFileSuite(
+            'api_tests.txt',
+            optionflags=doctest.ELLIPSIS, package='pkg_resources',
+            ),
+        ))
+    if sys.platform == 'win32':
+        suite.addTest(doctest.DocFileSuite('win_script_wrapper.txt'))
+    return suite
 
 def makeSetup(**args):
     """Return distribution from 'setup(**args)', without executing commands"""

setuptools/tests/win_script_wrapper.txt

+Python Script Wrapper for Windows
+=================================
+
+setuptools includes wrappers for Python scripts that allows them to be
+executed like regular windows programs.  There are 2 wrappers, once
+for command-line programs, cli.exe, and one for graphica programs,
+gui.exe.  These programs are almost identical, function pretty much
+the same way, and are generated from the same source file.  In this
+document, we'll demonstrate use of the command-line program only.  The
+wrapper programs are used by copying them to the directory containing
+the script they are to wrap and with the same name as the script they
+are to wrap.  In the rest of this document, we'll give an example that
+will illustrate this.
+
+Let's create a simple script, foo-script.py:
+
+    >>> import os, sys, tempfile
+    >>> sample_directory = tempfile.mkdtemp()
+    >>> open(os.path.join(sample_directory, 'foo-script.py'), 'w').write(
+    ... """#!%(python_exe)s
+    ... import sys
+    ... input = repr(sys.stdin.read())
+    ... print sys.argv[0][-14:]
+    ... print sys.argv[1:]
+    ... print input
+    ... if __debug__:
+    ...     print 'non-optimized'
+    ... """ % dict(python_exe=sys.executable))
+
+Note that the script starts with a Unix-style '#!' line saying which
+Python executable to run.  The wrapper will use this to find the
+correct Python executable.
+
+We'll also copy cli.exe to the sample-directory with the name foo.exe:
+
+    >>> import pkg_resources
+    >>> open(os.path.join(sample_directory, 'foo.exe'), 'wb').write(
+    ...     pkg_resources.resource_string('setuptools', 'cli.exe')
+    ...     )
+
+When the copy of cli.exe, foo.exe in this example, runs, it examines
+the path name it was run with and computes a Python script path name
+by removing the '.exe' suffic and adding the '-script.py' suffix. (For
+GUI programs, the suffix '-script-pyw' is added.)  This is why we
+named out script the way we did.  Now we can run out script by running
+the wrapper:
+
+    >>> import os
+    >>> input, output = os.popen4(os.path.join(sample_directory, 'foo.exe')
+    ...               + r' arg1 "arg 2" "arg \"2\\\"" "arg 4\\" "arg5 a\\b')
+    >>> input.write('hello\nworld\n')
+    >>> input.close()
+    >>> print output.read(),
+    \foo-script.py
+    ['arg1', 'arg 2', 'arg "2\\"', 'arg 4\\', 'arg5 a\\\\b']
+    'hello\nworld\n'
+    non-optimized
+
+This example was a little pathological in that it exercised windows
+(MS C runtime) quoting rules:
+
+- Strings containing spaces are surrounded by double quotes.
+
+- Double quotes in strings need to be escaped by preceding them with
+  back slashes.
+
+- One or more backslashes preceding double quotes quotes need to be
+  escaped by preceding each of them them with back slashes.
+
+Specifying Python Command-line Options
+--------------------------------------
+
+You can specify a single argument on the '#!' line.  This can be used
+to specify Python options like -O, to run in optimized mode or -i
+to start the interactive interpreter.  You can combine multiple
+options as usual. For example, to run in optimized mode and 
+enter the interpreter after running the script, you could use -Oi:
+
+    >>> open(os.path.join(sample_directory, 'foo-script.py'), 'w').write(
+    ... """#!%(python_exe)s  -Oi  
+    ... import sys
+    ... input = repr(sys.stdin.read())
+    ... print sys.argv[0][-14:]
+    ... print sys.argv[1:]
+    ... print input
+    ... if __debug__:
+    ...     print 'non-optimized'
+    ... sys.ps1 = '---'
+    ... """ % dict(python_exe=sys.executable))
+
+    >>> input, output = os.popen4(os.path.join(sample_directory, 'foo.exe'))
+    >>> input.close()
+    >>> print output.read(),
+    \foo-script.py
+    []
+    ''
+    ---
+
+
+We're done with the sample_directory:
+
+    >>> import shutil
+    >>> shutil.rmtree(sample_directory)