Bashでスクリプトを書く際によく自分が使っている小技や関数などです。
またBashでは他のプログラミング言語以上に$
や:
などの記号の使い方が独特でググラビリティが低いので、基本文法などもチートシートとしてまとめておきます。
- 基本文法
- 便利なスニペット
- Tips
- 標準出力を標準エラー出力にリダイレクト
- 標準エラー出力を標準出力にリダイレクト
- プロセスIDを取得する($$)
- コマンドの戻り値を取得する ($?)
- パイプでつないだコマンドの戻り値を取得する (${PIPESTATUS[0]})
- 未定義の変数を使用するとそこでスクリプトを終了する (set -u)
- コマンドがエラーだった場合そこでスクリプトを終了する (set -e)
- set -eの状態でエラー後も処理を続ける (&&:)
- パイプで繋いだコマンドがエラーのとき終了させる
- スクリプト終了時にコマンドを実行する
- 一時ファイルを作る
- 一時ディレクトリを作る
- rootユーザでのみ実行を許可する
- 標準入力からデータを受け取る
- ヒアドキュメント
- スクリプトのロック(多重起動防止)
基本文法
変数と配列
変数
# 変数 v="variable" echo $v
配列。bashは配列しかデータ構造がない。つらい。。。
# 配列 declare -a array=() # 初期値 declare -a array=("a" "b" "c") # 要素数 echo ${#array[@]} # 先頭に追加 array=("x" "${array[@]}") # 末尾に追加 array=("${array[@]}" "d") # indexを指定して取得 echo ${array[0]} echo ${array[1]} # 配列全体を取得 echo ${array[@]} # 配列をforループで参照する for v in "${array[@]}" do echo $v done
参考:
制御構文
while
v=0 while [ $v -lt 10 ] do echo $v v=$((v+1)) done
for-in
for i in {0..9}; do echo $i done
if - elif - else
v="hoge" if [ "$v" = "hoge" ]; then echo "v is hoge" elif [ "$v" = "foo" ]; then echo "v is foo" elif [ ! "$v" = "foo" ]; then echo "v is not foo" else echo "v is unknown" fi
条件判定のあとのブロックを空にすることはできない。
変数は"で囲ったほうがいい。[ $v = "~" ];
は変数が空のときsyntaxエラーになる。参考。
条件の否定は先頭に!
。
if文のone-liner
[ "$v" = "hoge" ] && echo "v is hoge"
文字列比較
文字列が等しい (=)
v="hoge" if [ "$v" = "hoge" ]; then echo "equal" fi
文字列が等しくない (!=)
v="hoge" if [ "$v" != "foo" ]; then echo "not equal" fi
空文字、文字列長が0 (-z)
v="" if [ -z "$v" ]; then echo "zero length" fi
空文字でない、文字列長が0でない (-n)
v="aaa" if [ -n "$v" ]; then echo "not zero length" fi
数値比較
数値が等しい (-eq)
v=55 if [ "$v" -eq 55 ]; then echo "equal" fi
数値が等しくない (-ne)
v=55 if [ "$v" -ne 20 ]; then echo "not equal" fi
数値がより小さい (-lt)
v=10 if [ "$v" -lt 20 ]; then echo "less than" fi
数値がより大きい (-gt)
v=30 if [ "$v" -gt 20 ]; then echo "greater than" fi
ファイルの判定
存在する (-e)
v="/tmp/aaa" if [ -e "$v" ]; then echo "exists" fi
ファイルである (-f)
v="/path/to/file" if [ -f "$v" ]; then echo "file" fi
ディレクトリである (-d)
v="/tmp" if [ -d "$v" ]; then echo "directory" fi
シンボリックリンクである (-L)
v="/path/to/link" if [ -L "$v" ]; then echo "symbolic link" fi
関数
function hoge() { local v="local variable" # ローカル変数定義 echo $1 # n番目引数 echo $2 echo $@ # 引数全体 echo $# # 引数の数 return 0 # 戻り値 } hoge "arg1" "arg2" # 実行結果 # arg1 # arg2 # arg1 arg2 # 2
ブロック内を空にすることはできない。
戻り値は数値のみ。コマンドと同様に0
が正常でそれ以外はエラー。
$0
は関数名ではなく、呼び出し元スクリプト名が入る。
文字列を戻したいときはechoなどで標準出力を使う。
function hoge() { echo "hogehoge" } $v=$(hoge) echo $v
便利なスニペット
実行スクリプトがあるディレクトリを絶対パスで取得する
READLINK=$(type -p greadlink readlink | head -1) if [ -z "$READLINK" ]; then echo "cannot find readlink - are you missing GNU coreutils?" >&2 exit 1 fi resolve_link() { $READLINK "$1" } # get absolute path. abs_dirname() { local cwd="$(pwd)" local path="$1" while [ -n "$path" ]; do # cd "${path%/*}" does not work in "$ bash script.sh" # cd "${path%/*}" cd "$(dirname $path)" local name="${path##*/}" path="$(resolve_link "$name" || true)" done pwd -P cd "$cwd" } bin_dir="$(abs_dirname "$0")" # /usr/local/bin # などがbin_dirに取得できる
シンボリックに対応しないなら、以下の記述でもOK。
# スクリプトがシンボリックリンクから呼び出されたときは、リンクを辿らず、リンク先のパスを返す bin_dir=$(cd $(dirname $0); pwd)
参考:
https://github.com/rbenv/rbenv/blob/master/libexec%2Frbenv
標準出力にプリフィクスをつける
prefix() { local p="${1:-prefix}" local c="s/^/$p/" case $(uname) in Darwin) sed -l "$c";; # mac/bsd sed: -l buffers on line boundaries *) sed -u "$c";; # unix/gnu sed: -u unbuffered (arbitrary) chunks of data esac } # 使い方 echo "message" | prefix "[hoge] " # [hoge] message # 標準エラー出力も対象とする場合はエラーを標準出力にリダイレクトしてから行う echo "message" 2>&1 | prefix "[hoge] "
参考:
https://github.com/heroku/heroku-buildpack-php
標準出力にインデントをつける
プリフィクスをつける、の応用。
indent() { local n="${1:-4}" local p="" for i in `seq 1 $n`; do p="$p " done; local c="s/^/$p/" case $(uname) in Darwin) sed -l "$c";; # mac/bsd sed: -l buffers on line boundaries *) sed -u "$c";; # unix/gnu sed: -u unbuffered (arbitrary) chunks of data esac } # 使い方 # デフォルトで4スペースインデント echo "message" | indent # message # インデントサイズを引数に渡せる echo "message" | indent 6 # message
コンソールに確認用のプロンプトを出す
confirm() { local response # call with a prompt string or use a default read -r -p "${1:-Are you sure? [y/N]:} " response case $response in [yY][eE][sS]|[yY]) return 0 ;; *) return 1 ;; esac } # 使い方 confirm if [ $? -ne 0 ]; then exit 1 fi # Are you sure? [y/N]: # と表示されて入力まちになるのでyやyesを入力で続行。それ以外はexitする。 # 引数で表示メッセージを変えられる。 confirm "Please input yes!:" # set -eしておけばconfirmの戻りが1のとき即終了するのであと条件分岐を書く必要がない set -e confirm
参考:
コンソールにユーザー入力用のプロンプトを出す
ask() { local response # call with a prompt string or use a default read -r -p "${1:->} " response echo $response } # 使い方 v=$(ask) echo $v # > # と表示されて入力待ち状態になる。入力値はvに入る。 # プロンプトの表示も買えられる v=$(ask "[input your name]> ") echo $v
テキスト装飾(色、太字、アンダーライン)
if [ "${TERM:-dumb}" != "dumb" ]; then txtunderline=$(tput sgr 0 1) # Underline txtbold=$(tput bold) # Bold txtred=$(tput setaf 1) # red txtgreen=$(tput setaf 2) # green txtyellow=$(tput setaf 3) # yellow txtblue=$(tput setaf 4) # blue txtreset=$(tput sgr0) # Reset else txtunderline="" txtbold="" txtred="" txtgreen="" txtyellow="" txtblue=$"" txtreset="" fi # 使い方 (装飾後は${txtreset}で戻すようにして使う) ${txtred}this text is red${txtreset}
参考:
https://linuxtidbits.wordpress.com/2008/08/11/output-color-on-bash-scripts/
http://stackoverflow.com/questions/2924697/how-does-one-output-bold-text-in-bash
エラーメッセージを出して終了する
上記のテキスト装飾を使って、赤文字のエラーメッセージを出力してプログラムを終了させる
abort() { { if [ "$#" -eq 0 ]; then cat - else echo "${txtred}${progname}: $*${txtreset}" fi } >&2 exit 1 } # 使い方 abort "error message"
コンソールにラインを引く
hr() { printf '%*s\n' "${2:-$(tput cols)}" '' | tr ' ' "${1:--}" } # 使い方 hr # 引数でラインのキャラクタを変えられる(デフォルトは-) hr "=" # 第2引数でラインの長さを変えられる(デフォルトはターミナルの幅いっぱい) hr "=" 10
参考:http://wiki.bash-hackers.org/snipplets/print_horizontal_line
文字列を小文字->大文字に変換する
upper() { echo -n "$1" | tr '[a-z]' '[A-Z]' } # 使い方 v=$(upper "abcdefg") echo $v
オプションとサブコマンドを扱うためのテンプレート
よくあるhoge.sh [<options...>] <command>
という形式のコマンドをbashで作るためのテンプレート。
#!/usr/bin/env bash set -eu progname=$(basename $0) progversion="0.1.0" # actions. usage() { echo "Usage: $progname [OPTIONS] COMMAND" echo echo "Options:" echo " -h, --help show help." echo " -v, --version print the version." echo " -d, --dir <DIR> change working directory." echo echo "Commands:" echo " help show help." echo } printversion() { echo "${progversion}" } # parse arguments and options. declare -a params=() for OPT in "$@" do case "$OPT" in '-h'|'--help' ) usage exit 0 ;; '-v'|'--version' ) # パラメータを取らないオプション printversion exit 0 ;; '-d'|'--dir' ) # パラメータを取るオプション。 "-d /tmp"のようにスペースで区切ってパラメータを渡す。 if [[ -z "${2:-}" ]] || [[ "${2:-}" =~ ^-+ ]]; then echo "$progname: option '$1' requires an argument." 1>&2 exit 1 fi optarg="$2" cd $optarg shift 2 ;; '--'|'-' ) shift 1 params+=( "$@" ) break ;; -*) echo "$progname: illegal option -- '$(echo $1 | sed 's/^-*//')'" 1>&2 exit 1 ;; *) if [[ ! -z "${1:-}" ]] && [[ ! "${1:-}" =~ ^-+ ]]; then params+=( "$1" ) shift 1 fi ;; esac done # サブコマンドに対応して処理を実行 command="" && [ ${#params[@]} -ne 0 ] && command=${params[0]} case $command in 'help' ) usage exit 0 ;; '' ) usage exit 0 ;; *) echo "$progname: illegal command '$command'" 1>&2 exit 1 ;; esac
参考:
Tips
標準出力を標準エラー出力にリダイレクト
echo "error" 1>&2
標準エラー出力を標準出力にリダイレクト
echo "error" 2>&1
プロセスIDを取得する($$)
echo $$ # 99807
コマンドの戻り値を取得する ($?)
[ "aaa" = "bbb" ] echo $? # 1
パイプでつないだコマンドの戻り値を取得する (${PIPESTATUS[0]})
$?
は最後に実行されたコマンドの戻り値なので、前述のindent
関数などをパイプでつなげた場合、もとの実行コマンドの戻り値は取れない。この場合はPIPESTATUS
を使う。
[ "aaa" = "bbb" ] | indent; status=${PIPESTATUS[0]} echo $status # 1
参考: パイプでつないだコマンドの戻り値を調べる@bash | Mazn.net
未定義の変数を使用するとそこでスクリプトを終了する (set -u)
set -u
基本的に次のset -e
も含めてset -eu
としておくのがよさそう。
コマンドがエラーだった場合そこでスクリプトを終了する (set -e)
set -e
しておくと、エラー(戻り値0以外を戻すコマンド実行)があると即時終了する
set -eの状態でエラー後も処理を続ける (&&:)
安全のため基本的にset -e
をしておいていいが、これはスクリプト内のすべてのコマンド実行の戻り値に対して効果をもつので、困ることも多い。
たとえばコマンド実行の結果をみてメッセージを出したい場合、以下のように書いたりする。
set -e /path/to/yourcommand if [ $? -eq 0 ]; then echo "yourcommand OK" else echo "yourcommand NG" fi
しかし/path/to/yourcommand
がエラーの時は即時終了してしまうので[ $? -eq 0 ];
は評価されず、結果yourcommand NG
は絶対に表示されない。
このような場合はコマンドに&&:
を続けておくとうまく動作する。(動作原理は下記参考のリンク先参照)
set -e /path/to/yourcommand &&: if [ $? -eq 0 ]; then echo "yourcommand OK" else echo "yourcommand NG" fi
参考:
`set -e` しているときにコマンドの戻り値を得る - Qiita
パイプで繋いだコマンドがエラーのとき終了させる
パイプでつなぐとset -e
をしててもエラー時に終了しないので以下のようにかく。
foo | indent; status=${PIPESTATUS[0]}; [[ ! $status -eq 0 ]] && exit $status
スクリプト終了時にコマンドを実行する
trapコマンドでシグナルをハンドリングできる
echo "start" trap "echo 'after end'" 0 echo "end"
参考: シグナルと trap コマンド | UNIX & Linux コマンド・シェルスクリプト リファレンス
一時ファイルを作る
# 一時ファイルを作成 tmpfile=$(mktemp -t prefix.XXXXXXXX) echo $tmpfile # 終了時に一時ファイルを削除 trap "rm $tmpfile" 0
osxとlinuxで挙動が違う。どちらでもひとまず動くバージョン linuxだとプリフィクスの末尾に"X"が必要でここがランダムな文字列に置き換わる。 osxだとXがリプレースされずそのまま使用され、その後にランダムな文字列が追加される
一時ディレクトリを作る
# 一時ディレクトリを作成 tmpdir=$(mktemp -d -t prefix.XXXXXXXX) echo $tmpdir # 終了時に一時ディレクトリを削除 trap "rm -rf $tmpdir" 0
rootユーザでのみ実行を許可する
user=`whoami` if [ $user != "root" ]; then echo "you need to run it on the 'root' user." 1>&2 exit 1 fi
標準入力からデータを受け取る
if [ -t 0 ]; then echo "stdin is not pipe" 1>&2 exit 1 else cat - fi | yourcommand
ヒアドキュメント
ヒアドキュメント内のシェル変数は展開される
cat << EOF This is a heredoc home is $HOME EOF # This is a heredoc # home is /Users/kohkimakimoto
変数は展開をさせたくないときはコロンで囲む
cat << 'EOF' This is a heredoc home is $HOME EOF # This is a heredoc # home is $HOME
ヒアドキュメントの内容を変数に入れる
heredoc=`cat << 'EOF' This is a heredoc home is $HOME EOF` echo $heredoc # This is a heredoc # home is $HOME
ヒアドキュメントの内容をファイルに出力する
cat << 'EOF' > heredocfile This is a heredoc home is $HOME EOF cat heredocfile # This is a heredoc # home is $HOME
スクリプトのロック(多重起動防止)
exec 9< $0 perl -mFcntl=:flock -e "open(LOCK,'<&=9');exit(!flock(LOCK,LOCK_EX|LOCK_NB))" || { echo "duplicate process." >&2 exit 1 }
参照: