web-dev-qa-db-fra.com

Appel de la commande "source" depuis subprocess.Popen

J'ai un script .sh que j'appelle avec source the_script.sh. Appeler cela régulièrement, c'est bien. Cependant, j'essaie de l'appeler à partir de mon script python, via subprocess.Popen.

En l'appelant de Popen, j'obtiens les erreurs suivantes dans les deux appels de scénario suivants:

foo = subprocess.Popen("source the_script.sh")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/subprocess.py", line 672, in __init__
    errread, errwrite)
  File "/usr/lib/python2.7/subprocess.py", line 1213, in _execute_child
    raise child_exception
OSError: [Errno 2] No such file or directory


>>> foo = subprocess.Popen("source the_script.sh", Shell = True)
>>> /bin/sh: source: not found

Ce qui donne? Pourquoi ne puis-je pas appeler "source" depuis Popen, alors que je peux en dehors de python?

27
coffee

source n'est pas une commande exécutable, c'est un shell intégré. 

Le cas le plus courant d'utilisation de source consiste à exécuter un script Shell qui modifie l'environnement et à le conserver dans le Shell actuel. C'est exactement comme cela que virtualenv fonctionne pour modifier l'environnement python par défaut.

La création d'un sous-processus et l'utilisation de source dans le sous-processus ne feront probablement rien d'utile, cela ne modifiera pas l'environnement du processus parent, aucun des effets secondaires de l'utilisation du script source ne se produira.

Python a une commande analogue, execfile, qui exécute le fichier spécifié en utilisant l’espace de nom global python actuel (ou un autre, si vous en fournissez un), que vous pouvez utiliser de la même manière que la commande bash source

22

Vous pouvez simplement exécuter la commande dans un sous-shell et utiliser les résultats pour mettre à jour l'environnement actuel.

def Shell_source(script):
    """Sometime you want to emulate the action of "source" in bash,
    settings some environment variables. Here is a way to do it."""
    import subprocess, os
    pipe = subprocess.Popen(". %s; env" % script, stdout=subprocess.PIPE, Shell=True)
    output = pipe.communicate()[0]
    env = dict((line.split("=", 1) for line in output.splitlines()))
    os.environ.update(env)
27
xApple

Popen("source the_script.sh") cassé est équivalent à Popen(["source the_script.sh"]) qui tente en vain de lancer le programme 'source the_script.sh'. Il ne le trouve pas, d'où l'erreur "No such file or directory".

Popen("source the_script.sh", Shell=True) cassé échoue car source est une commande intégrée bash (tapez help source dans bash) mais le shell par défaut est /bin/sh qui ne le comprend pas (/bin/sh utilise .). En supposant qu'il pourrait y avoir un autre bash-isme dans the_script.sh, il devrait être exécuté en utilisant bash:

foo = Popen("source the_script.sh", Shell=True, executable="/bin/bash")

Comme @IfLoop a dit , il n'est pas très utile d'exécuter source dans un sous-processus car cela ne peut pas affecter l'environnement du parent.

Les méthodes basées sur os.environ.update(env) échouent si the_script.sh exécute unset pour certaines variables. os.environ.clear() peut être appelé pour réinitialiser l'environnement:

#!/usr/bin/env python
import os
from pprint import pprint
from subprocess import check_output

os.environ['a'] = 'a'*100
# POSIX: name shall not contain '=', value doesn't contain '\0'
output = check_output("source the_script.sh; env -0",   Shell=True,
                      executable="/bin/bash")
# replace env
os.environ.clear() 
os.environ.update(line.partition('=')[::2] for line in output.split('\0'))
pprint(dict(os.environ)) #NOTE: only `export`ed envvars here

Il utilise env -0 et .split('\0') suggéré par @unutbu

Pour supporter des octets arbitraires dans os.environb, le module json pourrait être utilisé (en supposant que nous utilisions la version Python où "json.dumps not parsable by json.loads" problème est corrigé):

Pour éviter de transmettre l'environnement via des canaux, le code Python peut être modifié pour s'appeler lui-même dans l'environnement de sous-processus, par exemple:

#!/usr/bin/env python
import os
import sys
from pipes import quote
from pprint import pprint

if "--child" in sys.argv: # executed in the child environment
    pprint(dict(os.environ))
else:
    python, script = quote(sys.executable), quote(sys.argv[0])
    os.execl("/bin/bash", "/bin/bash", "-c",
        "source the_script.sh; %s %s --child" % (python, script))
12
jfs

source est un shell intégré spécifique à bash (et les shells non interactifs sont souvent des shells légers, au lieu de bash). Au lieu de cela, appelez simplement /bin/sh:

foo = subprocess.Popen(["/bin/sh", "the_script.sh"])
2
phihag

Une variante de la réponse de @xApple car il est parfois utile de pouvoir générer un script Shell (plutôt qu'un fichier Python) pour définir des variables d'environnement et éventuellement effectuer d'autres opérations Shell, puis de propager cet environnement vers l'interpréteur Python. que de perdre cette information lorsque le sous-shell ferme.

La raison de cette variation est que l'hypothèse d'un format de sortie "env" à une variable par ligne n'est pas robuste à 100%: il me suffisait de traiter une variable (une fonction Shell, je pense) contenant un newline, qui a foiré cette analyse. Voici donc une version légèrement plus complexe, qui utilise Python lui-même pour formater le dictionnaire de l'environnement de manière robuste:

import subprocess
pipe = subprocess.Popen(". ./shellscript.sh; python -c 'import os; print \"newenv = %r\" % os.environ'", 
    stdout=subprocess.PIPE, Shell=True)
exec(pipe.communicate()[0])
os.environ.update(newenv)

Peut-être qu'il y a un moyen plus propre? Cela garantit également que l'analyse de l'environnement n'est pas gâchée si quelqu'un insère une instruction echo dans le script source. Bien sûr, il y a un exec ici, donc faites attention aux entrées non fiables ... mais je pense que cela est implicite dans une discussion sur la façon de générer/exécuter un script Shell arbitraire ;-)

UPDATE: voir le commentaire de @ unutbu sur @xApple answer pour une autre façon (probablement plus agréable) de gérer les nouvelles lignes dans la sortie env.

1
andybuckley

Il semble y avoir beaucoup de réponses à cela, je ne les ai pas toutes lues, alors elles l'ont peut-être déjà signalé mais, lorsque vous appelez des commandes Shell de la sorte, vous devez passer Shell = True à l'appel Popen. Sinon, vous pouvez appeler Popen (shlex.split ()). assurez-vous d'importer shlex. 

J'utilise réellement cette fonction dans le but de rechercher un fichier et de modifier l'environnement actuel.

def set_env(env_file):
    while True:
        source_file = '/tmp/regr.source.%d'%random.randint(0, (2**32)-1)
        if not os.path.isfile(source_file): break
    with open(source_file, 'w') as src_file:
        src_file.write('#!/bin/bash\n')
        src_file.write('source %s\n'%env_file)
        src_file.write('env\n')
    os.chmod(source_file, 0755)
    p = subprocess.Popen(source_file, Shell=True,
                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (out, err) = p.communicate()
    setting = re.compile('^(?P<setting>[^=]*)=')
    value = re.compile('=(?P<value>.*$)')
    env_dict = {}
    for line in out.splitlines():
        if setting.search(line) and value.search(line):
            env_dict[setting.search(line).group('setting')] = value.search(line).group('value')
    for k, v in env_dict.items():
        os.environ[k] = v
    for k, v in env_dict.items():
        try:
            assert(os.getenv(k) == v)
        except AssertionError:
            raise Exception('Unable to modify environment')
0
user1905107

Si vous souhaitez appliquer la commande source à d'autres scripts ou exécutables, vous pouvez créer un autre fichier de script d'habillage et appeler la commande "source" à l'aide de toute logique supplémentaire. Dans ce cas, cette commande source modifiera le contexte local dans lequel elle s'exécute, notamment dans le sous-processus créé par le sous-processus.Popen.

Cela ne fonctionnera pas si vous devez modifier le contexte python, où votre programme est en cours d'exécution.

0
Vlad Ogay