ShellScript
Bash
Linux
シェル芸

Bashの便利な構文だがよく忘れてしまうものの備忘録

シェルスクリプトは文法がシンプルで覚えると便利。他のプログラム言語だと何行もかかなければならないファイル操作や中身のデータ処理を、ちょちょいと数行レベルで書けとても強力だ。コマンドとの親和性が高い恩恵だろう1

旧来バッチ処理で本領を発揮するシェルスクリプトだったが、昨今はDocker(Dockerfileや起動スクリプトなど)で活用される機会も多いのではなかろうか。
しかしながら、他の言語ではみられない記法や文法のルールがあったり、その一方で似通っている書き方もあったりと、いろいろ混乱しがちなものこのBASHの特徴である(と私は思っている)。いちいちGoogle先生に確認しながら書くことも多かったりしたので、自分のメモがてら Bashのポイントをまとめた2

主な参照先(よくお世話になっているページ):

記法・特殊変数

基本

  • そんな高級な言語仕様じゃないし、わりとデバッグやテストもしにくい。
    • でも、他のプログラム言語だとコードを何行も書かなければならない処理も、コマンド数発とパイプのワンライナーでいけたりとか、その爽快感は魅力。
  • 他のプログラムにあるような、命令後に;(セミコロン)はいらない。ただし、1行に複数命令を各場合は、明示的に;で区切ることが可能。
  • コメントは#を用いる。#以降が無視される。
  • if ~ fi とか case ~ esac とか、他の言語じゃ見られないような、洒落た文法。
  • 括弧はいろいろな文法を表現できるようになっている。別記事にまとめてみたので、ご参照あれ。 - Bashの括弧のノウハウ(まとめ)

シェバング

スクリプト行頭の#!で始まる行をシェバング(シェバン、シバンとも)といい3、なんのプログラムで実行すべきかを指し示すことができる。シェルスクリプトの場合は、次のような形になるだろう。

#!/bin/sh
#!/bin/bash

スクリプトファイルに実行パーミッションを設定して直接実行すると、シェルはこの1行目を見て該当するコマンドを用いてスクリプトファイルを実行する。

Linuxではshbashへのリンクとなっているが、シェバングがshで実行されようとしている場合には、Bashはできるだけshの動作をしようとする。その結果Bash独自の記法が一部使えなくなるので注意だ。(こちらの解説が参考になる。)

特にこだわりがなければ、#!/bin/bash とするのが無難かもしれない。

shスクリプト
#!/bin/sh
grep 'fuga' < <(cat hoge.txt)    # エラー。sh は <() を解釈できない。
bashスクリプト
#!/bin/bash
grep 'fuga' < <(cat hoge.txt)    # OK. Bashの構文として解釈される。

変数・文字列

変数と代入

任意の文字列を使った変数が作れる4。参照時には$をつけるが、代入する際は$を使わない。代入は=を用いる。
右辺式が文字列の時で、(途中に空白などがなく)式として成り立っていればクォーテーションしなくても代入が可能。
=の前後に可読性目的で空白を入れてはいけない。言語的にエラーである。
他のスクリプト言語にあるような${VALNAME}のように{}で囲むことで、明示的に文字列と区別させることが可能。

GREETING='Hello! World!!'
MYNAME=HAL3
NOW=`date +'%Y/%m/%d %H:%M:%S'`
echo "${MYNAME}_$NOW> $GREETING"
# HAL3_2018/01/12 12:34:56> Hello! World!!

文字列連結とクォーテーションマーク

文字列連結

連結するには演算子などは必要なく、つなげれば良い。繋げる文字列の間での空白は不要。

$ HOGE="hoge"
$ FUGA="fuga"
$ echo $HOGE$FUGA      # hogefuga
$ echo "hoge""fuga"    # hogefuga

ダブルクォーテーションマーク

ダブルクォーテーションマークで囲んだ文字列では、式が展開される5

$ NUM=42
$ echo "THE ANSWER IS $NUM"           # THE ANSWER IS 42
$ echo "my HOSTNAME is `hostname`"    # my HOSTNAME is xxx

シングルクォーテーションマーク

シングルクォーテーションマークで囲んだ文字列では、式は展開されない5
!!!などはBashはヒストリを参照する時の特殊コマンドになるので、これを文字列に埋め込むにはシングルクォーテーションで囲むなどしてエスケープする必要がある。

$ echo 'THE ANSWER IS $NUM'          # THE ANSWER IS $NUM (展開されない)
$ echo 'Hello!!'                     # ""だと、 !! が直前のコマンドに置き換わる

文字列の中へのクォーテーションマークの展開

クォーテーションマークの中で、別なクォーテーションマークをエスケープなしで記述が可能である。

$ BOO='TSUNODA "''STAR''" HIRO'      # 'TSUNODA "' + 'STAR' + '" HIRO'
$ echo $BOO                          # TSUNODA "STAR" HIRO
$ POO="NISHIKINO '""STAR""' AKIRA"   # "NISHIKINO '" + "STAR" + "' AKIRA"
$ echo $POO                          # NISHIKINO 'STAR' AKIRA      
$ STAR="☆"
$ perl -e 'print qq(LUCKY '"'"$STAR"'"' STAR\n)'  # LUCKY '☆' STAR

位置パラメータ

スクリプト実行時に引数として渡された内容は、位置パラメータと呼ばれる特殊変数に保持される。これらはスクリプト中では、$1$9で表わせる。
10番目以降の位置パラメータは、${10}のように記述する。
なお、位置パラメータは関数への引数を展開する場合にも使う。

$ piyopiyo.sh "hoge" "fuga" 3 4 5 6 7 8 9 ten eleven
# $1    => hoge
# ${10} => ten

$*, $@, "$@" の違い

位置パラメータを集合的に扱う場合は、$*, $@, "$@"を利用する。
書き方によって、展開される内容が変わってくる。

sample.sh
echo '--- $* ---'; for P in $*; do echo $P; done
echo '--- $@ ---'; for P in $@; do echo $P; done        # $* と同じ
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の便利機能。たくさんある。ここに詳しく書かれている

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 default
bash
# 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

コマンド実行

スクリプトには任意のコマンドを挿入し実行できる。パイプなどももちろん使える。
コマンドの実行結果を変数や他のコマンドの引数に使いたい場合はバッククォートで囲む。Bashならば $( )でも良い。
また、直前のコマンドの実行結果は、特殊変数 $? に格納される。

#/bin/sh
TODAY=`date +'%Y%m%d'`
cat ./result.txt | grep "^$TODAY"

組み込みコマンド

echoprintfのように、コマンドとして存在しているもの(ex:/bin/echo)と、シェルの組み込みコマンドとしてのものがあったりする。
シェルスクリプト上ではパスを指定しないと組み込みのものが使われる。機能に差異があったりするので、前者を使う場合は明示的にパスを指定するなどして区別する。6

echo.sh
#!/bin/sh

echo -e '\u611B'        # 組み込みのほう
/bin/echo -e '\u611B'
実行結果
$ echo.sh
愛
\u611B

コマンド実行結果の終了コード

シェル上やスクリプト実行時、直前のコマンドの終了コードは特殊変数$?に代入される。終了コードは0(ゼロ)の場合は正常終了を意味し、それ以外はエラーやそれに準ずる状態である7

$ date
Wed Dec 28 14:37:26 JST 2016
$ echo $?
0
$ hogehoge                             # 存在しないコマンド
-bash: hogehoge: command not found
$ echo $?
127                                    # bash がセットしたエラーコード

なお、ifwhileも、0を真値、それ以外を偽値とした真偽判断を行なっている(後述testの項を参照)

バッククォートと$( )

 バッククォートを使って、`command`でコマンドの実行結果を変数に格納できる。伝統的な` `を使った記法よりも最近は$(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の部屋)の説明が分かりやすい
演算子やファイルテスト演算子の説明に関しては、こちらのページでまとめてくれている。

logic.sh
# 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 を入れることが多々ある。なぜか。
例えば、引数が与えられず $1 が空文字列になった場合、 x がないと 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"と、クォーテーションで囲む囲まないの違いがある。前者はヒアドキュメント部分でも変数展開は行い、後者は行わない。ダブルクォーテーションなのに変数展開は行わないのはなんか気持ち悪いので、自分はシングルクォーテーションで囲むようにしている。

here.sh
#!/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は、分割子を指定している。

adding.sh
#!/bin/sh

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

## sample: somedata.csv 
#1,1,2
#0,1,1
#1,2,3

行をまるっと使う場合は、read専用シェル変数$REPLYを使う。ただし、readに指定する変数がない場合のみ有効。

musics.sh
cat musiclist.txt | while read
do
    set -- $REPLY
    if [ "x$1" = "x70s" ]; then echo $REPLY; fi
done

## sample: musiclist.txt
#70s Can't Take My Eyes Off You
#70s Dschinghis Khan
#90s Kiss Of Life
#90s Space Cowboy
#70s Moskau

 なお、パイプ経由でwhileへデータを渡すような書き方だと、whileがサブシェルで起動されるため、その外側で定義した変数は更新されない。forや外部ファイルの入力リダイレクト(<)、名前付きパイプなどで行うとよい。(参考元:「bash で,サブシェルが起動される条件」)

db-saiyans.sh
#!/bin/sh

COUNT=0
cat dbdata.txt | while read NAME FLAG VALUE
do
    [ "x$FLAG" = "x1" ] && COUNT=`expr $COUNT + $VALUE`
    echo " - $COUNT"
done
echo "HAGE POWER LEVEL TOTAL: $COUNT"     # 0 

## sample: dbdata.txt 
#Gohan 0 2800
#Piccolo 1 3500
#Krillin 1 1770
db-saiyans2.sh
#!/bin/sh

# 入力リダイレクトを使った方法
COUNT=0
while read NAME FLAG VALUE
do
    [ "x$FLAG" = "x1" ] && COUNT=`expr $COUNT + $VALUE`
done < dbdata.txt
echo "HAGE POWER LEVEL TOTAL: $COUNT"     # 合算されている
db-saiyan3.sh
#!/bin/bash

# 名前付きパイプを使った方法( bash専用 )
COUNT=0
while read NAME VALUE
do
    [ "x$FLAG" = "x1" ] && COUNT=`expr $COUNT + $VALUE`
done < <(cat dbdata.txt)
echo "HAGE POWER LEVEL TOTAL: $COUNT"     # 合算されている

 上のThe read builtin commandにあった、Yes/Noアンサー。便利そうなので引用。

asksure.sh
#!/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

スクリプト自身のディレクトリの取得

ここの解説を参考にした。

bash
SCRIPTDIR=$(cd $(dirname $BASH_SOURCE); pwd)

デバッグTips

スクリプトを実行するときに、明示的にshまたはbashコマンドにオプションをつけて実行することで、簡単なデバッグか可能である。

シンタックスチェックをする -n オプション

-n オプションをつけて実行することで、シンタックスチェックをしてくれる。問題なければ何も出ない。
ただ、残念だがそれほど精度は高くない。コマンドのtypoがあったとしても、実行時にしか見つけられないので、通ってしまう。

なお、 -v オプションを共に用いることで、スクリプトのチェック内容が表示される。

e
$ sh -n test.sh    # if~fi や do~done での閉じ忘れの場合
test.sh: line 10: syntax error: unexpected end of file

実行結果を都度表示してくれる-xオプション

明示的にshまたはbashコマンドに -x オプションをつけて実行することで、コマンドの実行結果をいちいち表示してくれる。コマンドの実行結果や式の様子をトレースできるので、だいぶ楽になるだろう。

$ sh -x db-saiyans2.sh
+ COUNT=0
+ read NAME FLAG VALUE
+ '[' x0 = x1 ']'
+ read NAME FLAG VALUE
+ '[' x1 = x1 ']'
++ expr 0 + 3500
...

なお、 これも -v オプションで、そのコマンド内容が表示されるので、共に用いると良い。

コマンド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


  1. 加えてUNIXファイルシステムとパイプと哲学のおかげでもある。 

  2. もともとSolarisから入ったsh派なのでのでBashで何が使えるんだっけ?とか、Bash独自の機能についていってなかったりする。 

  3. 自分は「シェバング」と呼ぶ派。Wikipedia では「シバン」で書かれている1pingの読み方と同じように方言化されていてちょっと面白い。なおシェバングについて、これで指し示すコマンドをPATH環境変数に依存させるようにするならば、 envコマンドを使う手もある。 

  4. 色々な現場でコードを見てきたが、定数・変数の区別なく伝統的に大文字を使っていることが多かった。コマンドがほぼ小文字で書かれるため、可読性目的だったのではないかと推測。最近でこそ他言語のように定数・変数の命名規則について耳にすることもあるが、シェルスクリプトであまりそこをこだわっても綺麗に整理できない場合も多いので、伝統的なルールで良いのではないか、と思う。(そもそも命名規則まで持ち出すほど複雑化するようなスクリプトならば、他の言語の選択を検討せよ。) 

  5. PHPやPerl、Rubyなどはこの文法が参考にされている。 

  6. 組み込み側のマニュアルは、man bashを参照するとよい。 

  7. ただ、コマンドによっては、別なことを意味する場合がある。例えばdiffではExit status is 0 if inputs are the same, 1 if different, 2 if trouble.である。