読者です 読者をやめる 読者になる 読者になる

百日半狂乱

Shut the fuck up and write some code!!

Linuxターミナル、コマンドtips その3: stdin, stderr, stdout, リダイレクト, パイプ

Linux terminal bash CLI command pipe redirection

このtipsはこれからLinuxを使っていく必要がある人、特に端末操作に苦戦している人、もしくは端末操作に対して嫌悪感すら抱いている人に向けて書いたものです.作成の経緯はその1の冒頭および注釈に書きました.

前回の話題

  • ファイルを見つける(locate, find)
  • ファイルの中身を調べる(grep)

Linuxターミナル、コマンドtips その2: ファイルを見つける(locate, find)、ファイルの中身を調べる(grep)

今回の話題

  • コマンドや自作プログラムの入出力をコントロールする
    • stdin, stdout, stderrの理解とターミナル、キーボードとの関係
    • リダイレクト: ファイルから読み出す、ファイルに書き出す
    • パイプ: コマンドの入出力をを組み合わせる

標準入出力(stdin, stdout, stderr)の理解とターミナルとの関係

まずは標準入出力とプログラムとの関係を再確認

linuxにおいて、あらゆるプログラム(コマンド、Cプログラム、shellscript、C/C++/Java/Ruby/Perl/Pythonプログラム、etc...)の入出力は以下のように説明できる。

標準入力(stdin:0) ---> プログラム ---+---> 標準出力(stdout:1)
                                 |
                                 +--> 標準エラー出力(stderr:2)
  • これまで扱ってきたcat, ls, pwd, locate, find, grep等々、あらゆるコマンドは本質的にCプログラムをコンパイルした実行ファイルと変わらない
    • hello world!をprintfで出力するプログラムも、上記のコマンドと同じディレクトリに保存すればコマンドと同じように呼び出せる
  • 出力が2つに分かれている点に注意

参考: ふつうのLinuxプログラミング

上記のリンク先の連載(全4回)もしくは書籍を是非読んで欲しい.個人的に学習の初期で最も役に立った本の一つ.

標準入出力とターミナル、キーボードとの関係

あるコマンドを実行すると、ターミナルにその出力が表示されるが、それは標準入出力とターミナルの関係が以下のようになっているから。

キーボード ---> 標準入力(stdin:0) ---> プログラム ---+--> 標準出力(stdout:1) --------+---> ターミナル(shell)
                                               |                             |
                                               +--> 標準エラー出力(stderr:2) ---+
  • いまいち実感しにくいけど、標準入力はキーボードに紐付けられている
    • 実は、Unix系OSでは、キーボードなどの周辺機器デバイスやネットワークも全てファイルとして抽象化されており、インターフェースが統一されている
  • stdoutと、stderrが両方ともターミナル上に出力される点に注意

参考: Wikipedia:標準ストリーム

リダイレクト

プログラムを実行するとターミナル上に表示されるのは確認したが、それは単にデフォルトの出力先がターミナルというだけであって、linuxにおいては、その出力先を簡単にファイルに切り替えることができる.

stdout: 出力をリダイレクトする

すでに述べたように、プログラム自体はコマンドだろうが、自分で組んだプログラムの実行ファイルだろうが何でも良いが、ココでは例としてlsの出力をファイルに保存してみる。

$ ls > output-ls #lsコマンドの標準出力(stdout)をoutput-lsというファイルにリダイレクト
$ ls 1> output-ls #上記と全く同じ意味、stdoutの番号(ファイルディスクリプタ)は1番
$ cat output-ls #ファイルの中身を確認

ターミナル上では、横並びに表示されるのに、ファイル内部では行区切りで一つ一つ保存されていることが確認できる(これは後で示すパイプによって、他のコマンドと組み合わせ易くするため)

stdout: 既存のファイルに出力を追記する

上記で生成したoutput-lsという既存ファイルに、更に出力を追記するには>>を使う。

$ ls > output-ls #lsコマンドの標準出力(stdout)をoutput-lsというファイルにリダイレクト
$ ls >> output-ls #lsコマンドの標準出力(stdout)をoutput-lsというファイルに追記
$ cat output-ls #ファイルの中身を確認、追記されている
$ ls > output-ls #すでに存在するファイルに対しては上書きとなるため、元のデータが失われる

よくある致命的な失敗

>>で追記するべき所で、>で上書きしてしまい、元のデータを全てふっ飛ばす

stdin: 標準入力からデータを入力する

他の入力を必要とするコマンドをも同様だが、例えばcatやgrepコマンドは、引数に入力データ(=パターンマッチの対象)となるファイルを指定することもできるし、標準入力からデータを渡すこともできる

$ cat
$ grep 'hoge'

上記を実行すると、ctrl+dを押すまで、catやgrepにキーボード(=stdin)からデータを一行ずつ渡すことができる

catは単に入力文字をそのまま出力するし、grepはパターンがマッチした場合にだけ入力行を出力する

stdin: 標準入力データをリダイレクトで指定する

当然、catやgrepで上記のように一行一行入力するなどと言ったことは通常行わないため、通常ファイル単位でまとまったデータをコマンドに渡す

<で入力データファイルを指定することができる

$ grep 'hoge' < target_file.txt
$ grep 'hoge' 0< target_file.txt #上記と全く同じ意味

なんとなく、target_file.txt > grep 'hoge'などとしてしまいそうだけど、左向きの山カッコじゃないとダメなので注意(シェルは入力されたコマンドを前から順に解釈する)

stderr: 標準エラー出力を確認する

例えば、grepコマンドを単体で実行するとエラーメッセージが表示される

$ grep
使用法: grep [OPTION]... PATTERN [FILE]...
Try 'grep --help' for more information.

これを>でリダイレクトしてもエラーメッセージがファイルに書き込まれない

$ grep > output-grep
使用法: grep [OPTION]... PATTERN [FILE]...
Try 'grep --help' for more information.
$ cat output-grep #ファイルは空

このエラーメッセージは、標準エラー出力(stderr)である

標準エラー出力の番号(ファイルディスクリプタ)は2なので2>としてやれば良い

$ grep 2> output-grep
$ cat output-grep #ファイルに書き込まれていることを確認

stdoutとstderrを操る

stdoutとstderrを別々のファイルに書き出す

commandは任意のプログラム

command > output-stdout 2> output-stderr

stdoutとstderrを1つのファイルにマージする

command > output-stdout-stderr 2>&1

必ず出力先のファイルを指定した後に2>&1とする

stdoutとstderrを両方とも捨てる

command > /dev/null 2>&1

/dev/nullはあらゆる出力を無に帰すブラックホール

不要な出力を捨てる宛先としてよく用いられる

stdoutは捨てて、stderrをstdoutとして出力する

command 2>&1 > /dev/null

catコマンドやgrepコマンドのソースコードを読む

参考: GNU catとかGNU grepを読んでみた

下記のgit cloneを叩いてソースコードを入手

$ git clone git://git.sv.gnu.org/coreutils

普段使っているコマンドは特別なものでもなんでもなく、単なるCのプログラムであることがわかる

小休止: 2014/08/04のrmコマンドのバグフィックス

2014/08/04、rm --helpの内容が修正されていたのだが、すぐに確認可能な小さなヘルプメッセージのバグ?だったので紹介

Date:   Mon Aug 4 11:42:45 2014 +0100

    doc: indicate that FILE arguments are optional with rm -f
        
        * src/rm.c (usage): s/FILE/[FILE]/.
        *     Fixes http://bugs.gnu.org/18187
        *

手元のrmコマンドのバージョンが古いので、上記の修正前のヘルプが表示された

$ rm --help
Usage: rm [OPTION]... FILE...
Remove (unlink) the FILE(s).

git log -pで変更履歴を溯ると面白いかもしれない

パイプ

要望: あるコマンドの結果を他のコマンドの入力としたい

よくある話

リダイレクトの知識があれば、この要望はファイルを介して簡単に解決できる

$ ls > output-ls #stdoutをoutput-lsというファイルに書き出す
$ grep 'mycat' < output-ls #output-lsをgrepのstdinとする
$ grep 'mycat' output-ls #もしくは、ファイルを引数で指定してやる

でもメンドクサイ

しかも、いちいちgrepの入力となる一時ファイル(output-ls)が生成されてウザイ

解法: コマンドの出力(stdout)を直接他のコマンドの入力(stdin)とする

|でできる

 command1 | command2

データの流れをイメージしよう

以下のようになっている

stdin ---> command1 ---> stdout | stdin ---> command2 ---> stdout
              |                                 |
              +---> stderr                      +---> stderr

stdoutとstderrの両方をパイプで渡す

command1 2>&1 | command2

stderrだけをパイプで渡す

command1 2>&1 > /dev/null | command2

パイプの威力

パイプはいくらでも繋げられる(重要)

工夫次第で壮絶に煩わしい作業がたったの一行で済んでしまうことも多々ある

集計、ソート、出力の制御を同時にやる

hoge, fuga, foo, barがランダムに並んでいるファイル(random-data.txt)を集計して結果をターミナル上に表示しながらファイルに記録したい場合、

$ cat random-data.txt | sort | uniq -c | tee sorted-data.txt
   6000 bar
   5000 foo
   4000 fuga
   3000 hoge

teeコマンドは受け取ったデータを指定したファイルに書き込みつつ、ターミナル上に表示する便利なコマンド

既存のファイルに追記したい場合は、tee -a sorted-data.txtという感じで、-aオプションを用いると良い

シェル芸

上記のような、一行で処理が簡潔するようなプログラムをワンライナーとか一行野郎などと言ったりする

このワンライナーのドリル的なものがあるので紹介

シェル芸勉強会スライド一覧

まとめと補足

  • 普段使っているコマンドはシンプル
    • stdin, stdout, stderrとコマンドとの関係が頭の中で整理されると、リダイレクトやパイプを気軽に使えるようになるはず
  • linux(Unix系OS)におけるコマンドは行志向
    • 自作プログラムの出力もプレーンテキストかつ行志向なものにしておくと、既存のコマンドとの組み合わせが簡単にできるようになる
  • パイプの可能性は無限大
    • 気軽に試せて結果をすぐに確認できる
    • 複数繋げてみて結果がおかしかったら、途中までで一旦ターミナル上に出力させてみて、思った通りになっているかチェックしてみる
  • シェルという大きな存在を忘れない
  • 全てはファイルとして抽象化されている

UNIXという考え方

以下を読むとなお良い

UNIXという考え方―その設計思想と哲学

UNIXという考え方―その設計思想と哲学