Emulazione della "sorgente" di Bash in Python


91

Ho uno script che assomiglia a questo:

export foo=/tmp/foo                                          
export bar=/tmp/bar

Ogni volta che compilo eseguo 'source init_env' (dove init_env è lo script sopra) per impostare alcune variabili.

Per fare lo stesso in Python avevo questo codice in esecuzione,

reg = re.compile('export (?P<name>\w+)(\=(?P<value>.+))*')
for line in open(file):
    m = reg.match(line)
    if m:
        name = m.group('name')
        value = ''
        if m.group('value'):
            value = m.group('value')
        os.putenv(name, value)

Ma poi qualcuno ha deciso che sarebbe stato carino aggiungere una riga come la seguente al init_envfile:

export PATH="/foo/bar:/bar/foo:$PATH"     

Ovviamente il mio script Python è andato in pezzi. Potrei modificare lo script Python per gestire questa riga, ma poi si interromperà più tardi quando qualcuno presenterà una nuova funzionalità da utilizzare nel init_envfile.

La domanda è se esiste un modo semplice per eseguire un comando Bash e lasciarlo modificare il mio os.environ?


Risposte:


109

Il problema con il tuo approccio è che stai cercando di interpretare gli script bash. Per prima cosa provi a interpretare l'istruzione export. Quindi noti che le persone usano l'espansione variabile. Successivamente le persone inseriranno i condizionali nei loro file o elaboreranno le sostituzioni. Alla fine avrai un interprete di script bash in piena regola con un miliardo di bug. Non farlo.

Lascia che Bash interpreti il ​​file per te e poi raccogli i risultati.

Puoi farlo in questo modo:

#! /usr/bin/env python

import os
import pprint
import shlex
import subprocess

command = shlex.split("env -i bash -c 'source init_env && env'")
proc = subprocess.Popen(command, stdout = subprocess.PIPE)
for line in proc.stdout:
  (key, _, value) = line.partition("=")
  os.environ[key] = value
proc.communicate()

pprint.pprint(dict(os.environ))

Assicurati di gestire gli errori nel caso in cui bash fallisca source init_env, o bash stesso non venga eseguito, o il sottoprocesso non riesca a eseguire bash, o qualsiasi altro errore.

l' env -iall'inizio della riga di comando crea un ambiente pulito. ciò significa che otterrai solo le variabili d'ambiente da init_env. se si desidera l'ambiente di sistema ereditato, omettere env -i.

Leggi la documentazione sul sottoprocesso per maggiori dettagli.

Nota: questo catturerà solo le variabili impostate con l' exportistruzione, poiché envstampa solo le variabili esportate.

Godere.

Nota che la documentazione di Python dice che se vuoi manipolare l'ambiente dovresti manipolare os.environdirettamente invece di usare os.putenv(). Lo considero un bug, ma sto divagando.


12
Se ti interessano le variabili non esportate e lo script è fuori dal tuo controllo, puoi usare set -a per contrassegnare tutte le variabili come esportate. Basta cambiare il comando in: ['bash', '-c', 'set -a && source init_env && env']
ahal

Notare che questo fallirà sulle funzioni esportate. Mi piacerebbe se potessi aggiornare la tua risposta mostrando l'analisi che funziona anche per le funzioni. (es. funzione fff () {echo "fff";}; export -f fff)
DA

2
Nota: questo non supporta le variabili di ambiente su più righe.
BenC

2
Nel mio caso, l'iterazione di proc.stdout()rendimenti byte, così mi è stato sempre un TypeErrorsu line.partition(). La conversione in stringa ha line.decode().partition("=")risolto il problema.
Sam F

1
Questo è stato di grande aiuto. Ho eseguito ['env', '-i', 'bash', '-c', 'source .bashrc && env']per dare a me stesso solo le variabili di ambiente impostate dal file rc
xaviersjs

32

Usando il sottaceto:

import os, pickle
# For clarity, I moved this string out of the command
source = 'source init_env'
dump = '/usr/bin/python -c "import os,pickle;print pickle.dumps(os.environ)"'
penv = os.popen('%s && %s' %(source,dump))
env = pickle.loads(penv.read())
os.environ = env

Aggiornato:

Questo utilizza json, sottoprocesso e utilizza esplicitamente / bin / bash (per il supporto di Ubuntu):

import os, subprocess as sp, json
source = 'source init_env'
dump = '/usr/bin/python -c "import os, json;print json.dumps(dict(os.environ))"'
pipe = sp.Popen(['/bin/bash', '-c', '%s && %s' %(source,dump)], stdout=sp.PIPE)
env = json.loads(pipe.stdout.read())
os.environ = env

Questo ha un problema su Ubuntu: la shell predefinita è presente /bin/dash, che non conosce il sourcecomando. Per usarlo su Ubuntu, devi eseguire /bin/bashesplicitamente, ad esempio utilizzando penv = subprocess.Popen(['/bin/bash', '-c', '%s && %s' %(source,dump)], stdout=subprocess.PIPE).stdout(questo utilizza il subprocessmodulo più recente che deve essere importato).
Martin Pecka

22

Piuttosto che avere il codice sorgente dello script Python dallo script bash, sarebbe più semplice ed elegante avere un codice sorgente dello script wrapper init_enve quindi eseguire lo script Python con l'ambiente modificato.

#!/bin/bash
source init_env
/run/python/script.py

4
Può risolvere il problema in alcune circostanze, ma non tutte. Ad esempio, sto scrivendo uno script python che deve fare qualcosa come il sourcing del file (in realtà carica i moduli se sai di cosa sto parlando), e ha bisogno di caricare un modulo diverso a seconda di alcune circostanze. Quindi questo non risolverebbe affatto il mio problema
Davide

Questo risponde alla domanda nella maggior parte dei casi e lo userei ove possibile. Ho avuto difficoltà a far funzionare questo lavoro nel mio IDE per un determinato progetto. Una possibile modifica potrebbe essere eseguire il tutto in una shell con l'ambientebash --rcfile init_env -c ./script.py
xaviersjs

6

Aggiornata la risposta di @ lesmana per Python 3. Si noti il ​​cui uso env -iimpedisce che variabili di ambiente estranee vengano impostate / ripristinate (potenzialmente erroneamente data la mancanza di gestione per variabili env multilinea).

import os, subprocess
if os.path.isfile("init_env"):
    command = 'env -i sh -c "source init_env && env"'
    for line in subprocess.getoutput(command).split("\n"):
        key, value = line.split("=")
        os.environ[key]= value

L'utilizzo di questo mi dà "PERCORSO: variabile non definita" perché env -i annulla il percorso. Ma funziona senza env -i. Inoltre, fai attenzione che la riga potrebbe avere più "="
Fujii

4

Esempio di avvolgimento dell'eccellente risposta di @ Brian in una funzione:

import json
import subprocess

# returns a dictionary of the environment variables resulting from sourcing a file
def env_from_sourcing(file_to_source_path, include_unexported_variables=False):
    source = '%ssource %s' % ("set -a && " if include_unexported_variables else "", file_to_source_path)
    dump = '/usr/bin/python -c "import os, json; print json.dumps(dict(os.environ))"'
    pipe = subprocess.Popen(['/bin/bash', '-c', '%s && %s' % (source, dump)], stdout=subprocess.PIPE)
    return json.loads(pipe.stdout.read())

Sto usando questa funzione di utilità per leggere le credenziali aws e i file Docker .env con estensione include_unexported_variables=True.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.