sh
という外部コマンドをあたかも関数のようにインポートして使用できるライブラリを試してみました。
>>> from sh import echo
>>> echo('hello', 'world')
hello world
インストール
pip
を使ってインストールできる。
$ pip install sh
使い方
使い方はsh
モジュールから使用したいコマンドをインポートして実行するだけ。
>>> from sh import date
>>> date()
2015年 2月 1日 日曜日 22時50分13秒 JST
次のように書く事もできる。
>>> import sh
>>> sh.date()
2015年 2月 1日 日曜日 22時50分13秒 JST
引数
コマンドの引数は関数の引数として渡すことができる。
>>> from sh import echo
>>> echo('hello', 'world')
hello world
オプション
オプションはキーワード引数として渡すことができる。
>>> from sh import curl
>>> curl('http://google.com/', o='out.html', silent=True, L=True)
オプションも引数と同じように関数の引数として渡すこともできる。
オプションが引数の前になければいけないコマンドの場合はこちらを使う必要がある。
>>> curl('-o', 'out.html', '--silent', '-L', 'http://google.com/')
標準入力
キーワード引数_in
に文字列を指定するとコマンドに標準入力として渡すことができる。
>>> from sh import cat
>>> cat(_in='hello')
hello
_in
にはIterableを指定することも可能。
>>> cat(_in=['hello\n', 'world\n'])
hello
world
ファイルオブジェクトを渡すこともできる。
>>> cat(_in=open('test.txt'))
hoge
fuga
piyo
リダイレクト
キーワード引数_out
、_err
にファイル名を指定することで標準出力、標準エラーをファイルにリダイレクトすることができる。
>>> from sh import echo, cat
>>> echo('hello', 'world', _out='hello.txt')
>>> cat('hello.txt')
hello world
_out
、_err
にはコールバック関数を渡すことも可能。
>>> echo('hello', 'world', _out=lambda s: sys.stdout.write(s.upper()))
HELLO WORLD
出力のイテレータ
キーワード引数_iter
を指定すると結果を行毎に返すイテレータが取得できる。
tail -f
などの無限に出力が続くコマンドの場合、プロセスが終了しないため_iter
を使ってイテレータから出力を取得する必要がある。
>>> from sh import seq
>>> seq(1, 3)
1
2
3
>>> for n in seq(1, 3, _iter=True):
... print n.strip()
...
1
2
3
パイプ
関数を入れ子にすることで関数の出力を別の関数の入力にパイプで渡すことができる。
>>> from sh import echo, wc
>>> wc(echo('hello'), c=True)
6
あまり入れ子にすると分かりづらくなるので以前の記事事で紹介したtoolz のpipe
, thread_first
関数などと組み合わせて使うと良さそう。
>>> pipe('hello', echo, lambda x: wc(x, c=True))
6
>>> thread_first('hello', echo, (wc, '-c'))
6
sh
のパイプはデフォルトでは前のコマンドが終了してから次のコマンドに結果を渡すという動きをする。
tail -f
のような終了しないコマンドをそのまま実行すると帰ってこなくなるため、キーワード引数_pipe
でパイプラインの中で実行されていることを教えてやる必要がある。
>>> from sh import yes, tr
>>> it = tr(yes(), 'y','Y', _iter=True) # yesコマンドが終了しないためNG
>>> it = tr(yes(_piped=True), 'y','Y', _iter=True) # これはOK
>>> it.next()
u'Y\n'
グロブ
関数の引数にアスタリスクを渡しても展開されない。
>>> from sh import ls
>>> ls('./*') # 展開されないので./*を探そうとして通常失敗する。
展開するためにはsh.glob
を使用する。
標準ライブラリのglob.glob
を使ってはダメ。
>>> from sh import ls, glob
>>> ls(glob('./*')) # sh.globを使って展開
バックグラウンド実行
デフォルトではsh
の関数はプロセスの終了を待つ。バックグラウンドで実行したい場合はオプションに_bg=True
を付ける。wait
メソッドでバックグラウンドで実行したプロセスの終了を待つことができる。
>>> from sh import sleep
>>> sleep(10) # 10秒間ブロックする
>>> p = sleep(10, _bg=True) # すぐに帰ってくる
>>> p.wait() # プロセスの終了を待つ
終了コードとエラー処理
コマンドの終了コードはexit_code
で取得できる。正常に終了している場合には通常0になる。
>>> from sh import ls
>>> output = ls('/tmp')
>>> output.exit_code
0
コマンドが異常終了した場合には例外が発生する。特定の終了コードは ErrorReturnCode_<終了コード>
で捕捉できる。ErrorReturnCode
は全ての終了コードを捕捉する。
>>> from sh import ls, ErrorReturnCode_1
>>> try:
... output = ls('/hogehoge')
... except ErrorReturnCode_1:
... print "return code is 1"
...
return code is 1
>>> try:
... output = ls('/hogehoge')
... except ErrorReturnCode as e:
... print 'cmd:', e.full_cmd
... print 'returncode:', e.exit_code
... print 'stdout:', e.stdout
... print 'stderr:', e.stderr
...
cmd: /bin/ls /hogehoge
returncode: 1
stdout:
stderr: ls: /hogehoge: No such file or directory
コマンドが成功した場合でも0以外の終了コードを返すコマンドの場合にはキーワード引数_ok_code
にコマンドが成功した場合の終了コードのリストを指定する。
シグナル
以下の3つのメソッドでプロセスにシグナルを送ることができる。
- kill
- SIGKILLを送る
- terminate
- SIGTERMを送る
- signal(sig)
- 指定されたシグナルを送る
シグナルでプロセスが終了した場合にはwait
した際SignalException_<シグナル番号>
が発生する。また、exit_code
がシグナル番号をマイナスにしたものになる。
>>> from sh import sleep
>>> p = sleep(10, _bg=True)
>>> p.kill()
>>> try:
... p.wait()
... except ErrorReturnCode as e:
... print 'cmd:', e.full_cmd
... print 'returncode:', e.exit_code
... print 'stdout:', e.stdout
... print 'stderr:', e.stderr
...
cmd: /bin/sleep 10
returncode: -9
stdout:
stderr:
仕組み
使用できる外部コマンドは環境によって異なることからsh
は動的に外部コマンドに対応するオブジェクトを生成していることがわかります。
任意のコマンドをインポートできる仕組みは、おおよそ次のようなコードで実装されています。
モジュールのインポート時にsys.modules[__name__]
をModuleType
を継承したクラスで置き換えているのがポイント。
import sys
import subprocess
from types import ModuleType
class MySh(ModuleType):
def __init__(self, self_module):
self.__name__ = self_module.__name__
def __getattr__(self, name):
def command(*args):
return subprocess.check_output([name] + list(args))
return command
mysh = sys.modules[__name__]
sys.modules[__name__] = MySh(mysh)
このモジュールを使って次のようにecho
をインポートして実行する事ができます。
>>> from mysh import echo
>>> echo('hello', 'world')
'hello world\n'
参考資料
-
11 Python Libraries You Might Not Know
-
sh
モジュールを紹介していたブログ。他にも便利そうなモジュールが紹介されています。
-
-
Easily import a dynamically created module
- モジュールを動的に作成する方法の解説