概要

教える:30分
演習する。 15分
問題

  • コマンドを保存して再利用するには?

目的

  • ファイルの固定セットに対してコマンドまたはコマンドのシリーズを実行するシェルスクリプトを作成します。

  • ユーザーがコマンドラインで定義した一連のファイルに対して操作するシェルスクリプトを書く。

  • 自分や他の人が書いたシェルスクリプトを含むパイプラインを作成する。歴史的な理由から、ファイルに保存されたコマンドの束は通常シェルスクリプトと呼ばれますが、これらは実際には小さなプログラムであることに間違いはありません。

    molecules/ に戻って、シェルスクリプトとなる middle.sh という新しいファイルを作成するところから始めましょう:

    $ cd molecules$ nano middle.sh

コマンド nano middle.sh はテキストエディター ‘nano’ (shell で実行) 内でファイル middle.sh をオープンします。ファイルが存在しない場合は作成されます。テキストエディタを使ってファイルを直接編集するには、次の行を挿入するだけです:

head -n 15 octane.pdb | tail -n 5

これは以前に構築したパイプのバリエーションです: ファイル octane.pdb の 11-15 行を選択するものです。

そして、ファイルを保存し (nano の Ctrl-O) 、テキストエディタを終了します (nano の Ctrl-X). ディレクトリ moleculesmiddle.sh というファイルがあることを確認します。

ファイルを保存したら、そのファイルに含まれるコマンドを実行するようにシェルに要求できます。私たちのシェルは bash という名前なので、次のコマンドを実行します:

$ bash middle.sh
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00

確かに、このパイプラインを直接実行したら得られるものとまったく同じスクリプト出力になります。

Text vs. Whatever

通常、Microsoft Word や LibreOffice Writer などのプログラムを「テキストエディタ」と呼びますが、プログラミングに関してはもう少し注意する必要があります。 デフォルトでは、Microsoft Word は .docx ファイルを使用して、テキストだけでなく、フォントや見出しなどの書式情報も保存します。 この余分な情報は文字として保存されないので、headのようなツールには意味がありません。彼らは、入力ファイルが標準的なコンピュータキーボードの文字、数字、句読点以外何も含んでいないことを期待しています。 したがって、プログラムを編集するときは、プレーンテキストエディタを使うか、ファイルをプレーンテキストで保存するように注意しなければならない。

任意のファイルから行を選択したい場合はどうするか。ファイル名を変更するたびに middle.sh を編集してもいいが、シェルでもう一度コマンドを入力して、新しいファイル名で実行するより時間がかかるかもしれない。

$ nano middle.sh

次に “nano” 内で octane.pdb というテキストを という特殊変数に置き換えます:

head -n 15 "" | tail -n 5

シェルの中では とは「コマンドライン上の最初のファイル名(あるいは他の引数)」 という意味です。これで、

$ bash middle.sh octane.pdb
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00

または別のファイルでこのようにスクリプトを実行できるようになった。

$ bash middle.sh pentane.pdb
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00ATOM 13 H 1 -1.183 0.500 -1.412 1.00 0.00

引数のダブルクォート

ループ変数をダブルクォートの中に置いたのと同じ理由で、ファイル名がスペースを含んでいた場合、をダブルクォートで囲みます。

現在、返される行の範囲を調整したい場合は、毎回 middle.sh を編集する必要があります。 この問題を解決するために、3つのコマンドライン引数を使用するようにスクリプトを構成してみましょう。 最初のコマンドライン引数 () の後、私たちが提供する追加の各引数は、特殊変数 , , を介してアクセス可能で、それぞれ第1、第2、第3のコマンドライン引数を参照することができます。

これを知っていれば、追加の引数を使用して、それぞれ headtail に渡す行の範囲を定義できます:

$ nano middle.sh
head -n "" "" | tail -n ""

これで実行できるようになりました。

$ bash middle.sh pentane.pdb 15 5
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00ATOM 13 H 1 -1.183 0.500 -1.412 1.00 0.00

コマンドへの引数を変更することにより、スクリプトの動作を変更することが可能です。

$ bash middle.sh pentane.pdb 20 5
ATOM 14 H 1 -1.259 1.420 0.112 1.00 0.00ATOM 15 H 1 -2.608 -0.407 1.130 1.00 0.00ATOM 16 H 1 -2.540 -1.303 -0.404 1.00 0.00ATOM 17 H 1 -3.393 0.254 -0.321 1.00 0.00TER 18 1

これは動作しますが、次に middle.sh を読む人はこれが何をするか理解するのに時間がかかるかもしれません。

$ nano middle.sh

コメントは#文字で始まり、行末まで続きます。コンピュータはコメントを無視しますが、(未来の自分を含め)人々がスクリプトを理解し使用できるようにするには非常に貴重です。読者を間違った方向に導くような説明は、全くないよりも悪い。

1つのパイプラインで多くのファイルを処理したい場合はどうすればよいのでしょうか。たとえば、.pdb ファイルを長さでソートしたい場合、次のように入力します:

$ wc -l *.pdb | sort -n

なぜなら、wc -l はファイルの行数をリストし (wc は ‘word count’ を意味し、-l オプションは代わりに ‘count lines’ という意味です)、sort -n は数値的にものを並べ替えるからです。これをファイルに書くこともできるが、そうするとカレントディレクトリにある .pdb 個のファイルしかソートされない。もし、他の種類のファイルもソートされたリストを得たいのであれば、それらの名前をスクリプトに取り込む方法が必要である。また、スペースを含む引数("$@" は特殊な構文で、"" "" …と同等)を処理するために $@ をダブルクオートで囲む必要があります。

以下はその例である。

$ nano sorted.sh
# Sort files by their length.# Usage: bash sorted.sh one_or_more_filenameswc -l "$@" | sort -n
$ bash sorted.sh *.pdb ../creatures/*.dat
9 methane.pdb12 ethane.pdb15 propane.pdb20 cubane.pdb21 pentane.pdb30 octane.pdb163 ../creatures/basilisk.dat163 ../creatures/minotaur.dat163 ../creatures/unicorn.dat596 total

固有種一覧

Leah には数百個のデータファイルがあって、それぞれがこのようにフォーマットされる。

2013-11-05,deer,52013-11-05,rabbit,222013-11-05,raccoon,72013-11-06,rabbit,192013-11-06,deer,22013-11-06,fox,12013-11-07,rabbit,182013-11-07,bear,1

このタイプのファイルの例はdata-shell/data/animal-counts/animals.txtに示されています。

animals.txtの固有種を出すには、cut -d , -f 2 animals.txt | sort | uniqというコマンドを使います。

コマンドライン引数として任意の数のファイル名を取り、上記のコマンドのバリエーションを使用して、これらのファイルのそれぞれに現れる固有種のリストを個別に表示する species.sh というシェルスクリプトを作成します。

Solution

# Script to find unique species in csv files where species is the second data field# This script accepts any number of file names as command line arguments# Loop over all filesfor file in $@doecho "Unique species in $file:"# Extract species namescut -d , -f 2 $file | sort | uniqdone

何か役に立つ一連のコマンドを実行したとします – たとえば、論文で使用するグラフを作成しました。 後で必要であればグラフを再作成できるので、コマンドをファイルに保存したいと思います。もう一度入力する代わりに (そして、間違って入力する可能性もあります)、次のようにします:

$ history | tail -n 5 > redo-figure-3.sh

ファイル redo-figure-3.sh には現在次のものが含まれています。

297 bash goostats NENE01729B.txt stats-NENE01729B.txt298 bash goodiff stats-NENE01729B.txt /data/validated/01729.txt > 01729-differences.txt299 cut -d ',' -f 2-3 01729-differences.txt > 01729-time-series.txt300 ygraph --format scatter --color bw --borders none 01729-time-series.txt figure-3.png301 history | tail -n 5 > redo-figure-3.sh

エディタでコマンドのシリアル番号を削除し、history コマンドを呼び出した最終行を削除すると、その図をどのように作成したかを完全に記録することができました。

なぜコマンドを実行する前に履歴に記録するのか?

コマンドを実行した場合:

$ history | tail -n 5 > recent.sh

ファイル内の最後のコマンドは history コマンドそのもの、すなわち。つまり、シェルは実際に実行する前に history をコマンドログに追加しているのである。 実際、シェルは常にコマンドを実行する前にログに追加しています。 あるコマンドが原因で何かがクラッシュまたはハングアップした場合、その問題を調査するために、そのコマンドが何であったかを知ることは有用である。

実際には、ほとんどの人がシェル プロンプトでコマンドを数回実行して正しいことを確認し、それをファイルに保存して再利用することによってシェル スクリプトを開発します。 スクリプトの作成

Nelle の上司は、彼女のすべての分析が再現可能でなければならないと主張しました。 すべてのステップをキャプチャする最も簡単な方法は、スクリプトにすることです。

最初に、Nelle のデータ ディレクトリに戻ります。

$ cd ../north-pacific-gyre/2012-07-03/

彼女はエディタを実行し、次のように書き込みます。

$ bash do-stats.sh NENE*.txt

このようにすることもできます:

$ bash do-stats.sh NENE*.txt | wc -l

処理したファイル名ではなく、単に処理したファイルの数だけ出力するようにするため。

Nelle のスクリプトについて注意すべき点は、実行者がどのファイルを処理するかを決定できることです。欠点は、常にこれらのファイルだけを選択することで、スクリプトを編集しないと、すべてのファイル (Z ファイルを含む) や南極の同僚が作成する ‘G’ または ‘H’ ファイルに対して実行することができません。もし、もっと冒険をしたければ、コマンドライン引数をチェックするようにスクリプトを修正し、何も提供されなければ NENE*.txt を使用することができる。もちろん、これは柔軟性と複雑さの間の別のトレードオフをもたらす。

シェル スクリプトの変数

molecules ディレクトリに、次のコマンドを含む script.sh というシェル スクリプトがあると想像してください。

head -n  tail -n  

moleculesディレクトリにいるときに、次のコマンドを入力します:

bash script.sh '*.pdb' 1 1

次のどの出力を期待しますか?

  1. moleculesディレクトリ内の.pdbで終わる各ファイルの先頭行と最終行の間のすべての行
  2. moleculesディレクトリ内の.pdbで終わる各ファイルの先頭行と最終行の間のすべての行 directory
  3. The first and last line of each file in molecules directory
  4. An error because quotes around *.pdb

Solution

正解は2である。

特殊変数$1、$2、$3はthescriptに与えられたコマンドライン引数を表し、実行されるコマンドは次のようになります:

$ 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'で、これはheadtailによってthescript内に展開されます。

Find the Longest File With a Given Extension

ディレクトリ名とファイル名拡張子を引数に取り、そのディレクトリ内でその拡張子を持つ行数の最も多いファイル名を出力する longest.sh というシェルスクリプトを書いてみてください。 例えば、

$ bash longest.sh /tmp/data pdb

は、/tmp/data にある .pdb ファイルのうち、最も行数の多いものの名前を表示する。

Solution

# Shell script which takes two arguments:# 1. a directory name# 2. a file extension# and prints the name of the file in that directory# with the most lines which matches the file extension.wc -l /*. | sort -n | tail -n 2 | head -n 1

パイプラインの最初の部分 wc -l /*. | sort -n は、各ファイルの行数を数え、数値順にソートします(最大行が最後です)。 複数のファイルがある場合、wc は最後のサマリー行も出力し、全ファイルの行数の合計を示す。

wc -l /*. | sort -n | tail -n 1 で最後のサマリー行を見ます。出力を確実に理解するために、パイプラインをバラバラに構築することができます。このディレクトリには、あなたが作成した他のファイルに加えて、いくつかの.pdbファイルが含まれています。次の3つのスクリプトをそれぞれbash script1.sh *.pdbbash script2.sh *.pdbbash script3.sh *.pdbとして実行すると、どのようになるか説明しなさい。

# Script 1echo *.*
# Script 2for filename in   do cat $filenamedone
# Script 3echo [email protected]

Solutions

それぞれの場合、シェルは *.pdb でワイルドカードを展開し、結果のファイル名のリストをスクリプトへの引数として渡します。

スクリプト 1 は、名前にドットを含むすべてのファイルのリストを表示します。スクリプトに渡された引数は、実際にはスクリプトのどこでも使用されません。

スクリプト 2 は、.pdbファイル拡張子を持つ最初の 3 ファイルの内容を表示します。, , はそれぞれ第1、第2、第3引数を意味する。

Script 3 はスクリプトへのすべての引数 (すなわちすべての .pdb ファイル) と .pdb が続くものを表示する。

cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb.pdb

Debugging Scripts

Nelle の north-pacific-gyre/2012-07-03 ディレクトリの do-errors.sh というファイルに次のスクリプトを保存したとする。

# Calculate stats for data files.for datafile in "$@"do echo $datfile bash goostats $datafile stats-$datafiledone

これを実行すると、出力が空白になります。原因を調べるには、-x オプションを使用してスクリプトを再実行します。

bash -x do-errors.sh NENE*.txt

出力は何を示していますか。どの行がエラーの原因になっていますか。

Solution

-xオプションは、bashをデバッグモードで実行させます。これは、実行された各コマンドを印刷し、エラーを見つけるのに役立ちます。 ループ変数名にタイプミスがあり、変数 datfile が存在しないため、空の文字列を返しています。

Key Points

  • コマンドをファイル(通常シェルスクリプトと呼ばれる)に保存して再利用することができます。

  • bash はファイルに保存したコマンドを実行します。

  • $@ はシェルスクリプトのすべてのコマンドライン引数を参照しています。 は、最初のコマンドライン引数、2番目のコマンドライン引数などを指します。

  • 値にスペースが含まれる可能性がある場合、変数を引用符で囲みます。

  • 処理するファイルをユーザーに決めさせることは、より柔軟で内蔵 Unix コマンドと一致します。

コメントを残す

メールアドレスが公開されることはありません。