ファイルを見つける

最終更新日:2024-11-22 | ページの編集

所要時間: 45分

概要

質問

  • ファイルをどのように見つけることができますか?
  • ファイル内の内容をどのように見つけることができますか?

目的

  • grep を使って、テキストファイルから特定のパターンに一致する行を選択する。
  • find を使って、特定のパターンに一致する名前のファイルやディレクトリを見つける。
  • 1つのコマンドの出力を別のコマンドのコマンドライン引数として使用する。
  • 「テキスト」ファイルと「バイナリ」ファイルの意味、およびなぜ多くの一般的なツールが後者を適切に処理できないかを説明する。

多くの人が「Google」を「探す」という意味の動詞として使うのと同じように、Unixプログラマーは「grep」という言葉をよく使います。 grep は「global/regular expression/print」の略で、初期のUnixテキストエディターで一般的に使用された操作の一連の名前です。 また、非常に便利なコマンドラインプログラムの名前でもあります。

grep は、特定のパターンに一致するファイル内の行を検索して表示します。 例として、Salon マガジンの1998年のコンテストから取った3つの俳句を含むファイルを使用します。 (作者 Bill Torcaso、Howard Korder、Margaret Segall に敬意を表します。俳句エラーメッセージのアーカイブ: ページ1ページ2。) この例では、writing サブディレクトリで作業します。

BASH

$ cd
$ cd Desktop/shell-lesson-data/exercise-data/writing
$ cat haiku.txt

出力

The Tao that is seen
Is not the true Tao, until
You bring fresh toner.

With searching comes loss
and the presence of absence:
"My Thesis" not found.

Yesterday it worked
Today it is not working
Software is like that.

単語「not」を含む行を見つけてみましょう:

BASH

$ grep not haiku.txt

出力

Is not the true Tao, until
"My Thesis" not found
Today it is not working

ここでは、not が検索しているパターンです。 grep コマンドは、指定したパターンに一致する行をファイル内で検索します。 使用方法は、grep と検索したいパターン、その後に検索対象のファイル名を入力します。

出力は、not を含む3行です。

デフォルトでは、grep は大文字小文字を区別して検索を行います。 また、選択した検索パターンが完全な単語である必要はありません。 次の例で確認してみましょう。

The というパターンを検索してみます。

BASH

$ grep The haiku.txt

出力

The Tao that is seen
"My Thesis" not found.

今回は、The を含む2行が出力され、そのうちの1行には「Thesis」という単語内に検索パターンが含まれています。

grep-w オプションを指定すると、単語境界を基準に一致する行を制限できます。

このレッスンの後半では、grep の検索動作を大文字小文字の区別に関して変更する方法も説明します。

BASH

$ grep -w The haiku.txt

出力

The Tao that is seen

単語境界は、行の開始や終了も含むため、スペースに囲まれた文字だけではありません。 場合によっては、単一の単語ではなくフレーズを検索したいこともあります。 grep ではフレーズを引用符で囲むことでこれが可能です。

BASH

$ grep -w "is not" haiku.txt

出力

Today it is not working

単語1つだけの場合は引用符が不要ですが、複数単語を検索する場合は引用符を使用することで、検索対象の用語やフレーズと検索対象のファイルを区別しやすくなります。 残りの例では、引用符を使用します。

便利なオプションとして、-n を使用すると、一致する行の行番号を表示できます。

BASH

$ grep -n "it" haiku.txt

出力

5:With searching comes loss
9:Yesterday it worked
10:Today it is not working

行番号5、9、10に「it」が含まれています。

他のUnixコマンドと同様に、オプション(フラグ)を組み合わせることができます。 例えば、「the」という単語を含む行を検索する場合、 オプション -w を使用して単語境界で一致する行を見つけ、-n を使用して行番号を表示することができます。

BASH

$ grep -n -w "the" haiku.txt

出力

2:Is not the true Tao, until
6:and the presence of absence:

次に、-i オプションを使って大文字小文字を区別せずに検索します:

BASH

$ grep -n -w -i "the" haiku.txt

出力

1:The Tao that is seen
2:Is not the true Tao, until
6:and the presence of absence:

さらに、-v オプションを使用して検索を反転し、「the」を含まない行を出力します。

BASH

$ grep -n -w -v "the" haiku.txt

出力

1:The Tao that is seen
3:You bring fresh toner.
4:
5:With searching comes loss
7:"My Thesis" not found.
8:
9:Yesterday it worked
10:Today it is not working
11:Software is like that.

-r(再帰)オプションを使用すると、grep はサブディレクトリ内の一連のファイルを再帰的にパターン検索できます。

次の例では、shell-lesson-data/exercise-data/writing ディレクトリ内で Yesterday を再帰的に検索してみます:

BASH

$ grep -r Yesterday .

出力

./LittleWomen.txt:"Yesterday, when Aunt was asleep and I was trying to be as still as a
./LittleWomen.txt:Yesterday at dinner, when an Austrian officer stared at us and then
./LittleWomen.txt:Yesterday was a quiet day spent in teaching, sewing, and writing in my
./haiku.txt:Yesterday it worked

grep には他にも多くのオプションがあります。 これらを確認するには、以下を入力します:

BASH

$ grep --help

出力

Usage: grep [OPTION]... PATTERN [FILE]...
Search for PATTERN in each FILE or standard input.
PATTERN is, by default, a basic regular expression (BRE).
Example: grep -i 'hello world' menu.h main.c

Regexp selection and interpretation:
  -E, --extended-regexp     PATTERN is an extended regular expression (ERE)
  -F, --fixed-strings       PATTERN is a set of newline-separated fixed strings
  -G, --basic-regexp        PATTERN is a basic regular expression (BRE)
  -P, --perl-regexp         PATTERN is a Perl regular expression
  -e, --regexp=PATTERN      use PATTERN for matching
  -f, --file=FILE           obtain PATTERN from FILE
  -i, --ignore-case         ignore case distinctions
  -w, --word-regexp         force PATTERN to match only whole words
  -x, --line-regexp         force PATTERN to match only whole lines
  -z, --null-data           a data line ends in 0 byte, not newline

Miscellaneous:
...        ...        ...

grepを使用する

次の出力を得るには、どのコマンドを使用しますか?

出力

and the presence of absence:
  1. grep "of" haiku.txt
  2. grep -E "of" haiku.txt
  3. grep -w "of" haiku.txt
  4. grep -i "of" haiku.txt

正解は3です。-wオプションは、完全な単語の一致のみを検索します。 他のオプションも「of」が他の単語の一部として現れる場合に一致します。

ワイルドカード

grep の真の力はそのオプションではなく、パターンにワイルドカードを含めることができる点にあります。 (これを技術的には正規表現と呼びます。grep の名前の中の「re」はこれを指します。) 正規表現は複雑かつ強力であり、複雑な検索を行いたい場合は、 私たちのウェブサイトのレッスンをご覧ください。 ここではその一例を紹介します。次のようにして、2文字目が「o」である行を見つけることができます:

BASH

$ grep -E "^.o" haiku.txt

出力

You bring fresh toner.
Today it is not working
Software is like that.

-Eオプションを使用し、パターンを引用符で囲むことで、シェルがそのパターンを解釈しようとするのを防ぎます。 (例えば、パターンに * が含まれている場合、シェルは grep を実行する前にそれを展開しようとします。) パターン内の ^ は、行の先頭を基準に一致を固定します。 . は1文字を表し(シェルでの ? と同様)、o は実際の「o」を表します。

種の追跡

Leah は、次のような形式で保存された数百のデータファイルを1つのディレクトリに持っています:

2012-11-05,deer,5
2012-11-05,rabbit,22
2012-11-05,raccoon,7
2012-11-06,rabbit,19
2012-11-06,deer,2
2012-11-06,fox,4
2012-11-07,rabbit,16
2012-11-07,bear,1

彼女は、種名を最初のコマンドライン引数として受け取り、ディレクトリ名を2番目の引数として受け取るシェルスクリプトを書きたいと考えています。 スクリプトは <species>.txt という1つのファイルを出力し、その中に日付とその種が各日に観察された数を記録します。 例えば、上記のデータを使用すると、rabbit.txt の内容は次のようになります:

2012-11-05,22
2012-11-06,19
2012-11-07,16

以下の各行には、個々のコマンドまたはパイプが含まれています。 Leah の目標を達成するために、それらを1つのコマンドの中で並べ替えましょう:

BASH

cut -d : -f 2
>
|
grep -w $1 -r $2
|
$1.txt
cut -d , -f 1,3

ヒント:man grep を使用して、ディレクトリ内でテキストを再帰的に検索する方法を調べ、 man cut を使用して、1行の複数のフィールドを選択する方法を確認してください。

このようなファイルの例は shell-lesson-data/exercise-data/animal-counts/animals.csv にあります。

grep -w $1 -r $2 | cut -d : -f 2 | cut -d , -f 1,3 > $1.txt

実際には、2つの cut コマンドの順序を入れ替えても機能します。 コマンドラインで、cut コマンドの順序を変更して、それぞれのステップの出力を確認してみてください。

上記のスクリプトを次のように呼び出します:

BASH

$ bash count-species.sh bear .

若草物語

Louisa May Alcott の Little Women を読み終えたあなたと友人は議論をしています。 4人の姉妹 Jo、Meg、Beth、Amy のうち、友人は Jo が最も多く言及されていると考えています。 しかし、あなたは Amy だと確信しています。 幸い、LittleWomen.txt というファイルに小説全体のテキストがあります (shell-lesson-data/exercise-data/writing/LittleWomen.txt)。 for ループを使用して、各姉妹が言及された回数を集計するにはどうすればよいでしょうか?

ヒント:解決法の1つは、grepwc、そして | を利用するかもしれません。 もう1つの解決法は、grep のオプションを活用するかもしれません。 プログラミングタスクを解決する方法は1つではないため、特定の解決法は正確な結果、優雅さ、可読性、速度の組み合わせによって選ばれます。

for sis in Jo Meg Beth Amy
do
    echo $sis:
    grep -ow $sis LittleWomen.txt | wc -l
done

別の、やや劣る解決法:

for sis in Jo Meg Beth Amy
do
    echo $sis:
    grep -ocw $sis LittleWomen.txt
done

この解決法が劣る理由は、grep -c は一致した行の数のみを報告するためです。 この方法で報告される一致数は、1行に複数の一致がある場合に少なくなります。

洞察力のある観察者は、キャラクター名が章タイトルで全大文字で表示されることがある(例:‘MEG GOES TO VANITY FAIR’)ことに気づくかもしれません。 これらもカウントしたい場合は、ケースを区別しない -i オプションを追加できます(ただし、この場合、どの姉妹が最も頻繁に言及されているかという答えには影響しません)。

grep はファイル内の行を検索しますが、find コマンドはファイルそのものを検索します。 find には多くのオプションがありますが、その最も簡単なものの使い方を説明するために、 以下に示す shell-lesson-data/exercise-data ディレクトリツリーを使用します。

出力

.
├── animal-counts/
│   └── animals.csv
├── creatures/
│   ├── basilisk.dat
│   ├── minotaur.dat
│   └── unicorn.dat
├── numbers.txt
├── alkanes/
│   ├── cubane.pdb
│   ├── ethane.pdb
│   ├── methane.pdb
│   ├── octane.pdb
│   ├── pentane.pdb
│   └── propane.pdb
└── writing/
    ├── haiku.txt
    └── LittleWomen.txt

exercise-data ディレクトリには1つのファイル numbers.txt と、4つのディレクトリ: animal-countscreaturesalkanes、および writing があり、それぞれにさまざまなファイルが含まれています。

まずは find . を実行してみましょう(shell-lesson-data/exercise-data フォルダ内でコマンドを実行してください)。

BASH

$ find .

出力

.
./writing
./writing/LittleWomen.txt
./writing/haiku.txt
./creatures
./creatures/basilisk.dat
./creatures/unicorn.dat
./creatures/minotaur.dat
./animal-counts
./animal-counts/animals.csv
./numbers.txt
./alkanes
./alkanes/ethane.pdb
./alkanes/propane.pdb
./alkanes/octane.pdb
./alkanes/pentane.pdb
./alkanes/methane.pdb
./alkanes/cubane.pdb

ここで使われている . はカレントディレクトリを意味し、検索をここから開始します。 find の出力はカレントディレクトリ以下のすべてのファイル ディレクトリの名前です。 最初は役に立たないように見えるかもしれませんが、find には多くのオプションがあり、 その出力をフィルタリングできます。このレッスンでは、それらの一部を紹介します。

最初のオプションは -type d で、「ディレクトリであるもの」を意味します。 確かに、find の出力は5つのディレクトリ(. を含む)の名前です:

BASH

$ find . -type d

出力

.
./writing
./creatures
./animal-counts
./alkanes

find が見つけるオブジェクトは特定の順序でリストされないことに注意してください。 -type d-type f に変更すると、すべてのファイルがリストされます:

BASH

$ find . -type f

出力

./writing/LittleWomen.txt
./writing/haiku.txt
./creatures/basilisk.dat
./creatures/unicorn.dat
./creatures/minotaur.dat
./animal-counts/animals.csv
./numbers.txt
./alkanes/ethane.pdb
./alkanes/propane.pdb
./alkanes/octane.pdb
./alkanes/pentane.pdb
./alkanes/methane.pdb
./alkanes/cubane.pdb

次に、名前で一致するものを検索してみましょう:

BASH

$ find . -name *.txt

出力

./numbers.txt

すべてのテキストファイルを見つけることを期待しましたが、./numbers.txt だけが表示されました。 問題は、シェルが * のようなワイルドカードをコマンドが実行される前に展開することです。 カレントディレクトリでは *.txt./numbers.txt に展開されるため、 実際に実行されたコマンドは以下と同じでした:

BASH

$ find . -name numbers.txt

find は指定された動作を実行しましたが、私たちが指定したものが誤っていました。

正しい結果を得るには、grep で行ったように、*.txt を引用符で囲んで シェルがワイルドカードを展開しないようにしましょう。 これにより、find は展開されたファイル名 numbers.txt ではなく、パターン *.txt を受け取ります。

BASH

$ find . -name "*.txt"

出力

./writing/LittleWomen.txt
./writing/haiku.txt
./numbers.txt

リスト表示と検索

lsfind は、適切なオプションを指定すれば似たようなことを実行できますが、 通常の状況では ls は見つけられるすべてをリストし、 find は特定のプロパティを持つものを検索して表示します。

コマンドラインの力はツールを組み合わせることにあります。 これまでにパイプを使う方法を見てきましたが、ここでは別の手法を見てみましょう。 先ほど説明したように、find . -name "*.txt" は現在のディレクトリ以下のすべてのテキストファイルをリストします。 これを wc -l と組み合わせて、これらのファイル内の行数をカウントするにはどうすればよいでしょうか?

最も簡単な方法は、find コマンドを $() の中に入れることです:

BASH

$ wc -l $(find . -name "*.txt")

出力

  21022 ./writing/LittleWomen.txt
     11 ./writing/haiku.txt
      5 ./numbers.txt
  21038 total

シェルがこのコマンドを実行するとき、最初に $() 内のコマンドを実行します。 次に、$() 式をそのコマンドの出力で置き換えます。 find の出力が3つのファイル名 ./writing/LittleWomen.txt./writing/haiku.txt、および ./numbers.txt であるため、 シェルは次のコマンドを構築します:

BASH

$ wc -l ./writing/LittleWomen.txt ./writing/haiku.txt ./numbers.txt

これが意図した通りの結果です。この展開は、*? のようなワイルドカードが展開されるときに シェルが行う処理と正確に同じですが、任意のコマンドを自分の「ワイルドカード」として使用できます。

findgrep を一緒に使うことも非常に一般的です。 最初にファイル名パターンに一致するファイルを検索し、 次にそのファイル内の別のパターンに一致する行を検索します。 例えば、現在のディレクトリ内の .txt ファイルで「searching」という単語を含むものを検索するには、次のようにします:

BASH

$ grep "searching" $(find . -name "*.txt")

出力

./writing/LittleWomen.txt:sitting on the top step, affected to be searching for her book, but was
./writing/haiku.txt:With searching comes loss

マッチングと除外

grep-v オプションはパターンマッチングを反転し、 パターンに一致しない行のみを表示します。 これを踏まえて、creatures ディレクトリ内の .dat ファイルをすべて検索し、 unicorn.dat を除外するには、次のコマンドのうちどれを使用すればよいでしょうか? 考えた後に、shell-lesson-data/exercise-data ディレクトリでコマンドをテストしてみてください。

  1. find creatures -name "*.dat" | grep -v unicorn
  2. find creatures -name *.dat | grep -v unicorn
  3. grep -v "unicorn" $(find creatures -name "*.dat")
  4. 上記のいずれでもない。

正解は1です。マッチ式を引用符で囲むことで、シェルによる

展開を防ぎ、 find コマンドに渡されるようになります。

オプション2もこの場合は機能します。 これは、シェルが *.dat を展開しようとしますが、カレントディレクトリに *.dat ファイルが存在しないため、 ワイルドカード式がそのまま find に渡されるためです。 この動作についてはエピソード3で最初に説明しました。

オプション3は正しくありません。これは、ファイル名ではなく、 ファイル内容の行を検索し、「unicorn」に一致しない行を探してしまいます。

バイナリファイル

これまで、テキストファイル内のパターン検索に焦点を当ててきましたが、 データが画像、データベース、またはその他の形式で保存されている場合はどうすればよいでしょうか?

grep を拡張して一部の非テキスト形式を扱えるようにするツールもありますが、 より一般的なアプローチは、データをテキストに変換するか、 データからテキストに似た要素を抽出することです。 この方法には一長一短があります。 簡単な操作が容易に行える反面、複雑な操作は通常不可能です。 例えば、画像ファイルから X および Y の次元を抽出して grep で処理するプログラムを書くことは容易ですが、 セルに数式が含まれるスプレッドシートで値を検索するものを書くのはどうでしょうか?

最後の選択肢として、シェルやテキスト処理には限界があることを認識し、 他のプログラミング言語を使用する方法があります。 この選択肢を取るとき、シェルをあまり厳しく評価しないでください。 多くの現代的なプログラミング言語がシェルから多くのアイデアを借用しており、 模倣は最高の賛辞でもあります。

Unix シェルは、それを使用する人々のほとんどよりも古いものです。 これが長く生き延びた理由は、かつて作られた最も生産的なプログラミング環境の1つであり、 おそらく最も生産的だからです。 その構文は難解に思えるかもしれませんが、シェルを習得した人々は さまざまなコマンドを対話的に試しながら学んだことを活かして作業を自動化できます。 グラフィカルユーザーインターフェイスは最初は使いやすいかもしれませんが、 シェルを学んだ後の生産性には匹敵しません。 そして、アルフレッド・ノース・ホワイトヘッドが1911年に書いたように、 「文明は、人間が考えなくても重要な操作を行える数を増やすことで進歩する」のです。

find パイプラインの読解力

次のシェルスクリプトについて、簡潔な説明コメントを書いてください:

BASH

wc -l $(find . -name "*.dat") | sort -n
  1. カレントディレクトリ以下で .dat 拡張子を持つすべてのファイルを再帰的に検索する
  2. それらのファイルの各行数をカウントする
  3. ステップ2の出力を数値順にソートする

まとめ

  • find は、特定のプロパティを持つファイルを検索するためのコマンドです。
  • grep は、ファイル内の特定のパターンに一致する行を選択します。
  • --help は、多くの Bash コマンドや Bash 内から実行可能なプログラムがサポートするオプションで、これらのコマンドやプログラムの使用方法に関する詳細情報を表示します。
  • man [コマンド] は、指定したコマンドのマニュアルページを表示します。
  • $([コマンド]) は、そのコマンドの出力を挿入します。