シェルスクリプトは文法がシンプルで覚えると便利。他の言語だと何行もかかなければならないファイル操作やその中身のデータ処理を、ちょちょいと1行〜数行で書けたりする。コマンドとの親和性が高い恩恵だろう。
今でもBashスクリプトをちょくちょく書くのだけど、スクリプトの作成頻度が低いからか他の言語と文法がごっちゃになったり、もともとSolarisから入ったsh
派なのでのでBashで何が使えるんだっけ?とか、割と忘れたり覚えてないものが多かったりする。
なので、メモがてら、Bashのポイントをまとめてみた。
主な参照先(よくお世話になっているページ):
- UNIXの部屋(68user's page) - http://x68000.q-e-d.net/~68user/unix/
- The Bash Hackers Wiki - http://wiki.bash-hackers.org/start
記法・特殊変数
基本
- そんな高級な言語仕様じゃないし、わりとデバッグやテストもしにくい。
- でも、他のプログラム言語だと手続きが必要な処理も、コマンド数発でいけたりとか魅力。
- 他のプログラムにあるような、命令後に
;
(セミコロン)はいらない。ただし、1行に複数命令を各場合は、明示的に;
で区切ることが可能。 -
if ~ fi
とかcase ~ esac
とか、他の言語じゃ見られないような、洒落た文法。
シェバング
スクリプト行頭の#!
で始まる行をシェバング(シェバン、シバンとも)といい1、なんのプログラムで実行すべきかを指し示すことができる。シェルスクリプトの場合は、次のような形になるだろう。
#!/bin/sh
#!/bin/bash
スクリプトファイルに実行パーミッションを設定して直接実行すると、シェルはこの1行目を判断して該当するコマンドを用いてスクリプトを実行する。
Linuxではsh
はbash
へのリンクとなっているが、シェバングがsh
で実行されようとしている場合には、Bashはできるだけsh
の動作を真似しようとする。その結果、Bash独自の記法が一部使えなくなるので注意だ。(こちらの解説が参考になる。)
#!/bin/sh
grep 'fuga' < <(cat hoge.txt) # エラー。sh として <() を解釈できない。
#!/bin/bash
grep 'fuga' < <(cat hoge.txt) # OK. Bashの構文として解釈される。
変数・文字列
変数と代入
任意の変数に代入する際は$
なし。参照する時は$をつける。代入は=
を用いて右辺式を左辺変数へ代入。
右辺式が文字列の時で、(途中に空白などなく)式として成り立って入ればクォーテートしなくても代入が可能。
=
の前後に可読性目的で空白を入れてはいけない。言語的にエラーである。
GREETING="Hello! World!!"
MYNAME=HAL3
NOW=`date +'%Y/%m/%d %H:%M:%S'`
echo "$MYNAME($NOW)> $GREETING"
文字列連結とクォーテーションマーク
文字列を連結する場合は特に演算子は必要ないので、コマンド引数の文字列の中に何か変数などを埋め込みたい場合は、文字列を使っているクォーテーションマークで区切って、その間に好きな変数を入れればよい。この理屈で、エスケープ無しで文字列の中にクォーテーションマークを入れることも可能。
$ echo "hoge""fuga"
hogefuga
$ HOGE='TSUNODA "'"STAR"'" HIRO' # TSUNODA "STAR" HIRO
$ FUGA="NISHIKINO "'"STAR"'" AKIRA" # NISHIKINO "STAR" AKIRA
$ STAR="☆"
$ perl -e 'print qq(LUCKY '"'"$STAR"'"' STAR\n)'
位置パラメータ
スクリプト実行時に引数として渡された内容は、位置パラメータと呼ばれる特殊変数に保持される。これらはスクリプト中では、$1
~$9
で表わせる。
10番目以降の位置パラメータは、${10}
のように記述する。
なお、位置パラメータは関数への引数を展開する場合にも使う。
$ piyopiyo.sh "hoge" "fuga" 3 4 5 6 7 8 9 ten eleven
# $1 => hoge
# ${10} => ten
$*, $@, "$@" の違い
位置パラメータを集合的に扱う場合は、$*
, $@
, "$@"
を利用する。
書き方によって、展開される内容が変わってくる。
echo '--- $* ---'; for P in $*; do echo $P; done
echo '--- $@ ---'; for P in $@; do echo $P; done # same as $*
echo '--- "$@" ---'; for P in "$@"; do echo $P; done
$ ./sample.sh "1 2" "hoge fuga"
--- $* ---
1
2
hoge
fuga
--- $@ ---
1
2
hoge
fuga
--- "$@" ---
1 2
hoge fuga
変数展開時の置換
Bashの便利機能。たくさんある。ここに詳しく書かれている。
echo $HOGE # (null)
echo ${HOGE:-hoge} # hoge ... just behalf
echo $HOGE # (null)
echo $FUGA # (null)
echo ${FUGA:=fuga} # fuga
echo $FUGA # fuga ... also substituting value as deafult
# change suffix of files
for F in *.JPG;do mv $F ${F//.JPG/.jpg};done;
# cut matched string at head of value
while read URL;do echo ${URL#http://};done < urls.txt
# cut matched string at foot of value
for F in *.tmp;do echo ${F%.tmp};done
# cut matched string as long as pattern can from head
myfilename=${0##*/} # same as basename
コマンド実行
スクリプトには任意のコマンドを挿入し、実行できる。パイプなどももちろん使える。
コマンドの実行結果を変数や他のコマンドの引数に使いたい場合はバッククォートで囲む。
#/bin/sh
TODAY=`date +'%Y%m%d'`
cat ./result.txt | grep "^$TODAY"
組み込みコマンド
echo
やprintf
のように、コマンドとして存在しているもの(ex:/bin/echo)と、シェルの組み込みコマンドとしてのものがあったりする。
シェルスクリプト上ではパスを指定しないと組み込みのものが使われる。機能に差異があったりするので、前者を使う場合は明示的にパスを指定するなどして区別する。2
#!/bin/sh
echo -e '\u611B' # 組み込みのほう
/bin/echo -e '\u611B'
$ echo.sh
愛
\u611B
コマンド実行結果の終了コード
シェル上やスクリプト実行時、直前のコマンドの終了コードは$?
に代入される。終了コードは0(ゼロ)の場合は正常終了を意味し、それ以外はエラーやそれに準ずる状態である3。
$ date
Wed Dec 28 14:37:26 JST 2016
$ echo $?
0
$ hogehoge # 存在しないコマンド
-bash: hogehoge: command not found
$ echo $?
127 # bash がセットしたエラーコード
なお、if
やwhile
も、0
を真値、それ以外を偽値とした真偽判断を行なっている(後述testの項を参照)。
バッククォートと$( )
バッククォートを使って、`commnd`
でコマンドの実行結果を変数に格納できる。伝統的な` `
を使った記法よりも最近は$(command)
を使う方が好まれるようだ。$()
は入れ子にできる。
#!/bin/sh
TODAY=`date +'%Y%m%d'`
echo $TODAY
NOWTIME=$(date +'%H%M%S')
echo $NOWTIME
EXAMPLE=$(expr $(date +%M) + $(date +%S))
echo $EXAMPLE
何もしないコマンド NOP
No OPeration のこと。何もせずに終了コード0(正常終了)を返す。
使う場面は、可読性目的でcase
文中においたり、無現ループなど。
また、引数を取れるので、本来のコマンドと一時的に置き換えたりして、デバッグや保守目的(暫定対応)などに使える。
if [ "x$wanna" = "xsleep" ]; then
: # sleep 60 # commented out for debug.
fi
判断式
test
真偽を判断するのにコマンドtest
を用いる。
シェルでは伝統的に、0が正、0以外が偽である。
またif
などと使われる[
も実はコマンドで、test
と同じ機能を提供する([ 式 ]
という形で用いる)。コマンドtest
についての説明は、ここ(UNIXの部屋)の説明が分かりやすい。
演算子やファイルテスト演算子の説明に関しては、こちらのページでまとめてくれている。
# if 文
if [ "x$1" = "-h" -o "x$1" = "--help" ]; then
echo "Usage: $0 [-h|--help]"
fi
# while 文
I=0
while [ $I -lt 5 ]; do
echo $I
I=`expr $I + 1`
done
cat list.txt | while read LINE; do
echo $LINE
done
# &&を使った例
[ -s ./target.txt ] && echo "ok, file exists"
ちなみに、上のサンプルもそうだけど、 if
で引数を判断する場合に、
if [ "x$1" = "x-h" ]; then
とわざわざ x
を入れているか。
入れない場合の、
if [ "$1" = "-h" ]; then
だと、もし引数に何も与えられなかくて$1
が空文字列の場合、if [ = "-h" ]; then
と解釈されてエラーが起こってしまう。これを回避するために適当な文字列を入れるという伝統的な小技である。(もちろん"x"
じゃなくてもよい)
case
割と柔軟な分岐を作ることができる。
;;
を忘れがちなので注意(; ;)。
while :
do
echo -n "What'up?) "
read K
case $K in
x) echo "X-("
;;
"q") echo "Bye" # "" で文字列評価してもOK
break # このbreakはループからの抜け出し
;;
*shirabero) # *,? のメタ文字利用がOK
echo "Nothing is there, Boss"
;;
[yY] | yes | YES ) # 論理和, [abc][a-z][!0-9] もOK
echo "Yeah!"
;;
[nN] | no | NO )
echo "Oops..."
;;
*) echo "What?" # デフォルト. 順番に評価されるので最後に置くとよい.
;;
esac
done
関数
関数はそれを呼ぶ場所より前に定義されていなければならない。
呼び出し時は、カッコは不要。
関数とローカル変数
funcname() {
local FUGA
set -- $*
HOGE=$1
FUGA=$2 # FUGA はローカル変数
}
# Call
funcname
funcname "hoge" "fuga"
return
返せる値は255まで。(処理された)値を返すというよりはステータスコードを返す目的で使うためである。
somefunc() {
[ "x$1" = "x0" ] && return 1 # some script
}
(小技)eval を使って関数の返値を変数に代入
他の言語のように、代入式の右辺として使えないのがシェルの関数の短所。つまりは、シェルのは関数ではなくサブルーチンである。
しかし、次のようにeval
を使って、擬似的な関数呼び出し+代入が行える。
getdate() {
eval $1=`date '+%Y/%m/%d'`
}
getdate TODAY
echo $TODAY # ex) 2015/12/25
プログラムパターン
ファイル入出力
標準出力・標準エラー出力
標準エラー出力に出力するのはどうすんだっけ? って、老化による記憶力低下が原因で時々なる。
echo "STDOUT" # 標準出力へ
echo "STDOUT" >&1 # 標準出力へ
echo "STDERR" >&2 # 標準エラー出力へ
リダイレクタなどの整理は、こちらの記事によく整理されている。
execをつかって標準出力を一時的にファイルに出力する
>
の向きで主客を取り違えてしまうので注意かな。
exec 3>&1 >$tmpfile # 標準出力(1) を ファイルディスクリプタ3 に複製
cat hogehoge.txt # 任意のコマンド
exec 1>&3 # ファイルディスクリプタ3 を 1 に複製(標準出力を1に戻す)
ヒアドキュメント
<<EOF
といった具合に終端文字列を指定してヒアドキュメントを作成できる。(文字列EOF
部分は任意)
書き方<<EOF
と<<'EOF'
または<<"EOF"
と、クォーテーションで囲む囲まないの違いがある。前者はヒアドキュメント部分でも変数展開は行い、後者は行わない。ダブルクォーテーションなのに変数展開は行わないのはなんか気持ち悪いので、自分はシングルクォーテーションで囲むようにしている。
#!/bin/sh
NOW=`date "+%Y/%m/%d %H:%M:%S"`
cat <<EOF
Now is $NOW
EOF
cat <<'EOF'
Now is $NOW
EOF
cat <<"EOF"
Now is $NOW
EOF
$ ./here.sh
Now is 2018/03/14 13:23:11
Now is $NOW
Now is $NOW
組み込みコマンドの活用
set
set
は、--
オプションを使う事で、文字列を空白文字で分割して、位置パラメータにセットできる。
set -- $line # line <= "hoge fuga"
$chunk1=$1 # hoge
$chunk2=$2 # fuga
なお、分割対象の文字列中に、Glob文字(*
)があると、カレントディレクトリのファイル名が展開されてしまうので、これを無効にした場合は、-f
オプションをつける。
line='* hoge'
set -f -- $line
getopts
getopts
で、コマンド実行時に渡されるオプションパラメータの解析が簡便にできる。
while getopts a:h OPT
do
case $OPT in
a) SOMEVAL=$OPTARG # some command
;;
h) help # go help subroutine
;;
*) help
;;
esac
done
shift $(( $OPTIND - 1 )) # <- 記述を忘れがち!
TARGET=$1 # $OPTIND-1 分オプション引数が捨てられている
read
コメントよりBuild-inのread
が複数変数を指定できることを教えてもらった。
read
はいろいろなオプションも指定できる(The read builtin command)。
サンプル中のシェル変数IFS
は、分割子を指定している。
cat somedata.csv | while IFS=',' read p1 p2 p3
do
if [ "x$p1" = "x1" ]; then
printf "%d + %d = %d¥n", $p2, $p3, `expr $p2 '+' $p3`
fi
done
行をまるっと使う場合は、read
専用シェル変数$REPLY
を使う。ただし、read
に指定する変数がない場合のみ有効。
cat somedata.txt | while read
do
set -- $REPLY
if [ "x$1" = "xOK" ]; then echo $REPLY; fi
done
なお、パイプ経由でwhile
へデータを渡すような書き方だと、while
がサブシェルで起動されるため、その外側で定義した変数は更新されない。for
や外部ファイルの入力リダイレクト(<
)、名前付きパイプなどで行うとよい。(参考元:「bash で,サブシェルが起動される条件」)
COUNT=0
cat somedata.txt | while read NAME VALUE
do
[ "x$NAME" = "xHAGE" ] && COUNT=`expr $COUNT + $VALUE`
done
echo "HAGE TOTAL: $COUNT" # 0 のまま
# 入力リダイレクトを使った方法
COUNT=0
while read NAME VALUE
do
[ "x$NAME" = "xHAGE" ] && COUNT=`expr $COUNT + $VALUE`
done < somedata.txt
# 名前付きパイプを使った方法
COUNT=0
while read NAME VALUE
do
[ "x$NAME" = "xHAGE" ] && COUNT=`expr $COUNT + $VALUE`
done < <(cat somedata.txt)
上のThe read builtin commandにあった、Yes/Noアンサー。便利そうなので引用。
#!/bin/bash
asksure() {
echo -n "Are you sure (Y/N)? "
while read -r -n 1 -s answer; do
if [[ $answer = [YyNn] ]]; then
[[ $answer = [Yy] ]] && retval=0
[[ $answer = [Nn] ]] && retval=1
break
fi
done
echo # just a final linefeed, optics...
return $retval
}
### using it
if asksure; then
echo "Okay, performing rm -rf / then, master...." # おいw
else
echo "Pfff..."
fi
[[ ]]
はBashの構文で、基本的には [ ]
と同じ。ただし、[ ]
とは違って&&
や||
などが使える。こちらのまとめられた記事を参照するとよい。
trap
シグナルごとに設定可能。といっても使うシグナルは限られているか。KILL(9)はキャッチ不可。
# trap 'command' sing-num
trap 'echo "see you"' 0 # 0 ... exit
trap 'rm -f $tmpfile;exit 1' 2 # 2 ... INT(Ctrl+C)
trap 0 # reset (no command, just 1 arg)
misc
シェル変数 $PIPESTATUS
自分はあまり使わないので、詳しく書いているリンクだけ掲載。
コマンドパイプラインの終了コード - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog
スクリプト自身のディレクトリの取得
ここの解説を参考にした。
SCRIPTDIR=$(cd $(dirname $BASH_SOURCE); pwd)
コマンドTips
シェルスクリプト構文ではないけど、スクリプト内で使うと便利なコマンド関連Tips。
tar と gzip を使ったパイプ処理による圧縮
gzip
は -c
オプションで、標準入力・出力とで圧縮対象データのやり取りをする。
tar
は -
で、対象を標準入力・出力にすることができる。
もちろん、 bzip2
bunzip2
などで同じことが可能。
$ # making tarball & gzipping
$ tar cvf - target_dir | gzip -9c > target_dir.bkup20160704.tar.gz
$ # ungipping & expand tarball
$ gunzip -c target_dir.bkup20160704.tar.gz | tar xvf -
もちろん、Linuxなどが使っているGNU gzipなら、gzip
のオプションで、最初から圧縮されたTarBall .tgz
ファイルが作れる。
date コマンドによる日付のパース
date --date="$somestr" '+%Y/%m/%d'
# not only date string but also: today, yesterday, last month, etc.
curl にパイプ経由でデータを渡す
'-d' オプションで、引数に@
を渡す。
$ cat text.txt | curl -sS -X POST -d @- http://example.com/boopoo
これを利用することで、シェルからJSON情報をPOSTで渡せる。
$ cat <<'EOF' | curl -sS -X POST -d @- http://example.com/boopoo
{
"a": 123,
"b": [
4,5,6
]
}
EOF
まぁ、 joコマンドを導入してもよいかもね。
$ jo a=123 b=$(jo -a 4 5 6) | curl -sS -X POST -d @- http://example.com/boopoo
改行コードを改行コード文字列に
なんのこっちゃかもだが、改行コードを¥n
という文字列にするということ。Perlで。
$ cat some.txt | perl -pe 's/\n/%5Cn/'
文字コード変換
iconv
を使うと良い。
$ iconv -f Shift_JIS -t UTF8 shift_jis.txt
$ curl -sS http://example.com/shift_jis_contents | iconv -f shift_jis -t utf8