主夫ときどきプログラマ

データベース、Webエンジニアリング、コミュニティ、etc

Bash初心者から初級者へのステップアップするためのTips10選

CLIでいろいろとコマンドは使っているんだけど、bashスクリプトが書けるかというと・・・。 という人がbashの文法や機能を知って初級者へとレベルアップするためのTipsを紹介します。

1. ${変数}

シェルで変数を保持することができます。変数には文字列や数値を代入することができ、値を参照する場合は変数名の前に$を付けます。変数値は $VAL でも ${VAL} でも同じように参照出来ますが ${} の方が可読性も上がるのでおすすめです。

$ A=10
$ echo ${A}
10

変数に代入する時 = の前後にスペースを入れてはいけません。変数がコマンドとして解釈されてしまいます。

$ B = 10
bash: B: command not found

bashのバージョン4では ${VAL^} で単語の先頭を大文字に、 ${VAL^^} で単語全体を大文字に変換してくれます。

$ A='hello world'
$ echo ${A}
hello world
$ echo ${A^}
Hello world
$ echo ${A^^}
HELLO WORLD

変数の値は基本的にテキストとして扱われますので一般的なプログラミング言語のように四則演算はできません。

$ A=10
$ B=15
$ echo ${A} + ${B}
10 + 15

この様な場合は後述する算術式展開 $(( 計算式 )) を使います。

2. while read n; do [command] $n; done

パイプから受け取った標準入力を1つずつ変数に保持して引数として別のコマンドに渡したい。そんな時によく使う方法が while read ... のイディオムです。 X.txt Y.txt をそれぞれ X.txt.bak Y.txt.bak との差分をみたい場合、ワンライナー(1行スクリプト)で書くとこのようになります。

$ ls *.txt | while read f ; do diff ${f} ${f}.bak; done

# 通常の書き方
$ ls *.txt |
while read f
do
  diff ${f} ${f}.bak
done

ls の出力を1行ずつ読み込み変数 f に保持し diff コマンドの引数として与えています。 羊を100匹数える場合はこのようになります。

$ seq 100 | while read n; do sleep 1 ; echo "羊が${n}"; done

3. $(( 計算式 )) 算術式展開

$(( 計算式 )) は算術式展開と呼ばれるbashの機能でいろいろな計算ができます。四則演算はもちろんのこと論理積論理和などのビット演算もできます。

# 四則演算
$ A=10
$ B=15
$ echo $(( ${A} + ${B} ))
25
$ echo $(( ${A} * ${B} ))
150

# べき乗
$ echo $(( ${A} ** ${B} ))
1000000000000000
$ echo $(( 2 ** 16 ))
65536

# modulo
$ M=$(( ${A} % ${B} ))
$ echo ${M}
10
$ echo $(( 13 % 8 ))
5

# 論理積
$ echo $(( 15 & 4 )) # 1111 と 0100 の論理積
4

# 論理和
$ echo $(( 10 | 5 )) # 1010 と 0101 の論理和
15

# 排他的論理和
$ echo $(( 13 ^ 11 )) # 1101 と 1011 の排他的論理和
6

4. || && 終了ステータス判定

コマンドはその動作が成功すると「終了ステータス」として0を返します。失敗すると0以外の数字が返ります。直前のコマンドの終了のステータスは $? 変数に保持されます。

# wget コマンドの例
$ wget 'http://google.co.jp' 2> /dev/null
$ echo ${?}
0
$ wget 'http://foo.bar.baz.co.jp' 2> /dev/null
$ echo ${?}
4

0は成功のステータス、 4は失敗のステータスで Network failure. の意味です。 2> /dev/null標準エラー出力を捨てて表示されないようにしています。

&&|| はこの終了ステータスを判定するbash演算子です。&& で成功した時の処理、|| で失敗した時の処理を書きます。

$ wget 'http://google.co.jp' 2> /dev/null && echo 'success!' || echo 'failure...'
success!
$ wget 'http://foo.bar.baz.co.jp' 2> /dev/null && echo 'success!' || echo 'failure...'
failure...

gerp は何かがパターンマッチすれば0を、何もマッチしなければ1を返します。そこで || 演算子を使うと処理を分岐することができます。

$ ls -aF | grep 'git' || echo 'no git file or dir'
no git file or dir


$  ls -aF | grep 'git' || echo 'no git file or dir'
.git/
.gitignore

5. -- オプションの打ち止め

-Rf~ というようなディレクトリが何かの拍子にできてしまいまった場合どうすればよいでしょうか。

$ ls -F
-Rf -f/ ~/

まちがっても $ rm -Rf ~ なんてコマンドを打ってはいけません。 -Rf はオプションとして解釈されてしまいます。このような場合は -- を使います。-- は特別な引数でそれ以降のオプションが打ち止めとなります。

$ rm -- -Rf
$ rmdif -- -f

~ は通常ホームディレクトリと解釈さるので、 -- を利用しても意味がありません。シングルクォートでくくる、\ でエスケープする、./ でカレントディレクトリを明示するなどが必要です。

$ rm '~'
$ rm \~
$ rm ./~

6. ヒアドキュメント

複数行のテキストをプロセスの入力にします。 << 終了文字列 を記述し複数行の文字列を入力後に 終了文字列 を記載すると入力終了の合図となります。

$ cat -n << EOF
I have a pen.
I have a apple.
EOF
     1 I have a pen.
     2 I have a apple.

標準入力に流し込むので echo ではなく cat で出力しています。この出力をリダイレクトやパイプに渡す場合は、1行目に書く必要があります。

# 出力をリダイレクト
$ cat << EOF > output.txt
I have a pen.
I have a apple.
EOF

$ cat output.txt 
I have a pen.
I have a apple.
# パイプで渡す
$ cat << EOF | jq
[{
"name":"Matz","age":51,"from":"Japan"},
{"name":"Dhh","age":36,"from":"Denmark"}
]
EOF
[
  {
    "name": "Matz",
    "age": 29,
    "from": "Japan"
  },
  {
    "name": "Dhh",
    "age": 36,
    "from": "Denmark"
  }
]

標準入力に流し込むので while イディオムの入力としても利用できます。

$ while read n; do echo $(( ${n} * 2 )); done << EOF
1
2
3
4
5
EOF
2
4
6
8
10

7. <<< ヒアストリング

<<< の右側に書いた文字列や変数の値をコマンドの標準入力に流し込みます。 ヒアドキュメントと同じように扱えます。パイプとリダイレクトも通常通り利用できます。

$ cat <<< 'Hello World!!'
Hello World!!

$ JSON='[{
"name":"Matz","age":51,"from":"Japan"},
{"name":"Dhh","age":36,"from":"Denmark"}
]'

# パイプ |
$ jq . <<< ${JSON}} | grep -i 'matz'
    "name": "Matz",

# リダイレクト >
$ jq .[].name <<< ${JSON} > name.txt
$ cat name.txt
"Matz"
"Dhh"

8. $( コマンド ) コマンド置換

コマンドの実行結果を文字列として利用するための機能です。 コマンドの引数として展開したり引数に保持したりします。

# dateコマンドの結果が文字列に展開される
$ JSON="{\"time\":\"$(date +%s)\"}"
$ echo $JSON
{"time":"1489992386"}

# yamlファイルだけを抽出して処理
$ for f in $( find . -type f | grep '\.yml$' )
do
    [command] ${f} # 何らかの処理
done

9. $(< ファイル )コマンド置換

コマンド置換の拡張的な機能でファイルの中身に置き換えられます。

# cpコマンドを使わずbashの機能だけでファイルをコピー
$ echo "$(< /tmp/foo )" > /tmp/foo.bak

コマンドの処理結果を一時ファイルに保持することで可読性が向上します。

$ find . -type f | grep '\.yml$' > /tmp/yaml.list.txt
$ for f in $(< /tmp/yaml.list.txt )
do
    [command] ${f} # 何らかの処理
done

10. <( コマンド ) プロセス置換

コマンドの入出力をファイルとして扱う機能です。引数にファイルを受け取るコマンドの入力として利用できます。

# cat は引数としてファイルを受け取る
$ cat -n <( echo 'Hello World' ) <( echo 'Dreams come true' )
     1 Hello World
     2 Dreams come true
# ファイルリストの差分を diff で得る
$ ls
a.txt      c.txt      e.txt      h.txt       j.txt      l.txt      o.txt      q.txt
a.txt.bak  c.txt.bak  f.txt      i.txt       j.txt.bak  m.txt      o.txt.bak  q.txt.bak
b.txt      d.txt      g.txt      i.txt.bak   k.txt      n.txt      p.txt      r.txt
b.txt.bak  d.txt.bak  g.txt.bak  index.html  k.txt.bak  n.txt.bak  p.txt.bak  r.txt.bak

$ diff <( ls | grep '.txt$' | cut -d. -f1 ) <( ls | grep '.txt.bak$' | cut -d. -f1 )
5,6d4
< e
< f
8d5
< h
12,13d8
< l
< m

シェルプログラミング実用テクニック 入門bash 第3版 ソフトウェアデザイン 2016年 06 月号 ソフトウェアデザイン 2017年 01 月号