rcmdnk's blog

[第2版]Python 機械学習プログラミング 達人データサイエンティストによる理論と実践 (impress top gear)

Pythonでシェルで設定されている環境変数を返してくれる関数いろいろありますが、 そのうちos.path.expandvarsは定義されてない変数は シェルスクリプトのように空文字にするのではなく、その文字列まま 返すという仕様だったという話。

Pythonで環境変数を取得する

Pythonで環境変数を取得するには、一番シンプルには

os.environ.get('SHELL')

の様にします。これにより、$SHELLで設定されている環境変数が取得できます。

もし設定されていない場は空文字ではなく、Noneが返されます。

ただし、この関数は第2引数に変数が設定されてない場合に返すものを指定することができ、

os.environ.get('SHELL', '')

の様にすればSHELLが設定されてないときには空文字が返されます。

もう一つ、os.path.expandvarsという関数があり、 これを使うと$XXX${XXX}といったシェルの変数的な文字列を変換してくれます。

pathの関数ですが、/を含むパス形式ではない通常の文字列でも その中に$XXX${XXX}が含まれていれば展開してくれます。 (区切りは通常のシェルスクリプトのように空白や/.など。もちろん{}で囲めばその中のみ使う。)

os.path.expandvars('$SHELL')

とすると上の場合と一緒になります。 expandvarsの場合は他の文字列と組み合わせて、

os.path.expandvars('$HOME/mydir')

のようにすることもでき、この場合は$HOMEだけが変換され、 /home/user/mydirの様な感じの文字列を返します。

これが、シェルスクリプト的な考えで行くと、$HOMEが設定されていない場合、

/mydir

が返されるような気がしますが、実際には

$HOME/mydir

という文字列をそのまま返します。

不正な変数名や存在しない変数名の場合には変換されず、そのまま返します。

Pythonでも他の言語でも、ほとんど場合は設定されてない変数があればエラーが出るわけで、 この様に指定されてない可能性のある変数、を許しながら、むしろ使いながら扱うのは シェルスクリプト特有の話なのでPythonの中で扱う際にはそういうユースケースは無しにする、 というのが一番かもしれません。

ただ、そうはいってもこんな感じで展開する以上、やはり シェルスクリプト的な感じで定義されてないものは空文字を返す、というのが欲しいところ。

未定義変数に対して空文字を返す

そんな関数がデフォルトで別にちゃんと定義されてるかな、と思ったんですが 現状はない模様。

なので、以下にあるように正規表現を使って $XXXなどを見つけて、os.environ.getで変数を探し、 未定義は空文字を返すようにして置き換える、というもの。

How to expand environment variables in python as bash does? - Stack Overflow

上を参考に、一番シンプルにやるには以下の様な関数を定義してやれば良いと思います。

expandpath.py
1
2
3
4
5
6
import re

def expandpath(path):
    return re.sub(r'(?<!\\)\$(\w+|\{([^}]*)\})',
                  lambda x: os.environ.get(x.group(2) or x.group(1), ''),
                  path).replace('\\$', '$')

まず、正規表現による検索で、$XXX、もしくは${XXX}となってるものを探しています。

\$(\w+|\{([^}]*)\})で変数部を抜き出します。

最初の(?<!\\)は変数の前がバックスラッシュで無いこと($がエスケープされてないこと)を要請しています。

この正規表現で各$XXX${XXX}の部分が該当して抜き出されますが、 re.subの第2引数ではこれらを変更する先の値を入れます。

関数も入れることが出来るのでここではlambda式を入れていますが、 この引数となるのは上でマッチした文字列そのものではなく、Matchオブジェクトです。

そもそも今回の例では()が入れ子になってるので2つ抜き出される可能性があるわけですが。

$XXXならXXX${XXX}なら{XXX}XXXが抜き出されています。

これらはMatch.group(1)Match.group(2)で取れますが、 $XXXの様な場合には中のカッコ内に該当が無いので2つ目がNoneになります。

なので、

  • ‘$XXX’ -> (‘XXX’, None)
  • ’${XXX}’ -> (‘{XXX}’, ‘XXX’)

となってるため、os.environ.getの引数に

x.group(2) or x.group(1)

を入れることで、必ずXXXになるようにしています。 (Noneの場合にはorを通してx.group(1)XXXまで行くため。)

os.environ.getの第2引数で空文字を入れることでもし定義されてなければ消すだけに。

re.subで変換したあと、最後に.replace('\\$', '$') をしてますが、これは シェルスクリプトで

$ echo "hoge\$hoge"
hoge$hoge

の様にエスケープが外れるのと同じ動作にしてあります。

まあこの辺気にするなら他のバックスラッシュも全部消すべきか?などともなるのですが、 今回は変数周りの動作ということでここまでで。

これでos.path.expandvarsに比べてよりシェルスクリプト的な 変数展開が出来る関数ができました。

その他諸々を考慮したもの

上の関数で環境変数は埋め込みますが、パスを考えたとき、$HOMEを示す~を使う可能性があります。 これは上の関数では変更されず、os.path.expanduserを使う必要があります。

もう一つ、よく使う変数で$HOSTNAMEというものがあると思います。 ただ実はこれ環境変数ではなく、シェル変数です。

$ env|grep HOSTNAME

としても何も出ません(少なくともmacOSやUbuntu上のBashでは)。

なのでサブプロセスとして立ち上がるPythonには変数が渡されず、使うことができません。 export HOSTNAMEとしたり、HOSTNAME=$HOSTNAME test.pyみたいにすれば使うこともできますが。

ただ、これ使いたい変数だったときがあって、そのため、

import os

hostname = os.uname()[1]
path.replace('$HOSTNAME', hostname).replace('$HOSTNAME', hostname)

みたいな感じで$HOSTNAMEと同じ値を持つos.uname()の2つ目の項目を取ってきて無理やり変換しました。

これらをまとめて

expandpath.py
1
2
3
4
5
6
7
import re

def expandpath(path):
    return re.sub(r'(?<!\\)\$(\w+|\{([^}]*)\})',
                  lambda x: os.environ.get(x.group(2) or x.group(1), ''),
                  os.path.expanduser(path).replace('$HOSTNAME', os.uname()[1])
                  .replace('${HOSTNAME}', os.uname()[1])).replace('\\$', '$')

みたいなものを作ると$HOSTNAME~も使えるパス展開関数ができます。

おまけ

上でHOSTNAMEが環境変数でない、といってますが、同じ様にシステムの値を示す、 HOSTTYPEOSTYPEMACHTYPEなども環境変数になっておらずos.environ.getで取れません。

HOSTTYPEに関してはUbuntuだと環境変数になっていましたが、macOSだとなってませんでした。

特にOSTYPEはmacOSやLinuxを区別するのに便利なので使えれば、と思うのですが。 $OSTYPEはmacOSだとdarwin19など、Linuxだとlinux-gnuなどになります。

Pythonの中で近い情報だと、

macOS:

  • os.uname()[0]: Darwin
  • os.uname()[3]: 19.6.0
  • sys.platform: darwin
  • platform.platform: macOS-10.15.7-x86_64-i386-64bit

Linux (WSL, Ubuntu):

  • os.uname()[0]: Linux
  • os.uname()[3]: 4.19.104-microsoft-standard
  • sys.platform: linux
  • platform.platform: Linux-4.19.104-microsoft-standard-x86_64-with-glibc2.2.5

な感じ。macOSだけならsys.platform + os.uname()[3].splot('.')[0]とかで出来ますが、 Linuxの場合にはこれだと$HOSTNAMEとは違うものになります。

ある程度自分で使う範囲でパターンを集めれば上の情報からでも 再現出来るようには出来るとは思いますが、 汎用的にすればどうすれば、と。

1
2
import subprocess
subprocess.Popen('echo $HOSTNAME', shell=True).wait()

の様にしてもシェル変数は取得できません。

PythonだけでOSを判定したるするだけなら、sys.platformとかで十分なのですが、 やりたいこととしては、Pythonのアプリで扱う設定ファイルに、 シェルスクリプトの感覚で変数を埋め込む、ということなので シェルで使っている変数をそのまま使いたいわけです。

以下のIssueとかで悩んでたりするのでなにか良い方法があれば教えていただきたいなと思ってます。

Add $OSTYPE value support for file command · Issue #98 · rcmdnk/homebrew-file

Sponsored Links
Sponsored Links

« Raspberry Piで測定した温度湿度気圧をGoogle Spreadsheetsに記録する HomebrewでミーティングアプリZoomのCaskがzoomusからzoomにrename »