Closed20

DenoでCLIツール作ってみる

kawarimidollkawarimidoll

Deno.argsで引数を取得できる

console.log(Deno.args);
❯ deno run main.ts param1 param2 param3
[ "param1", "param2", "param3" ]

stg/flagsparseを使うと引数をフラグで分けて取得できる

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

  console.log(Deno.args);

+ console.log(parse(Deno.args));
❯ deno run main.ts -a AA -b BB --long LONG param1 param2 param3
[
  "-a",     "AA",
  "-b",     "BB",
  "--long", "LONG",
  "param1", "param2",
  "param3"
]
{ _: [ "param1", "param2", "param3" ], a: "AA", b: "BB", long: "LONG" }

Deno.argsだけだと単に配列だがparseを使うことでオブジェクトになっている
_にはフラグと関係ない引数が入る

❯ deno run main.ts -a -b --long=LONG --long=LONG2 param1 param2 param3
[
  "-a",
  "-b",
  "--long=LONG",
  "--long=LONG2",
  "param1",
  "param2",
  "param3"
]
{ _: [ "param1", "param2", "param3" ], a: true, b: true, long: [ "LONG", "LONG2" ] }

パラメータが渡されなかった引数はtrueになる
同じ名前のものは配列にまとめられる

kawarimidollkawarimidoll

std/pathを使ってディレクトリパスを取得し、Deno.readDir()で内部を表示する

main.ts
import { parse } from "https://deno.land/std@0.100.0/flags/mod.ts";
import { resolve } from "https://deno.land/std@0.100.0/path/mod.ts";

const { _: [dir = "."] } = parse(Deno.args);

const dirFullPath = resolve(Deno.cwd(), String(dir));
console.log(dirFullPath);
for await (const entry of Deno.readDir(dirFullPath)) {
  console.log(entry.name);
}

Deno.cwd()の事項にはread permissionが必要

❯ deno run --allow-read main.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground
.vim
.DS_Store
main.ts
LICENSE
line_notify.ts
fetch_twilog.ts
tweet_with_ifttt.ts
README.md
logger.ts
.gitignore
.env
examples
.github
.env.example
env.ts
.git
app.log
assets
deps.ts
rss.ts
promote_zenn_article.ts
velociraptor.yml

❯ deno run --allow-read main.ts examples
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/examples
line_notify.ts
tweet_with_ifttt.ts
rss.ts
zenn_api.ts

ディレクトリの中身を表示できた

kawarimidollkawarimidoll

これを辿っていくのはちょっとtrickyということでstd/fswalkを使う

main.ts
  import { parse } from "https://deno.land/std@0.100.0/flags/mod.ts";
  import { resolve } from "https://deno.land/std@0.100.0/path/mod.ts";
+ import { walk } from "https://deno.land/std@0.100.0/fs/mod.ts";

  const { _: [dir = "."] } = parse(Deno.args);

  const dirFullPath = resolve(Deno.cwd(), String(dir));
  console.log(dirFullPath);
- for await (const entry of Deno.readDir(dirFullPath)) {
-   console.log(entry.name);
+ for await (const entry of walk(dirFullPath, { skip: [/\.git$/] })) {
+   console.log(entry);
  }

skip: [/\.git$/]オプションで.gitディレクトリの中は見ないようにしている
https://deno.land/std@0.100.0/fs/walk.ts

wak()の実行はunstableフラグが必要

❯ deno run --allow-read --unstable main.ts
Check file:///Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/main.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground
{
  path: "/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground",
  name: "deno-dev-playground",
  isFile: false,
  isDirectory: true,
  isSymlink: false
}
{
  path: "/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.vim",
  name: ".vim",
  isFile: false,
  isDirectory: true,
  isSymlink: false
}
{
  path: "/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.vim/coc-settings.json",
  name: "coc-settings.json",
  isFile: true,
  isDirectory: false,
  isSymlink: false
}
{
  path: "/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.DS_Store",
  name: ".DS_Store",
  isFile: true,
  isDirectory: false,
  isSymlink: false
}
{
  path: "/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/main.ts",
  name: "main.ts",
  isFile: true,
  isDirectory: false,
  isSymlink: false
}
{
  path: "/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/LICENSE",
  name: "LICENSE",
  isFile: true,
  isDirectory: false,
  isSymlink: false
}
(略)

配下のファイルが一覧できている

kawarimidollkawarimidoll

walk()skipの他にも以下のオプションを持つ:maxDepth, includeFiles, includeDirs,followSymlinks, exts, match
ということでこんなパラメータにする

  • type
    • dir ディレクトリを含める
    • file ファイルを含める
  • depth 深度を指定 デフォルトは2
main.ts
  import { parse } from "https://deno.land/std@0.100.0/flags/mod.ts";
  import { resolve } from "https://deno.land/std@0.100.0/path/mod.ts";
  import { walk } from "https://deno.land/std@0.100.0/fs/mod.ts";

- const { _: [dir = "."] } = parse(Deno.args);
+ const { depth = "2", type, _: [dir = "."] } = parse(Deno.args);
+ const types = type ? (Array.isArray(type) ? type : [type]) : ["file", "dir"];
+ const options = {
+   maxDepth: Number(depth),
+   includeFiles: !types.includes("file"),
+   includeDirs: !types.includes("dir"),
+   skip: [/\.git$/],
+ };

  const dirFullPath = resolve(Deno.cwd(), String(dir));
  console.log(dirFullPath);
- for await (const entry of walk(dirFullPath, { skip: [/\.git$/] })) {
+ for await (const entry of walk(dirFullPath, options)) {
    console.log(entry);
  }

これで実行する

❯ deno run --allow-read --unstable main.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground
deno-dev-playground/
.vim/
coc-settings.json
.DS_Store
main.ts
LICENSE
line_notify.ts
fetch_twilog.ts
tweet_with_ifttt.ts
README.md
logger.ts
dgc.ts
.gitignore
.env
examples/
line_notify.ts
tweet_with_ifttt.ts
rss.ts
zenn_api.ts
.github/
workflows/
.env.example
env.ts
app.log
assets/
deps.ts
rss.ts
promote_zenn_article.ts
velociraptor.yml

❯ deno run --allow-read --unstable main.ts --type=dir
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground
deno-dev-playground/
.vim/
examples/
.github/
workflows/
assets/

良さげだ

kawarimidollkawarimidoll

regexパラメータも追加しよう
入力されたものを正規表現としてオプションに渡す

main.ts
  import { parse } from "https://deno.land/std@0.100.0/flags/mod.ts";
  import { resolve } from "https://deno.land/std@0.100.0/path/mod.ts";
  import { walk } from "https://deno.land/std@0.100.0/fs/mod.ts";

  const { depth = "2", type, regex, _: [dir = "."] } = parse(Deno.args);
  const types = type ? (Array.isArray(type) ? type : [type]) : ["file", "dir"];
+ const match = regex
+   ? (Array.isArray(regex) ? regex : [regex]).map(
+     (str) => new RegExp(str),
+   )
+   : undefined;

  const options = {
    maxDepth: Number(depth),
    includeFiles: types.includes("file"),
    includeDirs: types.includes("dir"),
+   match,
    skip: [/\.git$/],
  };

  const dirFullPath = resolve(Deno.cwd(), String(dir));
  console.log(dirFullPath);
  for await (const entry of walk(dirFullPath, options)) {
    console.log(entry.name + (entry.isDirectory ? "/" : ""));
  }
❯ deno run --allow-read --unstable main.ts --regex=".*\.ts"
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground
main.ts
line_notify.ts
fetch_twilog.ts
tweet_with_ifttt.ts
logger.ts
dgc.ts
line_notify.ts
tweet_with_ifttt.ts
rss.ts
zenn_api.ts
env.ts
deps.ts
rss.ts
promote_zenn_article.ts
kawarimidollkawarimidoll

最後にヘルプも載せておこう

main.ts
// (略)
- const { depth = "2", type, regex, _: [dir = "."] } = parse(Deno.args);
+ const { depth = "2", type, help, regex, _: [dir = "."] } = parse(Deno.args);

+ if (help) {
+   console.log("denofind");
+   console.log("Usage");
+   console.log(`  denofind --type=file --regex="*.\.ts" --depth=3 target_dir`);
+   Deno.exit(0);
+ }
// (略)
❯ deno run --allow-read --unstable main.ts --help
denofind
Usage
  denofind --type=file --regex="*..ts" --depth=3 target_dir
kawarimidollkawarimidoll

これtree作れそうだな やってみよう
entry.path.replace(dirFullPath, ".")という手もあるけどstd/pathrelativeというのが定義されている

main.ts
- import { resolve } from "https://deno.land/std@0.100.0/path/mod.ts";
+ import { relative, resolve } from "https://deno.land/std@0.100.0/path/mod.ts";

// (略)
  const dirFullPath = resolve(Deno.cwd(), String(dir));
  for await (const entry of walk(dirFullPath, options)) {
-   console.log(entry.name + (entry.isDirectory ? "/" : ""));  
+  const relativePath = relative(dirFullPath, entry.path) +
+     (entry.isDirectory ? "/" : "");
+   console.log(relativePath);
  }
❯ vr start
/
.vim/
.vim/coc-settings.json
.DS_Store
main.ts
LICENSE
line_notify.ts
fetch_twilog.ts
tweet_with_ifttt.ts
README.md
logger.ts
.gitignore
.env
examples/
examples/line_notify.ts
examples/tweet_with_ifttt.ts
examples/rss.ts
examples/zenn_api.ts
.github/
.github/workflows/
.env.example
env.ts
app.log
assets/
deps.ts
rss.ts
promote_zenn_article.ts
velociraptor.yml
kawarimidollkawarimidoll

https://qiita.com/yone098@github/items/bba8a42de6b06e40983b
こちらを参考にこんな感じになった

main.ts
//(前略)
const dirFullPath = resolve(Deno.cwd(), String(dir));
const entries: WalkEntry[] = [];
for await (const entry of walk(dirFullPath, options)) {
  entries.push(entry);
}

entries.sort((a, b) => a.path > b.path ? 1 : -1).forEach((entry) => {
  console.log(
    entry.path === dirFullPath
      ? "."
      : relative(dirFullPath, entry.path).replace(
        entry.name,
        "|--" + entry.name,
      )
        .replace(/[^\/]+\//g, "|  "),
  );
});
❯ vr start
.
|--.DS_Store
|--.env
|--.env.example
|--.github
|  |--workflows
|  |  |--promote_zenn_article.yml
|  |  |--welcome-deno.yml
|--.gitignore
|--.vim
|  |--coc-settings.json
|--LICENSE
|--README.md
|--app.log
|--assets
|--deep
|  |--nested
|  |  |--dir
|  |  |  |--hidden
|  |  |  |  |--file.ts
|--deps.ts
|--env.ts
|--examples
|  |--line_notify.ts
|  |--rss.ts
|  |--tweet_with_ifttt.ts
|  |--zenn_api.ts
|--fetch_twilog.ts
|--line_notify.ts
|--logger.ts
|--main.ts
|--promote_zenn_article.ts
|--rss.ts
|--tweet_with_ifttt.ts
|--velociraptor.yml

うーん簡易版だな、せっかくだしwalkじゃなくてちゃんと再帰しよう

kawarimidollkawarimidoll

walkのソースを確認する
https://deno.land/std@0.100.0/fs/walk.ts

walk.ts
export async function* walk(
  root: string,
  {
    maxDepth = Infinity,
    includeFiles = true,
    includeDirs = true,
    followSymlinks = false,
    exts = undefined,
    match = undefined,
    skip = undefined,
  }: WalkOptions = {},
): AsyncIterableIterator<WalkEntry> {
  if (maxDepth < 0) {
    return;
  }
  if (includeDirs && include(root, exts, match, skip)) {
    yield await _createWalkEntry(root);
  }
  if (maxDepth < 1 || !include(root, undefined, undefined, skip)) {
    return;
  }
  try {
    for await (const entry of Deno.readDir(root)) {
      assert(entry.name != null);
      let path = join(root, entry.name);

      if (entry.isSymlink) {
        if (followSymlinks) {
          path = await Deno.realPath(path);
        } else {
          continue;
        }
      }

      if (entry.isFile) {
        if (includeFiles && include(path, exts, match, skip)) {
          yield { path, ...entry };
        }
      } else {
        yield* walk(path, {
          maxDepth: maxDepth - 1,
          includeFiles,
          includeDirs,
          followSymlinks,
          exts,
          match,
          skip,
        });
      }
    }
  } catch (err) {
    throw wrapErrorWithRootPath(err, normalize(root));
  }
}

Deno.readDirで現在のディレクトリの配下を表示し、そのうちディレクトリに関しては再帰している
https://doc.deno.land/builtin/stable#Deno.readDir

途中のassertの実装は以下 要するにfalsyのときエラーを投げる

assert.ts
export class DenoStdInternalError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "DenoStdInternalError";
  }
}

/** Make an assertion, if not `true`, then throw. */
export function assert(expr: unknown, msg = ""): asserts expr {
  if (!expr) {
    throw new DenoStdInternalError(msg);
  }
}

今回treeは見つけたファイルを再利用したいわけではなく単に表示したいのでジェネレータとして作る必要はない
単純に再帰関数で作ればよい

では実装しよう

kawarimidollkawarimidoll

公式のwalkを読み込むとunstabledフラグが必要なのでWalkEntryも読み込まずTreeEntryを作成

tree.ts
import { parse } from "https://deno.land/std@0.100.0/flags/mod.ts";
import { join, resolve } from "https://deno.land/std@0.100.0/path/mod.ts";

// WalkEntry of https://deno.land/std@0.100.0/fs/mod.ts
export interface TreeEntry extends Deno.DirEntry {
  path: string;
}

const tree = async (root: string) => {
  const entries: TreeEntry[] = [];
  for await (const entry of Deno.readDir(root)) {
    const treeEntry = { ...entry, path: join(root, entry.name) };
    entries.push(treeEntry);
  }

  for await (
    const entry of entries.sort((a, b) =>
      a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1
    )
  ) {
    console.log(entry.path);
  }
};

const { _: [dir = "."] } = parse(Deno.args);
await tree(resolve(Deno.cwd(), String(dir)));

これで実行する
現在のディレクトリの配下の情報が一覧される

❯ deno run --allow-read tree.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.DS_Store
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.env
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.env.example
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.git
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.github
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.gitignore
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.vim
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/app.log
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/assets
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/deep
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/deps.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/env.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/examples
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/fetch_twilog.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/LICENSE
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/line_notify.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/logger.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/main.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/promote_zenn_article.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/README.md
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/rss.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/tree.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/tweet_with_ifttt.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/velociraptor.yml

まだ一段階しか見ていない

kawarimidollkawarimidoll

表示する際にそれが

  • ファイルだったら名前を表示して次へ
  • ディレクトリだったらそこをルートとして再帰

.gitディレクトリを掘ってしまうと大変なので決め打ちで除外

tree.ts
(略)
    console.log(entry.path);
+     if (entry.isDirectory && entry.name !== ".git") {
+       await tree(entry.path);
+     }
(略)
❯ deno run --allow-read tree.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.DS_Store
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.env
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.env.example
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.git
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.github
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.github/workflows
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.github/workflows/promote_zenn_article.yml
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.github/workflows/welcome-deno.yml
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.gitignore
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.vim
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/.vim/coc-settings.json
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/app.log
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/assets
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/deep
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/deep/nested
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/deep/nested/dir
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/deep/nested/dir/hidden
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/deep/nested/dir/hidden/file.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/deps.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/env.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/examples
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/examples/line_notify.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/examples/rss.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/examples/tweet_with_ifttt.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/examples/zenn_api.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/fetch_twilog.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/LICENSE
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/line_notify.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/logger.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/main.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/promote_zenn_article.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/README.md
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/rss.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/tree.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/tweet_with_ifttt.ts
/Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-dev-playground/velociraptor.yml

深い階層まで見ることができている

kawarimidollkawarimidoll
  • フルパスではなく名前だけを表示するようにする
  • 階層を掘るときにprefixを渡すことにする
tree.ts
(略)
- const tree = async (root: string) => {
+ const tree = async (root: string, prefix = "") => {
(略)
-     console.log(entry.name);
+     console.log(prefix + entry.name);
    if (entry.isDirectory && entry.name !== ".git") {
-       await tree(entry.path);
+       await tree(entry.path, prefix + "  ");
    }
(略)
❯ deno run --allow-read tree.ts
.DS_Store
.env
.env.example
.git
.github
  workflows
    promote_zenn_article.yml
    welcome-deno.yml
.gitignore
.vim
  coc-settings.json
app.log
assets
deep
  nested
    dir
      hidden
        file.ts
deps.ts
env.ts
examples
  line_notify.ts
  rss.ts
  tweet_with_ifttt.ts
  zenn_api.ts
fetch_twilog.ts
LICENSE
line_notify.ts
logger.ts
main.ts
promote_zenn_article.ts
README.md
rss.ts
tree.ts
tweet_with_ifttt.ts
velociraptor.yml

階層に応じたインデントができている

kawarimidollkawarimidoll

階層に応じて罫線を表示させる
そのファイルがディレクトリ内の最後のファイルかどうかで出力を分岐させたいため事前にソートして最後のファイルを保存しておく必要がある

diffが面倒なのでtree()を更新した形ですべて再掲

tree.ts
const tree = async (root: string, prefix = "") => {
  const entries: TreeEntry[] = [];
  for await (const entry of Deno.readDir(root)) {
    entries.push({ ...entry, path: join(root, entry.name) });
  }

  if (entries.length == 0) {
    return;
  }

  const sortedEntries = entries.sort((a, b) =>
    a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1
  );
  const lastOne = sortedEntries[entries.length - 1];

  for await (const entry of sortedEntries) {
    const branch = entry === lastOne ? "└── " : "├── ";

    console.log(prefix + branch + entry.name);

    if (entry.isDirectory && entry.name !== ".git") {
      await tree(entry.path, prefix + "  ");
    }
  }
};
❯ deno run --allow-read tree.ts
├── .DS_Store
├── .env
├── .env.example
├── .git
├── .github
  └── workflows
    ├── promote_zenn_article.yml
    └── welcome-deno.yml
├── .gitignore
├── .vim
  └── coc-settings.json
├── app.log
├── assets
├── deep
  └── nested
    └── dir
      └── hidden
        └── file.ts
├── deps.ts
├── env.ts
├── examples
  ├── line_notify.ts
  ├── rss.ts
  ├── tweet_with_ifttt.ts
  └── zenn_api.ts
├── fetch_twilog.ts
├── LICENSE
├── line_notify.ts
├── logger.ts
├── main.ts
├── promote_zenn_article.ts
├── README.md
├── rss.ts
├── tree.ts
├── tweet_with_ifttt.ts
└── velociraptor.yml

良さげ

kawarimidollkawarimidoll

prefixを調整すれば途切れた枝をつなげることが可能

tree.ts
    if (entry.isDirectory && entry.name !== ".git") {
+      const indent = entry === lastOne ? "  " : "│  ";
-      await tree(entry.path, prefix + "  ");
+      await tree(entry.path, prefix + indent);
    }
❯ deno run --allow-read tree.ts
├── .DS_Store
├── .env
├── .env.example
├── .git
├── .github
│  └── workflows
│    ├── promote_zenn_article.yml
│    └── welcome-deno.yml
├── .gitignore
├── .vim
│  └── coc-settings.json
├── app.log
├── assets
├── deep
│  └── nested
│    └── dir
│      └── hidden
│        └── file.ts
├── deps.ts
├── env.ts
├── examples
│  ├── line_notify.ts
│  ├── rss.ts
│  ├── tweet_with_ifttt.ts
│  └── zenn_api.ts
├── fetch_twilog.ts
├── LICENSE
├── line_notify.ts
├── logger.ts
├── main.ts
├── promote_zenn_article.ts
├── README.md
├── rss.ts
├── tree.ts
├── tweet_with_ifttt.ts
└── velociraptor.yml

つながった

kawarimidollkawarimidoll

オプションに対応しよう
walk.tsより、オプションのインターフェースとそれに関する条件判断を追加
ディレクトリは必ず表示したいのでincludeDirsは不要

tree.ts
export interface TreeOptions {
  maxDepth?: number;
  includeFiles?: boolean;
  followSymlinks?: boolean;
  exts?: string[];
  match?: RegExp[];
  skip?: RegExp[];
}

function include(
  path: string,
  exts?: string[],
  match?: RegExp[],
  skip?: RegExp[],
): boolean {
  if (exts && !exts.some((ext): boolean => path.endsWith(ext))) {
    return false;
  }
  if (match && !match.some((pattern): boolean => !!path.match(pattern))) {
    return false;
  }
  if (skip && skip.some((pattern): boolean => !!path.match(pattern))) {
    return false;
  }
  return true;
}

オプションは標準のtreeを参考に
https://www.atmarkit.co.jp/ait/articles/1802/01/news025.html

  • -aでdotfiles表示(通常は表示しない)
  • -dでディレクトリのみ
  • -L numで階層を制限

またdiffが面倒なのでtree関数をすべて表示

tree.ts
const tree = async (
  root: string,
  prefix = "",
  {
    maxDepth = Infinity,
    includeFiles = true,
    followSymlinks = false,
    exts = undefined,
    match = undefined,
    skip = undefined,
  }: TreeOptions = {},
) => {
  if (maxDepth < 1 || !include(root, undefined, undefined, skip)) {
    return;
  }

  const entries: TreeEntry[] = [];
  for await (const entry of Deno.readDir(root)) {
    if (entry.isFile && !includeFiles) {
      continue;
    }
    entries.push({ ...entry, path: join(root, entry.name) });
  }

  if (entries.length == 0) {
    return;
  }

  const sortedEntries = entries.sort((a, b) =>
    a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1
  );
  const lastOne = sortedEntries[entries.length - 1];

  for await (const entry of sortedEntries) {
    const branch = entry === lastOne ? "└── " : "├── ";

    const suffix = (entry.isDirectory) ? "/" : "";

    if (include(entry.path, exts, match, skip)) {
      console.log(prefix + branch + entry.name + suffix);
    }

    if (entry.isDirectory && entry.name !== ".git") {
      const indent = entry === lastOne ? "  " : "│  ";
      await tree(entry.path, prefix + indent, {
        maxDepth: maxDepth - 1,
        includeFiles,
        followSymlinks,
        exts,
        match,
        skip,
      });
    }
  }
};

const {
  a,
  d,
  L,
  _: [dir = "."],
} = parse(Deno.args);
console.log(dir);
await tree(resolve(Deno.cwd(), String(dir)), "", {
  maxDepth: L,
  includeFiles: !d,
  followSymlinks: false,
  exts: undefined,
  match: undefined,
  skip: a ? undefined : [/(^|\/)\./],
});

結果は長くなるので省略

kawarimidollkawarimidoll

.gitignoreを読み込もう

Deno.run()git status --ignored -sを動かして無視されているファイルを探すことができる
なおこの中に.gitは含まれていないので手動で追加する

show_gitignore.ts
const process = Deno.run({
  cmd: ["git", "status", "--ignored", "-s"],
  stdout: "piped",
  stderr: "piped",
});
const outStr = new TextDecoder().decode(await process.output());
process.close();
const ignoredList = outStr.replace(/^[^!].+$/gm, "").replace(/^!! /mg, "")
  .split("\n").filter((item) => item).concat(".git");
console.log(ignoredList);

通常は.gitignoreを読み込んで対象ファイルと.gitを読み飛ばし、uオプション(命名はripgrepを参考にした)を追加するとそれらを見るものとする

tree.ts
(略)
const {
  a,
  d,
  L,
  u,
  _: [dir = "."],
} = parse(Deno.args);

const skip = [];
if (!a) {
  skip.push(/(^|\/)\./);
}
if (!u) {
  const process = Deno.run({
    cmd: ["git", "status", "--ignored", "-s"],
    stdout: "piped",
    stderr: "piped",
  });
  const outStr = new TextDecoder().decode(await process.output());
  process.close();
  const ignoredList = outStr.replace(/^[^!].+$/gm, "").replace(/^!! /mg, "")
    .split("\n").filter((item) => item).concat(".git");

  skip.concat(...ignoredList.map((str) => new RegExp(str)));
}

console.log(dir);
await tree(resolve(Deno.cwd(), String(dir)), "", {
  maxDepth: L,
  includeFiles: !d,
  followSymlinks: false,
  exts: undefined,
  match: undefined,
  skip,
});

実行時にはallow-runオプションが必要

❯ deno run --allow-read --allow-run tree.ts -a -u
kawarimidollkawarimidoll

hオプションでヘルプを追加 完璧だ

tree.ts
if (h) {
  const msg = `denotree
  'tree' powered by Deno

  USAGE
    denotree [dirname] : Show children of dirname. default dirname is pwd.

  OPTIONS
    a     : Show dotfiles
    d     : Show only directories
    u     : Show git ignored files
    L=num : Limit depth
    h     : Show this help message
  `;
  console.log(msg);
  Deno.exit(0);
}
このスクラップは2021/07/08にクローズされました