コマンドの繋ぎ方

一つひとつのコマンドは単機能*1であっても、コマンド同士を連携させることで、高度な情報に加工できる仕組みって、素晴らしいと思う。

$ cat .bash_history|cut -f1 -d' '|sort|uniq -c|sort -nr|head -n10
 114 cat
  64 echo
  40 ls
  28 rm
  26 ps
  20 growlnotify
  20 _echo
  19 sleep
  17 ls_
  16 du

例えば、上記のコマンドは「最近よく使ったコマンドベスト10」を表示してくれる。(bash用)
結果を見ると、ろくにコマンドを使っていない(使えていない)のがバレバレだけど、.bash_historyというコマンド履歴のファイルから、一瞬にしてベスト10に集計してしまう作業が、たった一行で完結してしまう能力を知った時、俄然、コマンドへの興味が湧いてきた。

| パイプ

  • コマンドの処理結果は、通常は画面に表示される。
$ echo ABC
ABC
  • リダイレクトでファイルを指定すれば、ファイルに書き込まれる。
$ echo ABC >file
$ cat file
ABC
  • パイプで繋ぐことで、次のコマンドに引き渡せる。
$ echo ABC|tr A-Z a-z
abc
  • trは1文字単位で変換するコマンド。
  • echoが渡したABCを受け取って、AからZを、aからzに変換している。(つまり、大文字を小文字に変換している)
  • ちなみに、パイプで繋ぐことのできないコマンドもある。
$ echo ABC|echo

$ echo 1 + 1|expr
expr: syntax error

最近よく使ったコマンドベスト10になるまでの過程を追ってみた

  • コマンド履歴ファイルの内容を表示する。
$ cat .bash_history
alias
cat test2.log
cat test.log
du test2.log
du test2.log|cut -f 1
cd
cd documents
...(中略)...
  • 次に、スペース区切りで分割して、最初の一列目のみ取り出す。
$ cat .bash_history|cut -f1 -d' '
cat
cat
du
du
cd
cd
...(中略)...
  • さらに、コマンドをアルファベット順に並べ替える。
$ cat .bash_history|cut -f1 -d' '|sort
./ls
.ls
/bin/ls
/bin/ps
/bin/ps
/bin/ps
...(中略)...
  • さらに、同じコマンドをカウントしながら、重複を取り除く。
$ cat .bash_history|cut -f1 -d' '|sort|uniq -c
   1 ./ls
   1 .ls
   1 /bin/ls
  11 /bin/ps
   1 @
  11 [
...(中略)...
  • カウント数が大きい順に並べ替えて、先頭から10行目までを表示する。(完了)
$ cat .bash_history|cut -f1 -d' '|sort|uniq -c|sort -nr|head -n10
 115 cat
  64 echo
  40 ls
  28 rm
  26 ps
  20 growlnotify
  20 _echo
  19 sleep
  17 ls_
  16 du

改行

  • 最もシンプルには、1行に1コマンドを書いて、行末で改行する方法。
  • 改行がコマンドと次のコマンドの区切り記号になる。
$ cd ~/desktop
$ ls -l
total 209888
-rw-r--r--   1 zari  staff     319318 10 10  2010 Quicksilverショートカット1.PNG
-rw-r--r--   1 zari  staff     320334 10 10  2010 Quicksilverショートカット2.PNG
-rw-r--r--@  1 zari  staff       1929  6  1 17:48 textarea_autofit.html
drwxr-xr-x   3 zari  staff        102  1 12 19:08 今日.app/
drwxr-xr-x   3 zari  staff        102  3 29  2010 前月.app/
drwxr-xr-x@  3 zari  staff        102  1 12 15:53 翌日.app/
drwxr-xr-x   3 zari  staff        102  3 29  2010 翌月.app/
drwxr-xr-x   3 zari  staff        102  1 12 17:15 年月日.app/
drwxr-xr-x  52 zari  staff       1768 12 18  2010 整理整頓/
...(中略)...

改行をエスケープ

  • パイプで繋がったあまりにも長い1行は非常に読みにくい。
  • 改行を\によってエスケープすることで、次の行も連続する1行と見なされる。
$ cat .bash_history\
> |cut -f1 -d' '\
> |sort\
> |uniq -c\
> |sort -nr\
> |head -n10
 113 cat
  66 echo
  40 ls
  28 rm
  26 ps
  20 growlnotify
  20 _echo
  19 sleep
  17 ls_
  15 wc

;セミコロン

  • ; は、改行と同等の意味を持つ。
  • ; で区切れば、1行に幾つでもコマンドを並べられる。
$ sleep 10; growlnotify -m "完了!"
  • 上記コマンドを実行すると、10秒後にgrowlで「完了!」と通知される。
  • 但し、完了と通知されるまで、コマンドは実行中となる。

&アンパサンド

  • & は、直前のコマンドをバックグラウンドで実行する。
  • と同時に、コマンド同士の区切り記号としての役割もある。
$ sleep 10 & growlnotify -m "完了!"
  • 上記コマンドを実行すると、すぐにgrowlで「完了!」と通知される。
  • sleep 10 はバックグラウンドで実行され、次のgrowlnotify -m "完了!" がすぐに実行された結果である。

()括弧

  • ところで、上記のコマンドは本意でなくて、本当は以下のように動作して欲しい。
    • ターミナル上ではすぐに次のコマンド入力待ちになって欲しい。
    • 10秒後にgrowlで「完了!」と通知される。
  • 一連のコマンドをまとめてバックグラウンドで実行するには、()あるいは{}括弧で囲う。
$ (sleep 10; growlnotify -m "完了!") &
# あるいは...
{ sleep 10; growlnotify -m '完了'; } &
  • 上記コマンドを実行すると、ターミナル上ではすぐに次のコマンド入力待ちになる。
  • 10秒後にgrowlで「完了!」と通知される。
  • ()と{}の違いは...
    • ()は、同じbash内でコマンドを実行する。
    • {}は、サブシェルという子プロセスのbashを起動して、そこでコマンドを実行する。
      • {}の場合、{}内の両端にスペースを挿入して、末尾には;を書いておく必要がある。
$ (ps a) &
[1] 28416
PID   TT  STAT      TIME COMMAND
25278 s000  Ss     0:00.02 login -pf zari
25279 s000  S+     0:00.44 -bash
28416 s000  R      0:00.00 ps a

$ { ps a; } &
[1] 28417
PID   TT  STAT      TIME COMMAND
25278 s000  Ss     0:00.02 login -pf zari
25279 s000  S+     0:00.44 -bash
28417 s000  S      0:00.00 -bash
28418 s000  R      0:00.00 ps a
  • man bashには、()がサブシェル、{}が現状のシェル環境で実行と書いてあるが、実際に試してみると自分の環境ではmanとは逆の仕様のようだ。

&&

コマンド1 && コマンド2
  • コマンド1が正常終了した場合のみ、コマンド2が実行される。
  • もう少し正確に言えば、コマンド1が終了ステータス0の場合のみ、コマンド2が実行される。

例:

$ cat file && echo "内容を確認してください。"

上記コマンドは...

  • fileが存在すれば、その内容を表示して、さらに「内容を確認してください。」と表示する。
  • fileが存在しなければ、標準エラー出力のみ表示する。(echoは実行されない。)

||

コマンド1 || コマンド2
  • コマンド1がエラー終了した場合のみ、コマンド2が実行される。
  • もう少し正確に言えば、コマンド1が終了ステータス0以外の場合のみ、コマンド2が実行される。

例:

$ cat file || echo "ファイルが見つかりません。"
  • 上記コマンドは、fileが存在しない時だけ、標準エラー出力を表示して、さらに「ファイルが見つかりません。」と表示する。
  • ファイルが存在すれば、その中身を表示する。(echoは実行されない。)

実例

&&と()の違い
  • sleepコマンドでエラーが発生することはまず考えられないので、以下のコマンドはどちらも同じ結果をもたらす。
$ (sleep 10; growlnotify -m "完了") &
$ sleep 10 && growlnotify -m "完了" &
  • 演算子の優先順位(1、2、3の順で優先される。番号内での順位は同じ)
    1. |
    2. && ||
    3. ; &
  • &&の優先順位の方が、&より優先されるので、()括弧で囲ったような効果になるのだ。
  • ところが、makeコマンドだとエラーが発生する可能性も十分ある。
$ (make; growlnotify -m "完了") &
$ make && growlnotify -m "完了" &
  • 上段は、エラーが発生しても、しなくても、「完了!」が通知される。
  • 下段は、正常終了した時だけ、「完了!」が通知される。(エラーが発生すると通知されない)
&&とifの違い
  • &&を活用すると、冗長なif文の代わりになりそうだが、微妙な違いがある。
$ [ -e file ] && echo '存在します。'
存在します。
$ [ -e _file ] && echo '存在します。'
$ echo $?
1

$ if [ -e file ]; then echo '存在します。'; fi; 
存在します。
$ if [ -e _file ]; then echo '存在します。'; fi; 
$ echo $?
0
  • 存在しない時の終了ステータスが、違ってくるのだ。
  • AppleScriptのdo shell scriptからコマンドを実行すると、この違いは大きい。


do shell script "[ -e ~/desktop/file ] && echo '存在します。'"
do shell script "[ -e ~/desktop/_file ] && echo '存在します。'"

  • 上段は、ファイルが存在するので、「存在します。」という結果が返ってくる。
  • 下段は、ファイルが存在しないと、終了ステータス0以外なので、do shell scriptがエラーになって、実行が中止されてしまう。
  • do shell script内では、面倒でもif文を使う必要があるのだ。


do shell scriptをバックグラウンドで実行するには?
  • 単純に&を付けてバックグラウンドで実行すれば良さそうだが、以下の書き方では「完了!」が通知されるまでの10秒間、AppleScriptが実行中になってしまう...。


do shell script "(sleep 10; /usr/local/bin/growlnotify -m '完了!') &"

  • 正しくは、標準出力・標準エラー出力を破棄した上で、バックグラウンドで実行するのだ。
  • 以下の書き方なら、AppleScriptの実行はすぐに終了し、10秒後に「完了!」通知が表示される。


do shell script "(sleep 10; /usr/local/bin/growlnotify -m '完了!') >& /dev/null &"
do shell script "(sleep 10; /usr/local/bin/growlnotify -m '完了!') >/dev/null 2>&1 &"

参考ページ

以下のページがたいへん参考になりました。感謝です!

*1:決して単機能ではないのだけど