Замена процесса не разрешена подпроцессом Python с оболочкой = True?

Вот игрушечный пример подстановки процессов, который отлично работает в Bash:

$ wc -l <(pwd)
1 /proc/self/fd/11

Так почему же та же команда выдает синтаксическую ошибку при вызове из подпроцесса Python с shell=True?

>>> subprocess.check_call('wc -l <(pwd)', shell=True)
/bin/sh: 1: Syntax error: "(" unexpected
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/path/to/my/python/lib/python3.5/subprocess.py", line 581, in check_call
    raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command 'wc -l <(pwd)' returned non-zero exit status 2

person 1''    schedule 30.09.2016    source источник
comment
shell=True не использует Bash. Он использует /bin/sh. (Я не уверен, что это где-то настраивается.)   -  person user2357112 supports Monica    schedule 30.09.2016
comment
Да, это проблема. Спасибо!   -  person 1''    schedule 30.09.2016
comment
См. также stackoverflow.com/questions/5725296/ в чем именно отличия.   -  person tripleee    schedule 17.10.2017


Ответы (3)


/bin/sh: 1: Синтаксическая ошибка: "(" неожиданно

У вас есть башизм. Это недопустимо в соответствии с POSIX, который реализует /bin/sh.

person dsh    schedule 30.09.2016
comment
Спасибо! Решение состоит в том, чтобы вызвать check_call с помощью executable='/bin/bash'. - person 1''; 30.09.2016

Альтернативное решение — перенести большую часть кода оболочки на сам Python. Например:

from subprocess import Popen, PIPE, check_call

p1 = Popen(["pwd"], stdout=PIPE)
p2 = check_call(["wc", "-l"], stdin=p1.stdout)

Часто это может быть первым шагом на пути к полному устранению необходимости использовать subprocess, поскольку он разбивает работу на более мелкие фрагменты, для которых вам легче увидеть, как это сделать в самом Python.

person chepner    schedule 30.09.2016
comment
Тогда можно было бы сделать все это изначально; with open(".") as d: result = len(d.readlines()) хотя я думаю, что Python не позволит вам open создать каталог как таковой. - person tripleee; 06.01.2021
comment
Правильным способом Python будет что-то вроде sum(1 for _ in pathlib.Path.cwd().glob('*')). - person chepner; 06.01.2021
comment
Гм, нет, здесь учитывается количество файлов в каталоге, а не количество строк в имени каталога. Но это все-таки игрушечный пример. - person tripleee; 06.01.2021
comment
На самом деле len(os.getcwd().split('\n')), вероятно, лучше, чем моя первая попытка. - person tripleee; 06.01.2021
comment
Я никогда так внимательно не читал оригинал. Однако получение количества строк в имени кажется еще менее полезным, чем количество файлов в каталоге. - person chepner; 06.01.2021

Если вы хотите использовать функции Bash (массивы, подстановка команд, здесь строки, или множество других расширений и улучшений, не относящихся к POSIX), вам нужно явно переопределить оболочку по умолчанию:

subprocess.check_call(
    'wc -l <(pwd)',
    executable='/bin/bash',  # the beef
    shell=True)

или - несколько более неуклюже - запустить явный экземпляр Bash:

subprocess.check_call(
    ['/bin/bash', '-c', 'wc -l <(pwd)'])

Обратите внимание, что в последнем случае мы избегаем отдельного указания shell=True и передаем сценарий в виде списка строк (где третья строка представляет собой произвольно сложный и/или длинный сценарий в качестве аргумента bash -c).

(На самом деле существует ограничение по длине. Если ваша командная строка длиннее, чем константа ядра ARG_MAX, вам нужно вместо этого передать сценарий в файле или в качестве стандартного ввода в оболочку. В любой современной системе речь идет о мегабайтах сценария. , хотя.)

Запуск сложных сценариев оболочки (Bash или других) из Python в любом случае сомнительно; вы захотите как можно меньше делегировать subprocess и взять его оттуда в собственном коде Python.

person tripleee    schedule 04.01.2021