LoginSignup
20
17

More than 5 years have passed since last update.

shで外部コマンドを関数のように扱う

Posted at

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

あまり入れ子にすると分かりづらくなるので以前の記事事で紹介したtoolzpipe, 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 を継承したクラスで置き換えているのがポイント。

mysh.py
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'

参考資料

20
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
17