воскресенье, 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 что-нибудь поправят - а пока живём как живём. И такая ситуация будет сохраняться еще долго.

7 комментариев:

  1. if isinstance(args, types.StringTypes):
    args = ["/bin/sh", "-c", args]
    else:
    args = list(args)

    if executable is None:
    executable = args[0]

    а shell выкинуть на помойку

    ОтветитьУдалить
  2. Я рассказывал, как оно устроено — а не делился фантазиями на тему "как улучшить модуль subprocess". Собственно говоря, упомянутая в статье ссылка на баг как раз и предлагает убрать параметр shell.

    ОтветитьУдалить
  3. Это не баг, это фича.

    ОтветитьУдалить
  4. Использование shell=True и списка аргументов вполне осмысленно. Просто аргументы получает shell, а не команда, которую он выполняет.

    Пример:
    subprocess.Popen(['echo "$0: $@"', 'me', 'arg0', 'arg1'], shell=True)

    ОтветитьУдалить
  5. В этом случае лучше использовать
    subprocess.Popen(['/bin/sh', 'echo "$0: $@"', 'me', 'arg0', 'arg1'], shell=False)

    Нет?

    ОтветитьУдалить
  6. Я говорю про работоспособность. Тут не идёт речь о том, как лучше (красивее, правильнее...) делать в коде.

    >> Так уж /bin/sh устроен, что он напрочь игнорирует то, что идет после -c command.
    Я придрался к этому утверждению. Мой пример показывает, что таки не игнорирует, а использует. И как использует.

    А если уж говорить про практику использования, то с учётом того, что под разными OS shell может быть разным, я бы вообще не стал полагаться на $@-подобные переменные.

    ОтветитьУдалить
    Ответы
    1. Ммм. Lex, вы правы.
      Я ещё раз попытался сформулировать как оно работает в этом случае в рамках одного предложения. У меня пока не получилось.
      Можете подсказать, как поправить статью чтобы не потерять смысла и подчеркнуть этот часто встречающийся ляп?

      Про разные shell.
      Так уж вышло, что subprocess использует только /bin/sh и ничто другое.
      Как ни странно, сломалось только на Android: http://bugs.python.org/issue16255
      Надо бы починить к выходу 3.4

      Удалить