1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

dokkuのソースコードでシェルスクリプトを勉強する

5月からの仕事がアプリケーション側でなくインフラ側になったので、急遽シェルスクリプトの勉強。powershellはちょいちょい扱ってはいたけども、Invoke-RestMethod(Linuxでのcurlコマンド)でjson取ってきて加工するみたいなことしかやってこなかったため知識不足甚だしい。
dokkuというbash200行ちょいでdockerベースのPaaS環境を構築できるプロジェクトがあり、せっかくなのでシェルスクリプトを学びつつPaaS構築のことも勉強できたら良いなと思い、dokkuのbootstrap.shのコードを見つつ勉強していきます。

bashスクリプトの先頭

#!/usr/bin/env bash
set -eo pipefail; [[ $TRACE ]] && set -x

シェルスクリプトの一行目に必ず記述する#!で始まる行はshebangと言われる。
ここで#!/bin/bashと指定すれば、絶対パスでのbash指定となります。

上記のように、#!/usr/bin/env bashと指定すれば、$PATH 上の bash が使われます。$PATHはコマンド検索パスを格納している環境変数printenv PATHで確認することができる。
メリットは、例えば $HOME/.opt 配下に最新の bash をインストールするなどした場合、$PATH にさえ入っていればそっちが使われるというのがあります。ちなみに$PATHに指定するときには、export PATH="$PATH:/opt/local/bin"とする。

set -eを宣言しておくとエラーが起きた行で中断するので、予想外のエラーを無視してスクリプトが処理を続行するのを防げます。ただ、パイプの中で起こったエラーは検知できず、右端のコマンドがエラーとなった時のみ有効です。set -o pipefailを指定することで、パイプ内のコマンドのエラーでも中断します。その2つを組み合わせてエラーを無視せず処理を中断するようにしたのが、set -eo pipefail

&&の前にあるコマンドを実行し、もし正常に終了した場合(戻り値が0)に&&の後ろにあるコマンドを実行する。set -xによってシェルのtraceが有効になる。実行したコマンドとその引数がトレース情報として標準エラー出力へ出力される。
[[ $TRACE ]]は調べたが分からなかった。

変数宣言と関数の書き方

main() {
  export DOKKU_DISTRO DOKKU_DISTRO_VERSION
  # shellcheck disable=SC1091
  DOKKU_DISTRO=$(. /etc/os-release && echo "$ID")
  # shellcheck disable=SC1091
  DOKKU_DISTRO_VERSION=$(. /etc/os-release && echo "$VERSION_ID")

  export DEBIAN_FRONTEND=noninteractive
  export DOKKU_REPO=${DOKKU_REPO:-"https://github.com/dokku/dokku.git"}

  ensure-environment
  install-requirements
  install-dokku
}

main "$@"

関数の書き方はfunction main() { ... }だが、functionは省略可能であり、通常省略されるため上記のソースのようになる。関数を呼び出すときはmain "$@"のように関数名を宣言すれば良い。引数として値を渡す場合には、関数名と並べて書いていく。今回の場合は、このスクリプトが呼び出された時の全ての引数をそのまま渡している($@は全ての引数を意味する)。
export 変数名環境変数の宣言となる。定数に関しては、Java命名規則と同じで定数+アンダースコアになる。 export DEBIAN_FRONTEND=noninteractiveのように環境変数の宣言と代入を同時に行うことができる。

変数の代入は変数=値の形となる。注意点としては、DOKKU_DISTRO = $(. /etc/os-release && echo "$ID")のように=の前後に空白を入れてしまうとうまく機能しません。

${変数値:-デフォルト値という書き方で変数を初期化することができる。export DOKKU_REPO=${DOKKU_REPO:-"https://github.com/dokku/dokku.git"}$DOKKU_REPOが存在していなければ、"https://github.com/dokku/dokku.git"で初期化する。

変数スコープとリダイレクト、終了ステータス

ensure-environment() {
  local FREE_MEMORY
  echo "Preparing to install $DOKKU_TAG from $DOKKU_REPO..."

  hostname -f > /dev/null 2>&1 || {
    echo "This installation script requires that you have a hostname set for the instance. Please set a hostname for 127.0.0.1 in your /etc/hosts"
    exit 1
  }

# 後半の処理は次のセクション

}

local FREE_MEMORYのようにlocalキーワードを変数宣言の前につけることによって、変数のスコープを関数内に狭めることができる。

echo は改行つきで値を表示するための命令。""(ダブルクォーテーション)の中では$変数または${変数}が展開されるが、''(シングルクォーテーション)の中では展開されない。

hostname -fでは、ホスト名とドメイン名からなるFQDNを表示する。
>command > /dev/null 2>&1これは標準エラー出力の結果を標準出力にマージして、/dev/nullに捨てることを意味する。つまり、標準出力された物も標準エラー出力された物も捨てるということ。詳しくは右記を参照。( 5分で一通り理解できる!Linuxのリダイレクト 使い方と種類まとめ

&&は左辺のコマンドが成功していたら右辺のコマンドも実行するものだが、||に関しては、左辺のコマンドが失敗していたら右辺のコマンドが実行される。このスクリプトの場合は、hostnameが設定されておらず、hostname -fのコマンドが評価され失敗したときに、「127.0.0.1ループバックアドレス)にhostnameを設定してください」とメッセージを出力し終了ステータス1をセットしてスクリプトを終了している。

コマンド終了時には「終了ステータス (exit-status)」と呼ばれるコマンドの成否を表す数値が特殊変数 $? に自動で設定される。一般的には、コマンド成功時には「0」、失敗時には「1」が設定される。exit コマンドに指定したパラメータ (0 もしくは 1~255 の正の整数値のみ可) が、そのシェルの終了ステータスとなる( bashの&&と||: みズとおかズ )。

パイプとawk、if文

ensure-environment() {
  
# 前半の処理は上のセクション

  FREE_MEMORY=$(grep MemTotal /proc/meminfo | awk '{print $2}')
  if [[ "$FREE_MEMORY" -lt 1003600 ]]; then
    echo "For dokku to build containers, it is strongly suggested that you have 1024 megabytes or more of free memory"
    echo "If necessary, please consult this document to setup swap: http://dokku.viewdocs.io/dokku/advanced-installation/#vms-with-less-than-1gb-of-memory"
  fi
}

grep 正規表現 ファイル名でファイル内の文字を検索し該当する行を抽出できる。/proc/meminfoは、カーネルが内部的に管理している枠組みでのメモリ情報を持っている。MemTotalカーネルが認識している全物理メモリを表す。
|(パイプ)はコマンドの入出力をコマンドへ引き渡す処理で、今回の場合はgrepで抽出した行MemTotal: 8069288 kBawkコマンドへ引き渡している。
awk 'パターン {アクション}' ファイル名で、テキストファイルを1行ずつ読み、パターンに合致した行に対して、アクションで指定された内容を実行する。パターンが指定されていない場合には、全ての行に対して処理を行う。テキストの各行を空白文字で区切って“フィールド”として処理するので、今回は1番目のフィールドがMemTotal:となり$1で表現できる。2番目のフィールドが8069288となり、これをprint(出力)している。

if文は、testを使用するもの、[(シングルブラケット)を使用するもの、[[(ダブルブラケット)を使用するものに分けられる。testを使用するのは直感的でないし他言語からくると若干困惑する。[(シングルブラケット)は、変数展開するときにダブルクォーテーションが必要であっったり、数値比較を行う場合には丸括弧で囲まなくてはいけないなどハマりポイントが多いため、[[(ダブルブラケット)を使用するべき。 test と [ と [[ コマンドの違い - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog

-lt比較演算子はless thanを意味しており、今回ではメモリが1003600kBより小さかった場合にメッセージを出力している。
ifの終了はfiキーワードを使用する。またthenキーワードは1行消費してしまい見にくいので条件文に;(セミコロン)をつけて続けて書いている。

case文

install-requirements() {
  echo "--> Ensuring we have the proper dependencies"

  case "$DOKKU_DISTRO" in
    debian|ubuntu)
      apt-get update -qq > /dev/null
      ;;
  esac
}

case文はcase 値 in 値1) 処理 ;; 値2) 処理 ;; esacと記述していく。この場合は、$DOKKU_DISTROdebianもしくはubuntuだった場合に、apt-get updateでパッケージ情報を更新している。-qqオプションを使用することによってエラー以外は表示しない。
パッケージをinstallする際に参照するインデックスファイルが存在しない可能性があるから?単純にパッケージリストを更新したいから?

dokkuのソースコードを追ってみる

install-dokku() {
  if [[ -n $DOKKU_BRANCH ]]; then
    install-dokku-from-source "origin/$DOKKU_BRANCH"
  elif [[ -n $DOKKU_TAG ]]; then
    local DOKKU_SEMVER="${DOKKU_TAG//v}"
    major=$(echo "$DOKKU_SEMVER" | awk '{split($0,a,"."); print a[1]}')
    minor=$(echo "$DOKKU_SEMVER" | awk '{split($0,a,"."); print a[2]}')
    patch=$(echo "$DOKKU_SEMVER" | awk '{split($0,a,"."); print a[3]}')

    use_plugin=false
    # 0.4.0 implemented a `plugin` plugin
    if [[ "$major" -eq "0" ]] && [[ "$minor" -ge "4" ]] && [[ "$patch" -ge "0" ]]; then
      use_plugin=true
    elif [[ "$major" -ge "1" ]]; then
      use_plugin=true
    fi

-n 文字列で文字列の長さが0より大きければ真となる。
sourceからdokkuのupgradeをする際に、DOKKU_BRANCHをmasterに設定することになっている( Dokku - The smallest PaaS implementation you've ever seen )。 設定されていた場合には、install-dokku-from-source関数が実行される。

通常インストールされる際には、以下のようなコマンドでインストールされる。
sudo DOKKU_TAG=v0.12.5 bash bootstrap.sh
この際にDOKKU_TAGに"v0.12.5"という値が入り条件が真となる。
${変数名//置換前文字列/置換後文字列}で置換前文字列に一致した全ての文字列が、置換される。今回の場合は"v"が排除される形となり、DOKKU_SEMVERには"0.12.5"という値が入る。
awkのsplit関数はsplit(分割する文字列,格納先の配列,分割文字列の指定)というような使い方をする。配列は(0, 12, 5)となりmajorには"0"、minorには"12"、patchには"5"の値が入る。

# install-dokku()の続き

    # 0.3.13 was the first version with a debian package
    if [[ "$major" -eq "0" ]] && [[ "$minor" -eq "3" ]] && [[ "$patch" -ge "13" ]]; then
      install-dokku-from-package "$DOKKU_SEMVER"
      echo "--> Running post-install dependency installation"
      dokku plugins-install-dependencies
    elif [[ "$use_plugin" == "true" ]]; then
      install-dokku-from-package "$DOKKU_SEMVER"
      echo "--> Running post-install dependency installation"
      sudo -E dokku plugin:install-dependencies --core
    else
      install-dokku-from-source "$DOKKU_TAG"
    fi
  else
    install-dokku-from-package
    echo "--> Running post-install dependency installation"
    sudo -E dokku plugin:install-dependencies --core
  fi
}