シェルスクリプトの基礎

ShellBeginner
オンラインで実践に進む

はじめに

シェルスクリプトは、コマンドをファイルに保存することで、同じ作業を一貫して繰り返せるようにします。信頼性の高い初心者向けスクリプトでは、使用するインタープリターを明示し、変数展開をクォートし、処理を実行する前に入力を検証し、他のコマンドが解釈できる終了ステータスを返します。

この実験では、小さな Bash スクリプトを段階的に作成します。Bash 経由でスクリプトを実行する方法と、実行可能ファイルとして直接実行する方法を学びます。また、変数や位置パラメーター、対話型入力、if とファイルテストによる分岐、終了コード、for による反復処理を使い、最後にこれらの要素を組み合わせた簡単なログ要約スクリプトを作成します。

最初のスクリプトを作成して実行する

このステップでは、Bash スクリプトを作成し、インタープリター経由で実行した後、直接実行できるようにします。

実験用ワークスペースに移動します。

cd /home/labex/project/shell-lab

ヒアドキュメントを使って hello.sh を作成します。先頭行はシバンで、ファイルを直接実行したときに Bash を使用するよう指定します。

cat > hello.sh <<'EOF'
#!/bin/bash

echo "Hello from a Bash script"
pwd
EOF

実行する前にファイルの内容を確認します。

cat hello.sh

ファイルを明示的に Bash に渡して実行します。実行権限を追加する前でも動作します。

bash hello.sh

実行権限を追加し、モードを確認します。

chmod u+x hello.sh
ls -l hello.sh

現在のディレクトリからファイルを実行します。./ はパスを指定する記号です。シェルは通常、現在のディレクトリを自動的に検索しません。

./hello.sh

変数と安全なクォートを使う

このステップでは、変数を代入して展開し、ダブルクォートによってスペースを含む値が保護される仕組みを確認します。

2 つの変数を使ったスクリプトを作成します。

cat > variables.sh <<'EOF'
#!/bin/bash

course="Linux for Noobs"
workspace="/home/labex/project/shell-lab"

echo "Course: $course"
echo "Workspace: $workspace"
printf 'Quoted argument count: %s\n' "$#"
printf 'First argument: %s\n' "${1:-not provided}"
EOF

シェルで変数を代入するとき、= の前後にスペースを入れてはいけません。ダブルクォートを使うと $course は展開されますが、Linux for Noobs は 1 つの値として扱われます。

スペースを含む 1 つの引数を指定してスクリプトを実行します。

bash variables.sh "beginner learner"

特殊変数 $# は位置パラメーターの数を表し、$1 は最初の引数を表します。${1:-not provided} は、引数が指定されていない場合に代替値を返します。

確認用に出力を保存します。

bash variables.sh "beginner learner" > variable-output.txt
cat variable-output.txt

位置パラメーターを検証する

このステップでは、引数を必須にし、使い方を示すメッセージを表示し、失敗時と成功時で異なる終了ステータスを返します。

greet-argument.sh を作成します。

cat > greet-argument.sh <<'EOF'
#!/bin/bash

if [ "$#" -ne 1 ]; then
  echo "Usage: $0 NAME" >&2
  exit 2
fi

name=$1
echo "Hello, $name"
exit 0
EOF

テストコマンド [ ... ] は、引数の数が 1 と等しくないかどうかを確認します。>&2 は使い方のメッセージを標準エラー出力へ送り、exit 2 は不正な使い方であることを通知します。

引数なしで実行し、その直後に $? を表示します。$? は直前に実行したコマンドの終了ステータスです。

bash greet-argument.sh
echo "missing argument status: $?"

期待されるステータスは 2 です。正しい引数を指定して実行します。

bash greet-argument.sh "Ada Lovelace"
echo "valid argument status: $?"

成功した挨拶メッセージを保存します。

bash greet-argument.sh "Ada Lovelace" > argument-greeting.txt

対話型入力を読み取る

このステップでは、ユーザーに入力を促し、1 行を安全に読み取り、空の入力にも対応します。

ask-name.sh を作成します。

cat > ask-name.sh <<'EOF'
#!/bin/bash

read -r -p "Enter your name: " name

if [ -z "$name" ]; then
  echo "A name is required" >&2
  exit 1
fi

echo "Welcome, $name"
EOF

read -r は、バックスラッシュを特別扱いせずに 1 行を読み取ります。-p はプロンプトを表示し、-z はクォートされた値が空かどうかを確認します。

スクリプトを実行します。

bash ask-name.sh

プロンプトが表示されたら、次のように入力します。

Grace Hopper

Welcome, Grace Hopper と表示されます。再現可能な形で結果を保存するには、同じスクリプトへ 1 行の入力をパイプします。

printf 'Grace Hopper\n' | bash ask-name.sh > interactive-greeting.txt
cat interactive-greeting.txt

ファイルテストで分岐する

このステップでは、ファイルテストと ifelifelse を使って、パスの種類を判定します。

classify-path.sh を作成します。

cat > classify-path.sh <<'EOF'
#!/bin/bash

path=${1:-}

if [ -z "$path" ]; then
  echo "Usage: $0 PATH" >&2
  exit 2
elif [ -d "$path" ]; then
  echo "directory: $path"
elif [ -f "$path" ]; then
  echo "file: $path"
else
  echo "missing: $path" >&2
  exit 1
fi

exit 0
EOF

-d はディレクトリかどうかを確認し、-f は通常ファイルかどうかを確認します。処理の順序が重要です。パスの種類を判定する前に、まず不正な使い方を処理しています。

3 種類の結果をすべて試します。

bash classify-path.sh logs
bash classify-path.sh notes.txt
bash classify-path.sh missing.txt
echo "missing path status: $?"

成功した結果をまとめて保存します。&& を使うと、最初のコマンドが成功した場合にのみ 2 番目のコマンドが実行されます。

bash classify-path.sh logs > path-results.txt && bash classify-path.sh notes.txt >> path-results.txt
cat path-results.txt

for ループで複数のファイルを処理する

このステップでは、ログファイルを順番に処理し、それぞれのファイルで一致するレコード数を数えます。

count-errors.sh を作成します。

cat > count-errors.sh <<'EOF'
#!/bin/bash

log_dir=${1:-logs}

if [ ! -d "$log_dir" ]; then
  echo "Log directory not found: $log_dir" >&2
  exit 1
fi

for log_file in "$log_dir"/*.log; do
  error_count=$(grep -c '^ERROR ' "$log_file" || true)
  printf '%s errors=%s\n' "$(basename "$log_file")" "$error_count"
done
EOF

ループでは、反復するたびに一致したパスが 1 つずつ log_file に代入されます。"$log_file" をクォートすることで、スペースを含むパスも安全に扱えます。一方、basename はディレクトリ部分を取り除きます。grep -c は一致する行数を数え、|| true は一致が 0 件の場合でも、より大きな厳格な処理全体が停止しないようにします。

ループを実行し、出力を保存します。

bash count-errors.sh logs | tee error-counts.txt

.log ファイルにつき 1 件ずつ結果が表示されます。

検証、ループ、終了ステータスを組み合わせる

このステップでは、ディレクトリを検証し、その中のログを要約し、成功または失敗を明確に報告する再利用可能なスクリプトを作成します。

summarize-logs.sh を作成します。

cat > summarize-logs.sh <<'EOF'
#!/bin/bash

input_dir=${1:-}
output_file=${2:-}

if [ -z "$input_dir" ] || [ -z "$output_file" ]; then
  echo "Usage: $0 INPUT_DIR OUTPUT_FILE" >&2
  exit 2
fi

if [ ! -d "$input_dir" ]; then
  echo "Input directory not found: $input_dir" >&2
  exit 1
fi

: > "$output_file"

for log_file in "$input_dir"/*.log; do
  lines=$(wc -l < "$log_file")
  errors=$(grep -c '^ERROR ' "$log_file" || true)
  printf '%s lines=%s errors=%s\n' "$(basename "$log_file")" "$lines" "$errors" >> "$output_file"
done

echo "Wrote summary to $output_file"
exit 0
EOF

: > "$output_file" コマンドは、ループを開始する前に出力先を安全に作成または空にします。必須の 2 つの引数を指定してスクリプトを実行します。

chmod u+x summarize-logs.sh
./summarize-logs.sh logs log-summary.txt
echo "summary status: $?"
cat log-summary.txt

存在しないディレクトリを指定して、失敗時の処理を確認します。

./summarize-logs.sh missing log-summary.txt
echo "missing directory status: $?"

成功時のステータスは 0、存在しないディレクトリを指定した場合のステータスは 1 になります。

まとめ

Bash スクリプトを作成・実行し、シバンと権限を使い、クォートした変数を展開し、位置パラメーターと対話型入力を読み取り、意味のある終了ステータスを返しました。また、if/elif/else による分岐、ファイルとディレクトリのテスト、for によるログの反復処理を行い、これらの要素を再利用可能な要約スクリプトにまとめました。

これらの基礎を身につけることで、後の自動化処理が読みやすくなり、安全に再実行できるようになります。また、他のコマンドやスケジュール実行されたジョブからも、結果を判定しやすくなります。