百日半狂乱

Shut the fuck up and write some code!!

Linuxターミナル、コマンドtips その4: シェルスクリプトのハマリ所とデバッグ手法

(2019/06/29追記) 実践的、網羅的かつ簡潔にまとまったドキュメントを見つけたのでメモ(日本語訳もある)*1

github.com

このtipsはこれからLinuxを使っていく必要がある人、特に端末操作に苦戦している人、もしくは端末操作に対して嫌悪感すら抱いている人に向けて書いたものです.作成の経緯はその1の冒頭および注釈に書きました.

前回の話題

  • コマンドや自作プログラムの入出力をコントロールする
    • stdin, stdout, stderrの理解とターミナル、キーボードとの関係
    • リダイレクト: ファイルから読み出す、ファイルに書き出す
    • パイプ: コマンドの入出力をを組み合わせる

Linuxターミナル、コマンドtips その3: stdin, stderr, stdout, リダイレクト, パイプ

今回の話題

シェルスクリプトのハマリ所とデバッグ手法

シェルスクリプトとは?なぜ学ぶべきか?

メリット: 繰り返し行う複数の作業をファイルにまとめて一度に実行できる

シェルスクリプトはざっくり言えばターミナルで手作業でやっていることをファイルにまとめて実行できるようにしたもの.ターミナルに慣れてくると、今度は何度も同じコマンドを叩き続けるのが億劫になってくる*2.例えば、Cプログラムを書いているとして、そのコンパイルと動作確認に二つのコマンドを実行する必要がある.

$ gcc -Wall -o hello hello.c
$ ./hello

まさか全部毎回手で打ち直すわけには行かない.すでにインクリメンタルサーチ(ctrl + r)を知っているのであれば、全てを打ち直す必要はないがそれも面倒である.例えば、run.bashというファイルに以下の3行を書く.

#!/bin/bash
gcc -Wall -o hello hello.c
./hello

以降、ターミナル上でbash run.bashと叩くだけで上記のシェルスクリプトを実行することができる.二つのコマンドが一つになった.bashと打つのすら面倒ならば、chmod +x run.bashによってファイルに実行権限を与えることができる.すると以降から./run.bashと叩くだけで上記スクリプトを実行することができる.もし、あなたがコンパイル->実行の作業を今後50回以上繰り返す場合、省略される作業の数および時間はどのくらいになるか?もし繰り返す作業が2つではなく3つや4つそれ以上だった場合にスクリプトを書ける人と書けない人の作業効率の差はどのくらいだろうか?

余談: 実はコマンド一回叩くだけで、コンパイル->実行できる

Cプログラムのコンパイルと実行は2回コマンドを叩く必要があると言ったが、実は一行で済む.

$ gcc -Wall -o hello hello.c && ./hello

こうすれば、コンパイルが成功した場合にのみプログラムを実行してくれる.一行なのでインクリメンタルサーチ(ctrl + r)で探して実行でも十分間に合う.

意識しよう

シェルスクリプトに書いていることは全てターミナル上でできるし、ターミナル上でやっていることは全て全てシェルスクリプトとしてまとめることができる.どちらも基本的には結局シェルが行を解釈して、1つずつ実行しているに過ぎないことを意識しよう.

シェルスクリプトの入門

入門的なサイトやブログはググれば山のように出てくる.例えば、シェルスクリプト入門 書き方まとめを元にざっと写経してみる.大体の機能を掴み、やりたいと思ったことができそうかどうかの判断ができるようになると大分違う.

シェルスクリプトのハマリ所

ここでは個人的に何度も時間を無駄にしてきたハマリ所をいくつか紹介*3

ハマリ所: 不親切なシンタックスに注意

時折シンタックスが不親切なので注意

変数代入

例えば、以下の変数代入はシンタックスエラー

var = "hoge" #エラー!
var= "hoge" #エラー!
var ="hoge" #エラー!

正しくは以下

var="hoge"

=の前後にスペースを入れてはならない

if文

if文を書いた時ももよくシンタックスエラーに出会う

以下はエラー

if[ $var="hoge" ]; then #エラー!
    echo $var
fi
if [$var="hoge" ]; then #エラー!
    echo $var
fi
if [ $var="hoge"]; then #エラー!
    echo $var
fi

正しくは以下

if [ $var="hoge" ]; then
    echo $var
fi

[]の前後にはスペースが必須

ハマり所:クォーティング

シェルスクリプトを書いていると、「変数展開をこのタイミングでして欲しい」とか、「ここでコマンドの実行結果が欲しい」などといった要望が出てくる

シングルクォーテーション

これで囲まれた文字列は、書いたまま文字列となる = 変数展開されない

#!/bin/bash
var=hoge
echo '$var' # -> $ $var

ダブルクォーテーション

これで囲まれた文字列にシェル変数が含まれている場合は変数展開されて、その結果が文字列となる

#!/bin/bash
var=hoge
echo "$var" # -> $ hoge

バッククォート

文字列をコマンドとみなして、実行結果に置き換わる

whoamiというコマンドがある、例えば現在のユーザー名がjackなら、

echo "I'm `whoami`" # -> I'm jack

デバッグ手法

早い段階でデバッグ手法を知っておくと、自分でシェルスクリプトを書く敷居がぐっと下がる.トラブルシューティングの方法さえ知っておけば、後は自分が何をしたいかという要求に答えることで、色々な作業を自動化できる.使い勝手も意識することで、他の人にも使ってもらえるスクリプトにすることも可能.

便利なデバッグオプション

シェルスクリプトを書くときはset -euしておく

シェルスクリプトのデバッグ

特に、-e, -uオプションはファイルを壊したりする危険から救ってくれるので、常に付けておいても良いくらいのオプション

#!/bin/bash -eu
var=hoge
echo "$var" # -> $ hoge

以下のようにすれば、ファイル内に-euオプションを書かなくても一時的に適用できる

bash -eu print_hoge.bash

実行前に構文解析する(エラーの有無を確認する)

-nオプションを使えば、実行前に構文解析を行なってくれる

#!/bin/bash -n
echo fuga
if[ $var=”hoge” ]; then
    echo $var
fi

これもbash -n hoge.bashでファイル内に-nオプションを書かなくても一時的に適用できる

失敗談

ある時、ファイル名に、*:を入れて何かをしようとした.シェルは*:特殊文字として扱うので、ファイル名にこのような文字を想定していると、所望の動作にならない.当時はデバッグ手法を知らずかなりの時間を無駄にしたが、上記のデバッグ手法を知っていればすぐに解決したはず.「何が起こっているのかを確かめられる」というのは、問題が発生した際に大変役に立つ.

チートシート

今更ながら抑えておきたいシェルスクリプト用チートシート

使い勝手を考える

使いやすいシェルスクリプトを書く

慣れてきたら意識したいこと等

シェルスクリプトのコーディングルール2014

シェルスクリプトを書くときに気をつける9箇条

バランス重要: どのような作業をシェルスクリプト化すべきかの判断

ちょっとした繰り返し作業には非常に役立つシェルスクリプト.使い捨てで良いのでどんどん書くべきと思っているが、なんでもかんでもシェルスクリプト化していると、時々その労力が無駄になることがある.シェルスクリプト化しようとしている作業が、今後どのくらい繰り返されそうかということと、それは今すぐ必要かということをスクリプトを書き始める前に考える癖をつけた方が良いかもしれない.物凄い労力をかけて壮絶に面倒な作業フローが全て自動化された時の高揚感は堪らないものがあるが、そのスクリプトは今後どの程度使い回されるか?今すぐ必要か?といったことに頭が回ってない場合に、酷い目*4に合うかもしれない.また、シェルスクリプトは自分にしか読めない秘伝のタレ化しやすい.最近は便利で可読性の高い自動化ツールがどんどん出てきていることを忘れない.

自戒を込めて.

*1:毎日それこそ息するように使っているものから、必要な時に覚えてなくて毎回ググっているものまで、ベタだけどまさに当時これが手元にあればなぁという内容になっていて良くできている。このブログを読んでくれるのも嬉しいけど、何ができて何を調べれば良いかをまず知る目的で一回このドキュメントも一通り読んでおくと、"あれどうやるんだっけなー"などとボヤきつつ当てもなくググる時間を減らせるはず。要チェック。

*2:億劫に感じない?ならば今日から億劫に感じる怠惰な人間になろう.参考: プログラマの三大美徳

*3:もし、他にこれで死ぬほどハマった!とかあれば是非教えて下さい

*4:=今週は(自己満足な自動化作業を行っていたため)進捗ありません