はじめに
井の中の蛙大海を知らず、されど空の青さを知る。nikkieです。
Bashについて、考え違いをしていたことに気づきました。
コマンド置換とパイプラインの違いに気づいた件を綴っていきます。
※シェルは練度に伸びしろがあるので、間違ったことを書いている場合やもっとよいやり方がある場合は、ぜひ@ftnextまでご連絡ください。
目次
用語おさらい
コマンド置換とは
『マスタリングLinuxシェルスクリプト 第2版』1.9 より
コマンドの実行結果を変数内に保管すること
2つの方法が紹介されます。
バッククォートを使うやり方と$()
を使うやり方です。
$ var1=`pwd` $ var2=$(pwd)
変数に保管した値はecho $var1
のようにして確認できます。
変数を使わずにコマンド置換を直接別のコマンドに渡すこともできます(イメージcommand2 $(command1)
)。
リファクタリングテクニックの中の「変数のインライン化」っぽいなと思います
パイプラインとは
|
を使ってコマンドをつなぐことと理解しています。
今回参照したのは以下です
- http://linuxjm.osdn.jp/html/GNU_bash/man1/bash.1.html 「シェルの文法」>「パイプライン」
- https://www.gnu.org/software/bash/manual/html_node/Pipelines.html
例えば、ファイルの重複した行を除いて何行か見るのは、パイプラインで書けます1。
$ cat tenshi.txt | sort | uniq | wc -l
誤解していたために、直面した事象
macOS 12.6.3のPC(zshがデフォルト)で、bashを立ち上げて動作確認しました。
以下のようなディレクトリ構造とします。
. ├── my_lib/ │ ├── __init__.py │ └── awesome.py └── tests/ ├── __init__.py └── test_awesome.py
find
コマンドを使って.py
ファイルを一覧にできます。
Man page of FIND
$ find my_lib tests -name '*.py' my_lib/__init__.py my_lib/awesome.py tests/__init__.py tests/test_awesome.py
すべての.py
ファイルのパスを、Pythonで実装したコマンドラインツールに渡すことを考えます。
動作環境は、Python 3.10.9です。
import argparse parser = argparse.ArgumentParser() parser.add_argument("file", nargs="+") args = parser.parse_args()
コマンド置換ではうまくいきます。
$ python command.py $(find my_lib tests -name '*.py') $ echo $? 0
パイプラインではうまくいきません2。
$ find my_lib tests -name '*.py' | python command.py usage: command.py [-h] file [file ...] command.py: error: the following arguments are required: file $ echo $? 2
コマンド置換では引数として渡るが、パイプラインは標準入力として渡る
いましがたbashのコマンド置換 $(command) とパイプライン command1 | command2の違いに気づいた気がする!
— nikkie にっきー (@ftnext) 2023年5月27日
command2 $(command1) だと動くのに
command1 | command2 だと引数が足りないエラーになるのなんでだー🤯とハマってましたが
パイプラインは標準入力に渡しているのであって、引数に渡さない
https://www.gnu.org/software/bash/manual/html_node/Pipelines.html にもあります。
The output of each command in the pipeline is connected via a pipe to the input of the next command.
意訳 パイプラインの各コマンドの出力はパイプを介して次のコマンドの入力へと繋がれる
パイプラインの場合、続くコマンドの標準入力に渡っていますが、コマンドの引数としては渡っていません。
だから、必須の引数file
がなく、引数のパースでエラーとなったのです。
Pythonスクリプトを使って確認
import sys print(f"{sys.argv=}") if not sys.stdin.isatty(): print(f"{sys.stdin.read().splitlines()=}")
https://stackoverflow.com/a/46907341 を参考にしました。
sys.argv
- https://docs.python.org/ja/3/library/sys.html#sys.argv
- argparseは
sys.argv
をパースしています
sys.stdin
- https://docs.python.org/ja/3/library/sys.html#sys.stdin
- インタプリタが使用する標準入力のファイルオブジェクト
- isattyメソッドは https://docs.python.org/ja/3/library/io.html#io.IOBase.isatty にドキュメントがあります
ストリームが対話的であれば (つまりターミナルや tty デバイスにつながっている場合) True を返します。
コマンド置換
コマンド置換で渡してみましょう。
$ python command.py $(find my_lib tests -name '*.py') sys.argv=['command.py', 'my_lib/__init__.py', 'my_lib/awesome.py', 'tests/__init__.py', 'tests/test_awesome.py']
コマンド置換で渡したファイルパスがsys.argv
に渡っています。
コマンド置換では標準入力で渡していないので、sys.stdin
についての出力はありません
パイプライン
パイプラインで渡してみます。
$ find my_lib tests -name '*.py' | python command.py sys.argv=['command.py'] sys.stdin.read().splitlines()=['my_lib/__init__.py', 'my_lib/awesome.py', 'tests/__init__.py', 'tests/test_awesome.py']
findコマンドで列挙したファイルパスは、標準入力で渡っていることが分かります。
終わりに
「コマンド置換では動くのに、パイプラインで動かないのなんでだー!」とハマっての学びをアウトプットしました。
数時間前までの私よ、この2つ同じものじゃないですから〜!
- コマンド置換では、コマンドの出力を引数として渡せていた
- パイプラインは、コマンドの出力を、繋いだコマンドの入力に渡す(引数としては渡さない)
違いを理解したことで、「両方に対応できるコマンドラインツールは、argparseなどでどう実装するんだろう」というのが気になっています。
このあたりは既存のライブラリなどを覗いてゆっくり考えていきたいと思います。
-
tenshi.txt
の内訳は macOSのbash(とzsh)で先頭から何行かを除いて残りの行を出力する - nikkie-ftnextの日記 にあります。また今回、『マスタリングLinuxシェルスクリプト 第2版』12.2.2より、sort | uniq
はsort -u
と書けることを知りました↩ -
argparseの
nargs="+"
を使って、位置引数file
に1つ以上値が渡ることを確認しています。1つも渡っていないので、パースエラーとなりました。ref: https://docs.python.org/ja/3/library/argparse.html#nargs↩