103.4 レッスン 2
Certificate: |
LPIC-1 (101) |
---|---|
Version: |
5.0 |
Topic: |
103 GNUおよびUnixコマンド |
Objective: |
103.4 ストリーム、パイプ、リダイレクトを使用する |
Lesson: |
2 of 2 |
はじめに
Unix哲学の1つに、それぞれのプログラムはその目的に専念し、範囲外の機能を取り入れべきではない、というものがあります(訳注: 日本語版Wikipedia「UNIX哲学」に詳しくまとめられています)。つまり、複数のプログラムを連携させれば複雑な結果を生成できるので、物事を単純に保つことが単純な結果しか生み出せないということは ない ことを意味しています。縦棒 |
は、パイプ とも呼ばれ、あるプログラムの出力を別のプログラムの入力に直接接続するパイプラインを作ります。また、コマンド置換 を使ってプログラムの出力を変数に格納して、別のコマンドの引数として使用することもできます。
パイプ
リダイレクトとは異なり、パイプではデータがコマンドラインの左から右に流れます。送り先は別のプロセスであり、ファイル名やファイル記述子、ヒアドキュメントではありません。パイプ |
は、前のコマンドの出力を次のコマンドの入力に接続し、すべてのコマンドを同時に起動することをシェルに指示します。たとえば、次のコマンドは、cat
によって標準出力に送られた /proc/cpuinfo
ファイルの内容を、wc
のstdinにパイプします。
$ cat /proc/cpuinfo | wc 208 1184 6096
wc
に入力ファイルを指定しない場合、stdinから読み込んだテキストの行数、単語数、文字数をカウントします。1つの複合コマンドに、複数のパイプを使うことができます。次の例では、2つのパイプを使用しています。
$ cat /proc/cpuinfo | grep 'model name' | uniq model name : Intel(R) Xeon(R) CPU X5355 @ 2.66GHz
cat /proc/cpuinfo
は /proc/cpuinfo
の内容を、grep 'model name'
にパイプします。grep
コマンドは model name
という文字列を含む行のみを選択しますが、このマシンには多くのCPUが搭載されているので、複数の行が出力されます。最後のパイプは、grep 'model name'
の出力を uniq
に接続します。uniq
は、同じ行の繰り返しを1行にまとめます。
1つのコマンドライン中に、リダイレクトとパイプを組み合わせることもできます。前の例は、より単純な形式に書き直せます。
$ grep 'model name' </proc/cpuinfo | uniq model name : Intel(R) Xeon(R) CPU X5355 @ 2.66GHz
grep
の引数にファイルパスを指定することができるので、実は grep
に入力リダイレクトを使う必要もありませんが、この例では、リダイレクトとパイプを組み合わせてコマンドを構築する方法を示しています。
パイプとリダイレクトは排他的です。つまり、1つのソースは、1つのターゲットのみにしかマップできません。しかし、tee
プログラムを使えば、出力をファイルに書きながら、画面にも表示する(stdoutに出力する)ことができます。先行するプログラムがその出力を tee
のstdinに送信し、tee
にはデータを格納するためのファイル名を指定します。
$ grep 'model name' </proc/cpuinfo | uniq | tee cpu_model.txt model name : Intel(R) Xeon(R) CPU X5355 @ 2.66GHz $ cat cpu_model.txt model name : Intel(R) Xeon(R) CPU X5355 @ 2.66GHz
連鎖の最後である uniq
の出力が表示されると共に、cpu_model.txt
ファイルに保存されます。tee
に指定したファイルを上書きするのではなく追記するには、tee
に -a
オプションを指定します。
パイプに送られるのは、プロセスの標準出力のみです。長時間かかるコンパイル処理を行う場合などに、後で調べるために標準出力と標準エラーの両方をファイルに保存したいことがあります。例えば、次のコマンドは、現在のディレクトリに Makefile が無い場合に、エラーのみを出力します。
$ make | tee log.txt make: *** No targets specified and no makefile found. Stop.
make
のエラーメッセージが画面に表示されますが、tee
には取り込まれず、空のファイル log.txt
ができました。パイプがstderrを取り込めるように、リダイレクトする必要があります。
$ make 2>&1 | tee log.txt make: *** No targets specified and no makefile found. Stop. $ cat log.txt make: *** No targets specified and no makefile found. Stop.
この例では、make
のstderrがstdoutにリダイレクトされたので、パイプを経由して tee
がそれを取り込んで、画面に表示すると共に log.txt
に保存します。エラーメッセージを保存しておけば、後で調べることができて便利です。。
コマンド置換
コマンドの出力を取り込む(保存する)別の方法に、コマンド置換 があります。バッククォート内にコマンドを置くと、Bashはそれを(実行したコマンドの)標準出力に置き換えます。次の例は、プログラムのstdoutを、別のプログラムの引数として使用する方法を示しています。
$ mkdir `date +%Y-%m-%d` $ ls 2019-09-05
プログラム date
の出力(YYYY-MM-DD 形式の日付)が、mkdir
の引数として使用されて、日付を名前とするディレクトリが作成されます。バッククォートの代わりに $()
を使用しても、同じ結果が得られます。
$ rmdir 2019-09-05 $ mkdir $(date +%Y-%m-%d) $ ls 2019-09-05
同じ方法で、コマンドの出力を変数に格納できます。
$ OS=`uname -o` $ echo $OS GNU/Linux
uname -o
コマンドは、稼働中のオペレーティングシステムの一般的な名前を出力します。コマンドの出力を変数に割り当てることは、スクリプトで非常に役立ち、さまざまな方法でデータを保存および評価することができます。
コマンドの出力内容によっては、コマンド置換が適切ではない場合があります。あるプログラムの出力を、別のプログラムの引数として使用するための仲介者となる xargs
というコマンドがあります。xarg
コマンドは、「xargs
の引数に指定したコマンド」を実行しますが、その引数として「標準入力から受け取ったテキスト」を追加します。次に示す xargs
の例は、find
が見つけたファイル(8つ)を引数として、identify
を実行します。
$ find /usr/share/icons -name 'debian*' | xargs identify -format "%f: %wx%h\n" debian-swirl.svg: 48x48 debian-swirl.png: 22x22 debian-swirl.png: 32x32 debian-swirl.png: 256x256 debian-swirl.png: 48x48 debian-swirl.png: 16x16 debian-swirl.png: 24x24 debian-swirl.svg: 48x48
例で実行している identify
プログラムは、ImageMagick(ほとんどの種類の画像ファイル扱うことができる描画ツール)の一部です。find
が出力したパスを xargs
が読み取って、それを引数に指定した identify
を実行します。identify
の -format
オプションは、指定されたファイルの情報を表示します。-format
は identify
へのオプションであり、xargs
のオプションではないことに注意してください。なお、find
が見つけた画像ファイルは、Debianディストリビューションのロゴファイルです。
xargs
に -n 1
オプションを指定すると、1回のコマンド実行につき1つの引数のみを指定します。例に挙げたケースで xargs -n 1
を指定すると、find
が見つけた8つのファイルを identify
に渡すのではなく、1つのファイルごとに identify
が8つ起動されます。同様に、-n 2
を指定すれば2つのファイルずつ identify
が4つ起動されますし、-n 3
を指定すれば3つのファイルで2つ、2つのファイルで1つの identify
が起動されます。(訳注: 複数の identify
が同時に起動されることに注意してください。-n
に指定する数値は、最大の 引数の数 です)。また、xargs
に(例に挙げた find
のように)複数行からなる入力を与える場合は、xargs
が起動するコマンドに与える引数の 行数 を -L
オプションで制限することもできます。
Note
|
例のケースでは、 |
find
が見つけたファイルのパス名に空白などが含まれている場合には、特別な処理が必要になります。find
で -print0
アクションを使用すると、出力の区切りとして(改行ではなく)Null文字を使用します。さらに、xargs
に -0
オプションを指定して、区切りがNull文字であることを指示します。これで、空白をや特殊文字を含むパス名を正しく見分けることができます。次の例の出力は省略しています。
$ find . -name '*avi' -print0 -o -name '*mp4' -print0 -o -name '*mkv' -print0 | xargs -0 du | sort -n
この例では、見つけたファイルが使用しているディスク容量を du
で調べ、サイズ順に表示します。(出力は省略しました)。find
では、検索条件ごとに、-print0
アクションを指定することに注意してください。(訳注: 括弧で条件式をまとめる方法もあります。)
デフォルトでは、xargs
は実行するコマンドの最後に、渡された引数を置きます。引数を置く位置を変更するには、-I
オプションを使用します。
$ find . -mindepth 2 -name '*avi' -print0 -o -name '*mp4' -print0 -o -name '*mkv' -print0 | xargs -0 -I PATH mv PATH ./
最後の例では、find
が見つけたすべてのファイルが、現在のディレクトリに移動されます。mv
では、ソースパスを先に指定しますから、xargs
の -I
オプション で引数に置き換える単語を指定し、mv
コマンドの適切な位置にその単語を置きます。(訳注: -I
オプションを指定すると、-L 1
が指定されたのと同じ動作になります。)また、Null文字を区切り文字とすれば、引数に置き換える単語を引用符で囲む必要がなくなります。
演習
-
スクリプトを自動的に実行する場合は、実行された日時を保存しておくと便利です。
date +%Y-%m-%d
コマンドは、現在の日付を YYYY-MM-DD 形式で表示します。コマンド置換を使用して、このコマンドの出力を、TODAY
というシェル変数に格納するにはどうしますか? -
シェル変数
TODAY
の値を、echo
コマンドで、sed s/-/./g
コマンドの標準入力に送るにはどうしますか? -
date +%Y-%m-%d
コマンドの出力を、sed s/-/./g
にヒア文字列として与えるにはどうしますか? -
convert image.jpeg -resize 25% small/image.jpeg
コマンドは、image.jpeg
を縮小したバージョンを作成して、small
ディレクトリ内に同名のファイルとして保存します。xargs
を使用して、filelist.txt
にリストされているすべての画像に対して、このコマンドを実行するにはどうしますか?
発展演習
-
/dev/sda1
パーティションのイメージを、dd < /dev/sda1 > sda1.img
コマンドで作成する、簡単なバックアップスクリプトがあります。データの整合性を確認できるように、sha1sum < sda1.img > sda1.sha1
コマンドで、SHA1ハッシュ値を格納したファイルsda1.sha1
も作成しています。tee
コマンドを使用して、この2つのコマンドを1つにまとめるにはどうしますか? -
tar
コマンドは、多くのファイルを、ディレクトリ構造を保持したまま1つのアーカイブファイルにまとめます。-T
オプションを使うと、アーカイブに格納するファイルのパス名を収めたファイルを指定することができます。たとえば、find /etc -type f | tar -cJ -f /srv/backup/etc.tar.xz -T -
は、find
が見つけたファイルを収めた圧縮tarファイルetc.tar.xz
を作成します。-T -
オプションは、標準入力からパス名のリストを読み取ることを指示しています。リストから読み取られたパス名は、tar
に対する引数の一つとして扱われるため、パス名に空白や特殊文字が含まれている場合には、予期しない動作を引き起こすことがあります(詳しくはtar
のマニュアルを参照してください)。find
とtar
にどのようなオプションを指定すれば、空白や特殊文字を含むパス名を正しく取り扱えるようにできますか? -
新しいリモートシェルセッションを開かなくても、
ssh
では1つのコマンドをリモートマシンで実行することができます:ssh user@storage "remote command"
。これを使えば、ローカルコマンドの標準出力を、リモートコマンドの標準入力に送ることもできます。ssh
経由で、ローカルファイルetc.tar.gz
をcat
して、リモートマシン(op@storage
)の/srv/backup/etc.tar.gz
に送るにはどうしますか?
まとめ
このレッスンでは、Linuxに備わっている伝統的なプロセス間通信について説明しました。コマンドパイプライン は2つのプロセス間に一方向の通信チャネルを作成し、コマンド置換 はプロセスの出力をシェル変数に格納します。レッスンでは次の手順を説明しました。
-
パイプ を使用して、プロセスの出力を別のプロセスの入力に流し込む方法。
-
tee
コマンドとxargs
コマンドの働き。 -
コマンド置換 でプロセスの出力を取り込み、変数に格納し、あるいは、別のコマンドの引数として使用する方法。
以下のコマンドと手順を取り上げました:
-
|
を使用したコマンドパイプライン。 -
バッククォート ないし
$()
によるコマンド置換。 -
tee
、xargs
、find
コマンド。
演習の解答
-
スクリプトを自動的に実行する場合は、実行された日時を保存しておくと便利です。
date +%Y-%m-%d
コマンドは、現在の日付を YYYY-MM-DD 形式で表示します。コマンド置換を使用して、このコマンドの出力を、TODAY
というシェル変数に格納するにはどうしますか?$ TODAY=`date +%Y-%m-%d`
または、
$ TODAY=$(date +%Y-%m-%d)
-
シェル変数
TODAY
の値を、echo
コマンドで、sed s/-/./g
コマンドの標準入力に送るにはどうしますか?$ echo $TODAY | sed s/-/./g
-
date +%Y-%m-%d
コマンドの出力を、sed s/-/./g
にヒア文字列として与えるにはどうしますか?$ sed s/-/./g <<< `date +%Y-%m-%d`
または、
$ sed s/-/./g <<< $(date +%Y-%m-%d)
-
convert image.jpeg -resize 25% small/image.jpeg
コマンドは、image.jpeg
を縮小したバージョンを作成して、small
ディレクトリ内に同名のファイルとして保存します。xargs
を使用して、filelist.txt
にリストされているすべての画像に対して、このコマンドを実行するにはどうしますか?$ xargs -I IMG convert IMG -resize 25% small/IMG < filelist.txt
または、
$ cat filelist.txt | xargs -I IMG convert IMG -resize 25% small/IMG
発展演習の解答
-
/dev/sda1
パーティションのイメージを、dd < /dev/sda1 > sda1.img
コマンドで作成する、簡単なバックアップスクリプトがあります。データの整合性を確認できるように、sha1sum < sda1.img > sda1.sha1
コマンドで、SHA1ハッシュ値を格納したファイルsda1.sha1
も作成しています。tee
コマンドを使用して、この2つのコマンドを1つにまとめるにはどうしますか?# dd < /dev/sda1 | tee sda1.img | sha1sum > sda1.sha1
-
tar
コマンドは、多くのファイルを、ディレクトリ構造を保持したまま1つのアーカイブファイルにまとめます。-T
オプションを使うと、アーカイブに格納するファイルのパス名を収めたファイルを指定することができます。たとえば、find /etc -type f | tar -cJ -f /srv/backup/etc.tar.xz -T -
は、find
が見つけたファイルを収めた圧縮tarファイルetc.tar.xz
を作成します。-T -
オプションは、標準入力からパス名のリストを読み取ることを指示しています。リストから読み取られたパス名は、tar
に対する引数の一つとして扱われるため、パス名に空白や特殊文字が含まれている場合には、予期しない動作を引き起こすことがあります(詳しくはtar
のマニュアルを参照してください)。find
とtar
にどのようなオプションを指定すれば、空白や特殊文字を含むパス名を正しく取り扱えるようにできますか?-print0
アクションと--null
オプション:$ find /etc -type f -print0 | tar -cJ -f /srv/backup/etc.tar.xz --null -T -
-
新しいリモートシェルセッションを開かなくても、
ssh
では1つのコマンドをリモートマシンで実行することができます:ssh user@storage "remote command"
。これを使えば、ローカルコマンドの標準出力を、リモートコマンドの標準入力に送ることもできます。ssh
経由で、ローカルファイルetc.tar.gz
をcat
して、リモートマシン(op@storage
)の/srv/backup/etc.tar.gz
に送るにはどうしますか?$ cat etc.tar.gz | ssh user@storage "cat > /srv/backup/etc.tar.gz"
または
$ ssh user@storage "cat > /srv/backup/etc.tar.gz" < etc.tar.gz