Try   HackMD

ワイルドカードってなに? - シェルもどきをgoで自作する #11

おさらい

これまで

シェルもどき「oreshell」を自作している。
前回はパイプの実装をした。

今回はワイルドカードの実装、動作確認。

ワイルドカードとは

シェルにはワイルドカードと呼ぶ,ファイル名を補完する機能がある. ワイルドカードを使ってパターンを指定し,長いファイル名を短いタイプでマッチさせたり,複数のファイル群にマッチさせたりすることができる. これをシェルのファイル名展開またはファイル名置換という
参考

以下にbashでファイル名の先頭が「a」で始まるファイルの内容を一度にcatコマンドで表示する例を示す。

$ ls *
a.txt a1.txt b.txt
$ cat a.txt
hoge
$ cat a1.txt
fuga
$ cat b.txt
hige
$ cat a*.txt
hoge
fuga

シェルがカレントディレクトリに対してパターンマッチ「a*.txt」でファイル群を検索し、マッチしたファイル名一覧を「展開」したあと、catコマンドに引数を渡している。

パターンマッチで使う文字とその意味

「*」は任意の0文字以上の文字列を意味する。
つまり「a*.txt」は

「a」で始まり「.txt」で終わるが、その間の文字種は(ある範囲内で)自由で長さも自由

というパターンマッチを意味する。
他にも「?」「[」「]」などを指定できる。

ファイル名展開の結果

bashがパターンマッチしてファイル名一覧を取得した後にどんな一覧を展開するかは、事前に「set -x」オプションを実行しておくとわかる。

$ set -x
$ cat a*.txt
+ cat a.txt a1.txt
hoge
fuga

catコマンドに2つの引数「a.txt」「a1.txt」を渡していることがわかる。

では、パターンマッチにヒットしない場合はどうなるか。
いま、カレントディレクトリには「c」で始まる名前のファイルは存在しない(とする)。
パターンマッチ「c*.txt」を指定すると、

$ cat c*.txt
+ cat 'c*.txt'
cat: 'c*.txt': そのようなファイルやディレクトリはありません

となる。

何が起きているのか

おおよそ以下のようなことが起きている。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

パターンマッチにヒットしない場合は、指定した文字列(先の例の場合だと「c*.txt」)をそのままプロセス作成時に使う。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

bashでは「Filename Expansion」と呼ぶ

gnuのbashのマニュアルではワイルドカードによるファイル名展開が「Filename Expansion」という名前になっている。
bashには他にも展開(Expansion)がある。どんな展開があるか知りたい人はコチラ
"tilde"、"Shell Parameter"などなんとなく「ああ、あれかな」と予想できそうだがここでは割愛。

oreshellに実装する

パターンマッチの実現方法と仕様

パターンマッチをゼロから作るとめちゃくちゃめんどくさいのでできれば作りたくない。
golangのfilepath.Glob()でおおよそ似たようなことができるのでoreshellではこれを使う。
filepath.Glob()のパターンマッチの仕様は以下の通り。

Match reports whether name matches the shell file name pattern. The pattern syntax is:

pattern:
	{ term }
term:
	'*'         matches any sequence of non-Separator characters
	'?'         matches any single non-Separator character
	'[' [ '^' ] { character-range } ']'
	            character class (must be non-empty)
	c           matches character c (c != '*', '?', '\\', '[')
	'\\' c      matches character c

character-range:
	c           matches character c (c != '\\', '-', ']')
	'\\' c      matches character c
	lo '-' hi   matches character c for lo <= c <= hi
Match requires pattern to match all of name, not just a substring. The only possible returned error is ErrBadPattern, when pattern is malformed.

ただ、現時点ではfilepath.Glob()のパターンマッチの「全」仕様がoreshell動作時に正しく動作するかは(めんどくさいので)確認しない。
今回は「*」が使えるかどうかだけ確認する。

修正したoreshellのコード

コチラ

つまづいたところ、ハマったところ

a)エスケープ、クォートをいつ除去するのか

一度実装して動作確認をしているとき、

$ cat a*.txt
hoge
fuga

と、期待した通りに動いたが
パターンマッチをクォートでくくると

$ cat "a*.txt"
hoge
fuga

となった。
期待していた動作は

$ cat "a*.txt"
cat: 'a*.txt': そのようなファイルやディレクトリはありません

である。
デバッグ調査したところ、構文解析して評価を行う時に

  1. エスケープ、クォートの除去
  2. ワイルドカードによるファイル名の展開を行う
  3. プロセス作成

という実装になっていることがわかった。
これを

  1. ワイルドカードによるファイル名の展開を行う
  2. 展開されなかった文字列からエスケープ、クォートの除去
  3. プロセス作成

の順番に変更すると期待した動作になった。

該当箇所

■補足
なぜ「エスケープ、クォートの除去」を行うか。
例:

$ grep "ab.*" *

1つ目の引数のクォートの内側の「ab.*」をgrepに渡したい。
しかし、「ab.*」をシェルのパターンマッチに渡したくない。
その場合は上記のようにクォートでくくる必要がある。(grepに渡すときにはクォートを除去する必要がある。)
2つ目の引数である「*」はクォートしていないためシェルのパターンマッチの対象になる。

■この修正後に気づいたこと

本資料を書いているときにgnuのbashのマニュアルのShell-Expansionsに、expansionがずらりと並んでいて最後にQuote Removalとある。
中を読んでみると

After the preceding expansions, all unquoted occurrences of the characters ‘\’, ‘'’, and ‘"’ that did not result from one of the above expansions are removed.
上記の展開の後、上記の展開のいずれかの結果ではない、引用符で囲まれていない文字 「\」 、 「」 、および 「"」 がすべて削除されます。(みらい翻訳)

自分が悩んだ「エスケープ、クォートをいつ除去するのか」の答えが書いてあった。orz

b)どの部分がワイルドカードによるファイル名展開の対象となるのか

oreshellの修正をしている間ずっと

ワイルドカードによるファイル名展開の対象はコマンドの引数だけ

と勝手に思いこんでいた。
いったん実装が終わった後にいろいろ調べてみて、コマンド名もワイルドカードによるファイル名展開の対象ということがわかった(というか、そうであることを思い出した)。

コマンド名をワイルドカードによるファイル名展開する例(bash):

$ set -x 
$ /bin/fi*
+ /bin/file /bin/finalrd /bin/fincore /bin/find /bin/findmnt
/bin/finalrd: POSIX shell script, ASCII text executable
/bin/fincore: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ddd138508cac9a9a8f442bf1ccd1e9a6259bba5e, for GNU/Linux 3.2.0, stripped
/bin/find:    ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b8b9756abacab10f704aec42954e3fd2292f1e85, for GNU/Linux 3.2.0, stripped
/bin/findmnt: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=53f49534d008a497eae7eaf3ca15d0ace6053b74, for GNU/Linux 3.2.0, stripped

/binに「file」というコマンドがある(linux/ubuntu202004)。「file」コマンドは引数として指定したファイルがどんな種類のファイルなのかを調べて表示する機能を持つ。
/binには「fi」で始まる名前のコマンドが他にも「finalrd」「fincore」「find」「findmnt」がある。
「/bin/fi*」をシェルで実行すると、シェルはワイルドカードによるファイル名の展開を行い、「/bin/file /bin/finalrd /bin/fincore /bin/find /bin/findmnt」という文字列にする。
シェルはこの文字列を元にプロセスの作成をカーネルに依頼する。

コマンド名をワイルドカードによるファイル名展開して何がうれしいのか、どんなメリットがあるのかはさっぱりわからない。。。
が、この時点のoreshellは上記と同じ結果にならなかったので、コマンド名もワイルドカードによるファイル名展開の対象となるようにoreshellを修正した。

動作確認

(ore) > ls *
a.txt  a1.txt  b.txt
(ore) > cat a.txt
hoge
(ore) > cat a1.txt
fuga
(ore) > cat b.txt
hige
(ore) > cat a*.txt
hoge
fuga
(ore) > cat c*.txt
cat: 'c*.txt': そのようなファイルやディレクトリはありません
(ore) > cat "a*.txt"
cat: 'a*.txt': そのようなファイルやディレクトリはありません
(ore) > /bin/fi*
/bin/finalrd: POSIX shell script, ASCII text executable
/bin/fincore: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ddd138508cac9a9a8f442bf1ccd1e9a6259bba5e, for GNU/Linux 3.2.0, stripped
/bin/find:    ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b8b9756abacab10f704aec42954e3fd2292f1e85, for GNU/Linux 3.2.0, stripped
/bin/findmnt: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=53f49534d008a497eae7eaf3ca15d0ace6053b74, for GNU/Linux 3.2.0, stripped

参考

UNIXのワイルドカード
http://www.edu.tuis.ac.jp/~mackin/java/2008/linux/wildcard.html

gnu bash マニュアル
https://www.gnu.org/software/bash/manual/html_node/index.html#SEC_Contents

Linux【ワイルドカードと正規表現】の違いと変換,展開の動作
https://milestone-of-se.nesuke.com/sv-basic/linux-basic/wildcard-regular-expression/

シェルにおけるワイルドカードの挙動についての整理 - Qiita
https://qiita.com/daisuke0115/items/76136703c90e1f17a1fc