Emulacja źródła Bash w Pythonie


92

Mam skrypt, który wygląda mniej więcej tak:

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

Za każdym razem, gdy buduję, uruchamiam 'source init_env' (gdzie init_env to powyższy skrypt), aby ustawić kilka zmiennych.

Aby osiągnąć to samo w Pythonie, uruchomiłem ten kod,

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)

Ale potem ktoś zdecydował, że fajnie byłoby dodać do pliku linię podobną do poniższej init_env:

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

Oczywiście mój skrypt w Pythonie się rozpadł. Mógłbym zmodyfikować skrypt Pythona, aby obsłużyć ten wiersz, ale potem po prostu zepsuje się, gdy ktoś wymyśli nową funkcję do użycia w init_envpliku.

Pytanie brzmi, czy istnieje łatwy sposób na uruchomienie polecenia Bash i pozwolenie mu zmodyfikować moje os.environ?


Odpowiedzi:


109

Problem z twoim podejściem polega na tym, że próbujesz interpretować skrypty bash. Najpierw spróbuj zinterpretować oświadczenie eksportowe. Wtedy zauważasz, że ludzie używają rozszerzania zmiennych. Później ludzie będą umieszczać warunki warunkowe w swoich plikach lub podstawiać procesy. W końcu będziesz miał pełny interpreter skryptów basha z gazylionami błędów. Nie rób tego.

Pozwól Bashowi zinterpretować plik za Ciebie, a następnie zbierz wyniki.

Możesz to zrobić w ten sposób:

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

Upewnij się, że obsługujesz błędy w przypadku, gdy bash się nie powiedzie source init_envlub sam bash nie może wykonać, lub podproces nie może wykonać bash, lub inne błędy.

env -ina początku wiersza poleceń tworzy czyste środowisko. oznacza to, że będziesz otrzymywać zmienne środowiskowe tylko z init_env. jeśli chcesz odziedziczone środowisko systemowe, pomiń env -i.

Przeczytaj dokumentację dotyczącą podprocesu, aby uzyskać więcej informacji.

Uwaga: spowoduje to przechwycenie tylko zmiennych ustawionych w exportinstrukcji, ponieważ envwypisuje tylko wyeksportowane zmienne.

Cieszyć się.

Zauważ, że dokumentacja Pythona mówi, że jeśli chcesz manipulować środowiskiem, powinieneś manipulować os.environbezpośrednio, zamiast używać os.putenv(). Uważam to za błąd, ale błądzę.


12
Jeśli zależy Ci na nieeksportowanych zmiennych, a skrypt jest poza Twoją kontrolą, możesz użyć set -a, aby oznaczyć wszystkie zmienne jako wyeksportowane. Po prostu zmień polecenie na: ['bash', '-c', 'set -a && source init_env && env']
ahal

Zwróć uwagę, że nie powiedzie się to w przypadku funkcji eksportowanych. Byłoby mi bardzo miło, gdybyś mógł zaktualizować swoją odpowiedź, pokazując analizę, która działa również w przypadku funkcji. (np. function fff () {echo "fff";}; export -f fff)
DA

2
Uwaga: to nie obsługuje wielowierszowych zmiennych środowiskowych.
BenC

2
W moim przypadku, iteracji nad proc.stdout()wydajnością bajty, więc ja dostaję TypeErrorna line.partition(). Konwersja na łańcuch z line.decode().partition("=")rozwiązanym problemem.
Sam F

1
To było bardzo pomocne. I wykonany ['env', '-i', 'bash', '-c', 'source .bashrc && env'], aby dać sobie tylko zmienne środowiskowe określone przez plik rc
xaviersjs

32

Używanie marynaty:

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

Zaktualizowano:

Używa json, podprocesu i jawnie używa / bin / bash (do obsługi 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

Ten ma problem na Ubuntu - jest domyślna powłoka /bin/dash, która nie zna sourcepolecenia. Aby użyć go na Ubuntu, musisz uruchomić go /bin/bashjawnie, np. Używając penv = subprocess.Popen(['/bin/bash', '-c', '%s && %s' %(source,dump)], stdout=subprocess.PIPE).stdout(to używa nowszego subprocessmodułu, który trzeba zaimportować).
Martin Pecka

22

Zamiast mieć źródło skryptu Python jako skrypt bash, prostsze i bardziej eleganckie byłoby posiadanie źródła skryptu opakowującego, init_enva następnie uruchamianie skryptu w Pythonie w zmodyfikowanym środowisku.

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

5
Może to rozwiązać problem w niektórych okolicznościach, ale nie we wszystkich. Na przykład piszę skrypt w języku Python, który musi wykonać coś takiego jak pozyskiwanie pliku (w rzeczywistości ładuje moduły, jeśli wiesz, o czym mówię) i musi załadować inny moduł w zależności od pewnych okoliczności. Więc to w ogóle nie rozwiązałoby mojego problemu
Davide

To w większości przypadków odpowiada na pytanie i używałbym go wszędzie tam, gdzie to możliwe. Trudno mi było wykonać tę pracę w moim IDE dla danego projektu. Jedną z możliwych modyfikacji może być uruchomienie całości w powłoce ze środowiskiembash --rcfile init_env -c ./script.py
xaviersjs

6

Zaktualizowana odpowiedź @ lesmana dla Pythona 3. Zwróć uwagę, że użycie tego narzędzia env -izapobiega ustawianiu / resetowaniu zewnętrznych zmiennych środowiskowych (potencjalnie niepoprawnie, biorąc pod uwagę brak obsługi wielowierszowych zmiennych env).

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

Użycie tego daje mi "PATH: undefined variable", ponieważ env -i kasuje ścieżkę. Ale działa bez env -i. Uważaj również, aby w wierszu było wiele znaków „=”
Fujii

5

Przykład zawijania doskonałej odpowiedzi @ Briana w funkcji:

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

Używam tej funkcji narzędzia do odczytywania poświadczeń aws i plików docker .env z plikami include_unexported_variables=True.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.