mcpField
S
shell-scripting

shell-scripting

github.com/sammcj開発167160 回インストール1 日前に更新
リポジトリ

shell-scripting について

shell-scriptingは、信頼性と保守性の高いBashスクリプトを書くためのガイダンススキルです。Shellに適した作業の見極めや、防御的な書き方、自動チェック、複雑さの判断基準を扱います。

主な機能

  1. 01厳格モードと移植性のあるshebang
  2. 02ShellCheckの指摘への対応
  3. 03変数の引用と入力検証
  4. 04配列と後処理の安全な扱い
  5. 05組み込み機能とプロセス置換の活用
  6. 06PythonやGoへ切り替える判断

活用シーン

  1. 01小規模な自動化やラッパースクリプトの作成
  2. 02Bashスクリプトの一般的な安全上の問題の確認
  3. 03後処理と入力検証の追加
  4. 04Shellの範囲を超えたスクリプトの見極め

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/sammcj/agentic-coding --skill shell-scripting

インストール後、Claude Code で/shell-scriptingを入力して読み込まれたか確認します。

詳細

Bash スクリプトのベストプラクティス

最新のベストプラクティスに従って、信頼性が高く保守しやすい Bash スクリプトを書くためのガイダンスです。過剰に複雑化せず、シンプルさ、自動化ツール、防御的なプログラミングを重視します。

シェルを使う場合と使わない場合

シェルを使う場面:
  • 小規模なユーティリティやシンプルなラッパースクリプト(100 行未満)
  • 他のプログラムやツールのオーケストレーション
  • シンプルな自動化タスク
  • ロジックが単純なビルド/デプロイスクリプト
  • 手軽なデータ変換パイプライン
シェルを使わない場面:
  • 複雑なビジネスロジックやデータ構造
  • パフォーマンスが重要なコード
  • 広範なエラー処理が必要なスクリプト
  • 約 100 行を超えるもの、または制御フローが単純でないもの
  • 配列以外の適切なデータ構造が必要な場合

スクリプトが約 100 行を超える、または複雑になってきたら、保守不能になる前に Python または Go での書き換えを提案してください。

必須の基礎

すべての Bash スクリプトには、次の要素を含める必要があります。

1. 適切なシバン
#!/usr/bin/env bash

理由:bash が /bin/bash にないシステム(macOS、BSD、NixOS など)でも移植性を保てます。

代替案:スクリプトが Linux でのみ実行されるとわかっていて、明示的なパスを使いたい場合は #!/bin/bash を使用します。

2. 厳格モード
set -euo pipefail

各フラグの動作:

  • -e:コマンドが失敗(終了ステータスが 0 以外)すると、直ちに終了します
  • -u:未設定の変数をエラーとして扱います
  • -o pipefail:パイプライン内のいずれかのコマンドが失敗すると(最後のコマンドだけでなく)、パイプラインを失敗として扱います

-x を追加する場合:デバッグ時に限り、本番スクリプトでは使わないでください(出力が煩雑になります)。

3. ShellCheck への準拠

コミットする前に、すべてのスクリプトに対して ShellCheck を実行します。

shellcheck script.sh

警告はすべて修正してください。ShellCheck は次の問題を検出します。

  • 変数の引用符不足
  • 非推奨の構文
  • よくあるバグや落とし穴
  • 移植性の問題
4. 基本的なスクリプト構成
#!/usr/bin/env bash
set -euo pipefail

# Brief description of what this script does

# Simple error reporting
die() {
    echo "Error: ${1}" >&2
    exit 1
}

# Your code here

安全性の基本パターン

常に変数を引用符で囲む

理由:単語分割やグロブ展開による問題を防ぎます。

# Wrong - dangerous
cp $source $destination
rm -rf $prefix/bin

# Correct - safe
cp "${source}" "${destination}"
rm -rf "${prefix}/bin"

# Special case: Always use braces with variables
echo "${var}"      # Good
echo "$var"        # Acceptable but less consistent
echo $var          # Bad - unquoted
必須変数を確認する
# Fail fast if required variables aren't set
: "${REQUIRED_VAR:?REQUIRED_VAR must be set}"

# Or with custom message
: "${DATABASE_URL:?DATABASE_URL is required. Set it in .env}"
入力を検証する
# Check file exists before operating on it
[[ -f "${config_file}" ]] || die "Config file not found: ${config_file}"

# Check command exists before using it
command -v jq >/dev/null 2>&1 || die "jq is required but not installed"

# Validate directory before cd
[[ -d "${target_dir}" ]] || die "Directory does not exist: ${target_dir}"

必須パターン

パターン 1:シンプルなスクリプトのテンプレート

単純なスクリプトには、次のテンプレートを使用します。

#!/usr/bin/env bash
set -euo pipefail

# Description: Process log files and extract errors

die() {
    echo "Error: ${1}" >&2
    exit 1
}

# Check dependencies
command -v jq >/dev/null 2>&1 || die "jq required"

# Validate arguments
[[ $# -eq 1 ]] || die "Usage: ${0} <logfile>"
logfile="${1}"
[[ -f "${logfile}" ]] || die "File not found: ${logfile}"

# Main logic
grep ERROR "${logfile}" | jq -r '.message'
パターン 2:終了時のクリーンアップ

確実にクリーンアップするには trap を使用します。

#!/usr/bin/env bash
set -euo pipefail

# Create temp directory and ensure cleanup
tmpdir=$(mktemp -d)
trap 'rm -rf "${tmpdir}"' EXIT

# Now use tmpdir safely - cleanup happens automatically
echo "Working in: ${tmpdir}"
パターン 3:安全な関数定義

関数はシンプルで、目的を絞ったものにします。

# Good: Simple, single-purpose function
check_dependency() {
    local cmd="${1}"
    command -v "${cmd}" >/dev/null 2>&1 || die "${cmd} not installed"
}

# Good: Local variables, clear purpose
process_file() {
    local file="${1}"
    local output="${2}"

    [[ -f "${file}" ]] || die "Input file missing: ${file}"

    # Do processing
    sed 's/foo/bar/g' "${file}" > "${output}"
}

重要:エラーを検出できるよう、コマンド置換から変数を取得する場合は、宣言と設定を別々に行います。

# Wrong - hides errors
local result="$(failing_command)"

# Correct - catches errors
local result
result="$(failing_command)"  # Will fail properly with set -e
パターン 4:安全な配列の扱い

配列は、空白を含むリストの扱いに便利です。

# Create array
declare -a files=("file one.txt" "file two.txt" "file three.txt")

# Iterate safely - always quote with [@]
for file in "${files[@]}"; do
    echo "Processing: ${file}"
done

# Build command arguments safely
declare -a flags=(--verbose --output "${output_file}")
mycommand "${flags[@]}" "${input}"

# Read command output into array
mapfile -t lines < <(grep pattern "${file}")
パターン 5:条件判定

Bash では、より安全で機能も豊富な [[ ]] を使用します。

# File tests
[[ -f "${file}" ]]          # File exists
[[ -d "${dir}" ]]           # Directory exists
[[ -r "${file}" ]]          # File readable
[[ -w "${file}" ]]          # File writable
[[ -x "${binary}" ]]        # File executable

# String tests
[[ -z "${var}" ]]           # String is empty
[[ -n "${var}" ]]           # String is not empty
[[ "${a}" == "${b}" ]]      # String equality (use ==, not =)

# Numeric comparison (use (( )) for numbers)
(( count > 0 ))
(( total >= minimum ))

# Combined conditions
[[ -f "${file}" && -r "${file}" ]] || die "File not readable: ${file}"
パターン 6:シンプルな引数の扱い

シンプルなスクリプトでは、位置引数を優先します。

#!/usr/bin/env bash
set -euo pipefail

# For 1-3 arguments, just use positional parameters
[[ $# -eq 2 ]] || die "Usage: ${0} <source> <dest>"

source="${1}"
dest="${2}"

[[ -f "${source}" ]] || die "Source not found: ${source}"

フラグが必要なスクリプトでは、シンプルに保ちます。

# Use environment variables instead of complex flag parsing
VERBOSE="${VERBOSE:-false}"
DRY_RUN="${DRY_RUN:-false}"

# Run like: VERBOSE=true DRY_RUN=true ./script.sh input.txt
パターン 7:一時ファイルではなくプロセス置換を使う

可能な限り、一時ファイルの作成を避けます。

# Instead of:
first_command > /tmp/output.txt
second_command < /tmp/output.txt
rm /tmp/output.txt

# Use process substitution:
second_command <(first_command)

# For multiple inputs:
diff <(sort file1.txt) <(sort file2.txt)
パターン 8:外部コマンドより組み込みコマンドを優先する

組み込みコマンドはより高速で信頼性も高くなります。

# Use bash parameter expansion over sed/awk for simple cases
filename="${path##*/}"           # basename
dirname="${path%/*}"             # dirname
extension="${filename##*.}"      # get extension
name="${filename%.*}"            # remove extension

# Use (( )) for arithmetic over expr
count=$(( count + 1 ))           # Not: count=$(expr ${count} + 1)

# Use [[ ]] over [ ] or test
[[ -f "${file}" ]]               # Not: test -f "${file}"

# Use ${#var} for string length
length="${#string}"              # Not: length=$(echo "${string}" | wc -c)

中級パターン

パターン 9:構造化ログ

ログ記録はシンプルで一貫性のあるものにします。

log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] ${1}" >&2
}

error() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: ${1}" >&2
}

# Usage
log "Starting process"
error "Failed to connect to database"
パターン 10:main 関数パターン

長めのスクリプト(50 行以上)では、main 関数を使用します。

#!/usr/bin/env bash
set -euo pipefail

setup() {
    # Dependency checks, variable initialisation
    command -v jq >/dev/null 2>&1 || die "jq required"
}

process() {
    # Main logic here
    log "Processing data"
}

cleanup() {
    # Cleanup if needed
    log "Cleanup complete"
}

main() {
    setup
    process
    cleanup
}

# Call main with all script arguments
main "${@}"
パターン 11:冪等な操作

スクリプトは何度実行しても安全であるべきです。

# Check before creating
if [[ ! -d "${target_dir}" ]]; then
    mkdir -p "${target_dir}"
fi

# Check before writing config
if [[ ! -f "${config_file}" ]]; then
    echo "DEFAULT_VALUE=true" > "${config_file}"
fi

# Use atomic operations
mv "${source}" "${dest}"  # Atomic on same filesystem
パターン 12:安全な while ループでの読み込み

while にパイプを使わないでください(サブシェルが作成されます)。

# Wrong - variables modified in subshell are lost
count=0
cat file.txt | while read -r line; do
    (( count++ ))
done
echo "${count}"  # Will be 0!

# Correct - use process substitution
count=0
while read -r line; do
    (( count++ ))
done < <(cat file.txt)
echo "${count}"  # Correct count

# Or use mapfile for simple cases
mapfile -t lines <file.txt
count="${#lines[@]}"

スタイルガイドライン

書式
  • インデント:スペース 4 つ。タブは使わない
  • 行の長さ:最大 120 文字
  • 長い文字列:ヒアドキュメントまたは埋め込み改行を使用します
# Long command - break at logical points
docker run \
    --name my-container \
    --volume "${PWD}:/data" \
    --env "FOO=bar" \
    my-image:latest

# Long string - use here-doc
cat <<EOF
This is a long message
that spans multiple lines
and is more readable this way.
EOF
命名規則
# Functions: lowercase with underscores
check_dependencies() { ... }
process_files() { ... }

# Local variables: lowercase with underscores
local input_file="${1}"
local line_count=0

# Constants/environment variables: UPPERCASE with underscores
readonly MAX_RETRIES=3
readonly CONFIG_DIR="/etc/myapp"

# Source files: lowercase with underscores
# my_library.sh
ファイル拡張子
  • 実行ファイル:.sh 拡張子を付けるか、拡張子なし(ユーザー向けコマンドでは拡張子なしを推奨)
  • ライブラリ:常に .sh 拡張子を付け、実行可能にはしない

関数のドキュメント化

自明でない関数はドキュメント化します。

# Good: Simple function, no comment needed (name says it all)
check_file_exists() {
    [[ -f "${1}" ]]
}

# Good: Complex function, documented
# Processes log files and extracts error messages
# Arguments:
#   $1 - Input log file path
#   $2 - Output directory
# Returns:
#   0 on success, 1 on failure
process_logs() {
    local logfile="${1}"
    local output_dir="${2}"
    # Implementation
}

避けるべきこと

次のものは使わない
# Don't use backticks - use $()
output=`command`          # Old style
output=$(command)         # Correct

# Don't use eval - almost always wrong
eval "${user_input}"      # Dangerous!

# Don't use expr - use (( ))
result=$(expr 5 + 3)      # Old style
result=$(( 5 + 3 ))       # Correct

# Don't use [ ] when [[ ]] is available
[ -f "${file}" ]          # POSIX compatible, less features
[[ -f "${file}" ]]        # Bash, safer and more features

# Don't use $[ ] for arithmetic - deprecated
result=$[5 + 3]           # Deprecated
result=$(( 5 + 3 ))       # Correct

# Don't use function keyword unnecessarily
function foo() { ... }    # Redundant
foo() { ... }             # Cleaner
アンチパターン
# Don't glob or split unquoted
rm ${files}               # DANGEROUS
rm "${files}"             # Safe

# Don't use ls output in scripts
for file in $(ls); do     # Breaks with spaces
for file in *; do         # Correct

# Don't pipe yes to commands
yes | risky-command       # Bypasses important prompts

# Don't ignore error codes
make build                # Did it work?
make build || die "Build failed"  # Better

複雑さに関する警告サイン

スクリプトに次のいずれかが含まれる場合は、Python/Go で書き直すことを検討してください。

  • 100 行を超える
  • 単純な配列を超える複雑なデータ構造
  • 配列の配列に対する入れ子のループ
  • 大量の文字列操作ロジック
  • 複雑な状態管理
  • 基本的な算術演算を超える数学的計算
  • 個々の関数の単体テストが必要
  • 単純な jq クエリを超える JSON/YAML の解析

応用編:ドライランパターン

何かを変更するスクリプトの場合:

DRY_RUN="${DRY_RUN:-false}"

run() {
    if [[ "${DRY_RUN}" == "true" ]]; then
        echo "[DRY RUN] ${*}" >&2
        return 0
    fi
    "${@}"
}

# Usage
run cp "${source}" "${dest}"
run rm -f "${old_file}"

# Run script: DRY_RUN=true ./script.sh

クイックリファレンスチェックリスト

bash スクリプトを完成と見なす前に、次を確認してください。

  • ShellCheck で警告が出ない
  • 適切な shebang(#!/usr/bin/env bash)がある
  • 厳格モード(set -euo pipefail)が設定されている
  • すべての変数が引用符で囲まれている("${var}")
  • 必要な依存関係が確認されている
  • 適切なエラーメッセージが stderr に出力される
  • 一時ファイルを使用する場合はクリーンアップ用の trap がある
  • 可能な限りスクリプトが冪等である
  • 100 行未満(または、それを超える十分な理由がある)
  • which ではなく command -v を使用している
  • 空白を含むリストには配列を使用している
  • eval、ls の出力解析、バッククォートを使用していない
  • 関数でローカル変数を使用している

まとめ

  1. シンプルに始める:過剰に作り込まないでください。ほとんどのスクリプトは 50 行未満にするべきです。
  2. ShellCheck を使う:ほとんどの問題を自動的に検出できます。
  3. すべてを引用符で囲む:$var ではなく "${var}" を使います。
  4. すぐに失敗させる:set -euo pipefail を設定し、入力を検証します。
  5. 切り替え時を見極める:複雑になってきたら、適切なプログラミング言語を使います。
  6. 複雑にせず組み合わせる:パイプやプロセス置換を使います。
  7. 冪等にする:スクリプトは何度実行しても安全であるべきです。
  8. エラー時の経路をテストする:スクリプトが安全に失敗することを確認します。

シェルスクリプトは、複雑なロジックを構築するためではなく、物事をつなぎ合わせるためのものです。シンプルで、安全で、目的を絞ったものにしてください。

コメント