> For the complete documentation index, see [llms.txt](https://hayashier.gitbook.io/article/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hayashier.gitbook.io/article/system-admin/shellscript-overview.md).

# Shell Script 入門

## Shell Script 入門

### Basic

#### 制御構文

* if

```
i=10
if [ $i -le 5 ]; then
    i=`expr $i + 5`
elif [ $i -le 10 ]; then
    i=`expr $i + 10`
else
    i=`expr $i + 1`
fi
```

* for

```
max=10
for ((i=0; i < $max; i++)); do
    echo $i
done
```

```
to_val=`expr 30 - 10 + 1`
for i in `yes "" | cat -n | head -30 | tail -$to_val`; do
    :
done
```

* while

```
i=10
while [ $i -le 30 ]; do
    i=`expr $i + 5`
done
```

```
ls file* |
while read f ; do
   cp $f $f.backup
done
```

read varのようにすると入力を受付け、変数varに格納される。

* case

/etc/rc0.d/S01halt

```
case "$0" in
   *halt)
        message=$"Halting system..."
        command="/sbin/halt"
        ;;  
   *reboot)
        message=$"Please stand by while rebooting the system..."
        command="/sbin/reboot"
        kexec_command="/sbin/kexec"
        ;;  
   *)  
        echo $"$0: call me as 'halt' or 'reboot' please!"
        exit 1
        ;;  
esac
```

条件判定部分は正規表現が使える。

* until

```
i=10
until [ $i -le 5 ]; do
    i=`expr $i - 1`
    echo $i
done
```

### 特殊変数

* `$#`
  * コマンドラインの引数
* `$1` 〜 `$9`, `$0`
  * 引数それぞれ。 Positional Parameters という。
* `$*`
  * `$0` 以外のコマンドライン引数
* `$@`
  * `$*` と類似。ただし `$@` とした時、位置パラメータを評価せずにコマンドに渡すことが出来る
* `$?`
  * シェルが最後に実行したコマンドの終了状態を保持している。ほとんどのコマンドは成功時には0を返す
* `$$`
  * 現在のシェルのプロセス番号を保持している
* `$-`
  * シェルにセットされているオプションを保持している
* `$!`
  * バックグラウンドで実行された直前のプロセスのプロセス番号を保持しています
* Reference
* Title: Shell 特殊変数
  * URL: <https://qiita.com/a\\_yasui/items/ec4f75b300410af8958d>

### 文字列

#### 文字列操作

* 文字列の一部抜粋
  * 左から : `cut -c -7`
  * 右から : `cut -c ``expr ${#var} + 1 - 7``-`
  * 左右から : `cut -c 9-13`
* 大文字・小文字変換
  * `tr "a-z" "A-Z"`
* 正規表現でマッチした文字列の取り出し
  * AWK : matchstr=`echo "STRING" | awk '{match($0, /PATTERN/); print substr($0, RSTART, RLENGTH)}'`
  * SED : matchstr=`echo "STRING" | sed 's/.*\(PATTERN\).*/\1/'`
  * GREP: matchstr=`echo "STRING" | grep -o 'PATTERN'`
* 特殊文字のトリミング
  * 左側のトリミング後、右側のトリミング
  * タブやスペースを取り除きたい場合は、trimming\_char=`printf " \t"`

    ```
    string="---abc-defgh----"
    trimming_chr="-"

    while [ "_$string" != "_${string#[$trimming_chr]}" ]; do
        string="${string#[$trimming_chr]}"
    done
    while [ "_$string" != "_${string#[$trimming_chr]}" ]; do
        string="${string%[$trimming_chr]}"
    done
    ```
* ファイル名・ディレクトリ名の取得
  * ファイル名取得
    * `basename`
    * `filename="${filepath##*/}"` # 左側からの最大マッチング
  * ディレクトリ名取得
    * `dirname`
    * `dirpath="${filepath%/*}"` # 右からの最小マッチング

#### 文字列抽出

* `${var:-word}` : `$var`が未定義か空文字の場合は文字列wordが読み出される
* `${var-word}` : `$var`が未定義の場合は文字列wordが読み出される
* `${var:=word}` : `$var`が未定義か空文字の場合は文字列wordが読み出され、かつ`$var`にも代入
* `${var=word}` : `$var`が未定義の場合は文字列wordが読み出され、かつ`$var`にも代入
* `${var:?word}` : `$var`が未定義か空文字の場合は文字列wordが読み出され、かつエラーの扱い
* `${var?word}` : `$var`が未定義の場合は文字列wordが読み出され、かつエラーの扱い
* `${var:+word}` : `$var`が未定義でも空文字でもなければ、文字列wordが読み出される
* `${var+word}` : `$var`が未定義でなければ、文字列wordが読み出される
* `$#{var}` : `$var`の文字数
* `${var#PATTERN}` : 左端からPATTERNについて最小マッチングで切り落とされて読み出される
* `${var##PATTERN}` : 左端からPATTERNについて最大マッチングで切り落とされて読み出される
* `${var%PATTERN}` : 右端からPATTERNについて最小マッチングで切り落とされて読み出される
* `${var%%PATTERN}` : 右端からPATTERNについて最大マッチングで切り落とされて読み出される
* `${@:2}` : 二つ目以降の引数を取得
* Reference
* Title: Shell 特殊変数
  * URL: <https://qiita.com/a\\_yasui/items/ec4f75b300410af8958d>

#### 文字クラス

一部の環境では動作せず、使わないほうが無難

* `[[:alnum:]]` : 英数字。`[0-9A-Za-z]`
* `[[:alpha:]]` : 英字。`[A-Za-z]`
* `[[:digit:]]` : 数字。`[0-9]`
* `[[:lower:]]` : 英字の小文字
* `[[:upper:]]` : 英字の大文字。`[A-Z]`
* `[[:blank:]]` : スペースとタブ
* `[[:punct:]]` : 記号 `! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ `` { | } ~`
* `[[:xdigit:]]` : 16進数。`[0-9A-Fa-f]`

(参考) [正規表現](https://bash.open-code.club/%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE/index.html)

#### 環境変数

**IFS**

* IFS（Internal Filed Separator）
  * 設定された値が、文字の区切りとして認識される
  * ファイル等を読み込んだりする場合に利用
    * CSVファイルの場合に`,`を設定

### メタ文字セット

#### BRE(基本正規表現)

* 置換後文字列用
  * &#x20;: n番目の`\(\)`で囲まれた範囲にマッチした文字列
  * `&` : マッチした文字列全体
  * `\x` : メタ文字`&`またはsed等で正規表現の始まりを住めすために用いた文字自身を指定したい場合
  * `\\` : `\`を指定する場合

(参照) どのUNIXコマンドでも使える正規表現

#### ERE(拡張正規表現)

* BREのときと比較して`\`をつけないなど、他多数

#### Others

ファイルの新規作成と追記

```
$ echo テスト > file
$ echo テスト >> file
```

ヒアドキュメント

```
$ cat << FIN > file
abc
def
FIN
```

ヒアストリング

```
$ A=テスト
$ sed 's/テ/ア/' <<< $A

# 以下の処理と同等
$ echo $A | sed 's/テ/ア/'
```

終了ステータス

```
$?
```

0で正常終了。正常ではないときは0以外の数字が終了ステータス。\
テストコマンドでは正なら0、偽なら1、変な引数が指定されたら2のようにそれ以外の値が返される。

パイプで処理すると途中の処理のステータスが配列として格納される。bashで配列を扱うことができる機能

```
${PIPESTATUS[@]}
```

あるファイルがなければシェルスクリプトを終了するという処理の場合、以下のようにして書くことができる。

```
[ -f "/etc/passwd" ] || exit 1
```

### 引数

* `コマンド名 -- -引数`のように`--`を指定することで、これ以降の`-引数`はオプションではなく、引数として解釈する
  * 実行例

    ```
    case "${arg}" in
    "--help") set -- "$@" "-h" ;;
    "--version") set -- "$@" "-V" ;;
    "--"*)
        echo "Failure: ${arg}"
        ;;
    *)
        set -- "$@" "${arg}"
        ;;
    esac
    ```
* [引数を処理する](https://shellscript.sunone.me/parameter.html)

### AWKとsed

* [awk入門 コマンドの使い方とスクリプトの書き方](http://itref.fc2web.com/unix/awk.html)
* [sed - ストリームエディタ](http://itref.fc2web.com/unix/command/sed.html)

#### AWK

* 関数
  * printf
  * sprintf
  * sub
  * gsub
  * gensub
  * length(t)
  * split(s,a,fs)
  * substr(s,p,n)
  * index(s,t)
  * match(s,r)
  * tolower(s)
  * toupper(s)
* 変数
  * ARGC
  * ARGV
  * ENVIRON
  * FILENAME
  * FNR
  * FS
  * NR
  * OFS
  * ORS
  * RS

```
$ seq 1 10 | xargs -n 5 > data
cat data
1 2 3 4 5
6 7 8 9 10
```

```
$ cat data | awk '{print $2,$4}'
2 4
7 9
```

```
$ cat data | awk '{a=3;print $(1+a)}'
4
9
```

`NF`は各行の列数を表す予約語

```
$ cat data | awk '{print $(NF-1)}'
4
9
```

```
$ cat data | awk '$4>6'
6 7 8 9 10
$ cat data | awk '$5==5'
1 2 3 4 5
$ cat data | awk '$5=="5"'
1 2 3 4 5
```

```
$ echo {a..g} | xargs -n 1
a
b
c
d
e
f
g
```

```
$ echo {a..g} | xargs -n 1 | awk 'NR>=4{print $1,$1,$1}'
d d d
e e e
f f f
g g g
$ echo {a..g} | xargs -n 1 | awk 'NR>=4{print $1,$1,$1}NR<=4{print $1,$1}'
a a
b b
c c
d d d
d d
e e e
f f f
g g g
```

標準入力より前処理、後処理

```
$ seq 1 5 | awk 'BEGIN{a=100000}{a+=1}END{print a}'
100005
$ echo {1..5} | awk 'BEGIN{a=100000}{for(i=1;i<=NF;i++){a+=$i}}END{print a}'
100015
```

文字列を表示

```
$ seq 1 3 | awk '{printf("%d円\n",$1)}'
1円
2円
3円
```

フォーマットを指定して表示。標準出力はしないため、一旦変数に文字列を書き出して出力

```
$ echo {a..z} | awk '{$9=sprintf("%s%s",$9,$9);print}'
a b c d e f g h ii j k l m n o p q r s t u v w x y z
```

```
# Once per one line
$ echo abcdfabcde | awk '{sub(/cd/,"xy",$0);print}'
abxyfabcde

# Many times per one line
$ echo abcdfabcde | awk '{gsub(/cd/,"xy",$0);print}'
abxyfabxye

# Once per line, return converted strings
$ echo abcdfabcde | awk '{$0 = gensub(/cd/,"xy",$0);print}'
abxyfabcde
```

3行目のみ表示

```
$ echo {a..e} | xargs -n 1 | awk "NR==3{print \$0}"
c
```

3行目から4行目まで表示

```
$ echo {a..e} | xargs -n 1 | awk "NR==3,NR==4{print \$0}"
c
d
```

cの行からdの行まで表示

```
$ echo {a..e} | xargs -n 1 | awk "/c/,/d/{print \$0}"
c
d
```

CSVファイルの第3フィールドの合計値を求める場合

```
$ total_size=`awk -F "," "{T=T+\\$3} END {print T}" "$filename"`
```

`-F,`のように指定すると文字列で区切る

* (参照)
  * [【 awk 】コマンド（応用編その4）――テキストの加工とパターン処理、split関数の活用](https://www.atmarkit.co.jp/ait/articles/1805/25/news035.html)
  * [初心者用 awk 講座](http://chianti.ucsd.edu/~rsaito/ENTRY1/WEB_RS3/PDF/JPN/Texts/AWK1.pdf)

#### sed

3行目のみ置換

```
$ echo {a..e} | xargs -n 1 | sed '3s/./???/'
a
b
???
d
e
```

2行目から最終行まで置換

```
$ echo {a..e} | xargs -n 1 | sed '2,$s/./???/'
a
???
???
???
???
```

bの行からdの行まで置換

```
$ echo {a..e} | xargs -n 1 | sed '/b/,/d/s/./???/'
a
???
???
???
e
```

指定した範囲(3行目から4行目を表示)。sedとしては入力された行をそのまま出力するのが基本動作なので-nを指定することで指定範囲の行が2行ずつ表示されることを抑えている

```
$ echo {a..e} | xargs -n 1 | sed -n '3,4p'        
c
d
```

-nを指定しない場合

```
$ echo {a..e} | xargs -n 1 | sed '3,4p'   
a
b
c
c
d
d
e
```

* xargsは標準入力から読み込んだ文字列をしていしたコマンドに引数として渡すコマンド
  * `-I@`のようにオプションで文字を指定すると、別途その文字を指定した位置に引数として渡す
  * `-n 1`のようにいくつの引数を渡すかを指定
  * `-P 5`のように何並列でプロセスを立ち上げるかを指定。`0`を指定するとできるだけプロセスを使うように指定

### jq

[jq Manual (development version)](https://stedolan.github.io/jq/manual/)

* `-r`でダブルクオテーションを削除
* `[]`や`{}`で囲むと配列形式やオブジェクト形式で出力可能
* `| @csv`のように渡すとCSV形式で出力可能
* `| {InstanceId, Tags:(.Tags|from_entries)} | select(.Tags.Name | contains("test"))'`のようにすることでタグ指定で対象の項目を取得。`Tags:`の部分は出力結果の表示名。from\_entriesで`"Key": "auto-stop"`、`"Value": "yes"`のような形式を`"auto-stop": "yes"`にまとめる
* `| length`で長さ取得
* `| unique[]`で配列に対して、重複排除。最後に`[]`をつけない場合、配列で結果表示
* `| fromjson`で文字列化されたJSONを復元
* `jq '.events[] | .timestamp/1000 | todate'`のように秒単位にした上で`| todate`を使用すると時間の表示形式を整形
* `jq '.Functions[] | {FunctionName, VpcId:(.VpcConfig.VpcId//"NoVPC")}'`のように`//`を指定すると左側がNULLのときは右側の要素に置き換える。
* `jq -r '.Reservations[] | .Instances[] | [.InstanceId, (.Tags | if (.==null) then null else ([.[] | "\(.Key)=\(.Value)"] | join(",")) end)] | @csv'`のように`if () then ~ else ~ end`を仕様できる。また、join()で指定文字列で文字列の配列を1つの文字列に結合

```
$ aws ec2 describe-instances | jq '.Reservations[] | .Instances[] | .InstanceId'
"i-0a6b646d73a79370a"
"i-096d0b925a6d835d5"
"i-07ee081ad9075e2f7"
"i-045b56c08c46b11bd"
"i-043890efa180c22a2"
"i-0048d288f55da347d"
"i-0d79cc953246cb398"
"i-0e130f4914a5938a0"
"i-079f1057910324a39"
```

### デバッグ

* デバッグ
  * 未定義変数参照時にエラー
    * スクリプトの冒頭に`#!/bin/sh -u`のように`-u`オプションを付与
    * `set -u`を宣言
* トレース
  * スクリプトの冒頭に`#!/bin/sh -x`のように`-x`オプションを付与
* デバッグメッセージの分離
  * FIFOパイプ
    * `mkfifo /tmp/pipeしたらecho "test" > /tmp/pipe`のように書き出す
    * 別端末から`while [ 1 ];do cat /tmp/pipe;done`
* パイプの中身
  * 途中で`tee /dev/stderr`にリダイレクトすることやFIFOパイプを利用

### Others

#### grepでタブ記号が入っている場合の検索

タブ記号をスペースに置き換える。`[:space:]`は文字クラスを表し、タブや半角スペースなどの空白文字を指す

```
$ cat /etc/services | grep http | tr '\t' ' ' | grep ' 80/'
http             80/udp     www www-http # World Wide Web HTTP
http             80/tcp     www www-http # World Wide Web HTTP

$  cat /etc/services | grep http | grep '[[:space:]]80/' 
http             80/udp     www www-http # World Wide Web HTTP
http             80/tcp     www www-http # World Wide Web HTTP
```

grepはデフォルトで基本正規表現。-Eオプションで拡張正規表現が利用でき、`{5}`で5回繰り返すといった表現が可能になる。

* wcコマンド
  * -m : ロケールを考慮した文字数カウント
  * -l : 行数カウント

`sed 's/./&\n/g'`で文字ごとに改行できるので、行数を数えることでも計算可能。`grep -o .`で任意の一文字を検索して一行ごとに出力。

```
$ echo 123456789 | sed 's/./&\n/g'
1
2
3
4
5
6
7
8
9
$ echo 123456789 | sed 's/./&\n/g' | wc -l
10
```

* `unique -c` : カウント

`<()は`プロセス置換でbashの機能

```
$ diff <( cd /var/log/test1 ; find ) <( cd /var/log/test2 ; find )
```

#### HTMLの編集

```
$ cat test.html
<!DOCTYPE html>
<!-- saved from url=(0022)https://hayashier.com/ -->
<html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
		<!-- Global site tag (gtag.js) - Google Analytics -->
		<script src="./test_files/osd.js"></script><script type="text/javascript" async="" src="./test_files/analytics.js"></script><script src="./test_files/f.txt" id="google_shimpl"></script><script async="" src="./test_files/js"></script>
		<script>
		  window.dataLayer = window.dataLayer || [];
		  function gtag(){dataLayer.push(arguments);}
		  gtag('js', new Date());
:
```

コメントアウト削除

```
$ cat test.html | sed 's/<!--/\n&\n/g' | sed 's/-->/\n&\n/g' | sed '/<!--/,/-->/d'
<!DOCTYPE html>
		<script src="./test_files/osd.js"></script><script type="text/javascript" async="" src="./test_files/analytics.js"></script><script src="./test_files/f.txt" id="google_shimpl"></script><script async="" src="./test_files/js"></script>
		<script>
		  window.dataLayer = window.dataLayer || [];
		  function gtag(){dataLayer.push(arguments);}
		  gtag('js', new Date());
```

タグ削除。以下、実体参照が正しく使用されている場合

```
$ cat test.html | sed 's/<!--/\n&\n/g' | sed 's/-->/\n&\n/g' | sed '/<!--/,/-->/d' | sed 's;<[^<]*>;;g' | awk 'NF!=0'
		  window.dataLayer = window.dataLayer || [];
		  function gtag(){dataLayer.push(arguments);}
		  gtag('js', new Date());

```

#### ファイルの同期

```
$ rsync -av ~/ --exclude='.Trash' 192.168.5.1:~/home/hayashier/
```

#### sedでまとめて置換

sedで置換するルールを予めファイルに列挙しておいてまとめて置換することができる

```
$ cat /var/log/apach2/access.log* | ./month.sed | tail -n 1
```

```
#!/bin/sed -f

s/Jan/01/
s/Feb/02/
s/Mar/03/
s/Apr/04/
s/Mar/05/
s/Jun/06/
s/Jul/07/
s/Aug/08/
s/Sep/09/
s/Oct/10/
s/Nov/11/
s/Dec/12/
```

#### sedで部分一致箇所の抜粋

`()`で囲っておくと、`\1`のようにして取り出せる

```
$ echo 'A B C [D] "E" F G " "H" "I"' | sed 's/^\(.*\) \(.*\) \(.*\) \[\(.*\)\] "\(.*\)" \(.*\) \(.*\) "\(.*\)" "\(.*\)"$/\1|\2|\3|\4|\5|\6|\7|\8|\9/'
A|B|C|D|E|F G|"|H|I
```

#### Apacheのアクセスログの分析例

* 改行やスペースを除く、文字でないバイナリを除去する
* データ内の区切り文字以外の`"`や区切り文字を含んでいてっもフィールドを区切れるようにバックスラッシュは`\\`と記録されているので、これを`%5C`にエンコーディング
* `"`は`\"`と記録されているので、これを`%22`にエンコーディング

```
#!/bin/bash -xv

B='\(.*\)'
D='"\(.*\)"'
P='\[\(.*\)\]'
STR='\1\x0\2\x0\3\x0\4\x0\5\x0\6\x0\7\x0\8\x0\9\x0'

sed 's;\\\\;%5C;g' < /dev/stdin             |
sed 's;\\";%22;g'                           |
sed "s/^$B $B $B $P $D $B $B $D $D\$/$STR/" |
sed 's/_/\\_/g'                             |
sed 's/ /_/g'                               |
sed 's/\x0\x0/\x0_\x0/g'                    |
sed 's/\x0\x0/\x0_\x0/g'                    |
tr '\000' ' '                               |
sed 's/ $//'
```

#### IPアドレスのソート

```
$ cat ip
192.168.1.2
10.245.0.1
10.95.20.1
40.1.212.4
203.113.10.1
```

```
$ sort -t . -k1,1n -k2,2n -k3,3n -k4,4n ip
10.95.20.1
10.245.0.1
40.1.212.4
192.168.1.2
203.113.10.1
```

以下の書籍等を参考にさせていただき、自分用の備忘録にまとめました。

* [覚えて便利 いますぐ使える! シェルスクリプトシンプルレシピ54](https://www.usp-lab.com/book.recipe.html)
* [コンパチブル・シェルスクリプティング（第5版）](http://richlab.org/coterie/csp.html)
* [シェルプログラミング実用テクニック](https://gihyo.jp/book/2015/978-4-7741-7344-3)
* [GihyoShellBookSamples](https://github.com/ryuichiueda/GihyoShellBookSamples)
* [Bash Reference Manual](https://www.gnu.org/software/bash/manual/bash.html)
* [とほほのBash入門](https://www.tohoho-web.com/ex/shell.html) !

### Others

* setコマンドの使い方
  * [【 set 】コマンド――シェルの設定を確認、変更する](https://atmarkit.itmedia.co.jp/ait/articles/1805/10/news023.html)
