🐊

Denoで簡易grepを実装

2021/12/06に公開

新しい言語を学ぶ時はその言語で何かを作ってみること
と言われるので(聞いたこと無いけど、多分)、Denoの雰囲気を掴む為にCLIを作ってみようと思う
※Denoを試す事を目的としているため正常系以外は考慮しない

それがこちら
https://github.com/l-freeze/dgrep

1. この記事で扱うDenoテク

  1. ファイルの読み込み
  2. コマンドライン引数の扱い方
  3. 標準出力

2. 作るコマンドの候補を考える

stdinstdoutを使いつつ簡単で簡単過ぎないものを作りたい

  1. cat
  2. grep

3. コマンドの選定する

cat:却下

公式ドキュメントにサンプルコードがある為
https://deno.land/manual@v1.16.4/examples/unix_cat

grep:採用

catの却下により消去法でgrepの作成に決定

4. 環境構築

Denoインストール

  • Windows10の人
powershell7
scoop install deno
  • WSL2の人
wsl
asdf plugin-add deno
asdf install deno 1.16.4
asdf global deno 1.16.4

インストール確認

$ deno
Deno 1.16.4
exit using ctrl+d or close()
> Deno.env.toObject();

VSCode設定

https://zenn.dev/uki00a/books/effective-deno/viewer/first-step
こちらの手順を参考に設定をする
(設定手順ありがとうございます。今まではVSCodeの設定開いて設定してました。)

5. 仕様を考える

grep --helpを元にコマンドのオプションを考える
と言いたい所だけど今回は良く使うコマンドのうち実装が簡単なもののみに限定する
| オプション | 説明 |
| ---- | ---- | ---- |
| -E --extended-regexp | PATTERNS are extended regular expressions |
| -i --ignore-case | ignore case distinctions in patterns and data |

デフォルトで--extended-regexpとしたいので今回作るのは実質egrep -i ./filepathという事になる

6. 作る

テスト用のファイル作成

Wikiのdenoの説明を引用してサンプルを作る

sample.txt
概要
Denoは現代のプログラマのための生産的で安全なスクリプト環境を目指している[5]。Node.jsと同様に、Denoはイベント駆動型アーキテクチャ(英語版)に重点を置いており、非ブロッキングコアIOユーティリティのセットとそのブロッキング版を提供している。DenoはWebサーバの作成や科学計算に利用することができる。発音はデノが近く一般的だが、ディーノと呼ばれることもある。

Node.jsとの比較
DenoとNode.jsはGoogle ChromeなどのChromiumベースのウェブブラウザで採用されているV8 JavaScriptエンジン上に構築されたランタイム環境である。どちらも内部イベントループがあり、スクリプトと広範なコマンドラインユーティリティを実行するためのコマンドラインインタフェースを提供している。

DenoがNode.jsと異なる主な点は以下の通りである[5]:

CommonJSの代わりに、ES Moduleをデフォルトのモジュールとシステムとして使用する。
ウェブブラウザと同様に、依存関係 (ローカル及びリモート) を読み込むためにURLを使用する。
リソースを取得するためのパッケージ管理システムが組み込まれているので、npmは不要である。
キャッシングメカニズムを備えたスナップショットTypeScriptコンパイラを使用することによるTypeScriptのサポート。
幅広いWeb APIを実装することによるウェブブラウザとの互換性の向上。
サンドボックスコードを実行するために、ファイルシステムとネットワークアクセスを制御することができる。
promise、ES6及びTypeScriptの機能を利用するためにAPIを再設計したこと。
コアAPIのサイズを最小限にしながら、外部に依存関係の無い大きな標準ライブラリを提供すること。
特権システムAPIを呼び出しとバインディングの使用のために、メッセージ受け渡しチャネルを使用すること。

実装(1)

まずは引数によるファイル指定は考えずに実装する

deno
const readText = await Deno.readTextFile("./sample.txt");
const regex = /node/i;
for(const row of readText.split(/\r\n|\r|\n/)){
    if(-1 != row.search(regex)){
        console.log(row);
    }
}

実行

deno run --allow-read .\main.ts
動いた
しかし考える間でもなく見えている問題点が二つ

  • 巨大なファイルで死ぬ
  • console.log使ってる

実装(2)

サイズの大きいファイルも扱えるようにBufReaderを使う

deno
import { BufReader,ReadLineResult } from "https://deno.land/std@0.117.0/io/mod.ts";
const filepath = "./sample.txt";
const regex = /node/i;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const lfUint8 = encoder.encode("\n");
const stdout = Deno.stdout;
const file = await Deno.open(filepath, {read: true});
const bufReader = BufReader.create(file);
let result: ReadLineResult|null;
if(Deno.build.os == 'windows'){
    console.log("windowsのエンコードはどないしたらええんや");
}
while ( (result = await bufReader.readLine()) != null ) {
    if(-1 != decoder.decode(result.line).search(regex)){
        await stdout.write(result.line);
        await stdout.write(lfUint8);
    }
}
file.close();

実行

deno run --allow-read main.ts
動いた
あとは-i-Eに対応するのみ

実装(3)

コマンドライン引数への対応

コマンドライン引数を扱う為にDeno.argsStandard Libraryflagsを使う

deno
import {parse} from "https://deno.land/std@0.117.0/flags/mod.ts";

const filepath = Deno.args[Deno.args.length-1];
if(filepath == undefined){
    console.log("ダメ");
    Deno.exit();
}
console.log(parse(Deno.args, {"--": false}))
const iFlag = !parse(Deno.args).i ? false : true;

試したところ面倒臭そうだったので使い勝手良さそうなライブラリを求めてThird Party Modulesargvを検索したところ、★たくさんのyargs@v17.3.0-denoが引っかかったのでとりあえず試す

deno
import yargs from 'https://deno.land/x/yargs/deno.ts';
import { Arguments } from 'https://deno.land/x/yargs/deno-types.ts';
const argv: Arguments  = yargs(Deno.args)
  .usage('dgrep [OPTION]... PATTERNS [FILE]..')
  .command('dgrep', 'deno grep')
  .alias('i', 'ignore-case')
  .describe('i', 'ignore case distinctions in patterns and data')
  .alias('n', 'line-number')
  .describe('n', 'print line number with output lines')
  .alias('h', 'help')
  .boolean(['i', 'n'])
  .default('i', false)
  //.demandOption(['i'])//required
  //.strictCommands()
  .parse();

  console.log(argv);

簡単に処理出来る事を確認出来たのでついでにnフラグを追加し、行番号も結果に表示出来るようにする

deno
//追加する
let lineNumber = 0;
while ( (result = await bufReader.readLine()) != null ) {
    lineNumber++;
}

7. 実装完了

完成したもの

https://github.com/l-freeze/dgrep

試す

deno検索
$ deno run --allow-read main.ts deno ./sample.txt
iオプション付きでdeno検索
deno run --allow-read main.ts -i deno ./sample.txt
Denoは現代のプログラマのための生産的で安全なスクリプト環境を目指している[5]。Node.jsと同様に、Denoはイベント駆動型アーキテクチャ(英語版)に重点を置いており、非ブロッキングコアIOユーティリティのセットとそのブロッキング版を提供している。DenoはWebサーバの作成や科学計算に利用することができる。発音はデノが近く一般的だが、ディーノと呼ばれることもある。
DenoとNode.jsはGoogle ChromeなどのChromiumベースのウェブブラウザで採用されているV8 JavaScriptエンジン上に構築されたランタイム環境である。どちらも内部イベントループがあり、スクリプトと広範なコマンド ラインユーティリティを実行するためのコマンドラインインタフェースを提供している。
DenoがNode.jsと異なる主な点は以下の通りである[5]:
行番号(nオプション)付きでウェブ検索
$ deno run --allow-read main.ts -n "ウェブ" ./sample.txt
5: DenoとNode.jsはGoogle ChromeなどのChromiumベースのウェブブラウザで採用されているV8 JavaScriptエンジン上に構築されたランタイム環境である。どちらも内部イベントループがあり、スクリプトと広範なコマンドラインユーティリティを実行するためのコマンドラインインタフェースを提供している。
10: ウェブブラウザと同様に、依存関係 (ローカル及びリモート) を読み込むためにURLを使用する。
13: 幅広いWeb APIを実装することによるウェブブラウザとの互換性の向上。

8. コンパイル

denoの入っていない環境でも動かせるようにする為にコンパイルをする

wsl
$ deno compile --allow-read main.ts
Check file:///path/dgrep/main.ts
Bundle file:///path/dgrep/main.ts
Compile file:///path/dgrep/main.ts
Emit dgrep
wsl
$ ls
dgrep  main.ts  sample.txt

出来た
コンパイルとは名ばかりのまるでdeno本体とコードをくっつけているだけかのようなファイルサイズ(84M)

9. 作ったコマンド単体で試す

ダブルクォートで括らずに日本語を検索
./dgrep リソース ./sample.txt
リソースを取得するためのパッケージ管理システムが組み込まれているので、npmは不要である。
inオプションセットで検索
$ ./dgrep -in script ./sample.txt
5: DenoとNode.jsはGoogle ChromeなどのChromiumベースのウェブブラウザで採用されているV8 JavaScriptエンジン上に構築されたランタイム環境である。どちらも内部イベントループがあり、スクリプトと広範なコマンドラインユーティリティを実行するためのコマンドラインインタフェースを提供している。
12: キャッシングメカニズムを備えたスナップショットTypeScriptコンパイラを使用することによるTypeScriptのサポート。
15: promise、ES6及びTypeScriptの機能を利用するためにAPIを再設計したこと。
正規表現で検索
wsl
$ ./dgrep -in "j........t|common" ./sample.txt
5: DenoとNode.jsはGoogle ChromeなどのChromiumベースのウェブブラウザで採用されているV8 JavaScriptエンジン上に構築されたランタイム環境である。どちらも内部イベントループがあり、スクリプトと広範なコマンドラインユーティリティを実行するためのコマンドラインインタフェースを提供している。
9: CommonJSの代わりに、ES Moduleをデフォルトのモジュールとシステムとして使用する。

まとめ

動作重過ぎ(作り方が悪いだけかもしれないけど)
サイズでかすぎ

Discussion