Когда-то для запуска процессов питон имел целый зоопарк различных 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
возможны четыре комбинации,
две из которых - не правильные. Рассмотрим их по порядку.
args
- список строк иshell=False
. Всё работает отлично.-
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'
! Вот так, целиком, без разделения на параметры (процедура, сама по себе неоднозначная в общем виде).То есть использовать этот способ попросту нельзя.
-
shell=True
,args
- строка. Опять всё хорошо. -
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 что-нибудь поправят - а пока живём как живём. И такая ситуация будет сохраняться еще долго.
if isinstance(args, types.StringTypes):
ОтветитьУдалитьargs = ["/bin/sh", "-c", args]
else:
args = list(args)
if executable is None:
executable = args[0]
а shell выкинуть на помойку
Я рассказывал, как оно устроено — а не делился фантазиями на тему "как улучшить модуль subprocess". Собственно говоря, упомянутая в статье ссылка на баг как раз и предлагает убрать параметр shell.
ОтветитьУдалитьЭто не баг, это фича.
ОтветитьУдалитьИспользование shell=True и списка аргументов вполне осмысленно. Просто аргументы получает shell, а не команда, которую он выполняет.
ОтветитьУдалитьПример:
subprocess.Popen(['echo "$0: $@"', 'me', 'arg0', 'arg1'], shell=True)
В этом случае лучше использовать
ОтветитьУдалитьsubprocess.Popen(['/bin/sh', 'echo "$0: $@"', 'me', 'arg0', 'arg1'], shell=False)
Нет?
Я говорю про работоспособность. Тут не идёт речь о том, как лучше (красивее, правильнее...) делать в коде.
ОтветитьУдалить>> Так уж /bin/sh устроен, что он напрочь игнорирует то, что идет после -c command.
Я придрался к этому утверждению. Мой пример показывает, что таки не игнорирует, а использует. И как использует.
А если уж говорить про практику использования, то с учётом того, что под разными OS shell может быть разным, я бы вообще не стал полагаться на $@-подобные переменные.
Ммм. Lex, вы правы.
УдалитьЯ ещё раз попытался сформулировать как оно работает в этом случае в рамках одного предложения. У меня пока не получилось.
Можете подсказать, как поправить статью чтобы не потерять смысла и подчеркнуть этот часто встречающийся ляп?
Про разные shell.
Так уж вышло, что subprocess использует только /bin/sh и ничто другое.
Как ни странно, сломалось только на Android: http://bugs.python.org/issue16255
Надо бы починить к выходу 3.4