Heavy Watal

シェルスクリプト

https://www.gnu.org/software/bash/manual/html_node/

if-then

# basic
if test -e ~; then
  echo 'test -e ~'
fi

# popular
if [ -e ~ ]; then
  echo '[ -e ~ ]'
fi

# bash/zsh extension; not POSIX
if [[ -e ~ ]]; then
  echo '[[ -e ~ ]]'
fi

# shortcut with exit status
test -e ~ && echo 'test -e ~ && echo' || echo 'not printed'
[ -e ~ ] && echo '[ -e ~ ] && echo' || echo 'not printed'
[[ -e ~ ]] && echo '[[ -e ~ ]] && echo' || echo 'not printed'
[[ ! -e ~ ]] && echo 'not printed' || echo '[[ ! -e ~ ]] || echo'

AND/OR

[ -e ~ ] && [ -d ~ ] && echo '[ -e ~ ] && [ -d ~ ]'
[ -e ~ ] || [ -f ~ ] && echo '[ -e ~ ] || [ -f ~ ]'
[[ -e ~ && -d ~ ]] && echo '[[ -e ~ && -d ~ ]]'
[[ -e ~ || -f ~ ]] && echo '[[ -e ~ || -f ~ ]]'

文字列

EMPTY=""
NOTEMPTY="content"
[ -z "$EMPTY" ] && echo '-z "$EMPTY"'
[ -n "$NOTEMPTY" ] && echo '-n "$NOTEMPTY"'
[ "$NOTEMPTY" != "$EMPTY" ] && echo '"$NOTEMPTY" != "$EMPTY"'
[ "$NOTEMPTY" = "content" ] && echo '"$NOTEMPTY" = "content"'
[[ "$NOTEMPTY" =~ "tent$" ]] && echo '"$NOTEMPTY" =~ "tent$"'

ファイル、ディレクトリ

x="${HOME}/.bashrc"
[ -e $x ] && echo "x exists"
[ -d $x ] && echo "x is a directory"
[ -f $x ] && echo "x is a regular file"
[ -s $x ] && echo "x is a regular file with size >0"
[ -L $x ] && echo "x is a symlink"
[ -r $x ] && echo "x is readable"
[ -w $x ] && echo "x is writable"
[ -x $x ] && echo "x is executable"

数値比較

x=1
y=2
[ $x -eq $y ] && echo "x == y"
[ $x -ne $y ] && echo "x != y"
[ $x -lt $y ] && echo "x <  y"
[ $x -le $y ] && echo "x <= y"
[ $x -gt $y ] && echo "x >  y"
[ $x -ge $y ] && echo "x >= y"

Looping Constructs

https://www.gnu.org/software/bash/manual/html_node/Looping-Constructs.html

基本形。クオートもカッコも不要:

for x in 1 2 3; do
  echo $x
done
# 1
# 2
# 3

for x in "1 2 3" のようにクオートすれば当然1要素扱いになるが、 一旦変数に代入したあとの挙動はシェルによって異なるので要注意: zshでは1要素扱い、dashやbashではスペース区切りで分割。

STRING="1 2 3"
for x in $STRING; do
  echo $x
done

Special Parameters

https://www.gnu.org/software/bash/manual/html_node/Special-Parameters.html

$0        # The full path of the script
$1        # First argument
$#        # The number of arguments ($0 is not included)
$*        # 全ての引数。"$*" はひと括り: "$1 $2 $3"
$@        # 全ての引数。"$@" は個別括り: "$1" "$2" "$3"
$-        # シェルの起動時のフラグ、setコマンドを使って設定したフラグの一覧
$$        # シェル自身のプロセスID
$!        # シェルが最後に起動したバックグラウンドプロセスのプロセスID
$?        # 最後に実行したコマンドのexit値

Parameter Expansion

https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html

string=abcdef
echo ${string}   # abcdef
echo ${#string}  # 6

うっかりアンダースコアで変数名をつなげてしまいがちなので、 常に{カッコ}つける癖をつける:

for CITY in sendai yokosuka; do
  echo hello_$CITY_people     # hello_
  echo hello_${CITY}_people   # hello_sendai_people
done

変数が未定義or空だったり、空白や特殊文字を含んでいたりして事故りがちなので、 基本的にダブルクォートを付ける癖をつける:

EMPTY=""
[ -n $EMPTY ] && echo "Bug! this is printed"
[ -n "$EMPTY" ] && echo "OK, this is not printed."

DIR=${HOME}/directory with space
# with: command not found
echo $DIR
# /Users/watal/directory

DIR="${HOME}/directory with space"
echo $DIR
# /Users/watal/directory with space
cd $DIR
# cd: /Users/watal/directory: No such file or directory
cd "$DIR"
# cd: /Users/watal/directory with space: No such file or directory

シングルクォートの中の変数は展開されずそのまま渡される:

echo "$HOME"  # /Users/watal
echo '$HOME'  # $HOME

部分列

# ${parameter:offset}
# ${parameter:offset:length}
echo ${string:0:3}  # abc
echo ${string:1:3}  # bcd
echo ${string:2}    # cdef
echo ${string: -2}  # ef

置換

文字列の置換には正規表現を使える sed が便利。 でもパスや拡張子のちょっとした操作なら、 わざわざ外部コマンドやパイプを使わずにシェル(bash)の機能だけで実現できる。

# ${STR#pattern}        # sed -e "s/^pattern//")     shortest
# ${STR##pattern}       # sed -e "s/^pattern//")     longest
# ${STR%pattern}        # sed -e "s/pattern$//")     shortest
# ${STR%%pattern}       # sed -e "s/pattern$//")     longest
# ${STR/pattern/repl}   # sed -e "s/pattern/repl/")  longest
# ${STR//pattern/repl}  # sed -e "s/pattern/repl/g") longest
# ${STR/#pattern/repl}  # sed -e "s/^pattern/repl/") longest
# ${STR/%pattern/repl}  # sed -e "s/pattern$/repl/") longest

FILEPATH=/root/dir/file.tar.gz
echo ${FILEPATH##*/}       # file.tar.gz
echo ${FILEPATH##*.}       # gz
echo ${FILEPATH#*.}        # tar.gz
echo ${FILEPATH%/*}        # /root/dir
echo ${FILEPATH%.*}        # /root/dir/file.tar
echo ${FILEPATH%%.*}       # /root/dir/file
echo ${FILEPATH/%.*/.zip}  # /root/dir/file.zip

変数が設定されていない場合にどうするか

              # if VAR is null, then
${VAR:-WORD}  # return WORD; VAR remains null
${VAR:=WORD}  # return WORD; WORD is assigned to VAR
${VAR:?WORD}  # display error and exit
${VAR:+WORD}  # nothing occurs; otherwise return WORD

Misc.

Command Substitution

https://www.gnu.org/software/bash/manual/html_node/Command-Substitution.html

コマンドの結果を文字列として受け取る方法は `command`$(command) の2つあるが、 後者のほうが入れ子など柔軟に使えるのでより好ましい。

Arithmetic Expansion

https://www.gnu.org/software/bash/manual/html_node/Arithmetic-Expansion.html

簡単な数値計算は $((expression)) でできる。

Shell Arithmetic

Process Substitution

https://www.gnu.org/software/bash/manual/html_node/Process-Substitution.html

1つのコマンドから出力を受け取るだけならパイプ command1 | command2 で足りるけど、 2つ以上から受け取りたいときはプロセス置換 command2 <(command1a) <(command1b) を使う。

標準入力や標準出力を受け付けないコマンドでも、 ファイル名を引数で指定できればプロセス置換で対応できる。 例えばgzip圧縮・展開の一時ファイルを作りたくない場合とか:

somecommand infile outfile
gunzip -c infile.gz | somecommand /dev/stdin /dev/stdout | gzip -c >outfile.gz
somecommand <(gunzip -c infile.gz) >(gzip -c >outfile.gz)

Arrays

https://www.gnu.org/software/bash/manual/html_node/Arrays.html

POSIXでは未定義の拡張機能であり、bashとzshでも挙動が異なる。

array=(a b c d e f)  # bash        | zsh
echo ${array}        # a           | a b c d e f
echo ${array[0]}     # a           |
echo ${array[1]}     # b           | a
echo ${array[-1]}    # (error)     | f
echo ${array[@]}     # a b c d e f | a b c d e f
echo ${#array}       # 1           | 6
echo ${#array[@]}    # 6           | 6

文字列と同様にコロンを使う方法ならzshでもゼロ基準なので安全。

echo ${array[@]:0:3}  # a b c
echo ${array[@]:1:3}  # b c d
echo ${array[@]:2}    # c d e f
echo ${array[@]: -2}  # e f

array[*]array[@] はクオートで括られる挙動が異なる。 前者はひと括り、後者はそれぞれ括られる。

for x in "${array[*]:3}"; do
  echo $x
done
# d e f

for x in "${array[@]:3}"; do
  echo $x
done
# d
# e
# f

オプション

シェル起動時に bash -u script.sh とするか、 スクリプト内で set -u とする。 途中で set +u として戻したりもできる。

後述のように注意は必要だけどとりあえず走りすぎない設定で書き始めたい:

set -eux -o pipefail
-e
エラーが起きたらすぐ終了する。 これが無ければスクリプトの最後まで走ろうとする。
挙動が変わる要因が多すぎて (サブシェルか、条件文がついてるか、POSIXモードか、bashバージョンなど) 逆に難しくなるので使わないほうがいいという見方もある。 それが気になるほど込み入ってきたらもはやシェルスクリプトの出番ではなく、 Pythonとかでかっちり書いたほうがよいのでは。
source している中で exit するとシェルごと落ちることにも注意。
-u
定義されていない変数を展開しようとするとエラー扱い。
-v
シェルに入力されるコマンドを実行前にそのまま表示する。 よくあるverboseオプションなので、 スクリプトに書いてしまうより必要に応じてコマンドに足すほうが自然か。
-x
変数を展開してからコマンドを表示する。 -v との併用も可能。
-o pipefail
パイプの左側でエラーになったら終了する。 読み手側が早めに切り上げて SIGPIPE が発生しても止まるので注意。
POSIXではないがbashでもzshでも利用可能。
-o posix
bash/zsh拡張を切ってPOSIX準拠モードで動く。 sh として呼び出されるとこれになる。

関連書籍