ShellScript
Bash
Linux
UNIX

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

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

今でもBashスクリプトをちょくちょく書くのだけど、スクリプトの作成頻度が低いからか他の言語と文法がごっちゃになったり、もともとSolarisから入ったsh派なのでのでBashで何が使えるんだっけ?とか、割と忘れたり覚えてないものが多かったりする。

なので、メモがてら、Bashのポイントをまとめてみた。

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

記法・特殊変数

基本

  • そんな高級な言語仕様じゃないし、わりとデバッグやテストもしにくい。
    • でも、他のプログラム言語だと手続きが必要な処理も、コマンド数発でいけたりとか魅力。
  • 他のプログラムにあるような、命令後に;(セミコロン)はいらない。ただし、1行に複数命令を各場合は、明示的に;で区切ることが可能。
  • if ~ fi とか case ~ esac とか、他の言語じゃ見られないような、洒落た文法。

シェバング

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

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

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

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

shスクリプト
#!/bin/sh
grep 'fuga' < <(cat hoge.txt)    # エラー。sh として <() を解釈できない。
bashスクリプト
#!/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

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

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

sample.sh
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の便利機能。たくさんある。ここに詳しく書かれている

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
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

コマンド実行

スクリプトには任意のコマンドを挿入し、実行できる。パイプなどももちろん使える。
コマンドの実行結果を変数や他のコマンドの引数に使いたい場合はバッククォートで囲む。

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

組み込みコマンド

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

echo.sh
#!/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 がセットしたエラーコード

なお、ifwhileも、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の部屋)の説明が分かりやすい
演算子やファイルテスト演算子の説明に関しては、こちらのページでまとめてくれている。

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 を入れているか。
入れない場合の、

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"と、クォーテーションで囲む囲まないの違いがある。前者はヒアドキュメント部分でも変数展開は行い、後者は行わない。ダブルクォーテーションなのに変数展開は行わないのはなんか気持ち悪いので、自分はシングルクォーテーションで囲むようにしている。

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は、分割子を指定している。

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
bash
# 名前付きパイプを使った方法
COUNT=0
while read NAME VALUE
do
    [ "x$NAME" = "xHAGE" ] && COUNT=`expr $COUNT + $VALUE`
done < <(cat somedata.txt) 

 上の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

シェルスクリプト構文ではないけど、スクリプト内で使うと便利なコマンド関連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. 自分は「シェバング」と呼ぶ派。Wikipedia では「シバン」で書かれている1pingの読み方の流派に似ているね。 

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

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