Linuxで複数のコマンドを1行で連続実行する
以前にプロジェクトにいた人が書いて、今も動いているスクリプトの中で多用されているものの、仕組みがよく分からなかったので、調べて整理しました。
前のコマンドの終了を待たずに、次のコマンドを実行
CMD & CMD
前のコマンドが実行された瞬間に、次のコマンドが実行されます。
前のコマンドはバックグラウンド処理になります。
次のコマンドを書かずに、コマンドの後ろに&を付けてもバックグラウンドになるので、これは連続実行とは少し違うかもしれません。
$ sleep 10 & echo "hello"
[1] 20000
hello
$ bg
bash: bg: job 1 already in background
$ bg
bash: bg: job has terminated
[1]+ Done sleep 10
echo の結果はすぐに表示されて、バックグラウンドで sleep が実行されました。
前のコマンドが終了したら、次のコマンドを実行
CMD ; CMD
前のコマンドがどのような結果でも、実行が終わり次第、次のコマンドが実行されます。
$ ls -l
total 0
$ ls -l ; echo "hello"
total 0
hello
$ ls -l test ; echo "hello"
ls: cannot access 'test': No such file or directory
hello
先に実行した ls の結果に関係なく、次の echo が実行されました。
前のコマンドが正常終了したら、次のコマンドを実行
CMD && CMD
正常終了とは、直前に実行したコマンドの終了ステータス $? や exit コードが 0 になっている状態のことです。
前のコマンドが異常終了した場合、次のコマンドは実行されません。
$ ls -l && echo "hello"
total 0
hello
$ ls -l test && echo "hello"
ls: cannot access 'test': No such file or directory
ls がエラーしなかったときは echo が実行され、ls がエラーのときは echo が実行されませんでした。
前のコマンドが異常終了したら、次のコマンドを実行
CMD || CMD
異常終了とは、直前に実行したコマンドの終了ステータス $? や exit コードが 0 以外になっている状態のことです。
前のコマンドが正常終了した場合、次のコマンドは実行されません。
$ ls -l || echo "hello"
total 0
$ ls -l test || echo "hello"
ls: cannot access 'test': No such file or directory
hello
ls がエラーしなかったときは echo が実行されず、ls がエラーのときは echo が実行されました。
終了ステータス・exit コードに関する補足
直前に実行したコマンドの終了ステータスは $? に入るので、echoで確認することができます。
echo $?
基本的に正常終了であれば 0 になりますが、エラーを出さずに終了していても、0 以外の値になることがあります。(例えば、grepは一致する行がないとき、1 になります。)
その辺りは各コマンドの仕様によるものと思われるので、終了ステータスを見て正常動作の判定を行うような場合は、きちんと確認しておく必要があります。
exit コードはシェルの終了時のステータスで、ssh接続先で実行したスクリプトの戻り値や while文から抜けるときの値として設定することができます。
ただし、システム上の意味のある値を無闇に設定してしまうと問題が起こる可能性があるので、独自の値を設定するときでも、0 か 1 にするのが無難ではないかと思います。
なお、exit コードも $? に値が入ります。
$ echo -ne "test1\ntest2\ntest3\n" | while read row; do echo $row; done
test1
test2
test3
$ echo $?
0
exit コードを特に設定していない場合、ループ中で最後に実行したコマンドの終了ステータスが $? に入ります。
$ echo -ne "test1\ntest2\ntest3\n" | while read row; do echo $row; if [ "$row" == "test2" ]; then exit 0; fi; done
test1
test2
$ echo $?
0
$ echo -ne "test1\ntest2\ntest3\n" | while read row; do echo $row; if [ "$row" == "test2" ]; then exit 1; fi; done
test1
test2
$ echo $?
1
exit 0 を設定することで $? を 0 に、exit 1 を設定することで $? を 1 にすることができました。
おまけ
3つ以上のコマンドを連続して実行することも可能です。
$ ls -l test && echo "hello1" && echo "hello2"
ls: cannot access 'test': No such file or directory
$ ls -l test || echo "hello1" || echo "hello2"
ls: cannot access 'test': No such file or directory
hello1
$ ls -l test || echo "hello1" && echo "hello2"
ls: cannot access 'test': No such file or directory
hello1
hello2
$ ls -l test && echo "hello1" || echo "hello2"
ls: cannot access 'test': No such file or directory
hello2
$ ls -l && echo "hello1" && echo "hello2"
total 0
hello1
hello2
$ ls -l || echo "hello1" || echo "hello2"
total 0
$ ls -l || echo "hello1" && echo "hello2"
total 0
hello2
$ ls -l && echo "hello1" || echo "hello2"
total 0
hello1
1つ目のコマンドから順番に1つずつ実行されていき、次のコマンドを実行しない条件になった時点で全体が終了すると予想していましたが、違いました。
&& と || は、直前に実行されたコマンドの終了ステータスに応じて、次のコマンドを実行するか決めるものなので、3つ並べた場合で、2番目が実行されなかったときには、3番目は1番目の終了ステータスを参照することになります。
コマンドを3つ並べて、1つ目が失敗したら全体が終了するように制御したい場合は、書き方を工夫する必要がありそうです。
2024/3/15 追記
3つ以上並べる場合、括弧を使ってまとめることができます。
$ ls -l || echo "hello1" && echo "hello2"
total 0
hello2
$ ls -l || (echo "hello1" && echo "hello2")
total 0
$ ls -l test || echo "hello1" && echo "hello2"
ls: cannot access 'test': No such file or directory
hello1
hello2
$ ls -l test || (echo "hello1" && echo "hello2")
ls: cannot access 'test': No such file or directory
hello1
hello2
括弧に入れることで、1つ目のコマンドがエラーのときだけ、2つ目と3つ目を実行させることができました。
どうやら、括弧内のコマンドは別の環境で実行される扱いになるようです。
試したところ、以下のようになりました。
括弧の中で変数を作っても、括弧の外には引き継がれない。
括弧の中でexitしてもログアウトはせず、exit コードが $? に反映される。
$ (test="hello")
$ if [ "$test" == "" ]; then echo "Null"; else echo "Not Null"; fi
Null
括弧内で作った変数 test は、括弧の外では空になっていました。
$ ls -l && (echo "OK" && exit 1)
total 0
OK
$ echo $?
1
ls も echo も正常終了しているので、その時点では $? は 0 になっていますが、括弧内で最後に exit 1 を実行したことで、$? は 1 になりました。