シェルスクリプトの難解部分を凝縮して解説する

# シェルスクリプトの難解部分を凝縮して解説する
シェルスクリプトを習得する際に自分が理解するのに時間がかかった箇所を紹介します。
言語習得の助けになれば幸いです。
追記: たくさんの編集リクエストありがとうございました。最早このページの内容は私のナレッジではないですね。
(Thanks to @akinomyoga, @magicant)
## 変数
シェルスクリプトでの変数は、他の言語の変数よりC言語のマクロに近いです。

```bash
var='echo'
$var "Hello World!"
# -> echo "Hello World!" と先に解釈される
# -> Hello World! が出力される
```

## 型
シェルスクリプトに型は基本的に文字列型しかありません (シェルによっては配列、連想配列に対応していることもあります)。数字は文字列として格納します。

```bash
if [ "1" -gt 0 ]; then echo "True"; fi
# -> True が出力される
if [ "1" = "1" ]; then echo "True"; fi
# -> True が出力される
```
## I/O
標準入力、標準出力、標準エラー出力は全てファイルのコンテンツとして扱います。(Cygwin系環境では内部動作が違うかもしれません)
`/dev/null` ファイルに出力した内容は破棄されます。

```bash
echo 'This is a content' > content.txt
# -> content.txt ファイル(なければ作成)の内容を This is a content にする
echo 'This is a content' >/dev/stderr
# -> 標準エラー出力に This is a content を出力する
# -> echo "(略)" 1>&2 と同義
```
## 条件分岐
`[` は `test` のエイリアス(後述)なので、条件式の書き方は `man test` を参照。

* `if` は引数をシェルスクリプトとみなして実行、正常終了すると `then` 〜 `fi` の中を実行するコマンド。
* `test` は条件式を評価して偽だった場合に異常終了するコマンド。 

```bash
if [ 1 -gt 0 ]
then
  echo '1 is greater than 0'
fi

# この2行は同義
# $? は前回のコマンドの終了ステータスを表す
which vim; if [ $? -eq 0 ]; then :; fi
if which vim; then :; fi

[ 0 -gt 1 ]
# -> 異常終了
```
しかし `if` の中身が1行かつ `else` がなければ、 `if` を使わなくても条件分岐が出来る。

* `cmd1 && cmd2` - `cmd1` が正常終了した場合のみ `cmd2` を実行
* `cmd1 || cmd2` - `cmd1` が異常終了した場合のみ `cmd2` を実行

先ほどの例を書き換えると

```bash
[ 1 -gt 0 ] && echo '1 is greater than 0'
```

メモ: 要は `if` 文は終了ステータスが0の時に実行されるのでよく混乱します。高級言語で 0 は偽だから...
## 関数の返り値
シェルスクリプトに `return` 文はありますが、これは関数の返り値を返すものではなく、終了ステータスを返すものです。
シェルのプロンプトに前回実行の終了ステータスが表示される環境を作ると習得が楽になります。

```bash
output_error() {
  echo "Error!" 1>&2
  # 終了ステータス: 0 以外はエラーとみなされる
  # -> プログラムの規模が大きい場合はエラー番号と紐付けておく
  return 1
}

output_error
# -> 標準エラー出力に Error! が出力される
# -> 異常終了したと判定される
```
では普通の言語のように文字列を返すにはどうすれば良いか。標準出力に出力します。

```bash
just_echo() {
  echo 'Hi'
}

just_echo
# -> 標準出力に Hi が出力される
var=$(just_echo)
var="`just_echo`"
# -> var に文字列 Hi が格納される
```
## コマンドの別名
シェルスクリプトに構文は少なく(シェル依存なら多い)、殆どがコマンド+パラメータの形で表現できます。
一見構文に見えるものでも、それはあるコマンドの別名かもしれません。

```bash
source dir/another.sh
# -> source は . の別名 (但し source は ksh/zsh/bash の拡張)
# -> . dir/another.sh と同義

if [ 0 -eq 0 ]
then
  # then, fi もコマンドのような扱いなので本来インデントは要らない
  # が、可読性のためにインデントしておく
  echo "Something"
fi
# -> [ は test の別名、ただしパラメータが ] で終わる必要がある
# -> だから内部に空白が必要なんですね
# -> つまり...
if test 0 -eq 0
then
  echo "Something"
fi
# -> と同義である
```
## パイプライン
パイプラインとは前に実行したコマンドの標準出力を出力せずに、次のコマンドの標準入力に渡すことを言います。

```bash
echo "Bye"
# -> Bye と出力
echo "Bye" | :
# -> : は何もしないコマンド
# -> 文字列 Bye を何もしないコマンドの標準入力に
# -> 何も出力されない
echo "Bye" | cat
# -> cat は標準入力の内容を標準出力に出力するコマンド
# -> 文字列 Bye を cat の標準入力に
# -> Bye と出力
```
## タプル
前述の通り、値はマクロのように扱われますので、値にスペースが入っていると別のパラメータとして扱われます

```bash
# 第1引数を表示するコマンド(関数)
show_1st() {
  echo $1
}

show_1st "Hello World"
# -> Hello World と出力

show_1st Hello World
# -> Hello と出力(World は第2引数)

# 配列 (ksh/zsh/bash の拡張)
array=( "A" "B C" "D" )
echo ${#array[@]}
# -> 3 が出力
array_copy=( $(echo ${array[@]}) )
echo ${#array_copy[@]}
# -> 4 が出力、増えてる...
```
## まとめて実行
複数のコマンドを1つのコマンドとみなして実行できます。

```bash
pwd
# -> /home/username が出力
{ cd dir; pwd; }
# -> 同環境で実行、最後にセミコロンが必須
# -> /home/username/dir が出力
pwd
# -> /home/username/dir が出力
```

```bash
pwd
# -> /home/username が出力
( cd dir; pwd )
# -> /home/username/dir が出力
# -> 別環境で実行、最後のセミコロンは省略可
pwd
# -> /home/username が出力、別環境での cd は影響を受けない
```
## 罠
* `declare` はシェルごとに挙動が違うので使わないほうがいい。
* `[ $var = 'x' ]` は `$var` が未定義 or 空文字列の際に `[ = 'x']` と同義になり、エラーになる可能性がある。 `[ "x${var}" = 'xx' ]` と書いておくと確実。
* StackOverflow の回答を鵜呑みにすると危ない。実行シェルに依存するコマンドかもしれない。
* built-inでないコマンドを呼ぶことは、プロセスを起動しているのと同義。構文で対応出来るならそれがベスト。

## 覚えておくと便利
* awk
* bc
* cat
* cut
* head, tail
* grep
* perl
* sed
* xargs

## 後書き
ざっくり理解することを目的としているので細かい指摘はして欲しくないようなでも僕の後学のためにして欲しいような。