【Deno 1.20〜】公式タスクランナー、`deno task`コマンドの使い方

タスクランナーとは、コマンドに対してショートカットを設定しておくことができる機能です。サーバー立ち上げやビルド用のコマンドをあらかじめタスクランナーに設定しておくことで、開発体験を向上することができます。Node.jsではnpm scriptsが一般的に使われています。

Deno1.20では、タスクランナーがDeno CLI本体に入りました。基本的にはnpm scriptsと同じ機能なのですが、デフォルトでクロスプラットフォーム対応になっているなど、違う所もあります。

## deno task コマンドの使い方

#### タスクの設定

まず、deno.json(c)でタスクの内容を設定します。
※deno.json(c)の構文については[こちらの記事](https://qiita.com/access3151fq/items/7aa44ee69b4a5ff867c7)を参照してください。

```jsonc:deno.jsonc（例）
{
  "tasks": {
    // "<タスク名>": "<タスク内容>" の形で定義する
    // deno task hello
    "hello": "deno eval \"console.log('hello')\"",
    // deno task world
    "world": "deno eval \"console.log('world')\"",
    // deno task args hello world # output: ["hello", "world"]
    "args": "deno eval \"console.log(Deno.args)\"",
    "echo": "echo hello world",
    "nesting": "echo $(deno eval \"console.log('hello world')\")",
    "concat1": "echo 1 && echo 2",
    "concat2": "deno eval \"Deno.exit(1)\" || echo 2",
    "pipe": "echo hello world | deno eval \"Deno.stdin.readable.pipeTo(Deno.stdout.writable)\"",
    "var": "VAR=hello && echo $VAR && deno eval \"console.log(Deno.env.get('VAR'))\"",
    "export": "export VAR=hello && echo $VAR && deno eval \"console.log(Deno.env.get('VAR'))\""
  }
}
```

このdeno.jsonを置いたディレクトリより**下層の**ディレクトリでは、定義したタスクが有効になります。
タスク名として使えるのは英数字+コロン（1文字目は必ず英字）です。（`^[A-Za-z][A-Za-z0-9_\\-:]*$`）
以下、このdeno.jsonを使って解説していきます。

#### タスクを実行

ターミナルで`deno task <タスク名>`というコマンドを打つと、タスクが実行されます。

```shell
> deno task hello
Warning deno task is unstable and may drastically change in the future
Task hello deno eval "console.log('hello')"
hello
```

タスク名の後に引数を渡すと、その引数はそのままコマンドに渡されます。

```shell
> deno task args hello world # hello worldが引数の部分
Warning deno task is unstable and may drastically change in the future
Task args deno eval "console.log(Deno.args)" "hello" "world"
[ "hello", "world" ]
```

#### タスクのリストを表示

ターミナルで`deno task`コマンド（引数無し）を打つと、タスクのリストが表示されます。

```shell
> deno task
Warning deno task is unstable and may drastically change in the future
Available tasks:
- args
    deno eval "console.log(Deno.args)"
- concat1
    echo 1 && echo 2
- concat2
    deno eval "Deno.exit(1)" || echo 2
- echo
    echo hello world
# ...(以下略)
```

## タスクの文法

コマンドの連結や入れ子、glob展開がサポートされています。

### 複数コマンドの実行

|文法|意味|
|---|---|
|`<command1> && <command2>`|`command1`が成功（exit code 0）したら`command2`を実行する|
|`<command1> \|\| <command2>`|`command1`が失敗（exit code 1）したら`command2`を実行する|
|`<command1> ; <command2>`|`command1`が終了したら成功・失敗に関係なく`command2`を実行する|
|`<command1> & <command2>`|`command1`と`command2`を並列に実行する|

```shell
# 1つ目が成功したら2つ目を実行
echo hello && echo world
# 1つ目が失敗したら2つ目を実行
echo hello || echo world
# 1つ目が終了したら2つ目を実行
echo hello ; echo world
# 1つ目と2つ目を並列実行
echo hello & echo world
```

### 出力のパイプ・リダイレクト

|文法|意味|
|---|---|
|`command1 \| command2`|`command1`のstdoutを`command2`にパイプする|
|`command1 \|& command2`|`command1`のstdoutとstderrを`command2`にパイプする|
|`command > filename`|`command1`のstdoutをファイルにパイプする(ファイルを上書き)|
|`command 2> filename`|`command1`のstderrをファイルにパイプする(上書き)|
|`command &> filename`|`command1`のstdoutとstderrをファイルにパイプする(上書き)|
|`command >> filename`|`command1`のstdoutをファイルにパイプする(追記)|

```shell
# 1つ目のstdoutを2つ目のstdinに渡す
echo hello world | deno eval 'Deno.stdin.readable.pipeTo(Deno.stdout.writable)'
# 1つ目のstderrを2つ目のstdinに渡す
deno eval 'console.log(1); console.error(2);' |& deno eval 'Deno.stdin.readable.pipeTo(Deno.stdout.writable)'

# stdoutをファイルに出力する
deno eval 'console.log(1); console.error(2);' > file.txt
# stderrをファイルに出力する
deno eval 'console.log(1); console.error(2);' 2> file.txt
# stdoutとstderrをファイルに出力する
deno eval 'console.log(1); console.error(2);' &> file.txt

# stdoutをファイルに追記する
deno eval 'console.log(1); console.error(2);' >> file.txt
# stderrをファイルに追記する
deno eval 'console.log(1); console.error(2);' 2>> file.txt
# stdoutとstderrをファイルに追記する
deno eval 'console.log(1); console.error(2);' &>> file.txt
```

### 出力の破棄

`/dev/null`へリダイレクトすることで出力を捨てることができます。(Windowsでも動作)

```shell
# stdoutを破棄する
deno run main.ts > /dev/null
# stderrを破棄する
deno run main.ts 2> /dev/null
# stdoutとstderrを破棄する
deno run main.ts &> /dev/null
```

### シェル変数と環境変数

シェル変数はシェル内でのみ参照できる変数、環境変数は生成したプロセス内でも参照できる変数です。

|文法|意味|
|---|---|
|`VAR_NAME=<val>`|シェル変数`VAR_NAME`に値を代入する（値は生成したプロセス内で利用できない）|
|`export VAR_NAME=<val>`|環境変数`VAR_NAME`に値を代入する（値は生成したプロセス内で利用できる）|
|`$VAR_NAME`|シェル変数や環境変数を使用する|

```shell
VAR=hello && echo $VAR
export VAR=hello && deno eval "console.log(Deno.env.get('VAR'))"
VAR=hello && echo $VAR
```

### コマンドの入れ子

|文法|意味|
|---|---|
|`$(<command>)`|コマンドを入れ子にする|

```shell
echo $(deno eval 'console.log(1)')
```

### 終了コードの反転

|文法|意味|
|---|---|
|`! <command>`|コマンドの終了コードの失敗/成功を反転する|

```shell
# 終了コードを反転させる
! deno eval 'Deno.exit(1)' # exit codeは0
! deno eval 'Deno.exit(0)' # exit codeは1
```

### glob展開

Deno v1.34以降、ファイル名に対してglob展開を行うことができます。
rustの[globライブラリ](https://crates.io/crates/glob)を使用して実装されているようです。そのドキュメントによると使用できる構文は以下のものです。

|文法|意味|
|---|---|
|`?`|任意の1文字に一致|
|`*`|任意の文字列に一致 (空文字列にも一致)
|`**`|カレントディレクトリと任意のサブディレクトリに一致<br>ただし、`**a`や`b**`, `***`のような構文はエラー
|`[...]`|括弧内の任意の文字と一致<br>(例：`[0-9]`/`[a-b]`/`[abc]`)<br>Unicode順に従って文字の範囲を指定することができる
|`[!...]`|括弧内にない任意の文字と一致<br>(例：`[!0-9]`/`[!a-b]`/`[!abc]`)

メタ文字(`?`, `*`, `[`, `]`)自体にマッチさせたい時は、各括弧`[]`構文を使用してマッチできます。(例: `[?]`)

- `[[]`と`[]]`はそれぞれ`[`と`]`という文字に対してマッチする。
- `[![]`と`[!]]`も同様に、それぞれ`[`と`]`という文字以外に対してマッチする。
- `[...]`内で`-`という文字にマッチさせたい時は、先頭か末尾に配置する<br>(例：❌`[a-b]`、⭕`[-ab]`)

```shell
# カレントディレクトリより下層にあるすべての.tsファイルにマッチ
echo **/*.ts
# カレントディレクトリの.tsファイルにマッチ
echo *.ts
# data0.csv から data9.csv まで連番でマッチ
echo data[0-9].csv
```



## クロスプラットフォーム対応

npm scriptsの場合、Windows/Linux/Mac OSの間でクロスプラットフォームなコマンドを書くためには[shx](https://www.npmjs.com/package/shx)などのパッケージを[インストールする必要がありました](https://qiita.com/t_o_d/items/9d731541f098b418617c#%E3%82%AF%E3%83%AD%E3%82%B9%E3%83%97%E3%83%A9%E3%83%83%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%82%92%E6%84%8F%E8%AD%98%E3%81%99%E3%82%8B)。
（例えば`rm`などはwindowsに存在しないので、こうしたパッケージを通して使う必要があった）

一方、deno taskでは**クロスプラットフォーム対応のコマンドが最初から組み込まれています**。
Deno1.34時点では以下のコマンドがデフォルトで組み込まれており、Windowsでも追加パッケージ無しで使えるようになっています。

- `cp` : ファイルをコピーする
- `mv` : ファイルを移動する
- `rm` : ファイルを削除する（例：`rm -rf [FILE]...`）
- `mkdir` : ディレクトリを作成する（例：`mkdir -p DIRECTORY...`）
- `pwd` : カレントディレクトリを表示する
- `sleep` : n秒待つ（例：`sleep 1`）
- `echo` : 文字列を表示する
- `cat` : ファイルの内容を読み取って出力する (ファイルが指定されていない場合はstdinを読み取って出力する)
- `exit` : シェルからexitする
- `unset` : 環境変数を削除する(例：`unset VAR_NAME`)
- `xargs` : 標準入力から渡されたデータを引数に渡す

使い方については、同名のUnixコマンドと同じです。

これらのコマンドがなぜWindowsでも使えるのかというと、[deno_task_shell](https://github.com/denoland/deno_task_shell/)リポジトリでシェルごと実装されているからです。もし他に追加してほしいコマンドがあれば、このリポジトリでissueを開くと追加してくれるそうです。
LinuxやMacでこの組み込みコマンドではなくOSのコマンドを使用したい場合は、`sh`コマンドを使います（例：`sh -c cp source destination`）

## まとめ

- deno.json(c)でタスクを使うと`deno task`コマンドから利用可能
- デフォルトでクロスプラットフォーム対応している

従来は、Denoのタスクランナーとして[velociraptor](https://velociraptor.run/)やmakefileが使われていました。一方で、Deno本体にクロスプラットフォームなタスクランナーを搭載してほしいという要望もあり、deno taskが実装されました。

これからは3rd party製のツールに頼ることなく、タスクランナーを使うことができます。

最近では標準ライブラリに`dotenv`モジュールが追加されるなど、基本的に全員がインストールすることになるツールはデフォルトで使えるようになっているのがDenoの嬉しい所です。