Meniru 'sumber' Bash dengan Python

92

Saya memiliki skrip yang terlihat seperti ini:

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

Setiap kali saya membangun, saya menjalankan 'source init_env' (di mana init_env adalah skrip di atas) untuk menyiapkan beberapa variabel.

Untuk mencapai hal yang sama dengan Python saya menjalankan kode ini,

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)

Tetapi kemudian seseorang memutuskan akan lebih baik untuk menambahkan baris seperti berikut ke init_envfile:

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

Jelas skrip Python saya berantakan. Saya dapat memodifikasi skrip Python untuk menangani baris ini, tetapi kemudian akan rusak nanti ketika seseorang datang dengan fitur baru untuk digunakan dalam init_envfile.

Pertanyaannya adalah apakah ada cara mudah untuk menjalankan perintah Bash dan membiarkannya memodifikasi perintah saya os.environ?

getekha
sumber

Jawaban:

109

Masalah dengan pendekatan Anda adalah Anda mencoba menafsirkan skrip bash. Pertama, Anda hanya mencoba menafsirkan pernyataan ekspor. Kemudian Anda melihat orang-orang menggunakan ekspansi variabel. Nanti orang akan menempatkan kondisional dalam file mereka, atau proses substitusi. Pada akhirnya Anda akan memiliki penerjemah skrip bash lengkap dengan trilyun bug. Jangan lakukan itu.

Biarkan Bash menafsirkan file untuk Anda dan kemudian mengumpulkan hasilnya.

Anda bisa melakukannya seperti ini:

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

Pastikan Anda menangani kesalahan jika bash gagal source init_env, atau bash itu sendiri gagal dijalankan, atau subproses gagal mengeksekusi bash, atau kesalahan lainnya.

yang env -ipada awal baris perintah menciptakan lingkungan yang bersih. itu berarti Anda hanya akan mendapatkan variabel lingkungan dari init_env. jika Anda ingin lingkungan sistem yang diwariskan, hilangkan env -i.

Baca dokumentasi tentang subproses untuk lebih jelasnya.

Catatan: ini hanya akan menangkap variabel yang disetel dengan exportpernyataan, karena envhanya mencetak variabel yang diekspor.

Nikmati.

Perhatikan bahwa dokumentasi Python mengatakan bahwa jika Anda ingin memanipulasi lingkungan, Anda harus memanipulasi os.environsecara langsung daripada menggunakan os.putenv(). Saya menganggap itu bug, tapi saya ngelantur.

lesmana
sumber
12
Jika Anda benar-benar peduli dengan variabel yang tidak diekspor dan skrip berada di luar kendali Anda, Anda dapat menggunakan set -a untuk menandai semua variabel sebagai diekspor. Ubah saja perintahnya menjadi: ['bash', '-c', 'set -a && source init_env && env']
ahal
Perhatikan bahwa ini akan gagal pada fungsi yang diekspor. Saya akan senang jika Anda dapat memperbarui jawaban Anda yang menunjukkan penguraian yang berfungsi untuk fungsi juga. (mis. fungsi fff () {echo "fff";}; ekspor -f fff)
DA
2
Catatan: ini tidak mendukung variabel lingkungan multiline.
BenC
2
Dalam kasus saya, iterasi proc.stdout()hasil byte, sehingga saya mendapatkan TypeErrorpada line.partition(). Mengonversi menjadi string dengan line.decode().partition("=")memecahkan masalah.
Sam F
1
Ini sangat membantu. Saya mengeksekusi ['env', '-i', 'bash', '-c', 'source .bashrc && env']untuk memberikan diri saya sendiri hanya variabel lingkungan yang ditetapkan oleh file rc
xaviersjs
32

Menggunakan acar:

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

Diperbarui:

Ini menggunakan json, subproses, dan secara eksplisit menggunakan / bin / bash (untuk dukungan 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
Brian
sumber
Yang ini memiliki masalah di Ubuntu - ada shell default /bin/dash, yang tidak tahu sourceperintahnya. Untuk menggunakannya di Ubuntu, Anda harus menjalankannya /bin/bashsecara eksplisit, misalnya dengan menggunakan penv = subprocess.Popen(['/bin/bash', '-c', '%s && %s' %(source,dump)], stdout=subprocess.PIPE).stdout(ini menggunakan subprocessmodul yang lebih baru yang harus diimpor).
Martin Pecka
22

Daripada memiliki sumber skrip Python Anda sebagai skrip bash, akan lebih sederhana dan lebih elegan untuk memiliki sumber skrip pembungkus init_envdan kemudian menjalankan skrip Python Anda dengan lingkungan yang dimodifikasi.

#!/bin/bash
source init_env
/run/python/script.py
John Kugelman
sumber
5
Ini mungkin menyelesaikan masalah dalam beberapa keadaan, tetapi tidak semuanya. Misalnya saya menulis skrip python yang perlu melakukan sesuatu seperti mencari file (sebenarnya memuat modul jika Anda tahu apa yang saya bicarakan), dan perlu memuat modul yang berbeda tergantung pada beberapa keadaan. Jadi ini tidak akan menyelesaikan masalah saya sama sekali
Davide
Ini menjawab pertanyaan dalam banyak kasus, dan saya akan menggunakannya sedapat mungkin. Saya mengalami kesulitan membuat ini berfungsi di IDE saya untuk proyek tertentu. Salah satu modifikasi yang mungkin adalah menjalankan semuanya dalam shell dengan lingkunganbash --rcfile init_env -c ./script.py
xaviersjs
6

Memperbarui jawaban @ lesmana untuk Python 3. Perhatikan penggunaannya env -iyang mencegah variabel lingkungan asing disetel / disetel ulang (berpotensi salah karena kurangnya penanganan untuk variabel env multiline).

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
sumber
Menggunakan ini memberi saya "PATH: variabel tak terdefinisi" karena env -i membatalkan setel. Tapi itu berhasil tanpa env -i. Juga berhati-hatilah karena garis mungkin memiliki beberapa '='
Fujii
5

Contoh membungkus jawaban bagus @ Brian dalam sebuah fungsi:

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

Saya menggunakan fungsi utilitas ini untuk membaca kredensial aws dan file docker .env dengan include_unexported_variables=True.

JDiMatteo
sumber