「新しいシェルプログラミングの教科書」を読んだメモ

一時ファイル名を作る

$$ で現在のプロセスのプロセス ID を取得できる。これは一時ファイルの名前生成に利用できる。

$ tmpfile=/tmp/$$
$ echo $tmpfile
/tmp/157490

文字列の長さを得る

expr を使う。

$ expr length "pewpewpew"
9
$ foo=hello; expr length $foo
5

$変数 と "$変数" の違い

$ cat test.sh 
#!/bin/bash

list=("foo" "bar" "hello world")
for i in ${list[@]}
do
    echo $i
done

echo -----

for i in "${list[@]}"
do
    echo $i
done
$ ./test.sh 
foo
bar
hello
world
-----
foo
bar
hello world

算術式評価

  • (( )) を使う
  • カッコ内の変数の型(以下の例では a )は文字列なことに注意する
$ x=10; y=20; ((a=x+y))
$ echo $a
30
$ a=1+1
$ echo $a
1+1

読み取り専用変数

declare -rreadonly は同じ動作になる。

$ declare -r foo=bar
$ foo=pew
bash: foo: 読み取り専用の変数です
$ readonly bark=bow
$ bark=wanwan
bash: bark: 読み取り専用の変数です

整数型の変数

  • declare -i で宣言する。通常の文字列型と違い、右辺に式が書ける
  • sum=x+y のように式の中で変数を参照する時に $ は不要
$ declare -i x=50
$ declare -i y=10
$ declare -i sum=x+y
$ echo $sum
60

配列

  • ( ) を書いて、中に要素を並べると配列になる
    • 明示的に宣言するには declare -a mylist のようにする
    • list=() のように () だけだと空の配列になる
  • 要素を参照するには ${配列名[インデックス]} する
  • ${#配列名[@]} で配列の要素数を得る
  • ${配列名[@]} で全ての要素を得る
  • 配列名[インデックス]=値 で要素を変更する
  • 要素削除には unset を使う
$ dogs=(koro sophia bunta)
$ echo ${dogs[0]}
koro
$ echo ${dogs}
koro
$ echo ${#dogs[@]}
3
$ dogs[0]=KORO
$ echo ${dogs[0]}
KORO
$ echo ${dogs[@]}
KORO sophia bunta
  • 先頭に要素追加
$ dogs=(louis "${dogs[@]}")
$ echo ${dogs[@]}
louis KORO sophia bunta
  • 末尾に要素追加
$ dogs+=(mona)
$ echo ${dogs[@]}
louis KORO sophia bunta mona
  • ${配列名[@]:開始位置:終了位置} で指定の要素を取り出せる
$ echo ${dogs[@]}
louis KORO sophia bunta mona foo
$ echo ${dogs[@]:1:3}
KORO sophia bunta

連想配列

  • declare -A book のようにする
  • 使い方は基本的に配列と同じ
$ declare -A book
$ book=([author]=taro [title]=fugafuga)
$ echo ${book[title]}
fugafuga
$ echo ${book[@]}
fugafuga taro
$ echo ${#book[@]}
2
$ book[publisher]=hogehoge
$ echo ${book[@]}
hogehoge fugafuga taro

パス名展開

パス名展開はシェルが行っている。一致するファイルが無い場合は、もとの記号がそのまま出力される。

$ ls
foo.cpp  foo.txt
$ echo foo.*
foo.cpp foo.txt
$ echo boo.*
boo.*
$ ls foo.*
foo.cpp  foo.txt
$ ls boo.*
ls: 'boo.*' にアクセスできません: そのようなファイルやディレクトリはありません

文字列の切り出し

${変数名:開始位置:終了位置} で切り出せる。

$ path=/etc/systemd/
$ echo ${path:1:3}
etc

拡張子を得る

  • ${変数名#パターン} では 前方一致 したパターン部分を除いて展開される
  • # だと最短一致、 ## だと最長一致になる。拡張子を得るなら ## がよく使われる
$ path=/tmp/foo.tar.gz
$ echo ${path#*.}
tar.gz
$ echo ${path##*.}
gz

ファイル名を得る

$ echo ${path##*/}
foo.tar.gz

ディレクトリ名を得る

  • ${変数名%パターン} では 後方一致 したパターン部分を除いて展開される
  • % で最短一致させる所がポイント
$ echo ${path%/*}
/tmp

if 文

  • シェルスクリプトには boolean型 が無い。if 文ではコマンド実行結果で条件判定を行う
  • コマンド実行結果が 0 なら真と判定される
  • : は常に 0 を返す、組み込みコマンド
$ cat test.sh
#!/bin/bash

# if の直後にはコマンドが来る
# ; が無いと then もコマンドの引数として認識されてしまう
if ls > /dev/null; then
    echo foo
fi

if [ "foo" = "foo" ]; then
    # noop する時は : を使う
    # : は常に 0 を返す、組み込みコマンド
    :
fi

# 常に真になる if 文
if : ; then
    echo true
fi
$ ./test.sh 
foo
true

test コマンド

  • 文字列や数値の比較、ファイルの存在などを判定するコマンド
  • if 文と組み合わせることが多い
  • [test は基本的に同じ
$ test "foo" = "bar"; echo $?
1
$ [ "foo" = "bar" ]; echo $?
1
$ [ "foo" = "foo" ]; echo $?
0
$ [ "foo" = "foo" -a 100 -ge 10 ]; echo $?
0

標準出力と標準エラー出力をまとめる

&> を使うと便利。

$ ls foo
ls: 'foo' にアクセスできません: そのようなファイルやディレクトリはありません
$ ls foo &> result

ヒアストリング

$ cat <<< hello
hello

サブシェル

() で囲われた処理は、現在のシェルの子プロセスとして新しく起動されたシェルで実行される。 () 内での環境変数などの変更は現在(親)のシェルに影響を与えない。

$ export foo=bar; (foo=pewpewpew); echo $foo
bar
$ pwd; (cd /); pwd
/home/fuga/test
/home/fuga/test

コマンドのグループ化

{} または () で複数のコマンドを1つのコマンドとしてまとめることができる。

$ { ls /dev/pts/; echo hello; } > out
$ cat out 
0
1
ptmx
hello

trap によるシグナルの補足

$ cat test.sh 
#!/bin/bash

handler()
{
    echo "catch INT"
    # exit しないとシェルスクリプトが最後まで実行される
    exit
}

trap handler INT

echo start
sleep 10
echo end
$ ./test.sh 
start
^Ccatch INT

grep コマンド

  • -v で検索パターンにマッチしなかった行が出力される
$ cat << EOF | grep -v -E "^a"
> aaa
> abc
> zzz
> bbb
> baa
> EOF
zzz
bbb
baa
  • -o で検索パターンにマッチした部分だけを出力する
$ cat << EOF | grep -o -E "foo|bar"
> aaafooaaa
> bbbddd
> aaa
> bbbbarbbb
> EOF
foo
bar

env コマンド

環境変数を一時的に変更してコマンドを実行する。bash の場合は 変数名=値 コマンド だけでも同じ動作になる。

$ cat main.go 
package main

import (
   "fmt"
   "os"
)

func main() {
    fmt.Println(os.Getenv("foo"))
}
$ foo=hello
$ env foo=bar go run main.go 
bar
$ foo=bar go run main.go 
bar
$ echo $foo
hello

Bats でのテスト

Bats のインストール。

$ sudo apt install bats
$ bats --version
Bats 1.2.0-dev

テストを記述するファイルの拡張子は bats にする。

$ cat mul.sh 
#!/bin/bash
echo $(($1 * $2))
$ cat mul.bats 
PATH="${BATS_TEST_DIRNAME}:$PATH"

@test 'mul test' {
  run mul.sh 2 3
  [[ $status -eq 0 ]]
  [[ $output == 6 ]]
}
$ bats mul.bats 
 ✓ mul test

1 test, 0 failures