Pour développer quelque peu les réponses précédentes, il y a un certain nombre de détails qui sont souvent négligés.
- Préférez
subprocess.run()
sur subprocess.check_call()
et amis sur subprocess.call()
sur subprocess.Popen()
sur os.system()
sur os.popen()
- Comprendre et probablement utiliser
text=True
, alias universal_newlines=True
.
- Comprendre la signification de
shell=True
o shell=False
et la manière dont elle modifie les citations et la disponibilité des commodités de la coquille.
- Comprendre les différences entre
sh
et Bash
- Comprendre comment un sous-processus est séparé de son parent, et ne peut généralement pas modifier le parent.
- Évitez d'exécuter l'interpréteur Python en tant que sous-processus de Python.
Ces sujets sont abordés plus en détail ci-dessous.
Préférez subprocess.run()
o subprocess.check_call()
Le site subprocess.Popen()
est un outil de travail de bas niveau, mais il est difficile de l'utiliser correctement et vous finissez par copier/coller de multiples lignes de code ... qui, comme par hasard, existent déjà dans la bibliothèque standard sous la forme d'un ensemble de fonctions enveloppantes de plus haut niveau à des fins diverses, qui sont présentées plus en détail dans ce qui suit.
Voici un paragraphe du documentation :
L'approche recommandée pour invoquer des sous-processus est d'utiliser la commande run()
pour tous les cas d'utilisation qu'elle peut gérer. Pour les cas d'utilisation plus avancés, la fonction sous-jacente Popen
peut être utilisée directement.
Malheureusement, la disponibilité de ces fonctions wrapper diffère selon les versions de Python.
-
subprocess.run()
a été officiellement introduit dans Python 3.5. Il est destiné à remplacer tous les éléments suivants.
-
subprocess.check_output()
a été introduit dans Python 2.7 / 3.1. Il est fondamentalement équivalent à subprocess.run(..., check=True, stdout=subprocess.PIPE).stdout
-
subprocess.check_call()
a été introduit dans Python 2.5. Il est fondamentalement équivalent à subprocess.run(..., check=True)
-
subprocess.call()
a été introduit dans Python 2.4 dans la version originale de subprocess
module ( PEP-324 ). Il est fondamentalement équivalent à subprocess.run(...).returncode
API de haut niveau vs. subprocess.Popen()
La version remaniée et étendue du subprocess.run()
est plus logique et plus polyvalent que les anciennes fonctions héritées qu'il remplace. Elle renvoie un CompletedProcess
qui possède diverses méthodes permettant de récupérer l'état de sortie, la sortie standard et quelques autres résultats et indicateurs d'état du sous-processus terminé.
subprocess.run()
est la solution idéale si vous avez simplement besoin d'un programme qui s'exécute et renvoie le contrôle à Python. Pour des scénarios plus complexes (processus d'arrière-plan, peut-être avec des E/S interactives avec le programme parent Python), vous devez toujours utiliser subprocess.Popen()
et t'occuper toi-même de toute la plomberie. Cela exige une compréhension assez complexe de toutes les pièces mobiles et ne doit pas être entrepris à la légère. Le plus simple Popen
objet représente le processus (éventuellement toujours en cours) qui doit être géré par votre code pour le reste de la durée de vie du sous-processus.
Il convient peut-être de souligner que juste subprocess.Popen()
crée simplement un processus. Si vous en restez là, vous avez un sous-processus qui s'exécute en même temps que Python, donc un processus "d'arrière-plan". S'il n'a pas besoin d'effectuer des entrées ou des sorties ou de se coordonner avec vous, il peut faire un travail utile en parallèle avec votre programme Python.
Évitez os.system()
y os.popen()
Depuis toujours (enfin, depuis Python 2.5), la fonction os
documentation des modules a contenu la recommandation de de préfér préférer subprocess
sur os.system()
:
Le site subprocess
fournit des moyens plus puissants pour lancer de nouveaux processus et récupérer leurs résultats ; il est préférable d'utiliser ce module plutôt que cette fonction.
Les problèmes de system()
sont qu'il est évidemment dépendant du système et qu'il n'offre pas de moyens d'interagir avec le sous-processus. Elle s'exécute simplement, avec la sortie standard et l'erreur standard hors de portée de Python. La seule information que Python reçoit en retour est le statut de sortie de la commande (zéro signifie le succès, bien que la signification des valeurs non nulles dépende aussi quelque peu du système).
PEP-324 (qui a déjà été mentionné ci-dessus) contient une justification plus détaillée de la raison pour laquelle os.system
est problématique et comment subprocess
tente de résoudre ces problèmes.
os.popen()
était encore plus fortement déconseillé :
Déprécié depuis la version 2.6 : Cette fonction est obsolète. Utilisez la fonction subprocess
module.
Cependant, depuis Python 3, il a été réimplémenté pour utiliser simplement la fonction subprocess
et redirige vers le site subprocess.Popen()
pour plus de détails.
Comprendre et utiliser habituellement check=True
Vous remarquerez également que subprocess.call()
a beaucoup des mêmes limitations que os.system()
. Dans le cadre d'une utilisation régulière, vous devriez généralement vérifier si le processus s'est terminé avec succès, ce qui subprocess.check_call()
y subprocess.check_output()
do (où ce dernier renvoie également la sortie standard du sous-processus terminé). De même, vous devriez généralement utiliser check=True
avec subprocess.run()
sauf si vous devez spécifiquement autoriser le sous-processus à renvoyer un état d'erreur.
En pratique, avec check=True
o subprocess.check_*
Python lancera un CalledProcessError
exception si le sous-processus renvoie un état de sortie non nul.
Une erreur fréquente avec subprocess.run()
est d'omettre check=True
et être surpris lorsque le code en aval échoue si le sous-processus a échoué.
D'autre part, un problème courant avec check_call()
y check_output()
était que les utilisateurs qui utilisaient aveuglément ces fonctions étaient surpris lorsque l'exception était levée, par exemple lorsque grep
n'a pas trouvé de correspondance. (Vous devriez probablement remplacer grep
avec du code Python natif de toute façon, comme indiqué ci-dessous).
Tout compte fait, vous devez comprendre comment les commandes de l'interpréteur de commandes renvoient un code de sortie, et dans quelles conditions elles renvoient un code de sortie non nul (erreur), et prendre une décision consciente sur la manière exacte de le gérer.
Comprendre et probablement utiliser text=True
alias universal_newlines=True
Depuis Python 3, les chaînes internes à Python sont des chaînes Unicode. Mais il n'y a aucune garantie qu'un sous-processus génère une sortie Unicode, ou des chaînes tout court.
(Si les différences ne sont pas immédiatement évidentes, l'ouvrage de Ned Batchelder Unicode pragmatique est une lecture recommandée, voire obligatoire. Il y a une présentation vidéo de 36 minutes derrière le lien si vous préférez, bien que lire la page vous-même prendra probablement beaucoup moins de temps).
Au fond, Python doit aller chercher une bytes
et l'interpréter d'une manière ou d'une autre. Si elle contient un blob de données binaires, elle ne devrait pas être décodé en une chaîne Unicode, parce que c'est un comportement sujet à des erreurs et à des bogues - précisément le genre de comportement embêtant qui a criblé de nombreux scripts de Python 2, avant qu'il y ait un moyen de distinguer correctement entre le texte codé et les données binaires.
Con text=True
vous dites à Python que vous attendez en fait des données textuelles dans l'encodage par défaut du système, et qu'elles doivent être décodées en une chaîne Python (Unicode) au mieux des capacités de Python (généralement UTF-8 sur tout système moyennement à jour, sauf peut-être Windows ?)
Si c'est pas ce que vous demandez en retour, Python vous donnera juste bytes
Les chaînes de caractères dans le stdout
y stderr
des cordes. Peut-être qu'à un moment donné, vous faire vous savez qu'il s'agissait de chaînes de texte après tout, et vous connaissez leur encodage. Alors, vous pouvez les décoder.
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 a introduit un alias plus court, plus descriptif et plus compréhensible. text
pour l'argument du mot-clé qui était auparavant appelé de manière quelque peu trompeuse universal_newlines
.
Comprendre shell=True
vs shell=False
Con shell=True
vous passez une seule chaîne de caractères à votre shell, et le shell s'en charge.
Con shell=False
vous passez une liste d'arguments au système d'exploitation, en contournant le shell.
Lorsque vous n'avez pas de shell, vous sauvegardez un processus et vous vous débarrassez d'une une quantité assez importante de complexité cachée, qui peut ou non abriter des bogues ou même des problèmes de sécurité.
D'autre part, lorsque vous n'avez pas de shell, vous ne disposez pas de la redirection, de l'expansion des caractères génériques, du contrôle des tâches et d'un grand nombre d'autres fonctionnalités du shell.
Une erreur courante consiste à utiliser shell=True
tout en passant à Python une liste de jetons, ou vice versa. Cela fonctionne dans certains cas, mais c'est vraiment mal défini et cela pourrait se briser de manière intéressante.
# XXX AVOID THIS BUG
buggy = subprocess.run('dig +short stackoverflow.com')
# XXX AVOID THIS BUG TOO
broken = subprocess.run(['dig', '+short', 'stackoverflow.com'],
shell=True)
# XXX DEFINITELY AVOID THIS
pathological = subprocess.run(['dig +short stackoverflow.com'],
shell=True)
correct = subprocess.run(['dig', '+short', 'stackoverflow.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 stackoverflow.com',
shell=True,
# Probably don't forget these, too
check=True, text=True)
La réplique courante "mais ça marche pour moi" n'est pas utile si vous ne comprenez pas exactement dans quelles circonstances il pourrait cesser de fonctionner.
Exemple de refactoring
Très souvent, les fonctionnalités du shell peuvent être remplacées par du code Python natif. Un simple Awk ou sed
Les scripts devraient probablement être simplement traduits en Python à la place.
Pour illustrer partiellement ce point, voici un exemple typique, mais légèrement stupide, qui fait intervenir de nombreuses fonctionnalités du shell.
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 'round-trip min/avg/max' in line:
print('{}: {}'.format(host, line))
Quelques éléments à noter ici :
- Con
shell=False
vous n'avez pas besoin des guillemets que l'interpréteur de commandes exige autour des chaînes de caractères. Mettre des guillemets de toute façon est probablement une erreur.
- Il est souvent judicieux d'exécuter le moins de code possible dans un sous-processus. Cela vous permet de mieux contrôler l'exécution à partir de votre code Python.
- Cela dit, les pipelines shell complexes sont fastidieux et parfois difficiles à réimplémenter en Python.
Le code remanié illustre également tout ce que l'interpréteur de commandes fait réellement pour vous avec une syntaxe très laconique - pour le meilleur et pour le pire. Python dit l'explicite est meilleur que l'implicite mais le code Python est plutôt verbeux et semble sans doute plus complexe qu'il ne l'est réellement. D'un autre côté, il offre un certain nombre de points où vous pouvez prendre le contrôle au milieu de quelque chose d'autre, comme trivialement illustré par l'amélioration que nous pouvons facilement inclure le nom de l'hôte avec la sortie de la commande shell. (Ce n'est en aucun cas difficile à faire dans le shell, non plus, mais au prix d'une autre diversion et peut-être d'un autre processus).
Constructions Shell communes
Pour être complet, voici de brèves explications de certaines de ces fonctionnalités de l'interpréteur de commandes, et quelques notes sur la façon dont elles peuvent être remplacées par des fonctionnalités natives de Python.
- L'expansion des caractères génériques peut être remplacée par
glob.glob()
ou très souvent avec de simples comparaisons de chaînes Python comme for file in os.listdir('.'): if not file.endswith('.png'): continue
. Bash dispose de diverses autres facilités d'expansion comme .{png,jpg}
l'expansion de l'accolade et {1..100}
ainsi que l'expansion du tilde ( ~
se développe vers votre répertoire personnel, et plus généralement ~account
vers le répertoire personnel d'un autre utilisateur)
- Les variables Shell comme
$SHELL
o $my_exported_var
peuvent parfois être simplement remplacées par des variables Python. Les variables exportées de l'interpréteur de commandes sont disponibles sous la forme, par exemple, de os.environ['SHELL']
(le sens de export
est de rendre la variable disponible pour les sous-processus -- une variable qui n'est pas disponible pour les sous-processus ne sera évidemment pas disponible pour Python s'exécutant en tant que sous-processus de l'interpréteur de commandes, ou vice versa. L'adresse env=
argument de mot-clé à subprocess
vous permet de définir l'environnement du sous-processus sous forme de dictionnaire, c'est donc une façon de rendre une variable Python visible par un sous-processus). Avec shell=False
vous devrez comprendre comment supprimer les guillemets ; par exemple, cd "$HOME"
est équivalent à os.chdir(os.environ['HOME'])
sans guillemets autour du nom du répertoire. (Très souvent cd
n'est pas utile ou nécessaire de toute façon, et de nombreux débutants omettent les guillemets autour de la variable et s'en tirent sans problème. jusqu'à ce qu'un jour... )
- La redirection vous permet de lire un fichier comme entrée standard et d'écrire votre sortie standard dans un fichier.
grep 'foo' <inputfile >outputfile
ouvre outputfile
pour l'écriture et inputfile
pour la lecture, et transmet son contenu comme entrée standard à grep
dont la sortie standard atterrit alors dans outputfile
. Il n'est généralement pas difficile de le remplacer par du code Python natif.
- Les pipelines sont une forme de redirection.
echo foo | nl
exécute deux sous-processus, où la sortie standard de echo
est l'entrée standard de nl
(au niveau du système d'exploitation, dans les systèmes de type Unix, il s'agit d'un seul handle de fichier). Si vous ne pouvez pas remplacer l'une ou les deux extrémités du pipeline par du code Python natif, vous devriez peut-être envisager l'utilisation d'un shell après tout, surtout si le pipeline comporte plus de deux ou trois processus (bien que vous puissiez considérer la fonction pipes
dans la bibliothèque standard de Python ou un certain nombre de concurrents tiers plus modernes et plus polyvalents).
- Le contrôle des travaux vous permet d'interrompre les travaux, de les exécuter en arrière-plan, de les ramener au premier plan, etc. Les signaux Unix de base permettant d'arrêter et de poursuivre un processus sont bien sûr également disponibles dans Python. Les signaux Unix de base pour arrêter et poursuivre un processus sont bien sûr également disponibles en Python. Mais les tâches sont une abstraction de plus haut niveau dans le shell qui implique des groupes de processus, etc.
- La citation dans l'interpréteur de commandes est potentiellement déroutante jusqu'à ce que vous compreniez que tout est essentiellement une chaîne de caractères. Donc
ls -l /
est équivalent à 'ls' '-l' '/'
mais les guillemets autour des littéraux sont totalement facultatifs. Les chaînes non citées qui contiennent des métacaractères de l'interpréteur de commandes sont soumises à l'expansion des paramètres, à la segmentation de l'espace blanc et à l'expansion des caractères génériques ; les guillemets doubles empêchent la segmentation de l'espace blanc et l'expansion des caractères génériques, mais permettent l'expansion des paramètres (substitution de variables, substitution de commandes et traitement des barres obliques inverses). C'est simple en théorie mais cela peut devenir déroutant, surtout lorsqu'il y a plusieurs niveaux d'interprétation (une commande shell à distance, par exemple).
Comprendre les différences entre sh
et Bash
subprocess
exécute vos commandes shell avec /bin/sh
à moins que vous ne demandiez spécifiquement le contraire (sauf bien sûr sous Windows, où il utilise la valeur de la variable COMSPEC
variable). Cela signifie que [diverses fonctionnalités propres à Bash, comme les tableaux, [[
etc.](https://stackoverflow.com/a/42666651/874188) ne sont pas disponibles.
Si vous devez utiliser la syntaxe Bash-only, vous pouvez transmettre le chemin d'accès au shell en tant que executable='/bin/bash'
(bien sûr, si votre Bash est installé ailleurs, vous devez ajuster le chemin).
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
est séparé de son parent, et ne peut le modifier
Une erreur assez courante est de faire quelque chose comme
subprocess.run('cd /tmp', shell=True)
subprocess.run('pwd', shell=True) # Oops, doesn't print /tmp
La même chose se produira si le premier sous-processus tente de définir une variable d'environnement, qui aura bien sûr disparu lorsque vous lancerez un autre sous-processus, etc.
Un processus enfant s'exécute de manière totalement distincte de Python, et lorsqu'il se termine, Python n'a aucune idée de ce qu'il a fait (en dehors des vagues indicateurs qu'il peut déduire de l'état de sortie et de la sortie du processus enfant). En général, un enfant ne peut pas modifier l'environnement du parent ; il ne peut pas définir une variable, changer le répertoire de travail ou, en d'autres termes, communiquer avec son parent sans la coopération de ce dernier.
La solution immédiate dans ce cas particulier est d'exécuter les deux commandes dans un seul sous-processus ;
subprocess.run('cd /tmp; pwd', shell=True)
bien qu'il soit évident que ce cas particulier n'est pas très utile ; utilisez plutôt la fonction cwd
l'argument du mot-clé, ou simplement os.chdir()
avant d'exécuter le sous-processus. De même, pour définir une variable, vous pouvez manipuler l'environnement du processus actuel (et donc aussi de ses enfants) via
os.environ['foo'] = 'bar'
ou passer un paramètre d'environnement à un processus enfant avec
subprocess.run('echo "$foo"', shell=True, env={'foo': 'bar'})
(sans parler du remaniement évident subprocess.run(['echo', 'bar'])
mais echo
est un mauvais exemple de quelque chose à exécuter dans un sous-processus en premier lieu, bien sûr).
Ne pas exécuter Python à partir de Python
C'est un conseil légèrement douteux ; il y a certainement des situations où il est logique ou même absolument nécessaire d'exécuter l'interpréteur Python en tant que sous-processus à partir d'un script Python. Mais très fréquemment, l'approche correcte est simplement de import
l'autre module Python dans votre script appelant et appeler ses fonctions directement.
Si l'autre script Python est sous votre contrôle, et qu'il ne s'agit pas d'un module, considérez que le transformer en un . (Cette réponse est déjà trop longue, je n'entrerai donc pas dans les détails ici).
Si vous avez besoin de parallélisme, vous pouvez exécuter des fonctions Python dans des sous-processus avec la commande multiprocessing
module. Il existe également threading
qui exécute plusieurs tâches dans un seul processus (ce qui est plus léger et vous donne plus de contrôle, mais aussi plus contraignant dans la mesure où les threads d'un processus sont étroitement couplés et liés à une seule et unique GIL .)
3 votes
Il semble y avoir une différence dans l'environnement selon la façon dont vous exécutez
cwm
. Peut-être avez-vous une certaine configuration dans votre.bashrc
qui configure l'environnement pour une utilisation interactive de bash ?0 votes
Avez-vous essayé d'exécuter la commande à partir de la ligne de commande lorsque vous êtes connecté au serveur ? Votre message indique simplement que vous l'avez "collée dans le terminal".
0 votes
@Sven : oui, je voulais dire que j'ai exécuté la commande directement dans le terminal du serveur.
0 votes
Il semble qu'il y ait une différence dans le PYTHONPATH selon la façon dont vous exécutez
cwm
. Ou peut-être y a-t-il une différence dans PATH, et une version différente decwm
sont appelés. Ou différentes versions de Python. Il est vraiment difficile de comprendre cela sans avoir accès à la machine...