シェルスクリプト
最終更新日:2024-11-22 | ページの編集
所要時間: 45分
概要
質問
- コマンドを保存して再利用するにはどうすればよいですか?
目的
- 固定されたファイルセットに対してコマンドまたはコマンドの一連の処理を実行するシェルスクリプトを書く。
- コマンドラインからシェルスクリプトを実行する。
- コマンドラインで指定されたファイルセットを操作するシェルスクリプトを書く。
- 自分や他の人が作成したシェルスクリプトを含むパイプラインを作成する。
ついに、シェルがいかに強力なプログラミング環境であるかを確認する準備が整いました。
繰り返し実行するコマンドをファイルに保存し、それらの操作を後で再実行するために、
1つのコマンドを入力するだけで済むようにします。
歴史的な理由から、ファイルに保存された一連のコマンドは通常
シェルスクリプト と呼ばれますが、
実際にはこれらは小さなプログラムです。
シェルスクリプトを書くことで作業が速くなるだけでなく、同じコマンドを何度も再入力する必要がなくなります。
また、タイポの可能性を減らし、再現性を向上させます。後で作業を見返したり、他の誰かが作業を見つけてそれを基にしたい場合でも、
スクリプトを実行するだけで同じ結果を再現できます。長いコマンドを思い出したり再入力したりする必要はありません。
まず alkanes/
に戻り、新しいファイル
middle.sh
を作成してシェルスクリプトにします:
nano middle.sh
コマンドは、テキストエディタ「nano」で
middle.sh
ファイルを開きます。
ファイルが存在しない場合は、新規に作成されます。次の行を挿入してファイルを編集します:
head -n 15 octane.pdb | tail -n 5
これは以前構築したパイプのバリエーションで、octane.pdb
ファイルの11行目から15行目を選択します。
ここではまだコマンドを実行していないことに注意してください。
コマンドをファイルに組み込んでいるだけです。
次にファイルを保存します(nanoでは Ctrl-O
)、
その後、テキストエディタを終了します(nanoでは
Ctrl-X
)。alkanes
ディレクトリに middle.sh
というファイルが作成されていることを確認します。
ファイルを保存したら、その中に含まれるコマンドをシェルに実行させることができます。
シェルは bash
と呼ばれるため、次のコマンドを実行します:
出力
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00
ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00
ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00
ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00
ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00
確かに、スクリプトの出力は、パイプラインを直接実行した場合とまったく同じ結果になります。
テキストとその他の形式
Microsoft WordやLibreOffice
Writerのようなプログラムは通常「テキストエディタ」と呼ばれますが、
プログラミングに関してはもう少し慎重になる必要があります。
Microsoft Wordはデフォルトで .docx
ファイルを使用して、テキストだけでなく、フォントや見出しなどの書式情報も保存します。
これらの追加情報は文字として保存されておらず、head
のようなツールには意味を持ちません。head
は標準のキーボード上の文字、数字、句読点のみを含む入力ファイルを想定しています。
したがって、プログラムを編集する際にはプレーンテキストエディタを使用するか、ファイルをプレーンテキスト形式で保存するよう注意する必要があります。
次に、任意のファイルの行を選択したい場合はどうでしょうか?
その都度 middle.sh
を編集してファイル名を変更するのは、シェルでコマンドを入力して実行するより時間がかかるかもしれません。
代わりに、middle.sh
を編集してより汎用的にします:
次に、「nano」で octane.pdb
のテキストを特別な変数
$1
に置き換えます:
head -n 15 "$1" | tail -n 5
シェルスクリプト内では、$1
は「コマンドライン上の最初のファイル名(または他の引数)」を意味します。
これでスクリプトを以下のように実行できます:
出力
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00
ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00
ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00
ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00
ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00
また、別のファイルでも以下のように実行できます:
出力
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00
ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00
ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00
ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00
ATOM 13 H 1 -1.183 0.500 -1.412 1.00 0.00
引数の周囲のダブルクオート
ループ変数をダブルクオートで囲む理由と同じく、ファイル名にスペースが含まれている場合に備えて、$1
をダブルクオートで囲みます。
現在、返される行の範囲を調整するたびに middle.sh
を編集する必要があります。
これを修正するために、スクリプトを3つのコマンドライン引数を使用するように構成します。
最初のコマンドライン引数($1
)の後に提供される追加の引数は、それぞれ
$2
, $3
という特別な変数を介してアクセスできます。
この仕組みを利用して、行範囲を head
と tail
に渡すようスクリプトを編集します:
head -n "$2" "$1" | tail -n "$3"
以下のように実行できます:
出力
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00
ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00
ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00
ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00
ATOM 13 H 1 -1.183 0.500 -1.412 1.00 0.00
引数を変更することでスクリプトの動作を変えることができます:
出力
ATOM 14 H 1 -1.259 1.420 0.112 1.00 0.00
ATOM 15 H 1 -2.608 -0.407 1.130 1.00 0.00
ATOM 16 H 1 -2.540 -1.303 -0.404 1.00 0.00
ATOM 17 H 1 -3.393 0.254 -0.321 1.00 0.00
TER 18 1
これでも動作しますが、次に middle.sh
を読む人が何をするスクリプトなのかを理解するのに少し時間がかかるかもしれません。
スクリプトの先頭に コメント
を追加することで改善できます:
# ファイルの中間部分の行を選択する。
# 使用法: bash middle.sh ファイル名 終了行 行数
head -n "$2" "$1" | tail -n "$3"
コメントは #
文字で始まり、その行の終わりまで続きます。
コンピュータはコメントを無視しますが、人々(特に将来の自分)がスクリプトを理解しやすくするために不可欠です。
唯一の注意点は、スクリプトを変更するたびにコメントが正確であることを確認する必要があることです。
間違った方向に読者を導く説明は、コメントがないよりも悪い場合があります。
複数のファイルを1つのパイプラインで処理したい場合はどうすればよいでしょうか?
例えば、.pdb
ファイルを長さ順に並べ替えたい場合、次のコマンドを入力します:
wc -l
はファイル内の行数をリストします
(wc
は “word count” を意味し、-l
オプションを追加すると “行を数える” を意味します)。sort -n
は数値順に並べ替えます。
これをファイルに入れることもできますが、その場合、現在のディレクトリ内の
.pdb
ファイルのリストを並べ替えるだけになります。
他の種類のファイルのリストを取得できるようにするには、ファイル名をスクリプトに渡す方法が必要です。$1
, $2
のような変数は使えません。なぜなら、ファイル数がわからないからです。
代わりに、特別な変数 $@
を使用します。これは「シェルスクリプトへのすべてのコマンドライン引数」を意味します。
また、引数にスペースが含まれている場合に備えて $@
をダブルクオートで囲む必要があります("$@"
は特別な構文で、"$1"
, "$2"
…
と同等です)。
例を示します:
# ファイルをその長さで並べ替える。
# 使用法: bash sorted.sh 1つ以上のファイル名
wc -l "$@" | sort -n
出力
9 methane.pdb
12 ethane.pdb
15 propane.pdb
20 cubane.pdb
21 pentane.pdb
30 octane.pdb
163 ../creatures/basilisk.dat
163 ../creatures/minotaur.dat
163 ../creatures/unicorn.dat
596 total
ユニークな種のリストを作成する
リアは数百のデータファイルを持っています。それぞれのファイルは以下のような形式です:
2013-11-05,deer,5
2013-11-05,rabbit,22
2013-11-05,raccoon,7
2013-11-06,rabbit,19
2013-11-06,deer,2
2013-11-06,fox,1
2013-11-07,rabbit,18
2013-11-07,bear,1
このようなファイルの例は、shell-lesson-data/exercise-data/animal-counts/animals.csv
にあります。
コマンド cut -d , -f 2 animals.csv | sort | uniq
を使用すると、animals.csv
内のユニークな種を抽出することができます。
この一連のコマンドを毎回入力するのを避けるために、科学者はシェルスクリプトを書くことを選ぶかもしれません。
コマンドライン引数として任意の数のファイル名を受け取り、それぞれのファイルに含まれるユニークな種のリストを出力するspecies.sh
という名前のシェルスクリプトを書いてください。
例えば、論文に使用するグラフを作成するために便利な一連のコマンドを実行したとします。
後で再度グラフを作成できるように、コマンドをファイルに保存したいとします。
再度コマンドを入力する代わりに(そして間違える可能性を減らすために)、次のようにします:
このコマンドで、ファイル redo-figure-3.sh
に以下の内容が保存されます:
297 bash goostats.sh NENE01729B.txt stats-NENE01729B.txt
298 bash goodiff.sh stats-NENE01729B.txt /data/validated/01729.txt > 01729-differences.txt
299 cut -d ',' -f 2-3 01729-differences.txt > 01729-time-series.txt
300 ygraph --format scatter --color bw --borders none 01729-time-series.txt figure-3.png
301 history | tail -n 5 > redo-figure-3.sh
エディタで一部編集し、行番号と最後の history
コマンドを削除することで、
正確にグラフを作成する手順を記録したスクリプトを得ることができます。
コマンドが原因でクラッシュやフリーズが発生した場合、そのコマンドが何であったかを知ることで問題の調査が可能になります。
コマンドが実行された後にのみ記録された場合、クラッシュが起きた時に最後に実行されたコマンドの記録を失う可能性があります。
実際には、多くの人がシェルスクリプトを開発する際、
まずシェルプロンプトでコマンドを何度か実行して正しいことを確認し、
その後、それらを再利用のためにファイルに保存します。
この方法では、データやワークフローに関する発見をリサイクルすることが可能です。history
を一度実行し、出力を少し編集してシェルスクリプトとして保存するだけで済みます。
ネルのパイプライン: スクリプトの作成
ネルの指導教官は、すべての分析が再現可能でなければならないと主張しました。
そのための最も簡単な方法は、すべてのステップをスクリプトに記録することです。
まず、ネルのプロジェクトディレクトリに戻ります:
nano
を使用してファイルを作成します…
以下の内容を含むスクリプトを作成します:
BASH
# データファイルの統計を計算する。
for datafile in "$@"
do
echo $datafile
bash goostats.sh $datafile stats-$datafile
done
このスクリプトを do-stats.sh
という名前で保存することで、
以下のコマンドを入力するだけで分析の最初の段階を再実行できるようになります:
また、以下のようにも実行できます:
これにより、処理されたファイル数のみが出力され、処理されたファイル名のリストは表示されません。
ネルのスクリプトの特徴の1つは、処理するファイルを実行者が決定できることです。
以下のようにも書けます:
BASH
# Site A と Site B のデータファイルの統計を計算する。
for datafile in NENE*A.txt NENE*B.txt
do
echo $datafile
bash goostats.sh $datafile stats-$datafile
done
この方法の利点は、常に正しいファイルを選択できることです。
「Z」ファイルを除外することを忘れる心配がありません。
しかし、欠点は、常にそのファイルだけを選択する点です。
すべてのファイル(「Z」ファイルを含む)や、南極の同僚が作成した「G」や「H」ファイルに対して実行するには、スクリプトを編集する必要があります。
さらに柔軟にするには、コマンドライン引数をチェックし、指定がなければ
NENE*A.txt NENE*B.txt
を使用するようスクリプトを変更することもできます。
もちろん、これは柔軟性と複雑さのトレードオフをもたらします。
シェルスクリプトの変数
alkanes
ディレクトリで、次のコマンドを含む
script.sh
という名前のシェルスクリプトがあるとします:
alkanes
ディレクトリで以下のコマンドを入力します:
次のうち、どの出力が期待されますか?
-
alkanes
ディレクトリ内の.pdb
ファイルの各ファイルの最初と最後の行の間にあるすべての行 -
alkanes
ディレクトリ内の.pdb
ファイルの各ファイルの最初と最後の行 -
alkanes
ディレクトリ内のすべてのファイルの最初と最後の行 -
*.pdb
の引用符が原因でエラー
正しい答えは 2 です。
特別な変数 $1
, $2
, $3
はスクリプトに与え
られたコマンドライン引数を表します。
したがって、実行されるコマンドは次のようになります:
BASH
$ head -n 1 cubane.pdb ethane.pdb octane.pdb pentane.pdb propane.pdb
$ tail -n 1 cubane.pdb ethane.pdb octane.pdb pentane.pdb propane.pdb
引用符で囲まれているため、シェルは *.pdb
を展開しません。
そのため、スクリプトの最初の引数は '*.pdb'
となり、head
と tail
によってスクリプト内で展開されます。
特定の拡張子を持つ最長のファイルを見つける
longest.sh
というシェルスクリプトを作成してください。このスクリプトは、引数としてディレクトリ名とファイル拡張子を受け取り、
そのディレクトリ内で指定された拡張子を持つファイルのうち、最も多くの行を含むファイルの名前を出力します。
例えば:
は、shell-lesson-data/exercise-data/alkanes
ディレクトリ内の .pdb
ファイルの中で最も多くの行を持つファイルの名前を出力します。
別のディレクトリでスクリプトをテストしてみてください。例:
BASH
# シェルスクリプト:
# 1. ディレクトリ名
# 2. ファイル拡張子
# を引数に受け取り、そのディレクトリ内で
# 指定された拡張子を持つ最も多くの行を含むファイル名を出力します。
wc -l $1/*.$2 | sort -n | tail -n 2 | head -n 1
このパイプラインの最初の部分、wc -l $1/*.$2 | sort -n
は、各ファイルの行数を数えて数値順にソートします(最大値が最後に来る)。
複数のファイルがある場合、wc
はすべてのファイルの合計行数を示す最終的なサマリー行も出力します。tail -n 2 | head -n 1
を使用して、このサマリー行を削除しています。
もし wc -l $1/*.$2 | sort -n | tail -n 1
を使用した場合は、最終的なサマリー行が表示されます。
出力を理解するために、パイプラインを段階的に構築して確認することができます。
スクリプト読解問題
再び shell-lesson-data/exercise-data/alkanes
ディレクトリを考えてみます。
このディレクトリには .pdb
ファイルがいくつか含まれており、その他のファイルが作成されている場合もあります。
以下の3つのスクリプトが、それぞれ
bash script1.sh *.pdb
、bash script2.sh *.pdb
、bash script3.sh *.pdb
として実行された場合の挙動を説明してください。
各ケースで、シェルはスクリプトに渡す前に *.pdb
のワイルドカードを展開し、
結果のファイル名リストをスクリプトに引数として渡します。
スクリプトのデバッグ
以下のスクリプトが north-pacific-gyre
ディレクトリ内の
do-errors.sh
に保存されているとします:
BASH
# データファイルの統計を計算する。
for datafile in "$@"
do
echo $datfile
bash goostats.sh $datafile stats-$datafile
done
このスクリプトを north-pacific-gyre
ディレクトリから次のように実行すると:
出力は空になります。
原因を特定するために、-x
オプションを使用してスクリプトを再実行します:
この出力には何が表示されますか?どの行がエラーの原因ですか?
-x
オプションは bash
をデバッグモードで実行します。
これにより、実行される各コマンドが出力され、エラーの特定に役立ちます。
この例では、echo
が何も出力していないことがわかります。
ループ変数名にタイポ(datfile
)があり、変数
datfile
が存在しないため、空の文字列が返されています。
まとめ
- コマンドを再利用するためにファイル(通常はシェルスクリプト)に保存する。
-
bash [ファイル名]
を使用してファイル内のコマンドを実行する。 -
$@
はシェルスクリプトのすべてのコマンドライン引数を参照する。 -
$1
,$2
などは、それぞれ最初のコマンドライン引数、2番目のコマンドライン引数を参照する。 - 値にスペースが含まれる場合に備えて、変数を引用符で囲む。
- 処理するファイルをユーザーが選択できるようにすることは、柔軟性が高く、Unixの組み込みコマンドとの一貫性が高い。