воскресенье, 27 марта 2011 г.

Интересная особенность использования subprocess

Когда-то для запуска процессов питон имел целый зоопарк различных popen* функций и классов, разбросанных по модулям os и popen2.

К счастью, начиная с версии 2.5 этот бардак был выброшен - остался единственный модуль subprocess, который и делает всю работу.

Статья относится только к POSIX системам - у Windows свои тараканы.

Поехали

Итак, запускаем процесс:

import subprocess

p = subprocess.Popen(args, shell=use_shell)

args может быть или строкой или списком строк, shell принимает False (значение по умолчанию) или True. Остальные многочисленные параметры сейчас не важны.

А теперь, внимание, вопрос: как это работает?

Привожу кусочек кода из subprocess.py, ответственный за подготовку параметров для вызова fork_exec (в разных версиях питона вызов подпроцесса делается чуть по разному - но сам принцип и приведенный ниже код не меняются):

if isinstance(args, types.StringTypes):
    args = [args]
else:
    args = list(args)

if shell:
    args = ["/bin/sh", "-c"] + args

if executable is None:
    executable = args[0]

Смотрим. Читаем еще раз, внимательно. До меня, например, дошло не с первой попытки.

Для рассматриваемых параметров args и shell возможны четыре комбинации, две из которых - не правильные. Рассмотрим их по порядку.

  1. args - список строк и shell=False. Всё работает отлично.
  2. shell=False, а args - строка. Следите за руками: строка превращается в список из одного элемента - этой самой строки args. Затем executable становится равным этому args. Всё ещё непонятно? Тогда пример:

    >>> p = subprocess.Popen('wc 1.fb2 -l')
    Traceback (most recent call last):
      ...
    OSError: [Errno 2] No such file or directory
    

    Файл, которого нет - это 'wc 1.fb2 -l'! Вот так, целиком, без разделения на параметры (процедура, сама по себе неоднозначная в общем виде).

    То есть использовать этот способ попросту нельзя.

  3. shell=True, args - строка. Опять всё хорошо.

  4. shell=True, args - список строк. Снова внимательно смотрим: shell запускает первый аргумент списка в качестве параметра. Остальные - в пролете.

    Т.е. ['ls', '-la'] транслируется в ['/bin/sh' '-c', 'ls', '-la']. Так уж /bin/sh устроен, что он напрочь игнорирует то, что идет после -c command. Не верите - проверьте сами. Правильная запись должна быть ['/bin/sh' '-c', 'ls -la'], что и получается когда args передаются строкой.

    Еще один вариант, который никогда не должен встречаться в программном коде.

Итог

Возможны только две комбинации параметров. Используйте список если запускаете без shell или строку и shell:

p = subpocess.Popen(['ls', '-la'], shell=False)
p = subpocess.Popen('ls -la', shell=True)

Нарушение правила ведет к трудно отлавливаемым ошибкам.

В довесок - ссылка на баг в bugs.python.org. Надеюсь, к версии 3.3 что-нибудь поправят - а пока живём как живём. И такая ситуация будет сохраняться еще долго.