de[v|b]log

ShellScript, Coffee, iOS/OSX Dev
Origin: Himajinworks.
About.

今日はまずgrepコマンドに対する簡単な実装をzshスクリプトで書いてみた。

ここ数日の記事でTODOとして「コマンドのwrapperを作成する」ということが上がってきていたので、zfc(zsh-filter-collection)としてリポジトリを作成し、作業を行った。実際のところwrapperではない。

進捗

1
2
3
4
679a7e7 [MOD] Update README and LICENSE
b9dabca [ADD] Wrote main body of grep
24ed686 [ADD] Added grep implementation
85a2115 [ADD] Initiaized with base items

完了項目

  • 単純なgrepコマンドの作成

今回は単純なgrepの作成として、「パイプで入力、もしくは引数にファイルを指定して入力」したものに対して、後に続く正規表現とonlyオプションが適用できるものを作成することをゴールとした。

zfcではzfc::grepのようなコマンド名をベースとして利用し作成を行っているため、長いがzfc::grepを以下でも用いる。

1
zfc::grep -h
1
2
3
4
5
6
7
8
9
10
11
12
SYNOPSIS
    zfc::grep::help [file] [-e|-regexp [pattern]] [-o|-only]

USAGE
    From file: zfc::grep::help filename -e 'regexp' (-o)
    From pipe: other_command | zfc::grep::help -e 'regexp' (-o)
    Help:      zfc::grep::help -h

OPTION
    -e, -regexp : Takes following regular expression
    -o, -only   : Shows only matched part
    -h, -help   : Shows this message

完成したものの動作をまずは以下で提示する。 今回入力ファイルとしてinput.csvを以下に定義する。

1
2
apple, 100, A
banana, 200, B

上記のinput.csvに対しての挙動が以下となる。

列に対するマッチ

1
zfc::grep input.csv -e '.*100'
1
apple, 100, A

部分に対するマッチ

1
zfc::grep input.csv -e '.*100' -o
1
apple, 100

このように、正規表現にマッチする行の取得、及び-oオプションによる正規表現にマッチした部分のみの出力が可能となった。

実装

今回-oの実装もあったため、先日の記事で紹介した${match}を利用した。

ファイルからの読み込み

ファイルからはこのように読み込める。

1
local lines=(${(@f)"$(cat ${file_path})"})

パイプからの読み込み

パイプからの読み込みはstdinを経由するため以下のようになる。

1
local lines=(${(@f)"$(cat < /dev/stdin)"})

マッチ

上記のようにし読み込んだ各行を以下のように処理することで、各行ごとにマッチ列を取得・出力できる。

1
2
3
4
5
6
7
8
9
10
11
12
local line
for line (${lines}); do
    [[ "${line}" =~ "(${regexp})" ]] && {
        [[ -n "${match}" ]] && {
            [[ ${match_only} == 1 ]] && {
                echo ${match[1]}
            } || {
                echo ${line}
            }
        }
    }
; done

ここで${match_only}-oが与えられたかどうか、${regexp}は入力された正規表現が入っている。

このようにすることで上記の処理を実現した。

問題点

  • 書いていて、本来は${(M)line:#${regexp}}のようにして正規表現を適用し実現しようと考えていたが、どうやっても${regexp}の部分が思ったように評価されなかったため今回はマッチを利用した。
  • 入力が大きくなると線形的に処理しているためとても遅くなる。zfc::grep <(seq 1 100000) -e '.*999'なんて地獄以外の何物でもなかった。

それから

今回引数を取るプラグインを作成するにあたって、一般的に使うgetoptsをオプション無し引数と併用する際の問題点に気づいた。

1
zfc::grep input.csv -e '.*100' -o

例として上の処理がある。この処理では入力ファイル名をオプション無しで渡すことになるが、この場合

1
2
while getopts 'e:ho' opt; do
    case $opt in

のように記述するとcaseのブロックに引っかからず終わってしまう。このため、第一引数にファイル名等のオプション無し引数をとった場合、shiftしなければならない。よって以下のように処理を書いた。 尚、今回zfc::grepではgrepに準じてパイプからの入力も受け取るため、/dev/stdinの処理も記述している。

1
2
3
4
5
6
7
local file_path=''
[[ "${1}" =~ '-.*' ]] && {
    file_path=/dev/stdin
} || {
    file_path=${1}
    shift
}

このようにすることでパイプからの入力なのかどうなのかを判別するとともに、後の処理で利用できるファイルパスを用意できる。