## 基本
### 変数の宣言
#### 基本
`declare` 推奨。
```bash
declare -r HOGE='hogehoge'
```
よく使うオプション
| オプション | 意味 |
| ---------- | ------------ |
| -a | 配列 |
| -f | 関数名 |
| -i | 整数 |
| -r | 読み取り専用 |
| -u | 大文字 |
| -l | 小文字 |
※ `declare -r` は `readonly` で代用可能
#### スコープ
関数などでスコープを制限する場合は `local` を付ける。
```bash
local var1=2
declare -i local var2=2
```
### 変数へ値を代入
```bash
a=10
```
※ イコールの前後に空白を入れるとエラーになる
変数は先頭に `
を付ける。
```bash
echo $a
echo ${a}
```
### 変数へ変数を代入
```bash
a=$b
```
*文字列演算子構文* を使うことでより簡潔な記述が可能。
### 配列の宣言
```bash
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 は真である |
記述例
```bash
if [ $1 -eq 8 ]
then
echo 'yes'
elif [ $1 -eq 7 ]
then
echo 'yeah'
else
echo 'no'
fi
```
セミコロンをつけるとthenを同一行にすることも可能
```bash
if [ $1 -eq 8 ]; then
echo 'yes'
elif [ $1 -eq 7 ]; then
echo 'yeah'
else
echo 'no'
fi
```
### 繰り返し文
#### 配列
```bash
array=(a1 a2 a3)
for i in ${array[@]}
do
echo ${i}
done
```
#### リストもどき
```bash
list='1 2 3'
for i in ${list}
do
echo ${i}
done
```
#### csv
```bash
list=$(echo '1,2,3' | sed -e 's/,/ /g')
for i in ${list}
do
echo ${i}
done
```
#### 可変長引数
```bash
for arg in $*
do
echo ${arg}
done
```
#### 実行ディレクトリのファイル一覧
```bash
for i in *
do
echo ${i}
done
```
#### コマンド結果
引数のファイルをcatした結果を取得し、順番に表示する。
```bash
for i in $(cat $1)
do
echo ${i}
done
```
### 文字列の分解と結合
#### 文字列の結合
```bash
str1='Lo'
str2='ve'
union=${str1}${str2}
echo ${union}
```
#### 文字列の分解
`variable` の a番目から、b文字を切り出す方法
```bash
sliced=${variable:a-1:b}
```
例えば、以下の出力結果は `567` になる。
```bash
variable="1234567890"
echo ${variable:4:3}
```
## 応用
### ヒアドキュメント
コマンドの入力を標準入力にすることができる。
スクリプト内で別ファイルを作成するときなどに有効。
```bash
# ラベル(下記ではEOF)が登場するまでが標準入力
$ cat << EOF
> a
> b
> EOF
a
b
```
また、ラベルをコーテーションで括ると、変数展開やコマンド置換が無効になる。
```bash
$ 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}` と記述する。
頻出表現
--------
### ログの装飾
```bash
# ログ出力設定
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}
}
```
### 例外処理
例外処理は ``$?`` の戻り値を確認する。
```bash
# 直前のコマンドに対して例外処理を行う
if [ $? != 0 ]; then
echo 'error'
exit 1
fi
```
複数箇所で使用する場合は関数化する方が便利。
```bash
# 直前のコマンドが正常終了していない場合にメッセージを表示して異常を返却
ErrCheck()
{
if [ $? != 0 ]; then
echo $1
return 1
fi
}
```
### URLデコード
```bash
echo -n "ふがふが ほげほげ" | nkf -wMQ | sed 's/=$//g' | tr = % | tr -d "\n"
```
### バックアップ作成
```bash
# ${dirs}配下の${file}を${dst}配下に階層付でコピー
$ find ${dirs} -name ${file} -print | cpio -pdv ${dst}
```
デバッグ
--------
### デバッグログを出す
`-x` オプションを指定すると全てのコマンドを出力する。
```bash
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 /usr` → `ls` を `/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で `ll` は `ls -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}`
変数が未定義の場合にデフォルト値を設定する。
* variableが存在し、かつ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を先頭とする位置パラメータの番号となる
`count` が `frogfootman` と設定されている場合 `${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が `@` または `*` の場合には、位置パラメータが順番に処理され、展開結果がそのリストとなる
## 入出力リダイレクタ
入出力リダイレクタの一覧
| 記述例 | 意味 |
| ----------------------------- | ---------------------------------------------------------------------------- |
| <code>cmd1 | cmd2</code> | パイプ(cmd1の標準出力をcmd2の標準入力にする) |
| `> file` | 標準出力をfileに切り替える |
| `< file` | 標準入力をfileに切り替える |
| `>> file` | 標準出力をfileに切り替える (fileが既に存在する場合は追加する) |
| <code>>| file</code> | 標準出力をfileへ強制する (noclobberの設定を無視する) |
| <code>n>| file</code> | ファイルディスクリプタ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からの入力を停止する |