bashで意外だったこと、普段気になってるアレは何なんだ？を記録するノート

bashで遊んでて、色々気づいたことを書きますね。
なお、このノートはどんどん上に伸びていきます。

##環境
```bash:環境
$ bash --version
GNU bash, version 4.3.46(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
$ uname -a
Linux ubuntu 4.4.0-53-generic #74-Ubuntu SMP Fri Dec 2 15:59:10 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.1 LTS
Release:        16.04
Codename:       xenial
```

## コマンドのスタブをつくる
単体テストでコマンドの終了コードを`0`以外にしたい時、同名関数でコマンドのスタブを作ります（`builtin echo`のようにビルトインコマンドで呼出しされるコマンドには対応していません）。

```bash:関数定義
$ function ls(){ return 1; }
$ export -f ls
$ type ls
ls is a function
ls () 
{ 
    return 1
}
```

```bash:テスト対象のスクリプト
$ cat sample.sh
#!/bin/bash

ls
printf "ls returned [%d]\n", $?
```

```bash:スクリプト実行
$ ./sample.sh
ls returned [1]
```


## findコマンドのイディオム

* 末尾の`+`は、`-exec`で指定したコマンドのコマンドラインが長くなりすぎないように、コマンド（例`grep`）に渡すファイル名の数を調整するオプションだそうです（つまり、コマンドのforkコストを最小限に抑えられる）。`{} +`の間にスペースが必要であることに注意。
* `grep`は対象ファイルが1件のみの場合、結果にファイル名を含めないようです。findでヒットしたファイル件数が1件のみの場合でも、`grep`の結果にファイル名を表示したい場合、ダミーとして`/dev/null/`を指定するとよいそうです。

```bash:
find / -type f -name "*.h" -exec grep "hoge" /dev/null {} +
```

##readコマンドには-rオプションをつける

`-r`オプションには、バックスラッシュをエスケープ文字として解釈されるのを抑止し、リテラルとして扱う効果があります。

```bash:
printf -- '-z *.log \\\n' |\
    while read -r var1 var2 var3; do
        echo [${var1}] [${var2}] [${var3}];
    done
# => [-z] [*.log] [\]
```

ちなみに、`-d `*delim*オプションというのもあるのですが、こちらは単語区切りではなく**行区切り**のデリミタなので、お間違えなきよう。また、`read`は空行も読み込むのでお忘れなきよう。

```bash:
printf 'aaa bbb ccc:ppp qqq rrr::xxx yyy zzz:' |\
    while read -d ':' -r a b c; do
         echo "[${a}] [${b}] [${c}]"
    done
# [aaa] [bbb] [ccc]
# [ppp] [qqq] [rrr]
# [] [] []
# [xxx] [yyy] [zzz]
```

##grepの対象ファイルが1個の場合、出力結果にファイル名が含まれない

なので、出力結果につねにファイル名を含めたい場合は、ダミーとして`/dev/null`を含めると良いみたいです。

```bash:
$ grep 'bbb' sample.log
bbb
$ grep 'bbb' sample.log /dev/null
sample.log:bbb
$ cat sample.log | grep 'bbb' - /dev/null
(standard input):bbb
```
###参考

+  [grepにおける/dev/nullなど（by shigemk2）](http://www.shigemk2.com/entry/20130330/1364654681)

##最終行の末尾改行の有無によって、イテレーションの結果が変わる

複数行のデータを読み込んで`while`等でイテレーションする際、最終行の末尾に改行文字が存在しない場合、最終行が無視されてしまいます。

```bash:
$ printf "aaa\nbbb\nccc" | while read line; do echo $line; done
aaa
bbb
$ printf "aaa\nbbb\nccc\n" | while read line; do echo $line; done
aaa
bbb
ccc
$ while read line; do echo $line; done < <(printf "aaa\nbbb\nccc")
aaa
bbb
$ while read line; do echo $line; done < <(printf "aaa\nbbb\nccc\n")
aaa
bbb
ccc
```

最終行末尾に改行のないファイルを`cat`してパイプにつなげてテキスト処理する場合など、大変危険。

##echoとprintfの違い

ここでは、人間がうっかりしやすいポイントをまとめます。

それぞれを一言で言うと

+ `echo`は第1引数～第N引数を空白文字区切りで連結し、末尾に改行を付け足したものを出力する。
+ `printf`は第1引数のみを出力する（第2引数～第N引数は第1引数に埋め込みされる）

となります。

`echo`は

+ 末尾に改行文字を付加して出力します。
+ 引数がクォートされていない場合、シェルは連続する区切り文字を引数区切りとみなして`echo`に渡すため、結果的に「分割後の引数を空白文字1個で連結し、末尾に改行文字を付加したもの」が出力されます。

```bash:
$ printf "$IFS" | od -Ax -tx1z
000000 20 09 0a                                         > ..<
000003
$ 
$ VAR=$(printf "a\t\t\tb\n\n\nc   d_"); VAR=${VAR%_}
$ 
###########################
# echoで出力（クォートなし）
###########################
$ echo $VAR | od -Ax -tx1z
000000 61 20 62 20 63 20 64 0a                          >a b c d.<
000008
# => 連続する区切り文字が1個の空白文字に置換。
# => 末尾に改行文字が付加される。
$
###########################
# echoで出力（クォートあり）
###########################
$ echo "$VAR" | od -Ax -tx1z
000000 61 09 09 09 62 0a 0a 0a 63 20 20 20 64 0a        >a...b...c   d.<
00000e
# => 区切り文字は加工されない。
# => 末尾に改行文字が付加される。
```

`printf`は

+ 出力結果は、末尾に改行文字が付加されません。末尾改行が必要な場合、`\n`を追加しておく必要があります。
+ C言語と同様、第1引数はフォーマットなので、`echo a b c`のように複数の文字列を同時に出力することはできません（`printf a b c`は`a`のみを出力し、`b c`は無視します）。代わりに、`printf '%s %s %s' a b c`などとする必要があります。

```bash:
$ printf "$IFS" | od -Ax -tx1z
000000 20 09 0a                                         > ..<
000003
$ 
$ VAR=$(printf "a\t\t\tb\n\n\nc   d_"); VAR=${VAR%_}
$ 
###########################
# printfで出力（クォートなし）
###########################
$ printf $VAR | od -Ax -tx1z
000000 61                                               >a<
000001
# => 区切り文字で分割された結果の第一引数のみ出力される。
###########################
# printfで出力（クォートあり）
###########################
$ printf "$VAR" | od -Ax -tx1z
000000 61 09 09 09 62 0a 0a 0a 63 20 20 20 64           >a...b...c   d<
00000d
# => 加工されずに出力される。
```

つい忘れがちな違いとして他にも、`echo`は`--`を文字列として解釈するのに対し、`printf`は`--`を「オプションの終わり」と解釈する、というのもあります。

```bash:
$ echo -- 'aaa'
-- aaa
$ printf -- 'aaa\n'
aaa
```

`-`（「正負を表す符号」ではなくオプションを意味するダッシュ記号）ではじまるオプションを受け取る組込みコマンドは予期しないオプションに対して`invalid option`エラーとなるのに対して、`echo`はそのまま文字列として解釈します。

```bash:
# echoは-xオプションを持たない
$ echo -neE 'aaa
> '
aaa
# オプションではなく文字列として解釈する
$ echo -x aaa
-x aaa
# printfは-xオプションを不正なオプションとして解釈する
$ printf -x aaa
-bash: printf: -x: invalid option
printf: usage: printf [-v var] format [arguments]
```

##コマンドの実行結果（複数行）を変数に代入して扱う

複数行あるコマンドの実行結果をテキスト処理する場合、パイプや一時ファイル以外にも、コマンドの実行結果を変数に代入してから扱うことも可能です。

```bash:
$ ps
   PID TTY          TIME CMD
  3357 pts/9    00:06:22 bash
 12848 pts/9    00:00:00 ps
$ result=$(ps)
$ 
$ echo "$result" | while read line; do echo "$line"; done
PID TTY          TIME CMD
3357 pts/9    00:06:22 bash
12849 pts/9    00:00:00 ps
$ 
$ printf "${result}\n" | while read line; do echo "$line"; done
PID TTY          TIME CMD
3357 pts/9    00:06:22 bash
12849 pts/9    00:00:00 ps
```

ただし、コマンド置換は末尾の改行文字を削除するため（後述）、例えば`printf`コマンドで出力したものをイテレーションすると（末尾改行の無い）最終行を無視するので、末尾に改行を付加する必要があります。あるいは、`echo "${result}"`とするか。

##ファイル件数を取得したが･･･lsとfindで結果が異なるのはなんで？
`ls`がドットファイルを除外していたり、`find -type f`に関しては通常ファイルのみを対象とするため、シンボリックリンク等が除外されている可能性があります（詳しくは`man`を）。

##「echo hoge」でエラーを発生させる方法

ファイルディスクリプタ#1をクローズして`echo hoge`するとエラーが発生します。事前に#1はバックアップをとっておきましょう。

```bash:
$ echo hoge
hoge
# 標準出力を#10に退避し、#1をクローズ
$ exec 10>&1 1>&-
$
$ echo hoge
-bash: echo: write error: Bad file descriptor
$ echo $? >&10
1
$
# #10に退避していた標準出力を#1に戻し、#10をクローズ
$ exec 1>&10 10>&-
$ echo hoge
hoge
```

##コマンド置換は末尾の改行が削除される

バイナリダンプすると、末尾の`0a`が削除されているのが分かります。

```bash:
$ printf 'abc\n' | od -Ax -tx1
000000 61 62 63 0a
000004
# 末尾にLF => LFを削除
$ printf "$(printf 'abc\n')" | od -Ax -tx1
000000 61 62 63
000003
# 末尾にCR-LF => LFを削除
$ printf "$(printf 'abc\r\n')" | od -Ax -tx1
000000 61 62 63 0d
000004
# 先頭・途中・末尾 => 末尾のLFのみ削除
$ printf "$(printf '\nab\nc\n')" | od -Ax -tx1
000000 0a 61 62 0a 63
000005
# 末尾の連続するLF => 連続するLFも削除
$ printf "$(printf 'abc\n\n\n\n\n')" | od -Ax -tx1
000000 61 62 63
000003
```

というように、コマンド置換の結果においては、末尾の連続するLFも削除されてしまうため、LF1個を変数に代入したい場合、捨て文字を末尾につけて、

```bash:
$ VAR="$(printf '\n_')"; VAR=${VAR%_}
$ printf "${VAR}" | od -Ax -tx1
000000 0a
000001
```

とする必要があります。タイプミスのように見えて見栄えは悪いですが、以下のように書くこともできます。

```bash:
$ VAR="
> "
$ printf "${VAR}" | od -Ax -tx1
000000 0a
000001
```

```:man_bash
Bash performs the expansion by executing command  and
replacing  the command substitution with the standard
output of the command,  with  any  trailing  newlines
deleted.  Embedded newlines are not deleted, but they
may be removed during word  splitting.   The  command
substitution  $(cat  file)  can  be  replaced  by the
equivalent but faster $(< file).
```

##制御演算子の&&と||の優先度は同じ

bashの制御演算子の`&&`,`||`は単純に左結合です。以下では`1`(False)が表示されます。

```bash:
true || false && false
echo $?    # => 1(False)
```

`&&`,`||`のオペランドの評価は遅延評価方式であり、コマンドの実行順序も左→右です。まあ、コマンドを実行するのに行きつ戻りつするのも変ですよね。

```bash:
(echo 1; true) || (echo 2; false) && (echo 3; false)
# => 1 3
```

また、`&&`,`||`で連結されたコマンドリストの終了ステータスは、最後に実行されたコマンドの終了ステータスになります。

ちなみに、算術演算子の`&&`,`||`の優先順位はC言語と同じく、`&&`の方が優先順位が高いです。以下では`True`が表示されます。

```bash:
if ((1 || 0 && 0)); then echo "True"; else echo "False"; fi
# => True
```

##if文やwhile文は複合コマンド
`if`文、`for`文、`(())`、`[[]]`、いろいろあります複合コマンド。

コマンドなので、パイプでつなげることも可能です。

```bash:○well-formed
if true; then yes; fi | head -n 1

while true; do builtin echo "y"; done | head -n 1
```

え、知ってた？
でも、ループ内でプロセスforkが発生する場合、注意が必要です。

```bash:
while true; do builtin echo "y"; done | head -n 1  # (1)
# => "y"を表示して終了

while true; do /bin/echo "y"; done | head -n 1     # (2)
# => "y"を表示してフリーズ
```

上の(1)では、`head`によりパイプがクローズされた後、`echo`がパイプに対して書き込みを試みますが、パイプはクローズ済みのため、前段プロセスのサブシェル（`while ... done`）が`EPIPE(141)`により終了します。これは予想通りの動きです。

しかし(2)では、`head`がパイプをクローズした以降に起動された`/bin/echo`は`EPIPE(141)`で異常終了するものの、親プロセス（`while ... done`）は終了しないため、延々と「`/bin/echo`コマンドが起動→`EPIPE(141)`で即終了」を繰り返します。

そのため、上記(2)を実行すると、画面上では「`y`が表示された後、フリーズしている」ように見えます。

##シグナルを受けて終了した時の終了ステータス
シグナル`n`を受けて終了した場合の終了ステータスは`128 + n`の結果となります。例えば、`SIGPIPE(13)`を受けて終了した場合、終了ステータスは`128 + 13 = 141(EPIPE)`となります。

##if リスト, while リスト, until リスト

`if`,`while`,`until`コマンドの第1引数にはリストをとるので、単純コマンドだけでなく、リストや複合コマンドをとることも可能です。

```bash:
$ if if true; then echo hoge; fi; then echo fuga; fi
hoge
fuga
$ while if true; then echo hoge; fi; do echo fuga; break; done
hoge
fuga
```

この手のトリッキーな記述は可読性が低下するため実用上はご法度ではあるのですが、シンタックスの許容範囲の境界線を明確に把握しておくと、（シェルスクリプトを書いてるとしょっちゅうあるのですが）シンタックスではまって、やたら時間を溶かすことも避けられるのではと思います。

残業時間も減って定時で帰宅して、そして家でbashを書くわけです（笑）。

##{,}は単語区切りにならない
bashにこなれないうちはよくハマるのですが、`{`,`}`はメタ文字ではないため、単語区切りになりません。

```bash:○well-formed
$ { echo hoge;};echo fuga
hoge
fuga
```

上の例（正しい）だと、`{ echo`のように間にブランクが必要ですし、`{}`内の最後のコマンドには末尾にメタ文字が必要です（`hoge;}`）。また、2番目の`echo`との間には、`};echo`のように`;`で区切ってリスト化する必要があります（`};echo`を`;`の代わりにブランクで区切るのは駄目で、なぜならスペースは単語区切りではあるのですが、ブランクでコマンドを連結してリスト化することはできないためです）。

このようにしないと単語が繋がってしまうため、`{echo`や`}echo`という奇怪な名前のコマンドを実行せよ、という意味になってしまいます。

##{}がとる構文

`{}`がとる構文は、以下です。

+ `{ list;}`
+ `{ list;<改行文字>}`

僕の環境だと`{ list&}`でも通るのですが、`man bash`によると「リストの最後は改行文字かセミコロンでなければならない」だそうです。むりやり規定に合わせるとすれば、こうなるかなあ。

+ `{ list&(exit);}`

##「:」は本当に何もしないコマンド？

`:`コマンドは、`true`と同じく終了ステータスをつねに`0`に書き換えます。コマンドの実行に意味が無く、終了ステータスを書き換えないという意味で、もっともNOPに近いのは`(exit)`じゃないでしょうか…。

```bash:
$ false
$ (exit)
$ echo $?
1
$ true
$ if (exit); then echo $?; else echo $?; fi
0
$ false
$ if (exit); then echo $?; else echo $?; fi
1
```

## 複合コマンドの終了ステータス

複合コマンドの終了ステータスは、「最後に実行したコマンドの終了ステータス」になります。

```bash:
$ if true; then (exit 2); elif true; then (exit 3); else (exit 4); fi
$ echo $?
2
$ if false; then (exit 2); elif true; then (exit 3); else (exit 4); fi
$ echo $?
3
$ if false; then (exit 2); elif false; then (exit 3); else (exit 4); fi
$ echo $?
4
```

では、条件部のコマンドで偽（`0`以外）と評価されたため、条件部以外のコマンドがひとつも実行されなかった場合、終了ステータスには何がセットされるでしょうか。

答えは「`0`」です。

```bash:
$ false; echo $?
1
$ if false; then :; elif false; then :; fi
$ echo $?
0
$ while false; do :; done
$ echo $?
0
$ for false; do :; done
$ echo $?
0
```

では、もう一つ。

`then`ブロック（または`else`ブロック）の中に入ったが、`$?`の値が更新されない場合、`if`文全体の終了ステータスには何がセットされるでしょうか？

答えは「条件部のコマンドの終了ステータス」です。

```bash:
$ true
$ if false; then (exit); else (exit); fi
$ echo $?
1
$ false
$ if true; then (exit); else (exit); fi
$ echo $?
0
```

上記の場合、`if`文全体の終了ステータスは、条件部の`false`コマンドや`true`コマンドの終了ステータスとなっています。

以上をふまえて冒頭の定義を正確に言いなおすと

+ いずれかの`then`ブロック（または`else`ブロック）に入った場合、`if`文全体の終了ステータスは「【条件部のコマンドを含めて】最後に実行されたコマンドの終了ステータス」である。
+ いずれのブロックにも入らなかった場合は、つねに「`0`」である。

と言えそうです。このことから、以下の`if`文の終了ステータスは「`0`」になります。

```bash:
$ false
$ if if false; then (exit); fi; then (exit); else (exit); fi
$ echo $?
0
```

念押しで、以下の`if`文の終了ステータスは「`1`」になりますね。

```bash:
$ if ! { if false; then (exit); fi;}; then (exit); else (exit); fi
$ echo $?
1
```

なお、`bash 4.3.46`では、構文上、`then`ブロックや`else`ブロックを空にすることはできないです。

```bash:×ill-formed
# `then`/`else`ブロック内が空行は不可（コメント行でもだめ）
if true; then
fi
```

##バックグラウンドプロセスの終了ステータスを取得する

```bash:
$ (sleep 10; exit 255) &
[1] 3090
$ wait $!
[1]+  Exit 255                ( sleep 10; exit 255 )
$ echo $?
255
```

`$!`は直前に実行したバックグラウンドプロセスのプロセスIDです。`wait`は、子プロセスの終了を同期待ちするコマンドで、`wait`自体の終了コードがバックグラウンドプロセスの終了コードになります。

```:man　bash
wait [-n] [n ...]
    Wait for each specified child process and return its termination status.  Each n may be a process ID or a job
    specification; if a job spec is given, all processes in that job's pipeline are waited  for.   If  n  is  not
    given,  all currently active child processes are waited for, and the return status is zero.  If the -n option
    is supplied, wait waits for any job to terminate and returns its exit status.  If n specifies a  non-existent
    process  or  job,  the  return  status  is  127.  Otherwise, the return status is the exit status of the last
    process or job waited for.
```

```bash:
$ exit 255& wait $!
[1] 2921
[1]+  Exit 255                exit 255
$ echo $?
255
```

##timeはシェルの予約語
コマンドじゃなかったとは！組込みコマンドでもなく、`if`や`while`のような予約語の一種です。紛らわしいですが、外部コマンドの`time`もあります。

```bash:
$ type time
time is a shell keyword
$ which time
/usr/bin/time
```

`man time`で出てくるのは、コマンド版のヘルプです。予約語の`time`は`if`と同じキーワードの一種なので、文法は`man bash`を参照します。

しかし、確実に予約語の`time`を選択してほしいとき、`builtin`はもちろん使えないし、どうすりゃいいのさ（コロコロを転がす）。例えば、複合コマンドの時間を測定したい時、外部コマンドの`time`を選択する処理系だと、シンタックスエラーになるんですよね（僕の環境では、たまたま予約語の`time`を使ってくれてるので問題ないですが）。

```bash:
time if true; then sleep 1; fi  # ○ well-formed

/usr/bin/time if true; then sleep 1; fi  # × ill-formed
# => -bash: syntax error near unexpected token `then'
```

##関数本体は{}で括らなければならない？

```bash:○well-formed
function f() if true; then echo "function is called."; fi

function f2()
    while true; do
        builtin echo "function is called."
        return
    done

function is_even() (($1%2==0))  ## 引数が偶数の時に真
```

関数本体は複合コマンドでありさえすればいいので、必ず`{}`で括らなければいけない、という決まりはないです。複合コマンドなので、`if`や`while`、`((...))`が使えるというわけです。

あ、単純コマンドやリストはNGです。例えばパイプでつなぐと、それはリストになってしまいます。

```bash:×ill-formed
function f()
    while true; do
        builtin echo "y"
    done | head -n 1    # => ×これはリストになってしまう
```

##$0は「特殊パラメータ」
用語の話ですが、`$0`は特殊パラメータのひとつであり、位置パラメータは`$1,$2,$3...`だそうです。今まで、ぜんぶ位置パラメータだと思っていました。

位置パラメータが2桁以上の数字の場合、数字をブレース`{}`で囲む必要があります（例：「`${10}`」）。

## GLOB展開されないケース

(1)`*.log`にパターンマッチするファイルが存在する場合、`*.log`がGLOB展開された結果が変数`x`に代入されます。しかし、(2)`*.log`にパターンマッチするファイルが存在しない場合、GLOB展開は行われず、「`*.log`」という文字列リテラルが`x`に代入されます。

```bash:
# (1)
:>./hoge.log
for x in ./*.log; do
    echo "x=${x}"    # => "./hoge.log"
done

# (2)
rm -f ./*.log
for x in ./*.log; do
    echo "x=${x}"    # => "./*.log"
done
```

##mkdir -p -mでの中間ディレクトリのパーミッション指定

`mkdir`コマンドは、`mkdir -m 777`のように作成するディレクトリのパーミッションを指定することが可能ですが、存在しない中間ディレクトリも同時に作成する`mkdir -p`との合わせ技で使う場合、`-m`オプションで指定したパーミッションは中間ディレクトリのパーミッションに影響しないため、事前に`umask`でデフォルトパーミッションマスクを変更しておく必要があります。

```bash:
#!/usr/bin/env bash

# (1) -mオプションを使用した場合
rm -fr ./a 
umask
mkdir -p -m 777 -- a/b/c

ls -ld a a/b a/b/c

# (2) umaskを使用した場合
rm -fr ./a 
umask 000 
umask
mkdir -p -- a/b/c

ls -ld a a/b a/b/c
```

```bash:実行結果
# (1)
0002
drwxrwxr-x 3 yz2cm yz2cm 4096 Feb 24 18:15 a
drwxrwxr-x 3 yz2cm yz2cm 4096 Feb 24 18:15 a/b
drwxrwxrwx 2 yz2cm yz2cm 4096 Feb 24 18:15 a/b/c
# (2)
0000
drwxrwxrwx 3 yz2cm yz2cm 4096 Feb 24 18:15 a
drwxrwxrwx 3 yz2cm yz2cm 4096 Feb 24 18:15 a/b
drwxrwxrwx 2 yz2cm yz2cm 4096 Feb 24 18:15 a/b/c
```

事後に`umask`を元に戻すのもめんどうなので、以下のようにサブシェルで処理させるときれいですかね。真面目にエラー処理も入れると、こうなりました。

```bash:
$ umask
0002    # =>カレントシェルのumaskは0002
$
$ rm -fr ./a
$ if [ ! -d ./a/b/c ]; then
>     (umask 000 && mkdir -p ./a/b/c && [ -d ./a/b/c ]) \
>         || echo '[E] Creating directory filed.'
> fi
$ ls -ld a a/b a/b/c
drwxrwxrwx 3 yz2cm yz2cm 4096 Mar  4 00:49 a
drwxrwxrwx 3 yz2cm yz2cm 4096 Mar  4 00:49 a/b
drwxrwxrwx 2 yz2cm yz2cm 4096 Mar  4 00:49 a/b/c
$
$ umask
0002    # =>カレントシェルのumaskは影響を受けない
```

##returnコマンドを書くことができる場所
C言語の`main()`関数と同じ感覚で、スクリプトファイルのトップレベル（？）に、つい`return`コマンドを書きそうになるんですが、`return`コマンドは以下の場所にのみ書くことができるそうです。

+ 関数内
+ `source`コマンドで実行されるスクリプトファイル

なお`return`は予約語ではなく、組込みコマンドです。

```bash:
$ type return
return is a shell builtin
$ which return
$
```

## bash -c command_string の後につづく引数の扱いは？
`man bash`によると、以下のように、`$0`から始まる位置パラメータに代入されるらしいのですが

```:man　bash
-c    If the -c option is present, then commands are read from the
      first non-option argument command_string. If there are argu‐
      ments after the command_string, they are assigned to the
      positional parameters, starting with $0.
```

`bash -c "./sample.sh" "x"`を実行したところ、`sample.sh`内での`$0`には`./sample.sh`が代入されていたので、マニュアルの言いたいことはよく分かりませんでした…。

･･･で、調べてみたところ、後続の引数が`$0,$1,$2,...`に設定されるのは、`command_string`の中だったようです（その場合、`command_string`は必ず一重引用符で囲む必要があります。二重引用符で囲むと、変数展開された結果を`bash`コマンドに渡してしまうため）。

```bash:
$ bash -c 'echo $0, $1, $2' x y z
x, y, z
$ bash -c "echo $0, $1, $2" x y z
-bash    # => 位置パラメータが展開された後の値が渡された
```

## 「-bash」の先頭のダッシュ「-」って何？
僕の環境だと、ターミナルにログインして、`echo $0`と打つと`-bash`と表示されます。この先頭の`-`って何？という話ですが、`login`が`exec`して`bash`になる時、`$0`の先頭に`-`をつけるのだそうです。

`-bash`の使い道としては「ターミナルからログインして、何度か`bash -i`でシェルをforkした後に`exit`して親に戻りたい時、`exit`しすぎるとターミナルが終了してしまうので、カレントシェルが`login`から`fork`したシェルの時だけ`exit`したい場合…でしょうか。`echo $0`の出力結果の先頭に「-」がついていれば、カレントシェルは`login`からforkしたシェルだと判別できます。

なお、単純に「カレントシェルがログインシェルであるか」を確認するには、`shopt -p login_shell`で確認できます。ログインシェルの場合は`shopt -s login_shell`、そうでない場合は`shopt -u login_shell`が表示されます（`-s`は`shopt`の指定変数が有効、`-u`は無効を意味する）。

```bash:
# ===============================
# SSHでのログイン直後
# ===============================
$ echo $0
-bash
$ shopt -p login_shell
shopt -s login_shell
$ 
# ===============================
# 対話的シェルを起動した場合
# ===============================
$ bash -i
$ echo $0
bash
$ shopt -p login_shell
shopt -u login_shell
$ 
# ===============================
# ログインシェルの場合
# ===============================
$ bash -l
$ echo $0
bash
$ shopt -p login_shell
shopt -s login_shell
```

## $*,$@は、$0の値を含むか
含みません。`man bash`によると、"Expands to the positional parameters, starting from one."だそうです。

## 日の目を見ないオプションたち

ここすごくつまんないので、読み飛ばし推奨です。

+ -s

`bash -i`などでシェルを対話型で起動する時、`-s`オプションの後に文字列を指定すると、それらの文字列が位置パラメータ（`$1,$2,...`）に設定されます。複数の文字列が指定可能です。

```bash:
$ bash -i -s hoge1 hoge2
$ echo $1
hoge1
$ echo $2
hoge2
$
```

+ -D（--dump-strings）

デバッグではありません。ファイル中の「二重引用符で囲まれて`$`が前置された文字列（例：`$"hogehoge"`）」を抽出します。翻訳対象のメッセージをリスト化する時に使うらしいのですが。

```bash:
$ bash -D <<EOT
>     $"hogehoge"
> EOT
"hogehoge"
```

なお、`-D`オプション指定時は、暗黙に`-n`オプションも指定されたことになるため、ファイル内に記述したコマンド等は実行されません。

+ --rcfile file

`~/.bashrc`の代わりに読み込むファイルを指定します。

```bash:
$ bash
~/.bashrc is loaded.
$
$ bash --rcfile ~/.bashrc_dummy
~/.bashrc_dummy is loaded.
```

##プロファイルの読み込み

通常ログインや`bash -l`、`su -`など、ログイン時は`/etc/profile`と`~/.bash_profile`が読み込みされます（以下の実行結果は、各ファイルにデバッグ文を入れて確認したものです）。

```bash:
# =========================
# SSHでのログイン直後
# =========================
/etc/profile is loaded.
~/.bash_profile is loaded.
$
$ bash -l
/etc/profile is loaded.
~/.bash_profile is loaded.
$
$ su - yz2cm
Password: 
/etc/profile is loaded.
~/.bash_profile is loaded.
```

なお、ログイン時に`~/.bash_profile`が見つからない場合、`~/.bash_login`が読み込みされます。それも見つからない場合、`~/.profile`が読み込みされ、その中で`~/.bashrc`が読み込みされます。

```bash:
$ # ~/.bash_profileが存在しない => ~/.bash_loginを読み込む
$ sudo mv ~/.bash_profile ~/.bash_profile_bk
$ bash -l
/etc/profile is loaded.
~/.bash_login is loaded.
$ 
$ # ~/.bash_loginも存在しない ⇒ ~/.profileを読み込む
$ mv ~/.bash_login ~/.bash_login_taihi
$ bash -l
/etc/profile is loaded.
~/.profile is loaded.
~/.bashrc is loaded.
$
```

対話モードでの起動時は、`~/.bashrc`が読み込みされます。

```bash:bash,bash_-i
$ bash
~/.bashrc is loaded.
$ bash -i
~/.bashrc is loaded.
$
$ su yz2cm
Password: 
~/.bashrc is loaded.
$
```

対話モードでの起動時に`--norc`を指定した場合や、ログイン時に`--noprofile`を指定した場合は、いずれも読み込みされません。

```bash:bash_--noXXX
$ bash --norc
$
$ bash --noprofile -l
$
```

なお、ログアウト時は`~/.bash_logout`が読み込みされます。

```bash:
$ bash -l
$ logout
~/.bash_logout is loaded.
```

##以下、個人的なメモ帳。

気にしないで下さい。

+ AIXなど、`ftp`コマンドがPassiveモードのオン/オフを指定するオプションを持たない場合、事前にデフォルトのモードを確認したい。そこで、`ftp`のコマンドインタープリタに`passive`サブコマンドを投げ、その結果を持ってデフォルトのモード（ActiveかPassiveか）を判定する。あとは、ヒアドキュメントの中に`ftp`サブコマンドをつらつら書く。

```bash:PassiveモードでFTP転送
# is_passive (0:Active 1:Passive)
is_passive=$([ "$(printf 'passive\nbye\n' | LC_ALL=C LANG=C ftp | sed 's/[\t ]\+/\t/g' | cut -f3 | cut -b-2 | tr '[a-z]' '[A-Z]')" == 'ON' ]; echo $?)

ftp <<EOT
$([ "${is_passive}" -eq 0 ] && echo "passive")
EOT
```