bashで忘れがちな機能とかいろいろの備忘録

![Bash-Final.jpg](https://qiita-image-store.s3.amazonaws.com/0/258841/efb5999f-66ac-ed91-08d6-fc5efe4b8f25.jpeg)

## 背景

シェルスクリプトを書くことが結構あるのでその備忘録代わりに記事を書いてみました。

## 特殊変数とは

特殊変数とは
シェルによって自動的に値が設定される特殊な変数がいくつかあり、それら特殊な変数を参照することにより、様々な情報を取得することができる。

| 変数名 | 説明 |
|---|---|
| $$ | 実行シェルのプロセス番号(自分のPID)を格納する変数。 |
| $? | コマンドの戻り値|
| $! | 最後に呼び出されたバックグラウンドプロセスのプロセス番号を格納する変数。 |
| $- | setで設定されたオプション |
| $1 | シェルスクリプト実行時に指定された引数を格納する変数。 |
| $# | 引数の個数 |
| $0 | シェルスクリプト自身のファイル名フルパス |
| $n | シェルスクリプトに与えられた引数を格納する変数。nは1,2,3…と指定できる。 |
| $@ | 実行時のコマンド名 |
| $* | シェルスクリプト実行時に指定された全引数が設定される変数を格納する変数。<br /> 出力に関してはデリミタを格納する環境変数 IFS の影響を受けない。 |
| ${@:X:Y} | 複数のパラメータのうち、X番目以降のY個のデータを取得するための変数を格納する変数。 |
| !$ | 最後に実行されたコマンドに指定していた引数を格納する変数。 |
| $PIPESTATUS | パイプで連結した各コマンドの終了ステータスが設定される変数。 |
| $LINENO | この変数を使用している行の行番号が設定される変数。  |

## 基本機能

bashの基本機能で使いやすい機能をご紹介

#### 文法チェックをしたい : noexec

「-n」をつけて実行することでスクリプト内のコマンドは実行されずに文法のみをチェックしてくれる。

``` bash 
# 文法エラーありパターン
$ bash -n test.sh 
test.sh: 行 5: 予期しないトークン `done' 周辺に構文エラーがあります
test.sh: 行 5: `done'

# 文法エラーなしパターン
$ bash -n test.sh
$ 
```

#### 実行内容のトレース : xtrace

「-x」をつけることでスクリプトの実行内容を出力できる
どのコマンドまで処理が行われたかどういった処理でエラーとなったのかを検出するときに使えます。

``` bash
bash -x test.sh 
+ LOOP_COUNT=3
++ seq 0 3
+ for i in `seq 0 ${LOOP_COUNT}`
+ ls -la
++ date
+ echo

+ for i in `seq 0 ${LOOP_COUNT}`
+ ls -la
++ date
+ echo

+ for i in `seq 0 ${LOOP_COUNT}`
+ ls -la
++ date
+ echo

+ for i in `seq 0 ${LOOP_COUNT}`
+ ls -la
++ date
+ echo
```

#### 未定義変数を検出 : nounset

「-u」を指定することで未定義の変数を検出してくれます。未定義変数を許可したい場合には使えないので注意が必要。
都度ifで判定とかが回避策になるのでしょうか？

``` bash
$ bash -u test.sh 
test.sh: 行 5: LOOP_COUNT: 未割り当ての変数です
```

#### 「*」等によるパス名展開の無効化 : noglob

「-f」を指定するとパス名展開を無効化できる。

``` bash
$ bash -f test.sh 
ls: '/*' にアクセスできません: そのようなファイルやディレクトリはありません
```

#### 終了ステータスが0以外のものが検出した時点でスクリプトを終了

「-e」でコマンドの終了ステータスが「0」以外のときに後続のスクリプトを実行せずに終了します。

``` bash
bash -e test.sh 
ls: 'aa' にアクセスできません: そのようなファイルやディレクトリはありませ
```

#### trapコマンドでシグナルをハンドル

シグナルハンドラも設定できます。
下記を例にします。

なんかしらの処理中にその処理を停止したい時があるとします。
その際に既に生成されているtmpファイルがCtrl + Cで止めるだけだと残っていまい自分で消す必要があります。
trapコマンドを使用すればCtrl + Cで止めたあとの処理を記述することでその処理が行われます。

``` bash
$ cat test.sh 
#/bin/bash

trap 'rm -f *.tmp' 1 2

touch test1.tmp
touch test2.tmp

# なんかしらの処理
sleep 30

# その後にtmpを削除
rm -f *.tmp
```

ちなみにtrapコマンドで指定できるシグナル一覧は「trap -l」で確認できます。

``` bash
$ trap -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

# ちなみにtrapコマンドの書式は下記
trap 'コマンド' [シグナル番号|シグナル名]
```

#### パイプで渡したコマンドの終了ステータスを見る

変数PIPESTATUSを確認すれば各コマンドの終了ステータスが確認できます。
以下はgrepコマンドが検索に失敗しているので終了ステータスが1となっています。

``` bash
$ ls | grep "a" | wc ; echo ${PIPESTATUS[@]}
      0       0       0
0 1 0 # 終了ステータスを表示

```

#### デフォルトのシェルを変えたい

使用できるシェルの種類を調べるには、/etc/shells を参照するか、もしくは、chsh -l コマンドを実行します。
シェルを変更するには、chsh コマンドを使用します。

``` bash
$ cat /etc/shells 
# /etc/shells: valid login shells
/bin/sh
/bin/bash
/bin/rbash
/bin/dash
```

## 組み込みコマンド

組み込みコマンドは「help」と打てば見れます
その中でもよく使うものだけピックアップ

``` bash
$ help
GNU bash, バージョン 4.4.19(1)-release (x86_64-pc-linux-gnu)
これらのシェルコマンドは内部で定義されています。`help' と入力して一覧を参照してください。
`help 名前' と入力すると `名前' という関数のより詳しい説明が得られます。
'info bash' を使用するとシェル全般のより詳しい説明が得られます。
`man -k' または info を使用すると一覧にないコマンドのより詳しい説明が得られます。

名前の後にアスタリスク (*) がある場合はそのコマンドが無効になっていることを意味します。

 job_spec [&]                                                     history [-c] [-d offset] [n] または history -anrw [filename]>
 (( expression ))                                                 if COMMANDS; then COMMANDS; [ elif COMMANDS; then COMMANDS; ]>
 . filename [arguments]                                           jobs [-lnprs] [jobspec ...] または jobs -x command [args]
 :                                                                kill [-s sigspec | -n 
(省略)
```

#### 子プロセスの終了を待つ:wait

シェルから起動しているプログラムが終了するのを待つ。PIDを指定すればそのプロセスが終了するのを待ち、引数を指定しなかった場合はバックグラウンドで起動したすべてのプロセスの終了を待ち、終了ステータスとして0を返す。

``` bash
#!/bin/bash
command1 &
pid1=$!           # command1のプロセスIDを変数pid1に格納
command2 &
pid2=$!           # command2のプロセスIDを変数pid2に格納
command3 &
wait $pid1 $pid2  # command1、command2の終了を待つ
command4
```

#### 代入不可の変数:readonly

変数の属性を読み込み専用に変更する

#### 変数のスコープを指定:local

シェル関数内において、そのシェル関数とその子関数内でのみ有効な変数を生成する。
変数のスコープ（有効範囲）が関数内であることを除けば、コマンドの意味や書式はdeclareと同じ。

#### オプション解析をしたい:getopts

``` bash
while getopts a:h OPT
do
    case $OPT in
        a) SOMEVAL=$OPTARG
            ;;
        h) help
            ;;
        *) help
            ;;
    esac
done

shift $(( $OPTIND - 1 ))
TARGET=$1
```

#### ファイルの種別判定

``` bash
$ alias aliasname='echo foo'
$ type aliasname
aliasname は `echo foo' のエイリアスです
$ function functionname { echo foo; }
$ type functionname
```

#### ヒアドキュメントで行頭のタブを無視

ヒアドキュメントを書くときにインデントでタブを入れるが表示には不要なときに使える
「<<」ではなく「<<-」にするだけで使える。
スペースは取ってくれないので注意

``` bash
$ cat test.sh 
#!/bin/bash

usage()
{
	cat <<- _EOF
	aaa
	bbb
 c # これはスペースなのでだめ
	_EOF

	exit 1
}

usage

$ bash test.sh 
aaa
bbb
 c
```

#### 余談：組み込みコマンドって何？

組み込みコマンドとはbash自体に実装されているコマンドであり、bashの振る舞いを変更したり、制御構文として機能したりするコマンドが用意されている。
組み込みコマンドと外部コマンドどちらにも存在するコマンドとして有名なのがechoです。基本的にシェルスクリプト内でechoを実行すると組み込みコマンドが呼ばれる仕組みとなっています。これは高速化が目的です。
以下のように外部コマンドと組み込みコマンドの差を見てみました。結果は一目瞭然。組み込みコマンドの方が高速ですね。

``` bash
# 組み込みコマンド
$ time (for ((i=0;i<10000;i++)) ; do echo "aaa bbb ccc" >/dev/null; done)
real	0m0.095s
user	0m0.054s
sys	0m0.039s

# 外部コマンド
$ time (for ((i=0;i<10000;i++)) ; do /bin/echo "aaa bbb ccc" >/dev/null; done)
real	0m13.287s
user	0m7.575s
sys	0m2.242s
```

## まとめ

意外と忘れがちなことを列挙してみた。
便利な機能はまだまだたくさんあると思うのでぜひとも教えてください

## 参考リンク

[検索ではあんまり出ないbashの便利技](https://qiita.com/rsooo/items/ef1d036bcc7282a66d7d)
[シェルスクリプト (Bash) では組み込みコマンド set を活用しましょう](https://laboradian.com/use-set-in-bash-shell-scripts/)
[bash - 組み込みコマンド](http://d.hatena.ne.jp/anmino/20090805/1249502378)