Shell Scriptを書くときのテクニック10選
κeenです。LLで書かれた動作のもっさりしてるコマンドラインツールよりシェルスクリプトが好きです。
しかしシェルスクリプトを書く時にはハマり所も多いです。ということでハマりそうなところと対処法を共有しますね。
1. 変数は基本クォート
変数には空白が入り得ます(特にパス名とか)。あるいは空である可能性もあります。
mv file1 file2 $target_dir
で$target_dir
が空だった時は
mv file1 file2
となってしまいます。file2が上書きされますね。恐ろしい。
クォートで括っておけば
mv file1 file2 "$target_dir"
mv file1 file2 ""
となり辺なディレクトリに書き込もうとしてエラーになります。まだマシですね。
基本、と書いたのはクォートしたくない場合もあるからです。例えばこんな時ですね。
items="foo bar baz"
for item in $items
do
echo "$item"
done
変数展開の後にトークン分割が行なわれるので空白で区切れば複数のトークンを1つの変数に入れられます。
2. -eオプションをつけよう
シェルには例外がありません。何かが失敗しても走り続けます。これは時に迷惑な挙動です。しかし -e
オプションをつけると0以外の終了ステータスが出た瞬間スクリプトが止まるようになります。
-e
オプションを有効にするには
#!/bin/sh
set -e
とすれば良いです。0以外のステータスで終了し得るコマンドの例外ハンドル的なことをしたければ
trying_command || true
とすれば確実に0で終了します。
また、スクリプトの途中で-e
を切り替えたければ
#!/bin/sh
set -e
# -eが有効
set +e
# -eが無効
set -e
# -eが有効
のようにset -e
とset +e
で制御出来ます。
3. カレントディレクトリはスクリプトを起動した場所
例えば ~/Shell/hoge.shにこう書いたとします。
#!/bin/sh
cat ./hoge.sh
pwd
それを実行するとこうなります
$ cd ~/Shell
$ sh hoge.sh
#!/bin/sh
cat ./hoge.sh
pwd
/home/kim/Shell
$ cd ../
$ sh Shell/hoge.sh
hoge.sh: no such file or directory
/home/kim
なのでシェルスクリプト内での相対パスは基本的に信用出来ません。
スクリプトの相対でパスを指定したいならこうしましょう。
#!/bin/sh
ROOT="$(cd $(dirname $0); pwd)"
cat "$ROOT/hoge.sh"
pwd
$(cd $(dirname $0); pwd)
がキモです。 $0には起動スクリプトが入ってます。 $ ./hoge.sh
と起動したなら ./hoge.sh
が、 $ ../hoge.sh
なら ../hoge.sh
が。そのスクリプトがあるディレクトリに移動(cd $(dirname $0)
) してpwd
するとスクリプトのあるディレクトリの絶対パスがとれます。
因みにこのイディオムはスクリプトにリンクを張られると困ります。 readlink(1)
を使う流儀もあるのですがreadlinkがMacとLinuxで全然違い、互換性を保てないのでおすすめしません。
4. sudo command » fileでパーミッションエラー
>>
(>
) を使った時にファイルに書き込んでるのは command
ではなくてシェルなのでシェルのアクセス権限でパーミッションエラーが出ます。sudoして書き込みたいなら
sudo command > file
ではなく
$ command | sudo tee file
を、
sudo command >> file
ではなく
$ command | sudo tee -a file
を使いましょう。
関連して、echo
は外部コマンドではなくシェルの組込みコマンドなのでsudoに渡すことが出来ません。これもファイルに書き込む目的ならechoとsudo teeをパイプで繋ぎましょう。
5. sudoのパスワードを渡せない
スクリプト内でsudoを扱うのは少しテクニックが必要になります。sudoに-Sを付けると標準入力からパスワードを読むようになります。かといってソースにベタ書きする訳にはいきません。 ということで、こうなります。
printf "password: "
read password
echo "$password" | sudo -S command
ユーザにパスワードを要求し、メモリに保存。必要な時にsudoに渡すという流れです。
シェルに依ってはreadに-sをつけることでエコーバックしなくなるのでディスプレイにパスワードが流れてくることはありません。未確認ですがdash(Debian系の/bin/sh)では出来なくてB shell(BSD系の/bin/sh)やBash、Zshなどの拡張POSIXシェルだと出来そうです。
余談ですが改行なしの印字にecho -n
は使えません。-n
シェルによってはオプションを認識しないので。printf(1)はPOSIXにあるのでポータブルに使えます。
因みにパスワードが初期化されてないならユーザに訊く、というのはこういう関数を実装すれば良さそうです。
password(){
if ! ${password+:} false
then
printf "password: "
read -s password
fi
}
6. sudoのパスワードを渡しつつ標準出力も渡したい
{ echo "$password" ; cat } | sudo -S command
しましょう。
7. リモートでスクリプトを実行したい
一旦scpでスクリプトを送ってから実行?そんな面倒なことしたくありません。
ssh user@remote <<SHELL
# some script
SHELL
で実行出来ます。これはログインシェルで実行します。ログインシェル如何に関わらずshで実行したいなら(ログインシェルがcshとかnologinとかは割とありえる)
ssh user@remote sh <<SHELL
# some script
SHELL
としましょう。
8. リモートでsudoのパスワードを渡したい
先のテクニックがそのまま使えます。
printf "password: "
read password
ssh user@remote sh <<SHELL
echo "$password" | sudo -S command
SHELL
クォート無しのヒアドキュメントのシェル変数の展開はローカルで行なわれるのでローカルにある変数がそのまま参照出来ます。
9. リモートにローカルに置いてあるファイルを送りたい
scpでも良いですがroot loginを許可してないと設定ファイルを/etcに置けないなどと不都合が生じます。
ssh user@remote sh <<SHELL
cat <<'EOF' >> some_remote_file
$(cat local_file)
EOF
SHELL
これは少し解説が必要でしょうか。
まず、先程も出てきた
ssh user@remote sh <<SHELL
SHELL
ですが普通のヒアドキュメントなので中の変数やコマンド置換を展開します。ということでリモートで実行されるのは
cat <<'EOF' >> some_remote_file
# the content of
# local_file
EOF
となります。そしてcatのヒアドキュメントはクォート付きなのでlocal_fileの中身がさらに変数展開されることはありません。
勿論、root権限で書き込みたかったら先程までのテクニックを組み合わせて
ssh user@remote sh <<SHELL
{ echo "$password" ; cat <<'EOF' } | sudo -S tee some_remote_file
$(cat local_file)
EOF
SHELL
として下さい.
10. リモートにあるファイルを編集したい
sed(1)を使いましょう。あるいは、sedだと辛いならローカルでファイルを編集、diffをとってpatch(1)を使いましょう。diffの送り方はもう分かりますね。
ssh user@remote sh <<SHELL
cat <<'EOF' | patch
$(cat file.diff)
EOF
SHELL
です。
いかがでしょうか。テクニックさえ抑えればシェルスクリプトは料理人だか板前だか知りませんが流行に流されるツールにも負けない力があると思います。動作も速い。 みなさんシェルスクリプト書きましょうね。