🌌

便利なShellの書き方

に公開
1

Shell スクリプトを書くときの個人的なテンプレ、ノウハウをまとめました

おまじない

ほぼ全Shellファイルに書いてるおまじない

  • !/bin/bash はシェバングと言って、スクリプトを実行するシェルをOSに指定するもの
  • set -xue はスクリプトの実行時にエラーを出力するための設定
    • -x 実行コマンドの表示
    • -u 未定義の変数を使用したときにエラーを出す
    • -e コマンドが失敗したときにスクリプトを終了する
  • $(cd "$(dirname "$0")"; pwd) はshファイルの配置ディレクトリに移動&絶対パスを取得するための処理
#!/bin/bash
set -xue

BASE_DIR=$(cd "$(dirname "$0")"; pwd)

オプション系

オプションが多い場合は改行する

オプションが多い場合は改行して、可読性を上げるようにしています

aws lambda invoke \
  --function-name "hoge" \
  --payload '{"key": "value"}' \
  --cli-binary-format "raw-in-base64-out" \
  --region "ap-northeast-1" \
  ./result.json

オプションはロング形式で書く

可読性観点でShellファイルはコマンドのオプションをロング形式で書くようにしています

# 短いオプション
cp -rfb ./hoge.txt ./tmp

# 長いオプション
cp --recursive --force --backup ./hoge.txt ./tmp

使いまわす or 動的に変わるオプションは変数に格納する

使いまわす or 動的に変わるオプションは変数に格納して、一貫性 & 再利用性を上げるようにしてます

  • どうしても可読性が下がってしまうので、変数名はわかりやすくする
  • 利用時には必ずダブルクオーテーションで括る
# 変数無し
cp --recursive --force --backup "~/hoge.txt" "~/tmp"

# 変数あり (名前微妙。。。)
OPTIONS="--force --backup"
cp --recursive "$OPTIONS" ~/hoge.txt ~/tmp

# 変数に動的なオプション (Debug / Verbose) を格納する
if [ "$1" = true ]; then
  PRINT_LEVEL="--debug"
fi
cp --recursive "$PRINT_LEVEL" ~/hoge.txt ~/tmp

パス系

可能な限りパスは絶対パスで書く

可能な限り相対パスを使わず、絶対パスで書くようにしています
相対パスを使うと処理の冪等性が失われる可能性があるため

  • 冪等性: 同じ処理を何度実行しても結果が変わらないこと
# 相対パス
cp ./hoge.txt ./tmp

# 絶対パス
cp ~/hoge.txt ~/tmp

mkdirのときは -p オプションをつける

ディレクトリを作成するときは、-p オプションをつけて、親ディレクトリが無い場合でもエラーにならないようにしています

mkdir -p ~/tmp

使いまわすパスは変数に格納する

使いまわすパスは変数に格納して、可読性 & 再利用性を上げるようにしてます

  • このとき変数のは必ずダブルクオーテーションで括り、スペースを含むパスでも正しく動作するようにしておく
# 変数無し
cp /opt/app/dir/hoge.txt /opt/app/dir/tmp

# 変数あり
TMP_DIR="/opt/app/dir"
cp "$TMP_DIR/hoge.txt" "$TMP_DIR/tmp"

ファイル書き出し

適したものを使うようにしています。

  • 単一行のテキストを書き出すとき
    • echo を使う
    • tee を使うと標準出力とファイルに同時に書き出すことができる
  • 複数行のテキストを書き出すとき
    • cat <<EOF を使う。※インデント崩れが生じるので注意
    • { echo "text1" && echo "text2" } >> <ファイル名>

cat <<EOF > <ファイル名>

ANY_TEXT="Hello, World!"

cat <<EOF > ~/hoge.txt
This is a sample text file.
$ANY_TEXT
EOF

# ブロックの中で使うときでも、左に寄せなければいけない
function write_text() {
  if [ -f ~/hoge.txt ]; then
    cat <<EOF >> ~/hoge.txt
This is a sample text file.
This is a sample text file.
This is a sample text file.
EOF
  fi
}

echo

# 1行ファイル
echo "Hello, World!" >> ~/hoge.txt

# 複数行ファイル
{ echo "text1" && echo "text2" } >> ~/hoge.txt

tee

# 標準出力とファイルに同時に書き出す
echo "Hello, World!" | tee ~/hoge.txt

sed/awk

趣旨とややズレるが、既存のファイルの内容を編集し、その結果を新しいファイルに保存する方法

# sedを使ってファイルの内容を置換する
# old_text を new_text に置換して新しいファイルに保存する
sed 's/old_text/new_text/g' ~/hoge.txt > ~/new_hoge.txt

# awkを使ってファイルの内容を加工する
# hoge.txt の1列目と3列目を抽出して新しいファイルに保存する
awk '{print $1, $3}' ~/hoge.txt > ~/new_hoge.txt

# 各行に"Price:"を追加して新しいファイルに保存
awk '{print "Price: " $0}' ~/hoge.txt > ~/new_hoge.txt

エラー処理

|| { echo "エラーメッセージ"; exit 1; }

コマンドが失敗したときにエラーメッセージを出力してスクリプトを終了する方法

# コマンドが失敗したときにエラーメッセージを出力してスクリプトを終了する
cp ~/hoge.txt ~/tmp || {
  echo "Failed to copy file"
  exit 1
}

trap

trap コマンドを使って、異常終了時に特定の処理を実行する方法

function trace() {
  echo "Script Error!"
  exit 1
}
trap trace ERR

# 失敗する可能性のあるコマンド
echo "Start!"
cp ~/hoge.txt ~/tmp # ここでエラーが発生すると、trace関数が呼ばれる
echo "End!"

# エラー発生時
# > Start!
# > Script Error!
# 'exit 1' なので、End! は出力されない

引数

デフォルト値を設定する

スクリプトの引数にデフォルト値を設定する方法

  • Shellの引数は $1, $2, ... で参照する
    • $0 はスクリプト自体の情報が入る
  • ${1:-"default_value"} のように書くことで、引数が指定されていない場合はデフォルト値を設定することができる
    • :- は「もし値が空っぽならば、デフォルト値を使う」という意味
    • それ以上の仕組み?知らんなあ。。。
引数が指定されていない場合はデフォルト値を設定する
ARG1=${1:-"default_value"}
ARG2=${2:-"default_value"}

サブコマンド

awsgit のようなサブコマンドを使う場合の書き方

  • case文を使って、サブコマンドごとに処理を分岐させる
  • caseの行数が多くなってしまうため、関数に分けておくことを強く推奨

SUBCOMMAND=${1:-"default_subcommand"}

function start() {
  echo "Starting..."
  # startの処理
}

function stop() {
  echo "Stopping..."
  # stopの処理
}

case "$SUBCOMMAND" in
  start)
    start
    ;;
  stop)
    stop
    ;;
  *)
    echo "Unknown command: $SUBCOMMAND"
    exit 1
    ;;
esac

所感

困ったら man しておけばおk
にしてもPowerShellに結構影響を受けているなぁ🤔

https://learn.microsoft.com/ja-jp/powershell/scripting/community/contributing/powershell-style-guide?view=powershell-7.5

Discussion