527 votes

Exécution de commandes Bash en Python

Sur ma machine locale, j'exécute un python script qui contient cette ligne

bashCommand = "cwm --rdf test.rdf --ntriples > test.nt"
os.system(bashCommand)

Cela fonctionne bien.

J'exécute ensuite le même code sur un serveur et j'obtiens le message d'erreur suivant

'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

Donc ce que j'ai fait ensuite, c'est que j'ai inséré un print bashCommand qui m'imprime que la commande dans le terminal avant de l'exécuter avec os.system() .

Bien sûr, je reçois à nouveau l'erreur (causée par os.system(bashCommand) ) mais avant cette erreur, il imprime la commande dans le terminal. Ensuite, j'ai simplement copié cette sortie et fait un copier-coller dans le terminal, puis j'ai appuyé sur Entrée et ça marche...

Est-ce que quelqu'un a une idée de ce qui se passe ?

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.

529voto

tripleee Points 28746

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

470voto

user225312 Points 22699

N'utilisez pas os.system . Il a été déprécié au profit de sous-processus . De la docs : "Ce module entend remplacer plusieurs modules et fonctions plus anciens : os.system , os.spawn ".

Comme dans votre cas :

import subprocess

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

16 votes

Cela n'a pas fait ce que je voulais quand j'ai eu besoin de faire un cd 'path\to\somewhere' suivi d'une autre commande bash qui devait être exécutée à cet endroit. @user225312

1 votes

Vous pouvez facilement mettre la commande comme ceci "nano /home/you/path/a.txt" ou n'importe quelle commande que vous voulez, vous pouvez même utiliser | &

61 votes

@AWrightIV Si vous avez besoin que votre sous-processus soit exécuté dans un répertoire de travail particulier, vous pouvez utiliser la fonction cwd argument à Popen : subprocess.Popen(..., cwd='path\to\somewhere')

58voto

Jakob Bowyer Points 12873

Appelez-le avec un sous-processus

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

L'erreur que vous obtenez semble être due au fait qu'il n'y a pas de module swap sur le serveur, vous devriez installer swap sur le serveur puis exécuter à nouveau le script.

3 votes

Le site swap est manifestement présent, car l'exécution de la commande depuis le shell fonctionne.

2 votes

Pas sur le serveur, quand il l'exécute sur le serveur, il y a une erreur d'importation.

0 votes

@mkn : "Ensuite, j'ai juste copié cette sortie et fait un copier-coller dans le terminal et appuyé sur entrée et ça marche..." -- Avez-vous essayé cela sur le serveur ou sur votre machine ?

30voto

Razor Points 189

Il est possible d'utiliser le programme bash, avec le paramètre -c pour exécuter les commandes :

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

23voto

David Daniel Points 53

Vous pouvez utiliser subprocess mais j'ai toujours pensé que ce n'était pas une façon "pythique" de procéder. J'ai donc créé Sultan (shameless plug) qui permet d'exécuter facilement des fonctions en ligne de commande.

https://github.com/aeroxis/sultan

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X