web-dev-qa-db-ja.com

subprocess.Popenから「ソース」コマンドを呼び出す

source the_script.shで呼び出す.shスクリプトがあります。これを定期的に呼び出すことは問題ありません。しかし、私はpythonスクリプトからsubprocess.Popenを介してそれを呼び出そうとしています。

Popenから呼び出すと、次の2つのシナリオ呼び出しで次のエラーが発生します。

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

何が得られますか? pythonの外部でできるのに、なぜPopenから「ソース」を呼び出せないのですか?

30
coffee

sourceは実行可能なコマンドではなく、シェルの組み込みコマンドです。

sourceを使用する最も一般的なケースは、環境を変更するシェルスクリプトを実行し、現在のシェルでその環境を保持することです。これが、virtualenvがデフォルトのpython環境を変更するために動作する方法です。

サブプロセスを作成し、サブプロセスでsourceを使用しても、おそらく有用なことは行われず、親プロセスの環境は変更されません。ソーススクリプトを使用することによる副作用は発生しません。 。

Pythonには類似のコマンドexecfileがあり、現在のpythonグローバルネームスペース(または、提供する場合は別のネームスペース)]を使用して指定したファイルを実行します。 bashコマンドsourceのように。

サブシェルでコマンドを実行し、その結果を使用して現在の環境を更新できます。

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)
28
xApple

破損したPopen("source the_script.sh")は、_'source the_script.sh'_プログラムの起動に失敗するPopen(["source the_script.sh"])と同等です。見つからないため、_"No such file or directory"_エラーが発生します。

sourceはbash組み込みコマンド(bashで_help source_と入力)であるため、壊れたPopen("source the_script.sh", Shell=True)は失敗しますが、デフォルトのシェルはそれを理解しない_/bin/sh_です(_/bin/sh_は_._)を使用します。 _the_script.sh_に他のbash-ismがある可能性があると仮定すると、bashを使用して実行する必要があります。

_foo = Popen("source the_script.sh", Shell=True, executable="/bin/bash")
_

@ IfLoopによる のように、サブプロセスでsourceを実行することは、親の環境に影響を与えないため、あまり役に立ちません。

os.environ.update(env)ベースのメソッドは、_the_script.sh_が一部の変数に対してunsetを実行すると失敗します。 os.environ.clear()を呼び出して環境をリセットできます。

_#!/usr/bin/env python2
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
_

それは _env -0_と@unutbuによって提案された.split('\0')を使用します

_os.environb_の任意のバイトをサポートするには、jsonモジュールを使用できます(Python version where "json.dumps not by parsable by json。ロード」の問題 は修正されました):

パイプを介して環境を渡すことを避けるために、Pythonコードを変更して、サブプロセス環境で自身を呼び出すことができます。

_#!/usr/bin/env python2
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))
_
14
jfs

sourceはbash固有のシェル組み込みです(非対話型シェルは、多くの場合bashではなく軽量のダッシュシェルです)。代わりに、/bin/shを呼び出すだけです。

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

@xAppleによる回答のバリエーション。環境変数を設定するために(Pythonファイル)ではなく)シェルスクリプトをソースにでき、他のシェル操作を実行できる場合があるため、次に、サブシェルが閉じたときにその情報を失うのではなく、その環境をPythonインタープリターに伝播します。

変動の理由は、「env」からの出力の1行につき1変数形式の仮定が100%堅牢ではないためです。ただ、変数(シェル関数)を処理する必要がありました。改行、その解析を台無しにしました。そこで、ここで少し複雑なバージョンを使用します。これはPython自体を使用して、環境ディクショナリを堅牢な方法でフォーマットします。

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)

たぶんすてきな方法がありますか?これにより、ソースとなるスクリプトにechoステートメントを挿入した場合に、環境解析が台無しになることもなくなります。もちろん、ここにはexecがいるので、信頼されていない入力には注意してください...しかし、それは任意のシェルスクリプトをソース/実行する方法についての議論で暗黙的だと思います;-)

UPDATE:envの改行を処理する別の(おそらくより良い)方法については @ unutbuの@xAppleの回答に関するコメント を参照してください出力。

1
andybuckley

これには多くの答えがあるようです。すべてを読んでいないので、すでに指摘しているかもしれません。ただし、このようなシェルコマンドを呼び出す場合は、Popen呼び出しにShell = Trueを渡す必要があります。それ以外の場合は、Popen(shlex.split())を呼び出すことができます。必ずshlexをインポートしてください。

私は実際にこの関数をファイルのソースおよび現在の環境の変更の目的で使用します。

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

ソースコマンドを他のスクリプトまたは実行可能ファイルに適用する場合は、別のラッピングスクリプトファイルを作成し、必要なロジックを追加して「ソース」コマンドを呼び出します。その場合、このソースコマンドは、実行しているローカルコンテキスト、つまりsubprocess.Popenが作成するサブプロセス内のローカルコンテキストを変更します。

プログラムが実行されているpythonコンテキストを変更する必要がある場合、これは機能しません。

0
Vlad Ogay