Запуск команд Bash в Python
На моей локальной машине я запускаю скрипт python, который содержит эту строку
bashCommand = "cwm --rdf test.rdf --ntriples > test.nt"
os.system(bashCommand)
Это отлично работает.
Затем я запускаю тот же код на сервере и получаю следующее сообщение об ошибке
'import site' failed; use -v for traceback
Traceback (most recent call last):
File "/usr/bin/cwm", line 48, in <module>
from swap import diag
ImportError: No module named swap
Тогда я вставил "print bashCommand", которая печатает меня, а не команду в терминале, прежде чем он запускает ее с os.system().
Конечно, я снова получаю ошибку (вызванную os.system(bashCommand)), но перед этой ошибкой она печатает команду в терминале. Затем я просто скопировал этот вывод и сделал копию вставки в терминал и нажал Enter, и это работает...
Кто-нибудь знает, что происходит?
12 ответов
Чтобы немного расширить предыдущие ответы, есть ряд деталей, которые обычно упускаются из виду.
- предпочитать
subprocess.run()
надsubprocess.check_call()
и друзья прошлиsubprocess.call()
надsubprocess.Popen()
надos.system()
надos.popen()
- Понять и, вероятно, использовать
text=True
акаuniversal_newlines=True
, - Понять значение
shell=True
или жеshell=False
и как это изменяет цитирование и доступность удобства оболочки. - Понять различия между
sh
и Баш - Понять, как подпроцесс отделен от своего родителя и, как правило, не может изменить родителя.
- Избегайте запуска интерпретатора Python как подпроцесса Python.
Эти темы рассматриваются более подробно ниже.
предпочитать subprocess.run()
или же subprocess.check_call()
subprocess.Popen()
Функция - это рабочая лошадка низкого уровня, но ее сложно использовать правильно, и вы в конечном итоге копируете / вставляете несколько строк кода... которые обычно уже существуют в стандартной библиотеке как набор высокоуровневых функций-оболочек для различных целей, которые более подробно представлены ниже.
Вот параграф из документации:
Рекомендуемый подход к вызову подпроцессов заключается в использовании
run()
функция для всех случаев использования, которые он может обрабатывать. Для более сложных вариантов использованияPopen
Интерфейс может быть использован напрямую.
К сожалению, доступность этих функций-оболочек отличается в разных версиях Python.
subprocess.run()
был официально представлен в Python 3.5. Он предназначен для замены всего следующего.subprocess.check_output()
был введен в Python 2.7 / 3.1. Это в основном эквивалентноsubprocess.run(..., check=True, stdout=subprocess.PIPE).stdout
subprocess.check_call()
был введен в Python 2.5. Это в основном эквивалентноsubprocess.run(..., check=True)
subprocess.call()
был введен в Python 2.4 в оригиналеsubprocess
модуль ( ПКП-324). Это в основном эквивалентноsubprocess.run(...).returncode
API высокого уровня против subprocess.Popen()
Реорганизованный и расширенный subprocess.run()
более логичен и более универсален, чем старые устаревшие функции, которые он заменяет. Возвращает CompletedProcess
объект, который имеет различные методы, которые позволяют вам получить состояние завершения, стандартный вывод и несколько других результатов и индикаторов состояния из готового подпроцесса.
subprocess.run()
это путь, если вам просто нужна программа для запуска и возврата управления в Python. Для более сложных сценариев (фоновые процессы, возможно, с интерактивным вводом-выводом с родительской программой Python) вам все равно нужно использовать subprocess.Popen()
и позаботься обо всей сантехнике самостоятельно. Это требует довольно сложного понимания всех движущихся частей и не должно быть предпринято легко. Проще Popen
Объект представляет (возможно, все еще работающий) процесс, которым нужно управлять из вашего кода в течение оставшейся части времени жизни подпроцесса.
Следует, наверное, подчеркнуть, что просто subprocess.Popen()
просто создает процесс. Если вы оставите все как есть, у вас будет подпроцесс, работающий одновременно с Python, так что это "фоновый" процесс. Если ему не нужно вводить или выводить или иным образом координировать с вами, он может выполнять полезную работу параллельно с вашей программой на Python.
избежать os.system()
а также os.popen()
С незапамятных времен (ну, начиная с Python 2.5) os
документация модуля содержала рекомендацию предпочесть subprocess
над os.system()
:
subprocess
модуль предоставляет более мощные средства для порождения новых процессов и получения их результатов; использование этого модуля предпочтительнее, чем использование этой функции.
Проблемы с system()
в том, что он явно зависит от системы и не предлагает способов взаимодействия с подпроцессом. Он просто работает со стандартным выводом и стандартной ошибкой вне досягаемости Python. Единственная информация, которую Python получает обратно, - это состояние выхода команды (ноль означает успех, хотя значение ненулевых значений также в некоторой степени зависит от системы).
PEP-324 (который уже упоминался выше) содержит более подробное обоснование того, почему os.system
проблематично и как subprocess
попытки решить эти проблемы.
os.popen()
еще сильнее обескуражен:
Устаревший с версии 2.6: эта функция устарела. Использовать
subprocess
модуль.
Понять и обычно использовать check=True
Вы также заметите, что subprocess.call()
имеет много тех же ограничений, что и os.system()
, При регулярном использовании вы должны обычно проверять, завершился ли процесс успешно, что subprocess.check_call()
а также subprocess.check_output()
do (где последний также возвращает стандартный вывод готового подпроцесса). Точно так же вы должны обычно использовать check=True
с subprocess.run()
если вам специально не нужно разрешить подпроцессу возвращать статус ошибки.
На практике с check=True
или же subprocess.check_*
Питон бросит CalledProcessError
исключение, если подпроцесс возвращает ненулевой статус выхода.
Распространенная ошибка с subprocess.run()
это опустить check=True
и удивляйтесь, когда последующий код завершится неудачей, если произошел сбой подпроцесса.
С другой стороны, общая проблема с check_call()
а также check_output()
было то, что пользователи, которые слепо использовали эти функции, были удивлены, когда возникло исключение, например, когда grep
не нашел соответствия. (Вам, вероятно, следует заменить grep
в любом случае с собственным кодом Python, как описано ниже.)
После всего, вам нужно понять, как команды оболочки возвращают код выхода и при каких условиях они возвращают ненулевой (ошибочный) код выхода, и принимать осознанное решение, как именно его следует обрабатывать.
Понять и, вероятно, использовать text=True
ака universal_newlines=True
Начиная с Python 3, строки внутри Python являются строками Unicode. Но нет никакой гарантии, что подпроцесс генерирует выходные данные Unicode или строки вообще.
В глубине души Python должен получить bytes
буфера и интерпретировать это как-то. Если он содержит двоичный объект двоичных данных, его не следует декодировать в строку Unicode, потому что это поведение подвержено ошибкам и вызывает ошибки - именно такое надоедливое поведение, которое пронизывало многие скрипты Python 2, до того, как появился способ правильно различать закодированный текст и двоичные данные.
С text=True
вы говорите Python, что фактически ожидаете возврата текстовых данных в кодировке системы по умолчанию, и что они должны быть декодированы в строку Python (Unicode) в меру возможностей Python (обычно UTF-8 в любой умеренно обновленной версии) система, разве что винда?)
Если это не то, что вы просите вернуть, Python просто даст вам bytes
строки в stdout
а также stderr
строки. Возможно, в какой-то момент вы знаете, что они были текстовыми строками, и вы знаете их кодировку. Затем вы можете декодировать их.
normal = subprocess.run([external, arg],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=True,
text=True)
print(normal.stdout)
convoluted = subprocess.run([external, arg],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=True)
# You have to know (or guess) the encoding
print(convoluted.stdout.decode('utf-8'))
В Python 3.7 появился более короткий и более понятный и понятный псевдоним text
для ключевого аргумента, который ранее несколько вводил в заблуждение universal_newlines
,
Понимаю shell=True
против shell=False
С shell=True
вы передаете одну строку в вашу оболочку, а оболочка берет ее оттуда.
С shell=False
Вы передаете список аргументов в ОС, минуя оболочку.
Когда у вас нет оболочки, вы сохраняете процесс и избавляетесь от довольно существенного количества скрытой сложности, которая может содержать или не содержать ошибки или даже проблемы с безопасностью.
С другой стороны, когда у вас нет оболочки, у вас нет перенаправления, подстановочного знака, управления заданиями и большого количества других функций оболочки.
Распространенной ошибкой является использование shell=True
а затем все еще передать Python список токенов, или наоборот. В некоторых случаях это срабатывает, но на самом деле плохо определено и может сломаться интересными способами.
# XXX AVOID THIS BUG
buggy = subprocess.run('dig +short stackru.com')
# XXX AVOID THIS BUG TOO
broken = subprocess.run(['dig', '+short', 'stackru.com'],
shell=True)
correct = subprocess.run(['dig', '+short', 'stackru.com'],
# Probably don't forget these, too
check=True, text=True)
# XXX Probably better avoid shell=True
# but this is nominally correct
fixed_but_fugly = subprocess.run('dig +short stackru.com',
shell=True,
# Probably don't forget these, too
check=True, text=True)
Общая реплика "но это работает для меня" не является полезным опровержением, если вы точно не понимаете, при каких обстоятельствах она может перестать работать.
Пример рефакторинга
Очень часто функции оболочки могут быть заменены собственным кодом Python. Простой Awk или sed
сценарии, вероятно, должны просто быть переведены на Python.
Чтобы частично проиллюстрировать это, вот типичный, но немного глупый пример, который включает в себя множество функций оболочки.
cmd = '''while read -r x;
do ping -c 3 "$x" | grep 'round-trip min/avg/max'
done <hosts.txt'''
# Trivial but horrible
results = subprocess.run(
cmd, shell=True, universal_newlines=True, check=True)
print(results.stdout)
# Reimplement with shell=False
with open('hosts.txt') as hosts:
for host in hosts:
host = host.rstrip('\n') # drop newline
ping = subprocess.run(
['ping', '-c', '3', host]
text=True,
stdout=subprocess.PIPE,
check=True)
for line in ping.stdout.split('\n'):
if 'round-trip min/avg/max' in line:
print('{}: {}'.format(host, line))
Некоторые вещи, чтобы отметить здесь:
- С
shell=False
вам не нужно цитировать, что оболочка требует вокруг строк. В любом случае помещать кавычки - это ошибка. - Часто имеет смысл запускать как можно меньше кода в подпроцесс. Это дает вам больше контроля над выполнением из вашего кода Python.
- Сказав это, сложные конвейеры оболочки утомительны и иногда сложны для повторной реализации в Python.
Реорганизованный код также показывает, насколько действительно полезна оболочка для вас с очень кратким синтаксисом - к лучшему или к худшему. Python говорит, что явное лучше, чем неявное, но код Python довольно многословен и, возможно, выглядит более сложным, чем на самом деле. С другой стороны, он предлагает ряд точек, где вы можете захватить контроль посреди чего-то другого, что тривиально иллюстрируется улучшением, которое мы можем легко включать в себя имя хоста вместе с выводом команды оболочки. (Это ни в коем случае не является сложной задачей в оболочке, но за счет еще одной утечки и, возможно, другого процесса.)
Общие конструкции оболочки
Для полноты изложения приведем краткие пояснения некоторых из этих функций оболочки и некоторые примечания о том, как их можно заменить собственными средствами Python.
- Расширение Globbing aka wildcard можно заменить на
glob.glob()
или очень часто с простыми сравнениями строк Python, такими какfor file in os.listdir('.'): if not file.endswith('.png'): continue
, Bash имеет различные другие возможности расширения, такие как.{png,jpg}
расширение скобки и{1..100}
а также расширение тильды (~
расширяется до вашего домашнего каталога, и в более общем плане~account
в домашний каталог другого пользователя) - Перенаправление позволяет вам читать из файла в качестве стандартного ввода и записывать стандартный вывод в файл.
grep 'foo' <inputfile >outputfile
открываетoutputfile
для написания иinputfile
для чтения и передает его содержимое в качестве стандартного ввода вgrep
, чья стандартная продукция затем попадает вoutputfile
, Обычно это не сложно заменить на собственный код Python. - Трубопроводы - это форма перенаправления.
echo foo | nl
запускает два подпроцесса, где стандартный выводecho
это стандартный вводnl
(на уровне ОС, в Unix -подобных системах это дескриптор одного файла). Если вы не можете заменить один или оба конца конвейера собственным кодом Python, возможно, в конце концов подумайте об использовании оболочки, особенно если конвейер содержит более двух или трех процессов (хотя посмотрите наpipes
модуль в стандартной библиотеке Python или ряд более современных и универсальных сторонних конкурентов). - Управление заданиями позволяет прерывать задания, запускать их в фоновом режиме, возвращать их на передний план и т. Д. Основные сигналы Unix для остановки и продолжения процесса, конечно же, доступны и в Python. Но задания - это высокоуровневая абстракция в оболочке, которая включает группы процессов и т. Д., Которые вы должны понимать, если хотите сделать что-то подобное из Python.
Понять различия между sh
и Баш
subprocess
запускает ваши команды оболочки с /bin/sh
если вы специально не запрашиваете иное (за исключением, конечно, в Windows, где он использует значение COMSPEC
переменная). Это означает, что различные функции Bash-only, такие как массивы, [[
и т. д. не доступны.
Если вам нужно использовать синтаксис Bash-only, вы можете передать путь к оболочке как executable='/bin/bash'
(где, конечно, если ваш Bash установлен где-то еще, вам нужно настроить путь).
subprocess.run('''
# This for loop syntax is Bash only
for((i=1;i<=$#;i++)); do
# Arrays are Bash-only
array[i]+=123
done''',
shell=True, check=True,
executable='/bin/bash')
subprocess
отделен от своего родителя и не может его изменить
Несколько распространенная ошибка - делать что-то вроде
subprocess.run('foo=bar', shell=True)
subprocess.run('echo "$foo"', shell=True) # Doesn't work
который, помимо недостатка элегантности, также выдает принципиальное непонимание "под" части названия "подпроцесс".
Дочерний процесс выполняется полностью отдельно от Python, и когда он завершается, Python не имеет представления о том, что он сделал (кроме неопределенных индикаторов, которые он может вывести из состояния выхода и вывода из дочернего процесса). Ребенок обычно не может изменить окружающую среду родителя; он не может установить переменную, изменить рабочий каталог или, во многих словах, связаться со своим родителем без сотрудничества с родителем.
Непосредственное исправление в этом конкретном случае заключается в запуске обеих команд в одном подпроцессе;
subprocess.run('foo=bar; echo "$foo"', shell=True)
хотя очевидно, что этот конкретный вариант использования вообще не требует оболочки. Помните, что вы можете манипулировать средой текущего процесса (и, следовательно, его дочерних элементов) через
os.environ['foo'] = 'bar'
или передать настройку среды дочернему процессу с
subprocess.run('echo "$foo"', shell=True, env={'foo': 'bar'})
(не говоря уже об очевидном рефакторинге subprocess.run(['echo', 'bar'])
; но echo
конечно, плохой пример того, что нужно запускать в подпроцессе).
Не запускайте Python из Python
Это немного сомнительный совет; безусловно, существуют ситуации, когда это имеет смысл или даже является абсолютным требованием для запуска интерпретатора Python как подпроцесса из сценария Python. Но очень часто правильным подходом является просто import
другой модуль Python в ваш вызывающий скрипт и вызовите его функции напрямую.
Если другой скрипт Python находится под вашим контролем и не является модулем, рассмотрите возможность его преобразования в один. (Этот ответ уже слишком длинный, поэтому я не буду вдаваться в подробности.)
Если вам нужен параллелизм, вы можете запускать функции Python в подпроцессах с помощью multiprocessing
модуль. Существует также threading
который выполняет несколько задач в одном процессе (который является более легким и дает больше контроля, но также более ограничен в том, что потоки в процессе тесно связаны и связаны с одним GIL.)
Не использовать os.system
, Это устарело в пользу подпроцесса. Из документации: "Этот модуль намеревается заменить несколько более старых модулей и функций: os.system
, os.spawn
".
Как в вашем случае:
bashCommand = "cwm --rdf test.rdf --ntriples > test.nt"
import subprocess
process = subprocess.Popen(bashCommand.split(), stdout=subprocess.PIPE)
output, error = process.communicate()
Назовите это с подпроцессом
import subprocess
subprocess.Popen("cwm --rdf test.rdf --ntriples > test.nt")
Похоже, ошибка, которую вы получаете, потому что на сервере нет модуля подкачки, вы должны установить на сервере своп, а затем снова запустить скрипт
Возможно, вы используете программу bash с параметром -c для выполнения команд:
bashCommand = "cwm --rdf test.rdf --ntriples > test.nt"
output = subprocess.check_output(['bash','-c', bashCommand])
Вы можете использовать "подпроцесс", но я всегда чувствовал, что это не "Pythonic" способ сделать это. Поэтому я создал Sultan (бесстыдный плагин), который позволяет легко запускать функции командной строки.
Также вы можете использовать "os.popen". Пример:
import os
command = os.popen('ls -al')
print(command.read())
print(command.close())
Выход:
total 16
drwxr-xr-x 2 root root 4096 ago 13 21:53 .
drwxr-xr-x 4 root root 4096 ago 13 01:50 ..
-rw-r--r-- 1 root root 1278 ago 13 21:12 bot.py
-rw-r--r-- 1 root root 77 ago 13 21:53 test.py
None
В соответствии с ошибкой на сервере отсутствует пакет с именем swap. это /usr/bin/cwm
требует этого. Если вы используете Ubuntu/Debian, установите python-swap
используя способности.
Чтобы запустить команду без оболочки, передайте команду в виде списка и выполните перенаправление в Python, используя [subprocess]
:
#!/usr/bin/env python
import subprocess
with open('test.nt', 'wb', 0) as file:
subprocess.check_call("cwm --rdf test.rdf --ntriples".split(),
stdout=file)
Примечание: нет > test.nt
в конце. stdout=file
реализует перенаправление.
Чтобы запустить команду с использованием оболочки в Python, передайте команду в виде строки и включите shell=True
:
#!/usr/bin/env python
import subprocess
subprocess.check_call("cwm --rdf test.rdf --ntriples > test.nt",
shell=True)
Вот эта оболочка отвечает за перенаправление вывода (> test.nt
в команде).
Чтобы запустить команду bash, которая использует bashisms, укажите явно исполняемый файл bash, например, для эмуляции подстановки процесса bash:
#!/usr/bin/env python
import subprocess
subprocess.check_call('program <(command) <(another-command)',
shell=True, executable='/bin/bash')
скопируйте и вставьте это:
def run_bash_command(cmd: str) -> Any:
import subprocess
process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
output, error = process.communicate()
if error:
raise Exception(error)
else:
return output
subprocess.Popen()
предпочтительнее
os.system()
поскольку он предлагает больше контроля и наглядности. Однако, если вы найдете
subprocess.Popen()
слишком многословный или сложный,
peasyshell
это небольшая оболочка, которую я написал выше, которая упрощает взаимодействие с bash из Python.
Питонический способ сделать это использует subprocess.Popen
subprocess.Popen
принимает список, где первый элемент - это команда, которая должна быть запущена, за которой следуют аргументы командной строки.
В качестве примера:
import subprocess
args = ['echo', 'Hello!']
subprocess.Popen(args) // same as running `echo Hello!` on cmd line
args2 = ['echo', '-v', '"Hello Again"']
subprocess.Popen(args2) // same as running 'echo -v "Hello Again!"` on cmd line
Я большой поклонник https://github.com/amoffat/sh и обычно указываю на это. Но на этот раз я хочу использовать обновленную версию гораздо более простого внедрения оболочки под названием pshlib https://gitlab.com/ewiger/pshlib.
Отказ от ответственности: я только что написал несколько более простую библиотеку, которую теперь проиллюстрирую альтернативным ответом.
Это позволяет отслеживать причинное встраивание команд оболочки в ваш код Python.
Вы можете разбить отдельные длинные строки bash на вложенные Python, например, многострочные операторы:
res = psh(
'VAR=world',
"""
echo This is
a multiline hello
$VAR!
""").output
print(res)
excepted = 'This is a multiline hello world!\n'
assert excepted == res
Итак, за ответ вы получите:
def cwm(rdf_file="test.rdf", with_ntriples="--ntriples", output_file="test.nt"):
res = psh(f"""
cwm
--rdf {rdf_file}
{with_ntriples}
> {output_file}
""").output
print(res)