Emulieren der Bash-Quelle in Python

91

Ich habe ein Skript, das ungefähr so ​​aussieht:

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

Jedes Mal, wenn ich baue, führe ich 'source init_env' aus (wobei init_env das obige Skript ist), um einige Variablen einzurichten.

Um dasselbe in Python zu erreichen, ließ ich diesen Code laufen,

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)

Aber dann entschied jemand , dass es schön wäre, der init_envDatei eine Zeile wie die folgende hinzuzufügen :

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

Offensichtlich fiel mein Python-Skript auseinander. Ich könnte das Python-Skript ändern, um diese Zeile zu verarbeiten, aber es wird später unterbrochen, wenn jemand eine neue Funktion für die init_envDatei entwickelt.

Die Frage ist, ob es eine einfache Möglichkeit gibt, einen Bash-Befehl auszuführen und ihn ändern zu lassen os.environ.

getekha
quelle

Antworten:

108

Das Problem bei Ihrem Ansatz ist, dass Sie versuchen, Bash-Skripte zu interpretieren. Zuerst versuchen Sie einfach, die Exportanweisung zu interpretieren. Dann bemerken Sie, dass Leute variable Erweiterung verwenden. Spätere Personen fügen Bedingungen in ihre Dateien ein oder verarbeiten Ersetzungen. Am Ende haben Sie einen ausgewachsenen Bash-Skript-Interpreter mit einer Unmenge von Fehlern. Tu das nicht.

Lassen Sie Bash die Datei für Sie interpretieren und sammeln Sie dann die Ergebnisse.

Sie können es so machen:

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

Stellen Sie sicher, dass Sie Fehler behandeln, falls bash fehlschlägt source init_envoder bash selbst nicht ausgeführt werden kann oder der Unterprozess bash nicht ausführt oder andere Fehler auftreten.

Das env -iam Anfang der Befehlszeile erstellt eine saubere Umgebung. Das heißt, Sie erhalten nur die Umgebungsvariablen von init_env. Wenn Sie die geerbte Systemumgebung möchten, lassen Sie sie weg env -i.

Weitere Informationen finden Sie in der Dokumentation zum Unterprozess .

Hinweis: Dies erfasst nur Variablen, die mit der exportAnweisung festgelegt wurden, da envnur exportierte Variablen gedruckt werden.

Genießen.

Beachten Sie, dass in der Python-Dokumentation angegeben ist , dass Sie die Umgebung os.environdirekt bearbeiten sollten, anstatt sie zu verwenden os.putenv(). Ich halte das für einen Fehler, aber ich schweife ab.

Lesmana
quelle
12
Wenn Sie sich für nicht exportierte Variablen interessieren und das Skript außerhalb Ihrer Kontrolle liegt, können Sie mit set -a alle Variablen als exportiert markieren. Ändern Sie einfach den Befehl in: ['bash', '-c', 'set -a && source init_env && env']
ahal
Beachten Sie, dass dies bei exportierten Funktionen fehlschlägt. Ich würde es lieben, wenn Sie Ihre Antwort aktualisieren könnten, indem Sie eine Analyse anzeigen, die auch für Funktionen funktioniert. (zB Funktion fff () {echo "fff";}; export -f fff)
DA
2
Hinweis: Dies unterstützt keine mehrzeiligen Umgebungsvariablen.
BenC
2
In meinem Fall proc.stdout()ergab das Iterieren über Bytes Bytes, also bekam ich ein TypeErrorOn line.partition(). Das Konvertieren in einen String mit hat line.decode().partition("=")das Problem gelöst.
Sam F
1
Das war super hilfreich. Ich habe ausgeführt ['env', '-i', 'bash', '-c', 'source .bashrc && env'], um mir nur die Umgebungsvariablen zu geben, die von der RC-Datei festgelegt wurden
xaviersjs
32

Mit Gurke:

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

Aktualisiert:

Dies verwendet json, subprocess und explizit / bin / bash (für die Ubuntu-Unterstützung):

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
Brian
quelle
Dieser hat ein Problem unter Ubuntu - die Standard-Shell, die es gibt /bin/dash, die den sourceBefehl nicht kennt . Um es unter Ubuntu zu verwenden, müssen Sie es /bin/bashexplizit ausführen , z. B. mithilfe von penv = subprocess.Popen(['/bin/bash', '-c', '%s && %s' %(source,dump)], stdout=subprocess.PIPE).stdout(dies verwendet das neuere subprocessModul, das importiert werden muss).
Martin Pecka
22

Anstatt dass Ihre Python-Skriptquelle das Bash-Skript ist, wäre es einfacher und eleganter, eine Wrapper-Skriptquelle zu haben init_envund dann Ihr Python-Skript mit der geänderten Umgebung auszuführen.

#!/bin/bash
source init_env
/run/python/script.py
John Kugelman
quelle
4
Es kann das Problem unter bestimmten Umständen lösen, aber nicht alle. Zum Beispiel schreibe ich ein Python-Skript, das so etwas wie das Sourcing der Datei ausführen muss (tatsächlich werden Module geladen, wenn Sie wissen, wovon ich spreche), und es muss abhängig von bestimmten Umständen ein anderes Modul laden . Das würde mein Problem also überhaupt nicht lösen
Davide,
Dies beantwortet die Frage in den meisten Fällen und ich würde sie verwenden, wo immer dies möglich ist. Es fiel mir schwer, diese Arbeit in meiner IDE für ein bestimmtes Projekt zu erledigen. Eine mögliche Änderung könnte darin bestehen, das Ganze in einer Shell mit der Umgebung bash --rcfile init_env -c ./script.py
auszuführen
6

Die Antwort von @ lesmana für Python 3 wurde aktualisiert. Beachten Sie, dass durch deren Verwendung env -iverhindert wird, dass fremde Umgebungsvariablen gesetzt / zurückgesetzt werden (möglicherweise falsch, da mehrzeilige env-Variablen nicht behandelt werden).

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
Al Johri
quelle
Wenn ich das benutze, bekomme ich "PATH: undefined variable", weil env -i den Pfad deaktiviert. Aber es funktioniert ohne das env -i.
Fujii
4

Beispiel für die hervorragende Antwort von @ Brian in einer Funktion:

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

Ich verwende diese Dienstprogrammfunktion, um aws-Anmeldeinformationen und Docker-ENV-Dateien mit zu lesen include_unexported_variables=True.

JDiMatteo
quelle