エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

bashスクリプティング研修の資料を公開します

f:id:taknakamura:20180817183543p:plain

こんにちは、エンジニアリングGの中村です。

以前にこのブログにてエムスリーでの社内研修について紹介しました。今回は、この中でのbashスクリプティング講座の資料を公開します。

www.m3tech.blog

弊社の中でもいろいろな用途でbashが使われていますが、bashは簡単に利用できるもののプログラミング言語としてはバグを生みやすい、辛い言語だと思います。 ここで紹介しているのはいわゆるコーディング規則というよりも、バグ防止と可読性向上のためのルールをTips集的にまとめたものです。 bashにおいてまだまだ注意するところはありそうですが、多少なりともわかりにくいスクリプトの削減になればと期待しています。

[追記: 2018-08-22]

はてブにて以下のコメントをいただきました。

bashスクリプティング研修の資料を公開します - エムスリーテックブログ

bashで50行以上になったらまずは違う言語使うでいいと思うのだが。。。

2018/08/21 20:00
b.hatena.ne.jp

これは全くそのとおりだと思います。一番大事な主張が、研修の中で口頭では伝えていたのですが資料に書き忘れているのに気づきました。

複雑な処理はbashではなく他の言語(pythonなど)を使った方がよいです。 この資料は、bashという言語がいろいろと注意点が多く、本気で対処するには辛い言語であるということを伝るためのものでもあります。 bashで無理にテクニックを駆使してやろうとするのではなく、使いやすい言語を選ぶことを検討した方がよいと思います。


bashスクリプティング講座

はじめに

bashスクリプトは誰でもとりあえずは作成できるという反面、記述が雑になりやすい傾向があるように思います。 ですが、一度作成されたものはその後も使い続けられ、後々になって思わぬバグがおきたり、メンテナンスがしづらくて苦労したりということになります。

そのため,bashといえども他の言語での開発と同様に読みやすく・わかりやすく書くように心がけてください。 言語面での制約が少ない分、わかりにくいコードになりやすい(そういうのを書けてしまう)ので、他よりも一層良いコーディングを意識する必要があります。

以下では、bashでバグになりやすい点や、読みやすくなるような書き方の指針、bashのテクニック的なことを上げています。

そもそも

また、そもそもbashを使うべきなのどうかを考えてください。 この資料を見て、実はbashがいろいろと辛い・大変な言語であることを感じてほしいと思っています。 ある程度以上の規模や複雑なことをするのであれば、bashではなく他の言語を使ってください。 もしbashを使うのであれば、ここから示す注意点や記法を守りつつ作成してください。

スクリプトの定型テンプレート

最初に、シェルスクリプトのおおよその書き方のテンプレートを以下に記載します。

main.sh

#!/bin/bash
#
# スクリプトの内容の説明
#
# 説明の詳細文を記述します。
#

# bashのスイッチ
set -euC

# 外部スクリプトのsource
#source ./setting.inc

# グローバル定数
readonly DEBUG=false
readonly WORKDIR=/tmp/mydir

# グローバル変数
INPUT_FILE=
OUTPUT_FILE=
FLAG=0

#
# 引数parse処理
#

function usage() {
  # パラメータの詳細を必ず明記すること
  cat <<EOS >&2
Usage: $0 [-o OUTPUT_FILE] [-a] INPUT_FILE
  INPUT_FILE      入力ファイル
  -o OUTPUT_FILE  出力ファイル
  -a              オプション。〜〜を〜〜として動作します
EOS
  exit 1
}

# 引数のパース
function parse_args() {
  while getopts "o:a" OPT; do
    case $OPT in
      o) OUTPUT_FILE=$OPTARG ;;
      a) FLAG=1 ;;
      ?) usage;;
    esac
  done

  shift $((OPTIND - 1))

  INPUT_FILE=${1:-}

  if [[ "$INPUT_FILE" == "" ]]; then
    usage
  fi
}

#
# 関数定義
#

function hoge() {
  local input_file="$1"
  cat <<EOS
  cat "$input_file"
EOS
}

function fuga() {
  local output_file="$1"
  cat <<EOS
  echo "Hello" > "$output_file"
EOS
}

function main() {
  local input_file="$1"
  local output_file="$2"
  local flab="$3"

  hoge "$input_file"
  fuga "$output_file"
}

# エントリー処理
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  parse_args "$@"
  main "$OUTPUT_FILE" "$INPUT_FILE" "$FLAG"
fi

test.sh

#!/bin/bash
#
# 動作確認用スクリプト
#
# 出来ればUnitTestとして記述する
# そこまでしなくても、この中で動作確認のための試し書きをして実行する
#

source ./main.sh

function test_fuga {
  fuga log_file.txt
  cat log_file.txt
}

function test_args {
  parse_args -o "output" input
  echo $INPUT_FILE
  echo $OUTPUT_FILE
}

function test_main {
  main item1 item2 0
}

#test_fuga
test_args
#test_main

bashの書き方でよくある問題

bashは便利な反面、値の解釈などで分かりにくい動きが多くあります。 例えば、以下の記事にていろいろな落とし穴が紹介されています。

本記事は上記の記事を参考にさせていただきつつ、社内で特にありがちなところを補足したものです。

[必須] 明確な意図が無いならシバン(#!〜〜) には /bin/shではなく/bin/bashを明示する

スクリプトの1行目のシバン(shbang)には、shを使うことを明確にしていないならbashを明示してください。

shとbashは正確には別物です。shはPOSIX仕様にそった機能のみが使えるものですが、bashはそれよりも機能が豊富です。
OSによっては/bin/shが別のシェルのシンボリックリンクである場合があります(dashだったり)。 また、/bin/bashのシンボリックリンクの場合もあったりしますが、実体はbashでもsh実行モードで動いていて実質POSIXしか使えないときもあります。

同じスクリプトをいくつものUnix,Linux系OSをまたいで流用するのであればshを意識して実装する必要があります。
ですが、社内のスクリプト(特に分析用・調査用のスクリプト)は、特定の環境でのみ動けばいいようなことがほとんどなのでbashを使っておいてください。

[必須] 変数を展開するときは必ず「"」で囲う

bashでの変数は普通のプログラミング言語とはかなり違うため、色々と注意する必要があります。 その中で大きなポイントとしては以下の2点です。

  1. 空白文字を変数に入れて扱う時
  2. 半角スペースやタブを含む値を扱う時

これらの直感的にはわかりにくい問題を回避するためにも、変数を利用するときは必ず「""」で囲うようにしてください。

変数が空白文字を保持しているときに起きる問題: その1

VAL1=

# 以下2つは同じ意味
some_func $VAL1 foo bar
some_func foo bar

上記の2つのコマンド実行は同じ意味になります。つまり、some_funcの第1引数は$VAL1ではなくfooとなります。 fooを第2引数として指定したいのであれば、$VAL1をダブルクオートで囲んで第1引数が空文字となるようにする必要があります。

VAL1=

# 以下2つは同じ意味
some_func "$VAL1" foo bar
some_func "" foo bar

変数が空白文字を保持しているときに起きる問題: その2

以下は、if文の構文エラーになってしまいます。

VAL1=

# 以下2つは同じ意味
if [ $VAL1 -eq 1 ]; then echo "OK"; fi
if [ -eq 1 ]; then echo "OK"; fi
→ 実行エラー発生。[ ] 内の構文が不正となる(第1引数が 「-eq」になるため左辺がない)

変数をダブルクオートで囲うと回避できます。

VAL1=

# 以下2つは同じ意味
if [ "$VAL1" -eq 1 ]; then echo "OK"; fi
if [ "" -eq 1 ]; then echo "OK"; fi
→ [ ] 内の第一引数が 「""」となるので成立する

値に半角スペースを含む場合に起きる問題

変数の値に半角スペースがあると、コマンド引数やループなどのときにスペース区切りで値が分解されてしまいます。

問題パターン1

# ファイル名にスペースを含む
FILE_NAME="some text file.txt"

# 以下は同じ意味
cp $FILE_NAME ./mydir/
cp some text file.txt ./mydir/
→ 3ファイルをコピーするような動作になり、そんなファイルはなくてエラー

問題パターン2

# ファイル名にスペースを含む
touch "some text file.txt"

# 実質的に以下は同じ
for file in $(ls *.txt); do echo $file; done
echo some; echo text; echo file.txt
→ ls の結果の文字列「some text file.txt」を空白で分解してループしてしまう

# 正しくは以下のように書く
for file in *.txt; do
  # `*.txt`にマッチするファイルが無い場合に、file='*.txt'としてループが1回処理されるのでチェック
  if [ ! -e "$file" ]; then continue; fi

  echo $file
done

[必須] if文は[〜]ではなく[[〜]]を使う

bashでは、条件判定部分を[とは別に[[を使えます。
[[の方が安全で機能が豊富なのでこちらを使うことを推奨します。

[[では前述の変数が空の場合のエラーが発生しません(ちゃんと条件判定として実行される)

VAL1=
# VAL1を "" で囲い忘れている
if [[ $VAL1 == '' ]]; then echo "OK"; fi
=> でもエラーにならず、"OK"と表示される

詳しくは以下参照してください。

[必須] 配列変数を展開するときは「"」で必ず囲う

コマンドやfunctionに渡された引数の全てを保持する「$@」という特殊変数を使う時は、必ず「"」で囲ってください。 値に半角スペースを含む場合に囲うかどうかで挙動が変わってしまいます。

# 以下のようにコマンド実行した場合
# myfunc 100 200 "foo bar" 300

# 以下2つのfor文の結果が異なる

for i in $@; do echo $i; done
#=> 100
#=> 200
#=> foo
#=> bar
#=> 300

for i in "$@"; do echo $i; done
#=> 100
#=> 200
#=> foo bar
#=> 300

「"」で囲わない場合は、値に半角スペースを含む場合にそこで分解されてしまいます。 例えば、スペースを含むファイル名を受け取るような場合に正しいファイル名を利用できないといったことが起こります。

この「"$@"」はこの4文字で固有の動作をする表記になっています。func "hoge $@"といった表記では成立しません。

また、引数の一覧を取得する別の方法に「$*」というものがありますが、こちらでは「"$*"」と書いても必ず半角スペースが分割されてしまいます。 なので、基本的に「$*」は使わず「"$@"」を使うようにしてください。

bashスクリプトのいい感じの書き方

スクリプトを書くときの全体的な記述の方針を以下にまとめます。

[必須] functionの記述方法

functionを定義するときは以下のフォーマットで書いてください。

  • "function"を明記
  • 名前のあとの「()」は任意
  • function内の最初で引数をローカルに保持

例としては以下のような書き方になります。

# helloと出力する関数
#
# 1: 追加表示するメッセージ
function func1() {
  local arg1="$1"
  echo hello "$arg1"
}

bashでは"function"を明記しなくても定義できますが、 grepなどでfunctionの定義を探しやすくするために明記することをお願いします。

[必須] functionなどのブロック内はインデントする

bashも普通のプログラミング言語と同様にコードブロックの中ではインデントして記述してください。
例外としてヒアドキュメントの場合などで行頭に空白を入れられない場合は除きます。

function func1() {
  # インデント
  echo "hoge"

  if [[ -e "file.txt" ]]; then
    echo "file"
  fi

  while true; do
    echo "loop"
    break
  done
}

function func2() {
  # ヒアドキュメント内部も問題なければインデント
  cat <<EOS | somework
    select *
    from doctor
EOS # ヒアドキュメントのマーカは例外

  # ヒアドキュメント内部が行頭に空白が入れられなければ例外
  cat <<EOS > some.yml
setting:
  text: Hello world!
EOS

  # インデントして書けるようにする方法
  cat <<EOS | sed -E 's/^ +\|//' > some.yml
    |setting:
    |  text: Hello world!
EOS
}

# サブプロセス実行なども同様
(
  echo "in subprocess"
  do_something
)

[必須] インデントはスペースのみ

ファイル中のインデントはスペースで2文字、もしくは4文字で記述してください。
タブ文字は不可。スペース・タブ混在は不可です。
タブ文字はgitlabなどでの表示の文字幅などが指定できないため、見た目を統一するためにスペースとしてください。

[必須] 1ファイル中の記述の配置

シェルスクリプトに記述することになる要素は以下のようなものがあります。

  • 外部スクリプトのsource
  • グローバル定数
  • グローバル変数
  • 引数parse処理
  • 関数定義
  • スクリプトのメイン処理のエントリー部分

これらの要素は、要素ごとに一塊になるように記述してください。 定数や関数の定義がファイル中に散在すると、見落としの原因になってスクリプトの全体を読みづらくなります。

各要素の記述順序は基本的に前述のテンプレートのような順序で記述するようにしてください。

[必須] main関数を作成してエントリーポイントとする

引数のパース処理やグローバルの定数・変数の定義を除いて何らかの処理を記述するときは、必ず関数として定義してください。 スクリプトのエントリーポイントとなるメイン処理も、main関数を定義してそれをスクリプトの最後で呼び出すように記述してください。

また、main関数の実行をif [[ "${BASH_SOURCE[0]}" == "${0}" ]]; thenでブロックすることで、スクリプトを直接実行したときのみ処理を行い、sourceで読み込まれたときは処理を行わないようにできます。

これには以下の利点があります。

  • 全てを関数化することでコードのインデントが揃って読みやすくなる
  • 事前準備処理と、メイン処理とを関数として分けて定義することで処理の意味を読み取りやすくなる
  • デバッグ作業やテストにてメイン処理を実行せず、個々の関数のみを動作させることができるようになる

[必須] 変数名の命名

関数、変数の命名については以下の表記としてください。

  • snake caseで命名する (例: use_this_format、OUTPUT_DIR)
  • グローバルな変数・定数は全て大文字(例: OUTPUT_DIR)
  • ローカル変数は小文字 (例: use_this_format)
  • 日付は値の書式が分かる様に foo_bar_date よりは foo_bar_yyyymmdd のようにしてもよい
  • 時間を表す数値などは、変数名で単位がわかるようにする。foo_sec (秒単位)、bar_min(分単位)など
  • 関数は処理の内容に沿ったスクリプト全体で意味が通る名前にする
    • NG: create_sql、OK: create_message_list_sql

[必須] 変数名は長くなってもわかりやすく付ける

bashはグローバル変数の利用が多くなりがちで、その性質から定義場所と利用場所が離れるなどして読解が難しくなることがあります。 そのため、グローバル変数については変に短くするよりも長いほうがいいと考えます。

変数名の例

  • ある程度省略がわかるものはOKとしますが、基本的には省略の必要はないと思います
    • OK: TBL (= table), LBL (= label )
    • NG: L (一文字だけの変数), 〜〜_L (接尾語などでも1文字はだめ)

[必須] コマンド、functionを作成するときはしっかりとコメントを記述する

コマンドやfunctionの使い方や作成された意図などについてコメントを残すようにしてください。 sourceして使う前提のファイルでも、そのファイル自体にも用途やそのファイルの意図を記載してください。

以下の点を明確になるように記載することを心がけてください。

  • そのスクリプト・コマンドの用途・使い方・利用上の注意点
  • 前提として定義しておく環境変数やコマンド、ファイルなどの依存関係
  • 引数やオプションの定義
  • アウトプットとなるものの定義(ファイル、DBのテーブルなど)

functionのコメント例

# CSV出力用に値をエスケープする
#
# $1: エスケープ対象の文字列
# 出力: 「"」で囲ったエスケープされた文字列
function csv_escape() {
    local value="$1"
    echo "\""$(echo "$value" | sed 's/"/""/g')"\""
}

[必須] コマンド実行結果の標準出力を変数で取得するときは「$(cmd)」表記を使う

コマンド実行の標準出力を取得するには、「`command`」と「$(command)」の2通り表記がありますが、必ず「$(command)」で記述してください。

「`」の表記は記号が小さく視認しづらいのと、「$()」であれば入れ子で記述することができるためです。

VAL=$(echo "hoge")

[必須] localやreadonlyを付けて変数を定義する

bashではfunction内で定義した変数もグローバル変数になります。 それを避けるためにfunction内で変数を定義するときはlocalを付けてください。 もし、それを付けずに意図的にグローバル変数を定義しようとしてるときは、何か無茶をしようとしていると思って再考してください。

ただし、localの変数のスコープは普通のプログラミング言語とは異なるので過信しすぎないように注意してください。
localはそれが宣言されたブロック内の呼び出し全てに影響します。

function hoge() {
  local val1=100 # これはfunction内部から呼び出されたコマンドの先まで影響する

  fuga
}

function fuga() {
  echo $val1
}

hoge
#=> 100 fuga内部のval1の出力は 100が表示される

echo "val1=[${val1}]"
#=> val1=[] ここではval1は参照できない

[必須] 値を変更しない定数を定義するときはreadonlyを付けて定義する

値を変更しない定数を定義するときは、readonlyを付けてください。 これをつけた定数は上書きで代入しようとするとエラーになります。

readonly HOGE=10

HOGE=100 # ここでエラー

[推奨] グローバル変数をできるかぎり使わない

bashは変数のスコープを制限しづらくグローバル変数を多用しがちです。
ですが、グローバル変数を使うと実装を読み解きづらくなるため基本的にはグローバル変数を使わないように実装してください。

functionを読み解くときに、そのfunctionだけを見れば処理が理解できるようになることを心がけてください。 グローバル変数を直接参照できるとしてもその変数を直接つかうのではなく、引数から渡すようにしてください。

ただし、何もかもの変数を渡して回ると逆に読みにくくなる場合もありますので、グローバルな定数やほぼ定数的に使うようなものは直接使ってもよいです。

NG

VAL1=$1

function func1() {
    echo "$VAL1"
}

func1

OK

readonly WORKDIR=/tmp/workspace
readonly LOGFILE=$WORKDIR/app.log
INPUT_MESSAGE=$1

function func1() {
    local message=$1
    echo "$message" > $LOGFILE
}

func1 "$INPUT_MESSAGE"

[推奨] 複雑な処理が必要になったらbash以外を使うことを考える

bashは簡易なことを行うなら便利ですが、複雑なデータ構造のものを扱う場合などには向きません。
そのため、bashでやるのが難しいことがでてきたらbashを使うのを諦めて別の言語やコマンドを使うことを考えてください。

bash以外のコマンド・言語

  • sed / tr コマンド
  • awk
  • perl / python / ruby

[必須] bashスクリプトにはset -euCを付ける

bashの実行時のオプションにスクリプトを作成する上で有用なものがあります。 オプションはbashコマンドの引数としてしてするか、スクリプト中にsetコマンドで指定できます。

スクリプトを作成するときは、このオプションの中で基本的にset -euCを付けてください。

#!/bin/bash

# 最初に設定
set -euC

echo "myscript"

シバン(#!/bin/bash)のところに記述することもできますが、それだとデバッグ時などでスクリプトをbashコマンドで直接実行するようなときにオプションが機能しなくなるのでsetで指定する方がよいと考えます。

# デバッグとして-xつきで実行
bash -x myscript.sh
#=> この実行方法だとシバンが使われない

オプションの意味は以下のとおりです:

-eオプション

スクリプト中のコマンド実行がエラー終了(終了コード 0以外での終了)した場合にスクリプトを停止します。 逆に言えば、このオプションなしではコマンドがエラー終了してもそのまま処理が継続してしまいます。 そのため、想定外のバグや突発的なエラー発生時に、誤って後続の処理をしてしまうようなことが無いように、このオプションは必ずつけるべきです。

注意点として、grepやlsなどのコマンドは結果が0件になるような場合に終了コードが0以外になるので、 そういう場合は一時的に-eオプションを解除するようにする必要があります。

set -e
echo "エラー終了する範囲"

set +e # 一時的にエラーで停止しないようにする
myvalue=$(grep "hoge" myfile.txt)
RET=$?  # コマンドの終了値を取得するときはsetの前に保存しないと、$?がsetコマンドの結果で上書きされる
set -e # 再度、有功化

# 以下のように最後に必ず正常終了するコマンドをつなげることでも回避できます
grep "hoge" mfile.txt || true

-uオプション

-uオプションを指定すると未定義変数を使おうとするとエラー終了するようになります。
これにより、変数名のタイプミスや初期化わすれでの変数の利用などを防ぐことができます。

set -u
echo "$VAL1" # ここでエラーになる

回避方法

set -u
# デフォルト値指定をすれば回避可能
echo "${VAL1:-}" # この場合はOK

-Cオプション

-Cオプションを付けるとリダイレクトでのファイル上書き時にエラー終了するようになります。 リダイレクトでファイルを生成するときに想定外に上書きしてしまうことを防ぐことができます。

set -C
touch file1.txt
echo "Hello" > file1.txt # ここでエラー

# 以下のように「>|」と記述すると上書きが可能
echo "Hello" >| file1.txt

[推奨] スクリプト終了時に必ず処理をさせたいときはtrapを利用する

trapコマンドを使うことで、スクリプトが終了したときに必ず実行する処理を指定することができます。
それをつかって、スクリプトの最後に掃除処理などを行うことができます。

TEMPFILE=""

function main() {
  echo "start main"

  # 強引な例ですが、ここでTEMPFILEを初期化
  TEMPFILE=$(mktemp)

  echo "hoge fuga" > $TEMPFILE
}

function cleanup() {
  # いつどんなときに実行されるかは分からないので、丁寧に事前チェックをする方がいい
  # 例えば、mktempが失敗してTEMPFILEが空のときにここが実行されるかも
  if [[ -n "$TEMPFILE" && -e "$TEMPFILE" ]]; then
    mv -n "$TEMPFILE" ~/.trash/  # 消さずにゴミ箱フォルダに移動
  fi
}

# EXIT時に、cleanup を実行
trap cleanup EXIT

main

bashスクリプトの実装テクニック

よくあるコマンドのTips

  • mkdir -p

    • 指定したパスを子階層も含めてすべて生成する
    • 既にディレクトリがある場合はエラーにもならず無視される
    • 既存ディレクトリの有無を確認してからmkdirする必要がない
  • mv -v, cp -v, rm -v

    • 多くのコマンドは -vをつけるとコマンドの実行ログ出力される
    • mv, cp, rmはそれぞれのファイル操作の元・先のファイルを表示するので動作結果を確認できる
  • mv -n, cp -n

    • ファイル操作系のコマンドに-nオプションを付けると上書き防止になる
    • 移動先、コピー先に既に同名のファイルがあった場合にエラー終了する

関数から複数の値を返す方法

関数から複数の値を返したい場合は以下のようにprintコマンドを使うと対応できます。 あまり、おすすめするわけではないですが、使う方法のテクニックとして紹介します。

function some_func() {
  local retval_output_file=$1
  local retval_temp_table=$2

  print -v $retval_output_file "%s" output.csv
  print -v $retval_temp_table "%s" temp_table_name
}

function main() {
 # 以下の`local`、`&&`、コマンド呼び出しの書き方を推奨
  local output_file temp_table && some_func output_file temp_table

  echo "$output_file" #=> output.csv
  echo "$temp_table"  #=> temp_table_name

}

ログの出し方

ログを出力するときに、処理中のファイルや関数、実行行数などを出力することができます。

function log() {
  local fname=${BASH_SOURCE[1]##*/}
  echo -e "$(date '+%Y-%m-%dT%H:%M:%S') ${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]} $@"
}

log "message"
#=> 2017-01-20T10:53:03 myscript.sh:10:main message

途中終了でも安全なファイル生成方法

ファイルを生成するときは、一旦別名のファイルに出力した上で最後にリネームするようにするとよいです。 ファイルの生成途中でエラーになったり、手動停止したりした場合に、作成途中のファイルを間違って最終データとして使ってしまわないようにすることができます。 また、再実行時に既にファイルがあるときはスキップするような処理にするときにも有功です。

if [[ ! -e target_file.csv ]]; then
  find ./hoge/ -name \*.txt >> target_file.csv.tmp
  echo "special.txt" >> target_file.csv.tmp
  mv target_file.csv.tmp target_file.csv
fi

一時的に環境変数を設定・上書きした状態でコマンドを実行する

シェルでコマンドを実行するときに、一時的に環境変数を上書きしてコマンド実行することができます。

# 言語設定
export LANG=ja_JP.UTF-8

# 日本語で日時を表示
date
#=> 2018年 1月23日 火曜日 15時33分09秒 JST

# 以下のときのみ、LANGを変える
LANG=C date
#=> Tue Jan 23 15:33:09 JST 2018

# LANG変数は変わっていない
echo $LANG
#=> ja_JP.UTF-8

また、以下のように「()」で囲うと、その範囲内のみサブプロセスで実行することになるため、 その内部での変数への変更は「()」の外側に影響しません。
これは、cdでのフォルダ移動も同じ効果があります。

VAL1=10
echo $VAL1
#=> 10

pwd
#=> ~/

(
  VAL1=100
  echo $VAL1
  #=> 100

  cd /usr/local
  pwd
  #=> /usr/local
)

echo $VAL1
#=> 10

pwd
#=> ~/

コマンドやfunctionに長い文字列を渡すときは標準入力を使う

表示用の文言やSQL文などの長い文字列を渡すような場合は、引数で渡すよりもパイプを使って標準入力をつかった方が良いです。

function func1() {
  # 引数が空か"-"の場合は標準入力から取得
  local val=$1
  if [[ -z "$val" || "$val" == "-" ]]; then
    val=$(cat -)
  fi

  echo $val
}

echo "foo" | func1
#=> foo

echo "bar" | func1 "-"
#=> bar

func1 "hoge"
#=> hoge

bashのデバッグ技法

read -p "pause"で一時停止

readコマンドをつかえばEnterキーを入力するまで停止できます。
処理の途中状態を確認するときや、trapなどで掃除処理を仕込んだ場合でそれが実行されるまえの状態を確認するようなときなどに便利です。

trap DEBUG, trap ERR

trap でシグナルとしてDEBUGを指定するとスクリプト中の全行の実行時に処理を割り込めます。

# ステップ実行と同じ動作が出来る
trap 'read -p "pause"' DEBUG

trap でシグナルとしてERRを指定するとコマンドのエラー終了時に処理を割り込めます。

# エラーが発生したときにそのときのstacktraceを表示する
trap caller ERR

スクリプトの途中でコマンドの引数を設定する

set --に続けて文字列を記述すると、それらを実行中のスクリプトの引数として上書きできます。 スクリプトのテストを記述するようなときに有用です。

#!/bin/bash
# sample.sh
echo ORIGINAL ARGS: "$@"

# スクリプトの引数を設定
set -- word1 "some value"

echo CHANGED ARGS: "$@"

上記を実行すると以下のようになります。

$ ./sample.sh hoge fuga
ORIGINAL ARGS: hoge fuga
CHANGED ARGS: word1 some value

set -xオプション

bashの-xオプションをはデバッグをするときに有用です。 これを指定すると、スクリプトが処理したコマンドが表示されます。 変数も実際の値に展開されて表示されるので、実際に処理された内容を把握することができます。

set -x
VAL1=100
echo "Output: ${VAL1:-hoge}"

実行すると以下のように表示されます

+ VAL1=100
+ echo 'Output: 100'
Output: 100

注意

パスワードを変数に保持して使うような処理で、-x付きのログをログファイルに保存したりしないこと。
-xは変数の内容(つまり、パスワード)を展開した状態で出力されるので、ログにパスワードが残ってしまいます。

参考資料

本資料の作成にあたって以下の記事などを参考にさせていただきました。

エンジニア募集

エムスリーでは自身で手を動かし、技術で医療の課題を解決するエンジニアを募集しています。 この記事(or 他の記事も)を読んで興味を持った方はぜひ下記リンクよりご応募ください!