Contents

基本文法

変数の宣言

基本

declare 推奨。

declare -r HOGE='hogehoge'

よく使うオプション

オプション 意味
-a 配列
-f 関数名
-i 整数
-r 読み取り専用
-u 大文字
-l 小文字

declare -rreadonly で代用可能

スコープ

関数などでスコープを制限する場合は local を付ける。

local var1=2
declare -i local var2=2

変数へ値を代入

a=10

※ イコールの前後に空白を入れるとエラーになる

変数は先頭に $ を付ける。

echo $a
echo ${a}

変数へ変数を代入

a=$b

文字列演算子構文 を使うことでより簡潔な記述が可能。

配列の宣言

array=(a1 a2 a3)

条件判定

記述例 意味
-a file file が存在する
-b file file が存在し、かつブロックデバイスファイルである
-c file file が存在し、かつキャラクタデバイスファイルである
-d file file が存在し、かつディレクトリである
-e file file が存在する (-aと同じ)
-f file file が存在し、かつ通常ファイルである
-g file file が存在し、かつsetgidビットがセットされている
-G file file が存在し、かつ実行グループIDによって所有されている
-h file file が存在し、かつシンボリックリンクである
-k file file が存在し、かつstickyビットがセットされている
-L file file が存在し、かつシンボリックリンクである
-n string string がnullではない
-N file file が最後の読み取りの後に変更されている
-O file file が存在し、かつ実行ユーザーIDによって所有されている
-p file file が存在し、かつパイプまたは名前付きパイプ(FIFOファイルである)
-r file file が存在し、かつ読み取り可能である
-s file file が存在し、空ではない
-S file file が存在し、かつソケットである
-t N ファイルディスクリプタNが端末を指している
-u file file が存在し、かつsetuidビットがセットされている
-w file file が存在し、かつ書き込み可能である
-x file file が存在し、ファイルの場合は実行可能、ディレクトリの場合は検索可能である
-z string string の長さがゼロ
fileA -nt fileB fileA が fileB よりも新しい
fileA -ot fileB fileA が fileB よりも古い
fileA -ef fileB fileA と fileB が同じファイルを指している
stringA = stringB stringA が stringB に等しい (POSIXバージョン)
stringA == stringB stringA が stringB に等しい
stringA != stringB stringA と stringB が一致しない
stringA =~ regexp stringA が 拡張正規表現 regexp と一致する
stringA < stringB 語彙の順番では stringA は stringB よりも前にある
stringA > stringB 語彙の順番では stringA は stringB よりも後ろにある
exprA -eq exprB 算術演算子 exprA と exprB は等しい
exprA -ne exprB 算術演算子 exprA と exprB は等しくない
exprA -lt exprB 算術演算子 exprA は exprB よりも小さい
exprA -gt exprB 算術演算子 exprA は exprB よりも大きい
exprA -le exprB 算術演算子 exprA は exprB 以下である
exprA -ge exprB 算術演算子 exprA は exprB 以上である
exprA -a exprB 算術演算子 exprA および exprB はともに真である
exprA -o exprB 算術演算子 exprA または exprB は真である

記述例

if [ $1 -eq 8 ]
then
    echo 'yes'
elif [ $1 -eq 7 ]
then
    echo 'yeah'
else
    echo 'no'
fi

セミコロンをつけるとthenを同一行にすることも可能

if [ $1 -eq 8 ]; then
    echo 'yes'
elif [ $1 -eq 7 ]; then
    echo 'yeah'
else
    echo 'no'
fi

繰り返し文

配列

array=(a1 a2 a3)
for i in ${array[@]}
do
    echo ${i}
done

リストもどき

list='1 2 3'
for i in ${list}
do
    echo ${i}
done

csv

list=$(echo '1,2,3' | sed -e 's/,/ /g')
for i in ${list}
do
    echo ${i}
done

可変長引数

for arg in $*
do
    echo ${arg}
done

実行ディレクトリのファイル一覧

for i in *
do
    echo ${i}
done

コマンド結果

引数のファイルをcatした結果を取得し、順番に表示する。

for i in $(cat $1)
do
    echo ${i}
done

文字列の分解と結合

文字列の結合

str1='Lo'
str2='ve'
union=${str1}${str2}

echo ${union}

文字列の分解

variable の a番目から、b文字を切り出す方法

sliced=${variable:a-1:b}

例えば、以下の出力結果は 567 になる。

variable="1234567890"
echo ${variable:4:3}

応用

ヒアドキュメント

コマンドの入力を標準入力にすることができる。
スクリプト内で別ファイルを作成するときなどに有効。

# ラベル(下記ではEOF)が登場するまでが標準入力
$ cat << EOF
> a
> b
> EOF
a
b

また、ラベルをコーテーションで括ると、変数展開やコマンド置換が無効になる。

$ HOGE=hogehogeeeee
# コーテーション無しでは${HOGE}も`ls | wc -c`も展開される
$ cat << EOF
${HOGE}
`ls | wc -c`
EOF
hogehogeeeee
62

# コーテーション有りでは${HOGE}も`ls | wc -c`もそのまま
$ cat << 'EOF'
${HOGE}
`ls | wc -c`
EOF
${HOGE}
`ls | wc -c`

一部だけ展開したくない場合は \${HOGE} と記述する。

頻出表現

ログの装飾

# ログ出力設定
readonly INFO="\x1b[32;01m"
readonly WARN="\x1b[35;01m"
readonly ERROR="\x1b[31;01m"
readonly NORMAL="\x1b[0m"
info() {
  echo -e ${INFO}[INFO]: $1${NORMAL}
}
warn() {
  echo -e ${WARN}[WARN]: $1${NORMAL}
}
error() {
  echo -e ${ERROR}[ERROR]: $1${NORMAL}
}

例外処理

例外処理は $? の戻り値を確認する。

# 直前のコマンドに対して例外処理を行う
if [ $? != 0 ]; then
    echo 'error'
    exit 1
fi

複数箇所で使用する場合は関数化する方が便利。

# 直前のコマンドが正常終了していない場合にメッセージを表示して異常を返却
ErrCheck()
{
    if [ $? != 0 ]; then
        echo $1
        return 1
    fi
}

URLデコード

echo -n "ふがふが ほげほげ" | nkf -wMQ | sed 's/=$//g' | tr = % | tr -d "\n"

バックアップ作成

# ${dirs}配下の${file}を${dst}配下に階層付でコピー
$ find ${dirs} -name ${file} -print | cpio -pdv ${dst}

デバッグ

デバッグログを出す

-x オプションを指定すると全てのコマンドを出力する。

sh -x test.sh

コマンドライン処理の流れ

コマンドラインに入力された文字列は以下のフローで処理される。
各フローの詳細は別途説明する。

1. トークンに分解
    + シングルコーテーションで囲まれている
        - 11へ
    + ダブルコーテーションで囲まれている
        - 6へ
    + その他
        - 2へ
2. 1つ目のトークンを確認
    + 開始キーワード
        - 1に戻り、次のコマンドを読み込む
    + その他のキーワード
        - 構文エラー
    + キーワードではない
        - 3へ
3. 1つ目のトークンを確認
    + エイリアスである
        - エイリアスを展開して、1へ
    + エイリアスではない
        - 4へ
4. {}展開
5. チルダ展開
6. パラメータ展開
7. コマンド置換
8. 算術置換
    + ダブルコーテーションで囲まれている
        - 11へ
9. ワードの抽出
10. パス名展開
11. コマンド検索 (関数、組み込みコマンド、実行可能ファイル)
12. コマンドの実行

    + evalがある
        - 引数を次のコマンドにする
    + evalがない
        - コマンドを実行

1. トークンに分解

下記規定のメタ文字でトークンに分解する。

  • スペース
  • タブ
  • 改行
  • ;
  • (
  • )
  • <
  • >
  • |
  • &

トークンの種類は以下

  • ワード
  • キーワード
  • 入出力リダイレクタ
  • セミコロン

2. 1つ目のトークンを確認

1つ目のトークンがキーワードかを判定する。

開始キーワードは以下

  • if
  • function
  • {
  • (

開始キーワードではないキーワードは以下

  • then
  • else
  • or
  • do
  • fi
  • done

3. 1つ目のトークンを確認

1つ目のトークンがエイリアスの場合展開する。

4. {}展開

{} を展開する。

例: a{b,c}ab ac

5. チルダ展開

~user のような ~ を展開する。

例: ~/home

6. パラメータ展開

変数を展開する。

例: ${FILES}file1 file2

7. コマンド置換

$(command)command を展開する。

例: `pwd` → /home/user

8. 算術置換

$((expression)) を展開する。

例: $((1+3))4

9. ワードの抽出

$IFS の文字を使用してワードに分解する。

10. パス名展開

\*, ?, [ & ] の組に対し、パス名展開またはワイルドカード展開する。

例: ls /usr/\*/usr/bin /usr/local

11. コマンド検索

1つ目のワードを以下のいずれかと見なし、コマンドを特定する。

  • 関数
  • 組み込みコマンド
  • $PATH 環境変数のいずれかのディレクトリにあるファイル

例: ls /usrls/usr/bin/ls と認識

12. コマンド実行

入出力リダイレクトなどを設定した後、コマンドを実行する。
evalがある場合は1に戻る。

具体的な実行例

以下の前提の元、各フローで実施される処理を記述する。

  • alias ll="ls -l" が設定されている
  • ユーザaliceのホームディレクトリ /home/alice.hist537 というファイルが存在する
  • $$ の値が 2537 である
  • コマンドは ll $(type -path cc) ~alice/.*$(($$%1000)) である
  1. 1で 入力を ll, $(type -path cc), ~alice/.*$(($$%1000)) の3ワードに分解する
  2. 2で ll はキーワードではないため3へ
  3. 3で llls -l のエイリアスなので、分解して1へ
  4. 1で 入力を ls, -l, $(type -path cc), ~alice/.*$(($$%1000)) の4ワードに分解する
  5. 2で ls はキーワードではないため3へ
  6. 3で ls はエイリアスではないため4へ
  7. 4で ls -l $(type -path cc) ~alice/.*$(($$%1000)){} を含まないので5へ
  8. 5で ls -l $(type -path cc) /home/alice/.*$(($$%1000)) とチルダ展開して6へ
  9. 6で ls -l $(type -path cc) /home/alice/.*$((2537%1000)) とパラメータ展開して7へ
  10. 7で ls -l /usr/bin/cc /home/alice/.*$((2537%1000)) とコマンド展開して8へ
  11. 8で ls -l /usr/bin/cc /home/alice/.*537 と算術展開して9へ
  12. 9で ls -l /usr/bin/cc /home/alice/.*537 はワード展開が不要のため10へ
  13. 10で ls -l /usr/bin/cc /home/alice/.hist537 とパス名展開して11へ
  14. 11で ls/usr/bin/ls のコマンドであると検索され12へ
  15. 12で /usr/bin/ls-l オプション かつ 引数 /usr/bin/cc /home/alice/.hist537 として実行される
  16. 12の実行にはevalが存在しないため終了

文字列演算子構文

変数で使用する演算子の構文一覧

${variable:-word}

変数が未定義の場合にデフォルト値を返す。

  • varialbeが存在し、かつnullでない場合に、その値を返す
  • それ以外の場合は、wordを返す。

${count:-0}count が未定義であれば 0 と評価される。

${variable:=word}

変数が未定義の場合にデフォルト値を設定する。

  • varialbeが存在し、かつnullでない場合に、その値を返す。
  • それ以外の場合は、variableにwordを設定して返すが、位置パラメータや特殊なパラメータをこの方法で代入することはできない

${count:=0}count が未定義であれば 0 を設定する。

${variable:?message}

変数が未定義の場合に発生するエラーを補足する。

  • varialbeが存在し、かつnullでない場合に、その値を返す
  • それ以外の場合は、variable:に続いてmessageを出力し、(対話型のシェルでは無い場合にのみ)現在のコマンドあるいはスクリプトを中止する
  • messageを省略すると、デフォルトで「parameter null or not set」が出力される

${count:?"undefined!"}count が未定義であれば count: undefined! を出力して終了する。

${variable:+word}

変数の存在を評価する。

  • varialbeが存在し、かつnullでない場合に、wordを返す
  • それ以外の場合は、nullを返す

${count:+1}count が未定義であれば 1 (「真」を意味する) を返す。

${variable:offset:length}

文字列の一部を返す (部分文字列またはスライスと言う)

  • 部分文字列を展開する。
  • $variable の値から、offsetの位置からlength文字の長さの部分文字列を取り出す
  • 文字の位置は0から数える。
  • lengthを省略すると、offsetの位置から $variable の終わりまでの部分文字列が返される。
  • offsetが0未満の場合は $variable の末尾から位置が数えられる。
  • variableが @ の場合、lengthはoffsetを先頭とする位置パラメータの番号となる

countfrogfootman と設定されている場合 ${count:4}footman を返し ${count:4:4}foot を返す。

パターンとパターン照合

変数から特定パターンを抽出する一覧

${variable#pattern}

variableの値の始めの部分とpatternが一致した場合、最も短く一致した部分を削除し、残りの部分を返す

${variable##pattern}

variableの値の始めの部分とpatternが一致した場合、最も長く一致した部分を削除し、残りの部分を返す

${variable%pattern}

variableの値の終わりの部分とpatternが一致した場合、最も短く一致した部分を削除し、残りの部分を返す

${variable%%pattern}

variableの値の終わりの部分とpatternが一致した場合、最も長く一致した部分を削除し、残りの部分を返す

${variable/pattern/string}

variableの値でpatternと最も長く一致した部分をstringと置換する。

  • 最初に一致した部分だけが置換される
  • patternが # で始まる場合は、variableの始めの部分と一致しなければならない
  • stringがnullの場合は、一致した部分が削除される
  • variableが @ または * の場合には、位置パラメータが順番に処理され、展開結果がそのリストとなる

${variable//pattern/string}

variableの値でpatternと最も長く一致した部分をstringと置換する。

  • 一致する部分は全て置換される
  • patternが # で始まる場合は、variableの始めの部分と一致しなければならない
  • stringがnullの場合は、一致した部分が削除される
  • variableが @ または * の場合には、位置パラメータが順番に処理され、展開結果がそのリストとなる

入出力リダイレクタ

入出力リダイレクタの一覧

記述例 意味
cmd1 | cmd2 パイプ(cmd1の標準出力をcmd2の標準入力にする)
> file 標準出力をfileに切り替える
< file 標準入力をfileに切り替える
>> file 標準出力をfileに切り替える (fileが既に存在する場合は追加する)
>| file 標準出力をfileへ強制する (noclobberの設定を無視する)
n>| file ファイルディスクリプタnの出力をfielへ強制する (noclobberの設定を無視する)
<> file fileを標準入力および標準出力として使用する
n<> file ファイルディスクリプタnの標準入力および標準出力としてfileを使用する
<< label ヒアドキュメント
n> file ファイルディスクリプタnをfileに切り替える
n< file ファイルディスクリプタnとしてfileを設定する
n>> file ファイルディスクリプタnをfileに切り替える (fileが既に存在する場合は追加する)
n>& 標準出力をファイルディスクリプタnに複製する
n<& 標準入力をファイルディスクリプタnから複製する
n>&m ファイルディスクリプタnを出力ファイルディスクリプタのコピーにする
n<&m ファイルディスクリプタnを入力ファイルディスクリプタのコピーにする
&> file 標準出力および標準エラーをfileに切り替える
<&- 標準入力を停止する
>&- 標準出力を停止する
n>&- ファイルディスクリプタnからの出力を停止する
n<&- ファイルディスクリプタnからの入力を停止する

トラブルシューティング

複数行コメントアウトしたい

以下のようにする。

: << '#_comment_out'
...
#_comment_out

文法が間違っていないのにエラーが出る

以下を全て満たしているかを確認する。

  • 改行コードが \n になっている
  • 代入演算子の左辺で変数に $ が付いていない
  • 代入演算子の右辺で変数に $ が付いている
  • 条件式で変数に $ が付いている
  • 数値型変数の定義で declare -i が付いている
  • = の前後に空白が入っていない

よく使うスニペット

シバン

#!/bin/bash

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

SCRIPT_DIR=$(dirname $0)

検査系

Utility

# Usageを表示して処理終了
function usage_exit() {
    echo "Usage: `basename $0` dierctory" 1>&2
    exit 1
}

# エラーメッセージを表示して処理終了
function error_exit() {
    echo "[ERROR] $1"
    exit 1
}

# コマンドが利用できるかの確認
function can_use() {
    type -p $1 || error_exit "Unable to find $1, please install it and run this script again"
}

複雑名引数指定

while getopts g:c:s:v opt
do
    case $opt in
        g) GROUP=$OPTARG
            ;;
        s) SERVERS=$OPTARG
            ;;
        c) CMD=$OPTARG
            ;;
        v) VERBOSE=true
            ;;
        \?) usage_exit
            ;;
    esac
done

# 組み合わせによるバリデーション
[ -z $GROUP ] && [ -z "$SERVERS" ] && usage_exit
[ $GROUP ] && [ "$SERVERS" ] && usage_exit
[ -z "$CMD" ] && usage_exit

引数が1つでない場合にNG

if [ $# -ne 1 ]; then
    usage_exit
fi