Llamando el comando “fuente” desde subprocess.Popen

Tengo un script .sh al que llamo con la source the_script.sh . Llamar a esto regularmente está bien. Sin embargo, estoy intentando llamarlo desde mi script de Python, a través de subprocess.Popen .

Llamándolo de Popen, recibo los siguientes errores en las siguientes dos llamadas de escenario:

 foo = subprocess.Popen("source the_script.sh") Traceback (most recent call last): File "", line 1, in  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 

¿Lo que da? ¿Por qué no puedo llamar “fuente” desde Popen, cuando puedo fuera de python?

source no es un comando ejecutable, es un shell incorporado.

El caso más habitual para utilizar la source es ejecutar un script de shell que cambie el entorno y retener ese entorno en el shell actual. Así es exactamente como funciona virtualenv para modificar el entorno de Python predeterminado.

Crear un subproceso y utilizar la source en el subproceso probablemente no hará nada útil, no modificará el entorno del proceso principal, no se producirá ninguno de los efectos secundarios de usar el script de origen.

Python tiene un comando análogo, execfile , que ejecuta el archivo especificado utilizando el espacio de nombres global actual de Python (u otro, si proporciona uno), que podría usar de manera similar a la source comando bash.

Simplemente puede ejecutar el comando en un subshell y usar los resultados para actualizar el entorno actual.

 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) 

Popen("source the_script.sh") roto Popen("source the_script.sh") es equivalente a Popen(["source the_script.sh"]) que intenta sin éxito lanzar el progtwig 'source the_script.sh' . No puede encontrarlo, por lo tanto, error "No such file or directory" .

Broken Popen("source the_script.sh", shell=True) falla porque source es un comando incorporado de bash (escriba help source en bash) pero el shell predeterminado es /bin/sh que no lo entiende (usa /bin/sh . ). Suponiendo que podría haber otro bash-ism en the_script.sh , debería ejecutarse utilizando bash:

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

Como dijo @IfLoop , no es muy útil ejecutar la source en un subproceso porque no puede afectar el entorno de los padres.

os.environ.update(env) métodos basados ​​en os.environ.update(env) fallan si the_script.sh ejecuta unset para algunas variables. os.environ.clear() podría llamar a os.environ.clear() para restablecer el entorno:

 #!/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 

Utiliza env -0 y .split('\0') sugeridos por @unutbu

Para admitir bytes arbitrarios en os.environb , se podría usar el módulo json (asumiendo que usemos la versión de Python donde el problema “json.dumps no se puede analizar con json.loads” está solucionado):

Para evitar pasar el entorno a través de tuberías, el código de Python podría cambiarse para invocarse en el entorno de subproceso, por ejemplo:

 #!/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)) 

source es una shell específica de bash incorporada (y las shells no interactivas suelen ser conchas de dashboard ligeras en lugar de bash). En su lugar, simplemente llame /bin/sh :

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

Una variación en la respuesta de @xApple, ya que a veces es útil poder generar un script de shell (en lugar de un archivo de Python) para establecer variables de entorno, y tal vez realizar otras operaciones de shell, y luego propagar ese entorno al intérprete de Python. que perder esa información cuando la subshell se cierra.

La razón de una variación es que la suposición de un formato de una variable por línea de “env” no es 100% robusta: solo tuve que lidiar con una variable (una función de shell, creo) que contiene una nueva línea, que arruinó ese análisis. Así que aquí hay una versión un poco más compleja, que usa Python para formatear el diccionario del entorno de una manera sólida:

 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) 

Tal vez hay una manera más limpia? Esto también garantiza que el análisis del entorno no se ensucie si alguien coloca una statement de eco en el script que se está obteniendo. Por supuesto, hay un ejecutivo aquí, así que tenga cuidado con los comentarios que no son de confianza … pero creo que eso está implícito en una discusión sobre cómo generar / ejecutar un script de shell arbitrario 😉

ACTUALIZACIÓN: vea el comentario de @unutbu en la respuesta de @xApple para una forma alternativa (probablemente más agradable) de manejar las nuevas líneas en la salida de env .

Si desea aplicar el comando de origen a otros scripts o ejecutables, puede crear otro archivo de script de envoltura y llamar a este comando con cualquier otra lógica que necesite. En ese caso, este comando de origen modificará el contexto local donde se ejecuta, es decir, en el subproceso que crea subprocess.Popen.

Esto no funcionará si necesita modificar el contexto de Python, donde se está ejecutando su progtwig.

Parece que hay muchas respuestas a esto, no las he leído todas, así que es posible que ya lo hayan señalado; pero, al llamar a comandos de shell como este, debe pasar shell = True a la llamada de Popen. De lo contrario, puede llamar a Popen (shlex.split ()). Asegúrese de importar shlex.

De hecho, utilizo esta función con el fin de obtener un archivo y modificar el entorno actual.

 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[^=]*)=') value = re.compile('=(?P.*$)') 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')