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
上を参考に、一番シンプルにやるには以下の様な関数を定義してやれば良いと思います。
1 2 3 4 5 6 |
|
まず、正規表現による検索で、$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つ目の項目を取ってきて無理やり変換しました。
これらをまとめて
1 2 3 4 5 6 7 |
|
みたいなものを作ると$HOSTNAME
や~
も使えるパス展開関数ができます。
おまけ
上でHOSTNAME
が環境変数でない、といってますが、同じ様にシステムの値を示す、
HOSTTYPE
、OSTYPE
、MACHTYPE
なども環境変数になっておらず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 |
|
の様にしてもシェル変数は取得できません。
PythonだけでOSを判定したるするだけなら、sys.platform
とかで十分なのですが、
やりたいこととしては、Pythonのアプリで扱う設定ファイルに、
シェルスクリプトの感覚で変数を埋め込む、ということなので
シェルで使っている変数をそのまま使いたいわけです。
以下のIssueとかで悩んでたりするのでなにか良い方法があれば教えていただきたいなと思ってます。
Add
$OSTYPE
value support for file command · Issue #98 · rcmdnk/homebrew-file