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

```sh``` という外部コマンドをあたかも関数のようにインポートして使用できるライブラリを試してみました。

```pycon
>>> from sh import echo
>>> echo('hello', 'world')
hello world

```

# インストール

```pip``` を使ってインストールできる。

```bash
$ pip install sh
```

# 使い方

使い方は```sh``` モジュールから使用したいコマンドをインポートして実行するだけ。

```pycon
>>> from sh import date
>>> date()
2015年 2月 1日 日曜日 22時50分13秒 JST

```

次のように書く事もできる。

```pycon
>>> import sh
>>> sh.date()
2015年 2月 1日 日曜日 22時50分13秒 JST

```
## 引数

コマンドの引数は関数の引数として渡すことができる。

```pycon
>>> from sh import echo
>>> echo('hello', 'world')
hello world

```

## オプション

オプションはキーワード引数として渡すことができる。

```pycon
>>> from sh import curl
>>> curl('http://google.com/', o='out.html', silent=True, L=True)
```

オプションも引数と同じように関数の引数として渡すこともできる。
オプションが引数の前になければいけないコマンドの場合はこちらを使う必要がある。

```pycon
>>> curl('-o', 'out.html', '--silent', '-L', 'http://google.com/')
```

## 標準入力

キーワード引数```_in``` に文字列を指定するとコマンドに標準入力として渡すことができる。

```pycon
>>> from sh import cat
>>> cat(_in='hello')
hello
```

```_in``` にはIterableを指定することも可能。

```pycon
>>> cat(_in=['hello\n', 'world\n'])
hello
world

```

ファイルオブジェクトを渡すこともできる。

```pycon
>>> cat(_in=open('test.txt'))
hoge
fuga
piyo

```

## リダイレクト

キーワード引数```_out```　、```_err``` にファイル名を指定することで標準出力、標準エラーをファイルにリダイレクトすることができる。

```pycon
>>> from sh import echo, cat
>>> echo('hello', 'world', _out='hello.txt')

>>> cat('hello.txt')
hello world

```

```_out```　、```_err``` にはコールバック関数を渡すことも可能。

```pycon
>>> echo('hello', 'world', _out=lambda s: sys.stdout.write(s.upper()))
HELLO WORLD

```

## 出力のイテレータ

キーワード引数```_iter``` を指定すると結果を行毎に返すイテレータが取得できる。
```tail -f``` などの無限に出力が続くコマンドの場合、プロセスが終了しないため```_iter``` を使ってイテレータから出力を取得する必要がある。

```pycon
>>> from sh import seq
>>> seq(1, 3)
1
2
3

>>> for n in seq(1, 3, _iter=True):
...     print n.strip()
... 
1
2
3
```

## パイプ

関数を入れ子にすることで関数の出力を別の関数の入力にパイプで渡すことができる。

```pycon
>>> from sh import echo, wc
>>> wc(echo('hello'), c=True)
       6

```

あまり入れ子にすると分かりづらくなるので[以前の記事](http://qiita.com/atsaki/items/e520cb6b3f3ae1541cfa)事で紹介した[toolz](https://github.com/pytoolz/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``` でパイプラインの中で実行されていることを教えてやる必要がある。

```pycon
>>> 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'
```

## グロブ

関数の引数にアスタリスクを渡しても展開されない。

```pycon
>>> from sh import ls
>>> ls('./*') # 展開されないので./*を探そうとして通常失敗する。
```

展開するためには```sh.glob``` を使用する。
**標準ライブラリの```glob.glob``` を使ってはダメ。**

```pycon
>>> from sh import ls, glob
>>> ls(glob('./*')) # sh.globを使って展開
```

## バックグラウンド実行

デフォルトでは```sh``` の関数はプロセスの終了を待つ。バックグラウンドで実行したい場合はオプションに```_bg=True``` を付ける。```wait``` メソッドでバックグラウンドで実行したプロセスの終了を待つことができる。

```pycon
>>> from sh import sleep
>>> sleep(10)              # 10秒間ブロックする
>>> p = sleep(10, _bg=True) # すぐに帰ってくる
>>> p.wait()               # プロセスの終了を待つ
```
## 終了コードとエラー処理

コマンドの終了コードは```exit_code``` で取得できる。正常に終了している場合には通常0になる。

```pycon
>>> from sh import ls
>>> output = ls('/tmp')
>>> output.exit_code
0
```

コマンドが異常終了した場合には例外が発生する。特定の終了コードは ```ErrorReturnCode_<終了コード>``` で捕捉できる。```ErrorReturnCode``` は全ての終了コードを捕捉する。

```pycon
>>> 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``` がシグナル番号をマイナスにしたものになる。

```pycon
>>> 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``` を継承したクラスで置き換えているのがポイント。

```py: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``` をインポートして実行する事ができます。

```pycon
>>> from mysh import echo
>>> echo('hello', 'world')
'hello world\n'
```

# 参考資料

* [11 Python Libraries You Might Not Know](http://blog.yhathq.com/posts/11-python-libraries-you-might-not-know.html)
  * ```sh``` モジュールを紹介していたブログ。他にも便利そうなモジュールが紹介されています。
* [Easily import a dynamically created module](http://pythondoeswhat.blogspot.jp/2011/12/easily-import-dynamically-created.html)
  * モジュールを動的に作成する方法の解説