4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

サーバー作業を爆速かつ安全にする、Enterキー主体の1行確認実行スクリプトの設計思想

4
Posted at

はじめに

サーバー作業の現場には、昔からこんなジレンマがあります。

  • 手順書のコマンドを1行ずつコピペ実行 → 貼り間違い・行飛ばし・二重実行のリスクが常につきまとう
  • スクリプト化して一気に流す → 途中で想定外の出力が出ても止まれない。本番でこれをやる勇気はない

この中間解として「スクリプトを1行ずつ、目視確認しながら実行するラッパー」を作りました。それ自体はよくある発想です。ただ、実際にサーバー上で使い込んでみると、確認UIの設計が想像以上に作業の質を左右することが分かりました。

最初に素直に作った「y/n確認方式」が実運用でどう不評だったか。そしてなぜ「Enterキー一本+コメント行自動スキップ+前後5行表示」に落ち着いたのか。80行ほどの小さなスクリプトを題材に、確認UIの設計を掘り下げます。コード全文は記事の後半に載せています。

最初の設計: y/n確認+進捗ファイル方式

最初の要件はシンプルでした。

スクリプト名を引数に受けて、1行ずつ実行するbashを作りたい。各行の実行前に前後の行を表示して、run?[y/n] で確認する。

これに加えて「途中でやめたら、次回は続きから再開したい」という要望があったので、初版はこう作りました。

  • 各行の実行前に run?[y/n]>> と確認。y で実行、n で終了
  • y のたびに進捗ファイル ask_<スクリプト名>.txt へ実行済み行番号を記録
  • 再実行時に進捗ファイルがあれば「前回の続き[3]から実行しますか?[y/n]」と確認

実行イメージはこうです。

$ sh ask.sh deploy.sh
[deploy.sh]を実行開始しますよろしいですか?[y/n]>> y
-----
>1:systemctl stop myapp
 2:cp -p app.war /opt/tomcat/webapps/
run?[y/n]>>y
-----
(実行結果)
-----
 1:systemctl stop myapp
>2:cp -p app.war /opt/tomcat/webapps/
 3:systemctl start myapp
run?[y/n]>>y
...

コメント込みで約65行。テストも通り、この時点では「まあ、こんなものだろう」と思っていました。

実サーバーで使って分かった3つの不満

作ったスクリプトを実際の検証サーバーに転送して手順書ベースの作業に使ってみると、机上では気づかなかった不満が立て続けに出てきました。

不満1: y/n入力がリズムを壊す

これが最大の問題でした。1行実行するたびに「y を押して Enter」の2打鍵。数十行の手順書なら、この儀式を数十回繰り返すことになります。

しかも冷静に考えると、n の存在意義がほとんどないのです。実行を止めたい場面とは「出力がおかしい」「手が滑りそう」といった緊急時であり、そのとき人間が反射的に押すのは n ではなく Ctrl+C です。つまり n は「使われない選択肢のために、毎回1打鍵を余分に要求する」機能でした。

実際に使ったユーザーからのフィードバックは、たった一言でした。

run?のときに[Enter]にしてください。Ctrl+Cで強制終了することは実行初期に記載のみでOK

不満2: 進捗ファイルは「状態管理」が増えるだけ

途中再開のための進捗ファイルは、一見便利そうで、実は運用の負債でした。

  • 作業ディレクトリに ask_deploy.sh.txt のようなファイルが残る(消し忘れると次回の実行に化けて出る)
  • 手順書スクリプト自体を修正すると行番号がズレて、進捗ファイルが嘘をつく
  • 「続きから再開しますか?」という確認が1つ増える

サーバー作業では「手順の3行目まで実行して、問題があったので手順書を直して再実行」がよく起こります。このとき機械が覚えている「前回の続き」は当てになりません。どこから再開すべきかは人間が一番よく知っているのです。

不満3: コメント行にまで確認が出る

実際の手順書スクリプトには、コメントや空行が大量に含まれます。実サーバーでの実行ログ(一般化しています)がこちらです。

$ sh ask.sh deploy.sh
[deploy.sh]を実行開始しますよろしいですか?[y/n]>> y
※途中で強制終了する場合は Ctrl+C を押してください
開始行を入力してください(Enterで最初から)>>
-----
>1:# アプリ停止
 2:systemctl stop myapp
run?[Enter]>>
-----
-----
 1:# アプリ停止
>2:systemctl stop myapp
 3:
run?[Enter]>>^C

1行目のコメント # アプリ停止 に対して run? と聞かれています。コメントの「実行」に確認を求められても困りますし、空行も同様です。手順書の半分がコメントだとすると、確認回数が実コマンド数の2倍になってしまいます。

改善1: y/n を捨てて Enter に一本化

フィードバックを受けて、確認プロンプトをこう変えました。

run?[y/n]>>y⏎    (2打鍵 × 行数)
    ↓
run?[Enter]>>⏎   (1打鍵 × 行数)

打鍵数が半分になった、という表面的な話以上に、これはメンタルモデルの転換だと考えています。

  • y/n方式: 「この行を実行してよいか?」という判断を毎回要求する。人間は Yes/No の意思決定を強制される
  • Enter方式: 「読んだら進む」というページ送り。人間の仕事は判断ではなく確認(読むこと)に集中する

数十行の作業で毎回「Yes」を意思決定していると、確実に慣れが生じて y が無意識の連打になります。こうなると確認の意味がありません。それなら最初から「Enterはページ送り、異常を見つけたときだけ Ctrl+C」と役割を分けたほうが、指は楽をして、目は仕事をする状態を維持できます。

中断手段を Ctrl+C に委譲したことで、コードもシンプルになりました。trap すら書いていません。開始時に1回だけ案内を出すだけです。

echo "※途中で強制終了する場合は Ctrl+C を押してください"

「n を実装しない」「trap を書かない」は手抜きではなく、シェル標準のインターフェース(Ctrl+C=SIGINT)に中断を任せるという設計判断です。独自の中断方法を作るほど、緊急時に「えっと、この場合どう止めるんだっけ」という迷いが生まれます。

改善2: 状態を持たない再開設計

進捗ファイル方式は廃止して、開始時に「どこから始めるか」を人間が入力する方式にしました。

開始行を入力してください(Enterで最初から)>>
  • Enterのみ → 1行目から
  • 数字(例: 14) → 14行目から
  • 範囲外・数字以外 → 再入力を促す

面白いのは、機能を置き換えた結果コードが約10行減ったことです(進捗ファイルの生成・記録・再開確認・完走時の削除、という一連の状態管理が丸ごと不要になったため)。再開機能を置き換えたらコードが減る。状態をファイルではなく人間側に持たせたからです。

進捗ファイル方式 開始行入力方式
再開位置の記憶 機械(ファイル) 人間
手順書を修正した場合 行番号がズレて破綻 人間が読み替えるだけ
残骸ファイル 残る(消し忘れリスク) なし
コード量 約65行 約63行

サーバー作業のような「実行のたびに状況が違う」用途では、中途半端に賢い状態管理より、ステートレス+人間への問い合わせ1回のほうが堅牢でした。

改善3: 「読める」表示の工夫

Enter連打で進める設計にした以上、目視確認の質は表示がすべてです。ここには3つの工夫を入れました。

(1) コメント行・空行は「表示するが実行しない」

# 始まりの行(インデント付き含む)と空行は、run? プロンプトを出さずに自動で次の実行対象行まで進みます。ただし前後表示には行番号付きでそのまま出します。

 1:# アプリ停止
>2:systemctl stop myapp
 3:
 4:cp -p app.war /opt/tomcat/webapps/
 5:# アプリ起動
 6:systemctl start myapp

run?[Enter]>>

コメントを表示から消してしまうと、手順書の文脈(「いま何のセクションを実行しているのか」)が失われます。実行はスキップ、表示は残す。この非対称が「流れを追いながらサクサク進む」感覚を作ります。

(2) 前後5行のコンテキスト表示

当初は前後1行(計3行)でしたが、実際に使うと「次に何が来るか」が見えず不安でした。前後5行(計最大11行)に広げたことで実行前に数手先まで目に入るようになり、「この先に rm があるな」といった予期が働きます。ミスを防ぐのは確認ダイアログではなく、この予期です。

(3) 現在行マーカーと桁揃え

現在行に > を付け、それ以外の行は行頭に半角スペースを入れて桁を揃えています。地味ですが、Enter連打の高速な流れの中で「いま実行されるのはどの行か」を一瞬で識別するには、この視覚的アンカーが効きます。

最終形のコード

最終版の全文です(84行、うちロジック本体は約60行)。sh ask.sh <対象スクリプト> で使います。

#!/bin/sh
# ask.sh : 指定したスクリプトを1行ずつ確認しながら実行する
#
#   開始時に開始行を入力できる(Enter だけなら最初の行から)。
#   run?[Enter] : Enter を押すとその行を実行して次の行へ
#   コメント行(# 始まり)と空行は実行せず自動スキップする(表示はされる)
#   途中でやめたいときは Ctrl+C で強制終了する

usage() {
    cat <<EOF
使い方: sh ask.sh <script>
  <script> を1行ずつ、実行前に前後5行を表示して Enter で実行します。
  開始時に開始行を入力できます(Enterで最初から)。
  コメント行(#)と空行は実行せずスキップします。
  途中で終了する場合は Ctrl+C を押してください。
EOF
    exit 1
}

[ $# -eq 1 ] || usage
[ -f "$1" ] || { echo "エラー: $1 が見つかりません"; exit 1; }

script=$1
total=$(grep -c '' "$script")  # 総行数

# $1 行目の内容を返す(Windows の CRLF 改行の \r は除去)
line() { sed -n "${1}p" "$script" | tr -d '\r'; }

# $1 行目が実行対象外(空行・空白のみ・# 始まりのコメント行)なら真
skip() {
    case "$(line "$1" | sed 's/^[[:space:]]*//')" in
        ''|\#*) return 0 ;;
        *)      return 1 ;;
    esac
}

# y/n を y か n が入力されるまで聞く(y:0 / n:1 を返す。EOF は n 扱い)
yn() {
    while :; do
        printf '%s' "$1"; read a || return 1
        case $a in y) return 0 ;; n) return 1 ;; esac
    done
}

yn "[${script}]を実行開始しますよろしいですか?[y/n]>> " || exit 0
echo "※途中で強制終了する場合は Ctrl+C を押してください"

# 開始行を入力で受け付ける(Enterのみ:1行目から / 1〜総行数の数字:その行から)
while :; do
    printf '開始行を入力してください(Enterで最初から)>> '
    read s || s=
    [ -z "$s" ] && { start=1; break; }
    case $s in
        *[!0-9]*) ;;  # 数字以外を含む場合は再入力
        *) [ "$s" -ge 1 ] && [ "$s" -le "$total" ] && { start=$s; break; } ;;
    esac
    echo "1〜${total}の数字を入力してください"
done

# メインループ : 前後5行を表示 → Enter 待ち → 実行(コメント行・空行は飛ばす)
i=$start
while [ "$i" -le "$total" ]; do
    if skip "$i"; then i=$((i+1)); continue; fi
    echo "-----"
    from=$((i-5)); [ "$from" -lt 1 ] && from=1
    to=$((i+5));   [ "$to" -gt "$total" ] && to=$total
    j=$from
    while [ "$j" -le "$to" ]; do
        if [ "$j" -eq "$i" ]; then
            echo ">${j}:$(line "$j")"
        else
            echo " ${j}:$(line "$j")"
        fi
        j=$((j+1))
    done
    echo
    printf 'run?[Enter]>>'
    read a || exit 0
    echo "-----"
    eval "$(line "$i")"
    i=$((i+1))
done

echo "${script}を実行終了します"

実装上のポイントをいくつか補足します。

eval で「同一シェル内」実行する理由

各行の実行は sh -c ではなく eval にしています。これにより行をまたいで変数や cd が引き継がれます。手順書スクリプトでは

TARGET_DIR=/opt/myapp/releases
cd $TARGET_DIR

のような書き方が普通に出てくるので、1行ごとに子シェルで実行してしまうと手順書が成立しません。逐次実行のラッパーであっても、実行環境は「1つのシェルで上から流した場合」と同じでなければならない、という要件です。

なお eval を使う以上、対象スクリプトは信頼できるもの(自分たちの手順書)に限るのは大前提です。ask.sh は権限昇格をしない「実行の進行役」であり、サンドボックスではありません。

skip判定は「先頭の空白を剥がしてから」

コメント判定は単純な # 前方一致ではなく、先頭の空白を除去してから判定しています。

case "$(line "$1" | sed 's/^[[:space:]]*//')" in
    ''|\#*) return 0 ;;   # 空行・空白のみ・#始まり → スキップ
esac

インデントされたコメントや、スペースだけの行も確実にスキップするためです。実際の手順書はきれいに整形されているとは限りません。

CRLF 対策を読み取り側に入れる

Windows で編集した手順書を Linux サーバーへ転送すると、CRLF 改行が混ざりがちです。行末の \r が残っていると eval 時に謎のエラーになるため、行の読み取り関数で常に除去しています。

line() { sed -n "${1}p" "$script" | tr -d '\r'; }

「実行前に dos2unix してください」という運用ルールにするのではなく、ツール側が吸収するほうが確実です(ただし ask.sh 自体が CRLF 化した場合は別なので、そこは転送時に注意が必要です)。

bash ではなく POSIX sh で書く

シバンは #!/bin/sh で、配列や [[ ]] などの bashism を使っていません。古めの環境や BusyBox 系でも動かせるようにするためで、「どのサーバーに転送しても動く」ことが道具としての信頼につながります。

まとめ: 確認UIの設計原則3箇条

80行のスクリプトですが、実運用のフィードバックを経て、確認UIについて次の3つの原則に行き着きました。

  1. 判断させない — 毎行の Yes/No は数十回目には無意識の連打になる。「Enter=ページ送り、Ctrl+C=緊急停止」と役割を分け、人間の注意は「読むこと」に使う
  2. 状態を持たない — 再開位置のような「実行のたびに変わる状態」は、ファイルに覚えさせるより人間に聞くほうが堅牢。機能を捨てたらコードも減った
  3. 文脈を見せる — 確認の質は表示で決まる。前後5行+現在行マーカーで「数手先の予期」を働かせ、コメント行は表示に残して手順の流れを保つ

「確認します」というと y/n ダイアログを条件反射で作りがちですが、確認とは判断の強制ではなく、止まれる流れを作ること。この視点は、CLIツールに限らず、デプロイパイプラインの承認フローなどにも通じる気がしています。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?