Bashスクリプトのチートシートと便利なスニペットまとめ
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
便利なスニペット
実行スクリプトがあるディレクトリを絶対パスで取得する
SOURCE="${BASH_SOURCE[0]}" while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done bin_dir="$( cd -P "$( dirname "$SOURCE" )/" && pwd )" # /usr/local/bin # などがbin_dirに取得できる
以下でも良い。
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")"
シンボリックに対応しないなら、以下の記述でもOK。
# スクリプトがシンボリックリンクから呼び出されたときは、リンクを辿らず、リンク先のパスを返す bin_dir=$(cd $(dirname $0); pwd)
参考:
https://github.com/hashicorp/consul/blob/master/scripts/build.sh
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 }
参照:
yum updateでエラー(Error: Package: 2:irqbalance-1.0.7-5.el6.x86_64)
古いCentOS6のサーバにyum update
をかけたら依存の問題で以下のエラーがでて、更新できなかった。
# yum update ... Error: Package: 2:irqbalance-1.0.7-5.el6.x86_64 (base) Requires: kernel >= 2.6.32-358.2.1 Installed: kernel-2.6.32-71.el6.x86_64 (@anaconda-CentOS-201106060106.x86_64/6.0) kernel = 2.6.32-71.el6 kernel = 2.6.32-71.el6 Installed: kernel-2.6.32-71.29.1.el6.x86_64 (@updates) kernel = 2.6.32-71.29.1.el6 kernel = 2.6.32-71.29.1.el6
カーネルが古すぎて、あたらしいカーネルを必要とするパッケージが入れられないのはわかるので、yum update kernel
などを行ってみたが
No Packages marked for Update
と表示されてアップデートできず。調べたところ/etc/yum.conf
にカーネルアップデートを除外するよう、デフォルトで設定が書かれていたからと判明する。
[main] cachedir=/var/cache/yum/$basearch/$releasever keepcache=0 debuglevel=2 logfile=/var/log/yum.log exactarch=1 obsoletes=1 gpgcheck=1 plugins=1 installonly_limit=5 bugtracker_url=http://bugs.centos.org/set_project.php?project_id=16&ref=http://bugs.centos.org/bug_report_page.php?category=yum distroverpkg=centos-release # This is the default, if you make this bigger yum won't see if the metadata # is newer on the remote and so you'll "gain" the bandwidth of not having to # download the new metadata and "pay" for it by yum not having correct # information. # It is esp. important, to have correct metadata, for distributions like # Fedora which don't keep old packages around. If you don't like this checking # interupting your command line usage, it's much better to have something # manually check the metadata once an hour (yum-updatesd will do this). # metadata_expire=90m # PUT YOUR REPOS HERE OR IN separate files named file.repo # in /etc/yum.repos.d exclude=kernel*
最後のexclude=kernel*
をコメントアウトしたら、無事アップデートできた。
IntelliJからMacの辞書(Dictionary.app)を開くプラグインをつくった
Macの辞書アプリはかなり便利で、何かキーワードを選択している状態でCTRL+CMD+Dのショートカットを押すとそのキーワードで辞書を引いてくれる。 主に英語ドキュメントなどにある分からない英単語を調べるのに使うのだけど、メインに使っているエディタのAtomとIntelliJ IDEAだとなんでかこのキーバインドが効かなくて残念な思いをしてた。そこでしばらく前にAtomから辞書を引けるようにしたパッケージとして以下を作った。
そしてここ最近Go言語をIntelliJ IDEAで書くようになったので、こっちでも辞書を引けるようにしたくて、さきほどちょちょいと作ってみた。
実装にあたってAtom版もIntelliJ版も両方ともすでに存在していたDashのプラグインを参考にした。 これにはエディタの選択領域からテキストを抽出して、外部コマンドとしてDashを起動するというコードある。 私がやったことは、そのあたりを参考元からを切りだしてDictionary.appを叩くように変えただけのもの。よって細かいところなどにはまるで気を使っていない実装なのだけど、個人的に使う分には必要十分に動くのでこれで満足している(^o^)
なお、Atomの方はパッケージの登録までしているのでAtomの設定画面から直接インストールできるが、IntelliJの方はJarをGithubのリリースページにおいただけなので、インストールするにはJarをダウンロードしてローカルファイルからインストールする必要があります。
sshのラッパーコマンドを作った
GoでSSHコマンドに便利機能を追加したコマンドを作りました。
2017/04/11 追記: この記事の内容は古くなっています!
2017/04/11時点の最新の仕様は、次の記事を参照してください
SSHラッパーコマンドEsshのv1.0.0をリリースしました - オープンソースこねこね
追記ここまで。以下は2015/11時点の古い情報となります。
機能として
- Luaで
~/.ssh/config
に相当する設定を書ける。 - zshの補完機能を使って、接続先一覧を出す。
- サーバ接続時にフックを仕込める。自分はスクリーンの色を変えるの使っている。
- 複数のリモートサーバにまとめてコマンドを実行する。
といったところです。詳細はリポジトリのREADMEを見ていだければと思います。 ビルド済みバイナリをリリースページにおいてあるので、インストールはダウンロードして解凍してパスの通ったディレクトリに配置すればOKです。
~/.ssh/config
を上書きするので~/.ssh/config
のバックアップを取っておくのをお忘れなく。
https://github.com/kohkimakimoto/zssh/releases/latest
使い方
zssh
はssh
のラッパーコマンドになっているのでssh
コマンドと同様に使えます。zssh
を実行するとssh
コマンドで実際の処理を実行する前に、設定ファイル~/.ssh/zssh.lua
を読み込んで~/.ssh/config
を生成するしくみになっています。
~/.ssh/zssh.lua
に以下のような設定を書いておくと
Host "web01.localhost" { ForwardAgent = "yes", HostName = "192.168.0.11", Port = "22", User = "kohkimakimoto", -- 小文字で始まる設定は、~/.ssh/configに出力されない。descriptionはzsh補完の説明文に使用される(後述) description = "my web01 server", } Host "web02.localhost" { ForwardAgent = "yes", HostName = "192.168.0.12", Port = "22", User = "kohkimakimoto", description = "my web02 server", }
zssh
実行時に次のような~/.ssh/config
を自動生成して上書きします。
Host web01.localhost ForwardAgent yes HostName 192.168.0.11 Port 22 User kohkimakimoto Host web02.localhost ForwardAgent yes HostName 192.168.0.12 Port 22 User kohkimakimoto
これで、sshコマンドと同様に以下のようにしてサーバにSSH接続できます。
zssh web01.localhost
ZSH補完
zsh補完をサポートしているので、以下のコードを~/.zshrc
に書いておくと
eval "$(zssh --zsh-completion)"
上に貼り付けたアニメgifのようにサーバが説明文付きで候補にでるようになります。
フック
hooks
の設定でサーバ接続時と切断時にローカルでLuaのコードを実行できます。
os.execute
でコマンドを実行できるので、以下のようにするとMacのターミナルの色を変更できます。
Host "web01.localhost" { HostName = "192.168.0.11", Port = "22", User = "kohkimakimoto", ForwardAgent = "yes", description = "my web01 server", -- フックの設定 hooks = { before = function() -- This is an example to change screen color to red. os.execute("osascript -e 'tell application \"Terminal\" to set current settings of first window to settings set \"Red Sands\"'") end, after = function() -- This is an example to change screen color to black. os.execute("osascript -e 'tell application \"Terminal\" to set current settings of first window to settings set \"Pro\"'") end, } }
ちなみに、ターミナルの色を変える方法は以前にも書きましたので、よければそちらも参照ください。
http://kohkimakimoto.hatenablog.com/entry/2015/04/02/211232
マクロ
コマンドを複数サーバにまとめて実行できます。
Host "web01.localhost" { HostName = "192.168.0.11", Port = "22", User = "kohkimakimoto", ForwardAgent = "yes", description = "my web01 server", tags = { role = "web" }, } Host "web02.localhost" { HostName = "192.168.0.12", Port = "22", User = "kohkimakimoto", ForwardAgent = "yes", description = "my web02 server", tags = { role = "web" }, } Macro "example" { -- 並列実行するか? parallel = true, -- 実行前に確認プロンプトを出す。 confirm = "Are you OK?", -- zsh補完時の説明 description = "example macro", -- 実行先サーバを指定。Host設定のtagsで設定したタグを指定する。指定しないとローカルでの実行になる。 on = {role = "web"}, -- ttyを使うか? tail -f でログを監視するときなどはtrueに tty = false, -- コマンドの内容 command = [[ ls -la ]], }
マクロ名を指定して実行できます。
$ zssh example
(メモ)iOSアプリのローカライズ
Xcodeでプロジェクトの設定からLocalizationsのセクションの[+]ボタンをクリックして言語を追加する。
Goでgraceful restartに対応したデーモンプロセスをつくる
Goで書かれたWebアプリのプロセスをデーモンにしたり、ダウンタイムなしでデプロイできるようにするための情報をいろいろ調べていたのですが、どうもSupervisorなど外部のツールを使ったりするのが定番か、herokuなどのPaaSにまかせてしまうという手法が多いようです。 でも個人的にGoの一番気に入っているところは依存のない独立した単一バイナリを生成できる点なので、Webアプリもなるべく外部に依存せずにどうにかできないものかと調査していました。
さてGoのデーモン化とgracefulのライブラリはそれぞれいくつかあるのですが、両方に対応したライブラリは見つからず、組み合わせたサンプルなども見当たらなかったので、各種ライブラリのソースを読んだりしながら、最終的に以下のものを取り上げてみました
- daemonize
- graceful restart
この2つを組み合わせてgraceful restartできるデーモンを作ろうとしたわけですが、上記のライブラリではdaemonziseもgracefulも内部的にforkの代わりにos.StartProcess
で外部コマンドとして自分自身を再起動し、環境変数を使って状態の制御をおこなうということをやっています。一緒に使うと、その辺が干渉してうまく動かない。。。
で、いろいろワークアラウンドをいれて、ひと通り動くようになったのが以下のコードです。やっていることはコード内のコメントを参照してください。
package main import ( "flag" "fmt" "io/ioutil" "log" "net/http" "os" "path/filepath" "strconv" "time" "github.com/VividCortex/godaemon" "github.com/facebookgo/grace/gracehttp" "github.com/kardianos/osext" ) var ( logfile = flag.String("l", "goserver.log", "log file") pidfile = flag.String("p", "goserver.pid", "pid file") ) func main() { flag.Parse() // os.Args[0]を絶対パスに書き換える。 // デーモナイズ後、カレントディレクトリが変わるので、 // 相対パスのままだとgracehttpがos.StartProcessするとき自分自身を指し示すファイルパスを解決できない。 bin, err := osext.Executable() if err != nil { log.Fatal(err) } os.Args[0] = bin // pidとlogのファイルのパスも絶対パスにする。 logfilepath, err := filepath.Abs(*logfile) if err != nil { log.Fatal(err) } pidfilepath, err := filepath.Abs(*pidfile) if err != nil { log.Fatal(err) } // デーモナイズする // "LISTEN_FDS"があるときはgraceful restart時なので、スキップさせる。 if os.Getenv("LISTEN_FDS") == "" { godaemon.MakeDaemon(&godaemon.DaemonAttr{}) } // 以下のロジックはデーモナイズ後の状態で実行される。 // ログの出力先をファイルに。 f, err := os.OpenFile(logfilepath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatal(err) } defer f.Close() log.SetOutput(f) // pidファイルのプロセスIDを書き込む pid := os.Getpid() if err = ioutil.WriteFile(pidfilepath, []byte(strconv.Itoa(pid)), 0666); err != nil { log.Fatal(err) } log.Printf("Generated pidfile %s (%d)\n", pidfilepath, pid) // WebAppサーバ mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { time.Sleep(5 * time.Second) // 動作検証用に5秒リクエストをつかみっぱなしにする(restartしても切れないことを見る) fmt.Fprintf(w, "hello world!") }) server := &http.Server{Addr: ":1234", Handler: mux} // graceful restartをサポートして起動 gracehttp.Serve(server) }
github.com/kardianos/osextは実行ファイルの絶対パスを取得するための便利ライブラリです。
これをビルドし起動した後、graceful restartするには-USR2
シグナルを送る。
kill -USR2 5678
シャットダウンもリクエストが途中で切れないgraceful shutdownになる。
kill 5678
動作させてみたところうまく動いているようではあるが、os.Args[0]
を書き換えたりしてキモいコードになっている。
daemonizeとgracefulをまとめて、ルーティングとmiddlewareまわりの構造を提供してくれる軽量フレームワークがあったらいいなあ、と妄想しています。