Вопрос-Ответ

Running Bash commands in Python

Выполнение команд 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, и это сработало...

Кто-нибудь имеет представление о том, что происходит?

Переведено автоматически
Ответ 1

Чтобы несколько расширить предыдущие ответы здесь, есть ряд деталей, которые обычно упускаются из виду.


  • Предпочитай subprocess.run() over subprocess.check_call() и дружи с subprocess.call() over subprocess.Popen() over os.system() over os.popen()

  • Понимаю и, вероятно, использую text=True, aka universal_newlines=True.

  • Поймите значение shell=True or shell=False и то, как это меняет кавычки и доступность удобств оболочки.

  • Понять различия между sh и Bash

  • Поймите, как подпроцесс отделен от своего родительского процесса и, как правило, не может изменить родительский процесс.

  • Избегайте запуска интерпретатора 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 модуле (PEP-324). По сути, он эквивалентен subprocess.run(...).returncode

Высокоуровневый API vs subprocess.Popen()

Переработанная и расширенная функция subprocess.run() более логична и универсальна, чем старые устаревшие функции, которые она заменяет. Он возвращает CompletedProcess объект, который имеет различные методы, позволяющие вам извлекать состояние выхода, стандартный вывод и несколько других результатов и индикаторов состояния из завершенного подпроцесса.

subprocess.run() is the way to go if you simply need a program to run and return control to Python. For more involved scenarios (background processes, perhaps with interactive I/O with the Python parent program) you still need to use subprocess.Popen() and take care of all the plumbing yourself. This requires a fairly intricate understanding of all the moving parts and should not be undertaken lightly. The simpler Popen object represents the (possibly still-running) process which needs to be managed from your code for the remainder of the lifetime of the subprocess.

It should perhaps be emphasized that just subprocess.Popen() merely creates a process. If you leave it at that, you have a subprocess running concurrently alongside with Python, so a "background" process. If it doesn't need to do input or output or otherwise coordinate with you, it can do useful work in parallel with your Python program.

Avoid os.system() and os.popen()

Since time eternal (well, since Python 2.5) the os module documentation has contained the recommendation to prefer subprocess over os.system():


The subprocess module provides more powerful facilities for spawning new processes and retrieving their results; using that module is preferable to using this function.


The problems with system() are that it's obviously system-dependent and doesn't offer ways to interact with the subprocess. It simply runs, with standard output and standard error outside of Python's reach. The only information Python receives back is the exit status of the command (zero means success, though the meaning of non-zero values is also somewhat system-dependent).

PEP-324 (which was already mentioned above) contains a more detailed rationale for why os.system is problematic and how subprocess attempts to solve those issues.

os.popen() used to be even more strongly discouraged:


Deprecated since version 2.6: This function is obsolete. Use the subprocess module.


However, since sometime in Python 3, it has been reimplemented to simply use subprocess, and redirects to the subprocess.Popen() documentation for details.

Understand and usually use check=True

You'll also notice that subprocess.call() has many of the same limitations as os.system(). In regular use, you should generally check whether the process finished successfully, which subprocess.check_call() and subprocess.check_output() do (where the latter also returns the standard output of the finished subprocess). Similarly, you should usually use check=True with subprocess.run() unless you specifically need to allow the subprocess to return an error status.

In practice, with check=True or subprocess.check_*, Python will throw a CalledProcessError exception if the subprocess returns a nonzero exit status.

A common error with subprocess.run() is to omit check=True and be surprised when downstream code fails if the subprocess failed.

On the other hand, a common problem with check_call() and check_output() was that users who blindly used these functions were surprised when the exception was raised e.g. when grep did not find a match. (You should probably replace grep with native Python code anyway, as outlined below.)

All things counted, you need to understand how shell commands return an exit code, and under what conditions they will return a non-zero (error) exit code, and make a conscious decision how exactly it should be handled.

Understand and probably use text=True aka universal_newlines=True

Since Python 3, strings internal to Python are Unicode strings. But there is no guarantee that a subprocess generates Unicode output, or strings at all.

(If the differences are not immediately obvious, Ned Batchelder's Pragmatic Unicode is recommended, if not outright obligatory, reading. There is a 36-minute video presentation behind the link if you prefer, though reading the page yourself will probably take significantly less time.)

Deep down, Python has to fetch a bytes buffer and interpret it somehow. If it contains a blob of binary data, it shouldn't be decoded into a Unicode string, because that's error-prone and bug-inducing behavior - precisely the sort of pesky behavior which riddled many Python 2 scripts, before there was a way to properly distinguish between encoded text and binary data.

With text=True, you tell Python that you, in fact, expect back textual data in the system's default encoding, and that it should be decoded into a Python (Unicode) string to the best of Python's ability (usually UTF-8 on any moderately up to date system, except perhaps Windows?)

If that's not what you request back, Python will just give you bytes strings in the stdout and stderr strings. Maybe at some later point you do know that they were text strings after all, and you know their encoding. Then, you can decode them.

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 introduced the shorter and more descriptive and understandable alias text for the keyword argument which was previously somewhat misleadingly called universal_newlines.

Understand shell=True vs shell=False

With shell=True you pass a single string to your shell, and the shell takes it from there.

With shell=False you pass a list of arguments to the OS, bypassing the shell.

When you don't have a shell, you save a process and get rid of a fairly substantial amount of hidden complexity, which may or may not harbor bugs or even security problems.

On the other hand, when you don't have a shell, you don't have redirection, wildcard expansion, job control, and a large number of other shell features.

A common mistake is to use shell=True and then still pass Python a list of tokens, or vice versa. This happens to work in some cases, but is really ill-defined and could break in interesting ways.

# XXX AVOID THIS BUG
buggy = subprocess.run('dig +short pythonly.ru')

# XXX AVOID THIS BUG TOO
broken = subprocess.run(['dig', '+short', 'pythonly.ru'],
shell=True)

# XXX DEFINITELY AVOID THIS
pathological = subprocess.run(['dig +short pythonly.ru'],
shell=True)

correct = subprocess.run(['dig', '+short', 'pythonly.ru'],
# 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 pythonly.ru',
shell=True,
# Probably don't forget these, too
check=True, text=True)

The common retort "but it works for me" is not a useful rebuttal unless you understand exactly under what circumstances it could stop working.

To briefly recap, correct usage looks like

subprocess.run("string for 'the shell' to parse", shell=True)
# or
subprocess.run(["list", "of", "tokenized strings"]) # shell=False

If you want to avoid the shell but are too lazy or unsure of how to parse a string into a list of tokens, notice that shlex.split() can do this for you.

subprocess.run(shlex.split("no string for 'the shell' to parse"))  # shell=False
# equivalent to
# subprocess.run(["no", "string", "for", "the shell", "to", "parse"])

The regular split() will not work here, because it doesn't preserve quoting. In the example above, notice how "the shell" is a single string.

Refactoring Example

Very often, the features of the shell can be replaced with native Python code. Simple Awk or sed scripts should probably just be translated to Python instead.

To partially illustrate this, here is a typical but slightly silly example which involves many shell features.

cmd = '''while read -r x;
do ping -c 3 "$x" | grep '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 'min/avg/max' in line:
print('{}: {}'.format(host, line))

Some things to note here:


  • With shell=False you don't need the quoting that the shell requires around strings. Putting quotes anyway is probably an error.

  • It often makes sense to run as little code as possible in a subprocess. This gives you more control over execution from within your Python code.

  • Having said that, complex shell pipelines are tedious and sometimes challenging to reimplement in Python.

The refactored code also illustrates just how much the shell really does for you with a very terse syntax -- for better or for worse. Python says explicit is better than implicit but the Python code is rather verbose and arguably looks more complex than this really is. On the other hand, it offers a number of points where you can grab control in the middle of something else, as trivially exemplified by the enhancement that we can easily include the host name along with the shell command output. (This is by no means challenging to do in the shell, either, but at the expense of yet another diversion and perhaps another process.)

Common Shell Constructs

For completeness, here are brief explanations of some of these shell features, and some notes on how they can perhaps be replaced with native Python facilities.


  • Globbing aka wildcard expansion can be replaced with glob.glob() or very often with simple Python string comparisons like for file in os.listdir('.'): if not file.endswith('.png'): continue. Bash has various other expansion facilities like .{png,jpg} brace expansion and {1..100} as well as tilde expansion (~ expands to your home directory, and more generally ~account to the home directory of another user)

  • Shell variables like $SHELL or $my_exported_var can sometimes simply be replaced with Python variables. Exported shell variables are available as e.g. os.environ['SHELL'] (the meaning of export is to make the variable available to subprocesses -- a variable which is not available to subprocesses will obviously not be available to Python running as a subprocess of the shell, or vice versa. The env= keyword argument to subprocess methods allows you to define the environment of the subprocess as a dictionary, so that's one way to make a Python variable visible to a subprocess). With shell=False you will need to understand how to remove any quotes; for example, cd "$HOME" is equivalent to os.chdir(os.environ['HOME']) without quotes around the directory name. (Very often cd is not useful or necessary anyway, and many beginners omit the double quotes around the variable and get away with it until one day ...)

  • Redirection allows you to read from a file as your standard input, and write your standard output to a file. grep 'foo' <inputfile >outputfile opens outputfile for writing and inputfile for reading, and passes its contents as standard input to grep, whose standard output then lands in outputfile. This is not generally hard to replace with native Python code.

  • Pipelines are a form of redirection. echo foo | nl runs two subprocesses, where the standard output of echo is the standard input of nl (on the OS level, in Unix-like systems, this is a single file handle). If you cannot replace one or both ends of the pipeline with native Python code, perhaps think about using a shell after all, especially if the pipeline has more than two or three processes (though look at the pipes module in the Python standard library or a number of more modern and versatile third-party competitors).

  • Job control lets you interrupt jobs, run them in the background, return them to the foreground, etc. The basic Unix signals to stop and continue a process are of course available from Python, too. But jobs are a higher-level abstraction in the shell which involve process groups etc which you have to understand if you want to do something like this from Python.

  • Quoting in the shell is potentially confusing until you understand that everything is basically a string. So ls -l / is equivalent to 'ls' '-l' '/' but the quoting around literals is completely optional. Unquoted strings which contain shell metacharacters undergo parameter expansion, whitespace tokenization and wildcard expansion; double quotes prevent whitespace tokenization and wildcard expansion but allow parameter expansions (variable substitution, command substitution, and backslash processing). This is simple in theory but can get bewildering, especially when there are several layers of interpretation (a remote shell command, for example).

Понять различия между sh и Bash

subprocess выполняет ваши команды оболочки с помощью /bin/sh, если вы специально не запросите иное (за исключением, конечно, Windows, где используется значение переменной COMSPEC). Это означает, что различные функции, доступные только для Bash, такие как массивы, [[ etc недоступны.

Если вам нужно использовать синтаксис только для Bash, вы можете передать путь к оболочке как 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')

A subprocess отделен от своего родительского элемента и не может его изменить

Довольно распространенной ошибкой является выполнение чего-то вроде

subprocess.run('cd /tmp', shell=True)
subprocess.run('pwd', shell=True) # Oops, doesn't print /tmp

То же самое произойдет, если первый подпроцесс попытается установить переменную окружения, которая, конечно, исчезнет при запуске другого подпроцесса и т.д.

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

Немедленное исправление в этом конкретном случае заключается в запуске обеих команд в одном подпроцессе;

subprocess.run('cd /tmp; pwd', shell=True)

хотя очевидно, что этот конкретный вариант использования не очень полезен; вместо этого используйте cwd аргумент ключевого слова или просто os.chdir() перед запуском подпроцесса. Аналогично, для установки переменной вы можете манипулировать средой текущего процесса (и, следовательно, также его дочерними процессами) с помощью

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.)

Ответ 2

Не использовать os.system. Он устарел в пользу подпроцесса. Из документации: "Этот модуль предназначен для замены нескольких старых модулей и функций: os.system, os.spawn".

Как в вашем случае:

import subprocess

bashCommand = "cwm --rdf test.rdf --ntriples > test.nt"
process = subprocess.Popen(bashCommand.split(), stdout=subprocess.PIPE)
output, error = process.communicate()
Ответ 3

Вызовите это с помощью подпроцесса

import subprocess
subprocess.Popen("cwm --rdf test.rdf --ntriples > test.nt")

Ошибка, которую вы получаете, по-видимому, связана с тем, что на сервере нет модуля swap, вам следует установить swap на сервер, а затем снова запустить скрипт

Ответ 4

Возможно, вы используете программу bash с параметром -c для выполнения команд:

bashCommand = "cwm --rdf test.rdf --ntriples > test.nt"
output = subprocess.check_output(['bash','-c', bashCommand])
python