UNIX講習会#2 パイプ・リダイレクト・プロセス置換


一部うまく表示できていない部分があるので，詳細は[こちら](http://unix-intro.sb.ecei.tohoku.ac.jp/text.html?02-pipe)からどうぞ．

---

## UNIXの哲学

> これがUNIXの哲学である。
>
> 一つのことを行い、またそれをうまくやるプログラムを書け。
>
> 協調して動くプログラムを書け。
>
> 標準入出力（テキスト・ストリーム）を扱うプログラムを書け。標準入出力は普遍的インターフェースなのだ。
> 
> — M. D. マキルロイ、UNIXの四半世紀

<small>[Wikipedia](http://ja.wikipedia.org/wiki/UNIX%E5%93%B2%E5%AD%A6)からコピペ</small>

---

## UNIXの哲学

> 1. 小さいものは美しい。
> 2. 各プログラムが一つのことをうまくやるようにせよ。
> 3. できる限り原型（プロトタイプ）を作れ。
> 4. 効率よりも移植しやすさを選べ。
> 5. 単純なテキストファイルにデータを格納せよ。
> 6. ソフトウェアの効率をきみの優位さとして利用せよ。
> 7. 効率と移植性を高めるためにシェルスクリプトを利用せよ。
> 8. 束縛するインターフェースは作るな。
> 9. 全てのプログラムはフィルタとして振る舞うようにせよ。
> 
> ガンカーズ: UNIXの哲学

<small>[Wikipedia](http://ja.wikipedia.org/wiki/UNIX%E5%93%B2%E5%AD%A6)からコピペ</small>

---

## すなわち

* UNIXのプログラムは基本的に単純なことだけを行うように書かれていることが多い
  * たとえばソートだけとか，最初の何行かを抜き出すだけとか
* パイプやプロセス置換で組み合わせることでより複雑なことができるようになる

---

## 標準入出力

* コマンドライン上で動くプログラムは標準入力，標準出力，標準エラー出力が存在する
* 標準入力も標準出力も標準ではターミナルに接続されている
* リダイレクト・パイプを利用するとこで接続先を変更

---

## cat で標準入力・標準出力を試す

* cat は何も引数を指定しなければ標準入力から標準出力にデータをそのまま受け渡す

<pre class="terminal"><span class="machine-output">$</span> <span class="user-input">cat</span> <kbd>Enter</kbd>
<span class="user-input">AAA</span> <kbd>Enter</kbd>
<span class="machine-output">AAA</span> <kbd>Control-D</kbd>
<span class="machine-output">$</span> </pre>

* <span class="user-input">AAA</span>は標準入力から<code>cat</code>に渡され，<span class="machine-output">AAA</span>は標準出力から出力されている
* <kbd>Control-D</kbd>は標準入力の終わりを指示する

---

## cat でリダイレクトを試す 1

* リダイレクトを使うと標準入出力をファイルに書き換えられる

<pre class="terminal"><span class="machine-output">$</span> <span
class="user-input">cat > hello.txt</span> <kbd>Enter</kbd>
<span class="user-input">Hello, world</span> <kbd>Enter</kbd>
<kbd>Control-D</kbd>
<span class="machine-output">$</span> <span
class="user-input">cat hello.txt</span> <kbd>Enter</kbd>
<span class="machine-output">Hello, world</span></pre>

* 一つ目のコマンドで`cat`の標準出力を`hello.txt`に置き換えている
* そのため，`Hello, world!`が2回表示されない

---

## cat でリダイレクトを試す 2

* リダイレクトを使うと標準入出力をファイルに書き換えられる

<pre class="terminal"><span class="machine-output">$</span> <span
class="user-input">cat < hello.txt</span> <kbd>Enter</kbd>
<span class="machine-output">Hello, world</span></pre>

* 今度は`cat`の標準入力を`hello.txt`に置き換えている
* ただ，私は`<`をあまり使わない
  * 通常，コマンドは基本的に左から右に流れる
  * パイプでも`mv`でも`cp`でも左から右に行く
  * 原則に反するのでミス防止のため使わない
  * なので，`<`を使わず，`cat`で出力させて，後述するパイプにつなげることが多い

---

## リダイレクトあれこれ

* `>>`でファイルを上書きでなく追記になる
* `2>`で標準エラー出力をリダイレクトできる
  * 標準エラー出力はこの後に出てくるパイプを利用していても通常はターミナルにつながっているので，エラーを画面上で確認できる
* `&>`で標準出力と標準エラー出力を同時にリダイレクトできる


* このあたりの仕様は使うシェルによって違うので要確認

---

## パイプと相性の良いソフトウエアたち

* パイプのありがたみを理解するために先に組み合わせて便利なプログラムを紹介する．


* `fgrep`: 特定の文字を含む行を抜き出す
* `tail`: ファイルの末尾を出力する
* `head`: ファイルの先頭を出力する
* `wc`: 行数や文字数を数える

---

## fgrep

<pre class="terminal"><span class="machine-output">$</span> <span
class="user-input">fgrep ssh /etc/group</span> <kbd>Enter</kbd>
<span class="machine-output">_sshd:*:75:
com.apple.access_ssh:*:399:</span></pre>

* `/etc/group`の中にある`ssh`を含む行を抜き出している
* 仲間に`grep`や`egrep`があり，こちらは正規表現が使える
* ファイル名を省略，もしくは`-`を指定すると標準入力から入力する
  * これはこの後で出てくるソフトウエアでも共通
  * 自分で各プログラムもそうすることを推奨する

---

## head/tail

<pre class="terminal"><span class="machine-output">$</span> <span
class="user-input">head -n 3 /etc/group</span> <kbd>Enter</kbd>
<span class="machine-output">##
# Group Database
#</span>
<span class="machine-output">$</span> <span class="user-input">tail -n 2 /etc/group</span> <kbd>Enter</kbd>
<span class="machine-output">com.apple.access_screensharing:*:398:
com.apple.access_ssh:*:399:</span></pre>

* ファイルの先頭と末尾を出力できる
* `-n X`で何行出力するかを指定できる
  * 省略した場合には10行

---

## wc

<pre class="terminal"><span class="machine-output">$</span> <span class="user-input">wc /etc/group</span> <kbd>Enter</kbd>
<span class="machine-output">     115     154    2317 /etc/group</span>
<span class="machine-output">$</span> <span class="user-input">wc -l /etc/group</span> <kbd>Enter</kbd>
<span class="machine-output">     115 /etc/group</span>
<span class="machine-output">$</span> <span class="user-input">wc -w /etc/group</span> <kbd>Enter</kbd>
<span class="machine-output">     154 /etc/group</span>
<span class="machine-output">$</span> <span class="user-input">wc -c /etc/group</span> <kbd>Enter</kbd>
<span class="machine-output">    2317 /etc/group</span></pre>

* Word Count の略
* 行数，単語数，文字数を数える
  * `-l`をつけるて行数を数えることが多い

---

## パイプを使う 1

* パイプはあるプログラムの標準出力と標準入力を接続する
* `|`の前後のプログラムをつなぐ

<pre class="terminal"><span class="machine-output">$</span> <span
class="user-input">cat /etc/group|fgrep appserver</span> <kbd>Enter</kbd>
<span class="machine-output">_appserverusr:*:79:
_appserveradm:*:81:</span></pre>

* この例では`cat /etc/group`で`/etc/group`の内容を標準出力に出力し，その結果からを`fgrep`で`appserver`を含む行だけ抜き出している

---

## パイプを使う 2

* パイプはいくつでもつなぐことができる
* 多くのプログラムにおいて入力ファイル名を省略するか`-`を指定すると標準入力からの入力になる

### 課題

* `/etc/group`の中に`us`を含む行が何行あるかを数えなさい

---

## 解答

<pre class="terminal"><span class="machine-output">$</span> <span class="user-input">cat /etc/group|fgrep us|wc -l</span> <kbd>Enter</kbd>
<span class="machine-output">      10</span></pre>

* パイプで`cat`と`fgrep`と`wc`をつないでいる


---

## tee

* リダイレクトで結果を保存したい
* でも，画面にも出力したい
* こういう場合には`tee`を使うと実現できる
  * `tee 出力先`

<pre class="terminal"><span class="machine-output">$</span> <span class="user-input">grep ssh /etc/group|tee result.txt</span>
<span class="machine-output">_sshd:*:75:
com.apple.access_ssh:*:399:</span></pre>

* `result.txt`に画面に出力されたものと同じ結果が出力されている

---

## フィルタ

* 標準入力から何かデータを受け取り，標準出力に出力するプログラムをフィ
  ルタと呼ぶ
* UNIXではあらゆるソフトウエアがフィルタとして動作するように設計されて
  いる
* 自分で書くソフトウエアもフィルタとして動作することが望ましい

---

## プロセス置換

* パイプの弱点
  * 1対1の関係しか作れない
  * 2つの入力から1つの出力をするのはできない
* プロセス置換を使うと2つ以上の入力から1つの出力を得ることができる
  * ファイルを指定するかわりにどのプログラムを実行するかを指定できる

---

## プロセス置換：例

<pre class="terminal"><span class="machine-output">$ </span><span class="user-input">cat <(fgrep ssh /etc/group) <(fgrep core /etc/passwd)</span>
<span class="machine-output">_sshd:*:75:
com.apple.access_ssh:*:399:
_coreaudiod:*:202:202:Core Audio Daemon:/var/empty:/usr/bin/false
_coremediaiod:*:236:236:Core Media IO Daemon:/var/empty:/usr/bin/false</span></pre>

* `/etc/group`から`ssh`を含む行を抜き出し，`/etc/passwd`からは`core`を
  含む行を抜き出し，その二つを`cat`で結合している
* 注意：`/etc/group`や`/etc/passwd`はユーザー情報が含まれるので安易に公開してはならない
  * OS Xでは問題ない場合が多いが推奨はしない

---

## 課題

* `/etc/group`から`u`を含む行を抜き出し，`/etc/passwd`から`A`を含む行を抜き出し，それぞれの先頭10行を表示させる
* ヒント
  * `head`は複数のファイルを指定できる

---

## 解答

<pre class="terminal"><span class="machine-output">$ </span><span class="user-input">head <(fgrep u /etc/group) <(fgrep A /etc/passwd)</span>
<span class="machine-output">==> /dev/fd/11 <==
# Group Database
# Note that this file is consulted directly only when the system is running
# in single-user mode.  At other times this information is provided by
# See the opendirectoryd(8) man page for additional information about
nogroup:*:-1:</span>
  ... 以下省略</pre>

* `==> /dev/fd/11 <==`の部分は通常はファイル名を表示するが，プロセス置換を用いるとこのような表示になる．実装上の問題なので特に気にしなくて良い

---

## コマンド置換

* 他にありがちな課題としてコマンドの引数を他のコマンドを実行した結果か
  ら指定したい場合がある
* この場合にはバッククオートが使える
  * 日本語キーボードの場合には<kbd>Shift-@</kbd>で入力

<pre class="terminal"><span class="machine-output">$ </span><span
class="user-input">cat `echo ~/.profile`</span>
<span class="machine-output"># MacPorts Installer addition on 2015-02-14_at_23:33:49: adding an appropriate PATH variable for use with MacPorts.
export PATH="/opt/local/bin:/opt/local/sbin:$PATH"
# Finished adapting your PATH environment variable for use with MacPorts.
</span></pre>

* `echo`は引数をそのまま標準出力に出力するだけのコマンド

---

## 課題

* `/etc/shells`に書かれているパス（ただし`#`で始まるコメント行は除く）について`ls -lh`を用いてファイルサイズを表示させなさい

---

## 解答

<pre class="terminal"><span class="machine-output">$ </span><span
class="user-input">ls -lh `fgrep / /etc/shells`</span>
<span class="machine-output">-r-xr-xr-x  1 root  wheel   614K 10 27 13:01 /bin/bash
-rwxr-xr-x  2 root  wheel   361K  9 10  2014 /bin/csh
-r-xr-xr-x  1 root  wheel   1.3M  9 10  2014 /bin/ksh
-r-xr-xr-x  1 root  wheel   614K 10 27 13:01 /bin/sh
-rwxr-xr-x  2 root  wheel   361K  9 10  2014 /bin/tcsh
-rwxr-xr-x  1 root  wheel   538K  9 10  2014 /bin/zsh</span></pre>

* `fgrep / /etc/shells`で`/`を含む行だけを抜き出してコメントを排除している

---

## まとめ

* プログラムは標準入力，標準出力，標準エラー出力をもつ
* これらは普段はターミナルに接続されているが，リダイレクトやパイプで接続先を変更できる
* プロセス置換を用いると複数のプログラムの標準出力を一つのプログラムのファイル入力に接続できる
* コマンド置換を用いると，あるプログラムの標準出力を他のプログラムの引数として利用できる

---

## おまけ？

* コマンドの使い方に困ったときには`man`を使う
* 使い方は`man コマンド名`

---

## man: 例

`$ man mv`

    MV(1)                     BSD General Commands Manual                    MV(1)
    
    NAME
         mv -- move files
    
    SYNOPSIS
         mv [-f | -i | -n] [-v] source target
         mv [-f | -i | -n] [-v] source ... directory
    
    DESCRIPTION
         In its first form, the mv utility renames the file named by the source


---

## manの読み方

* `SYNOPSIS`に使い方の例が書いてある
  * `[]`で囲まれた部分はオプションであり必須ではない
  * 下線が引いてあるところはユーザーが変更すべきところ
* `DESCRIPTION`以下に詳しい説明がある
* 人に聞く前に`man`を読みましょう


---

## man

* `man`はコマンドの説明だけでなく，C言語の関数の説明や設定ファイルの書き方などあらゆる項目が解説されている
* セクションごとに何を解説しているかが違う
  1. General commands
  2. System calls
  3. Library functions (C and others)
  4. Special files
  5. File formats and conventions
  6. Games and screensavers
  7. Miscellanea
  8. System administration commands and daemons

---

## man

* セクション番号を指定して呼び出すときには`man SECTION NAME`とする
* たとえば`printf`コマンドの解説を見たいときには`man 1 printf`とするが，C言語の`printf`関数の説明を見たければ`man 3 printf`とする
* Linux と OS X では同じコマンドでもオプションが違うものが多いので，必ず使用するマシンの上で確認する

---

## まとめ

困ったら`man`を読め