続・雑に始める CWL！

# はじめに
この記事は CWL Advent Calendar の24日目の記事です。

ところで [Wikipedia](https://ja.wikipedia.org/wiki/サンタクロース) によると、クリスマスイブにはサンタがプレゼントをくれるそうです。

- (命題) 今日はクリスマスイブ ⇒ 私はサンタクロースからプレゼントをもらえる
- (上の対偶) 私はサンタクロースからプレゼントをもらっていない ⇒ 今日はクリスマスイブではない

まだプレゼントを貰っていないので、まだクリスマスイブは始まっていません！
この記事は12月24日に公開されるはずなので、この記事が公開されることでクリスマスイブが始まり、私は何かプレゼントが貰えるはずです。

# 前回のつづき

[前回](https://qiita.com/tm_tn/items/4956f5ca523f7f49f386)は、既存のツールが最低限動くように CWL を書く方法を紹介しました。
この記事では、前回のツール定義を拡張して、よりそれらしい CWL を書く方法を紹介します。

前回はこれが

```console
$ head -n5 foobar.txt > foobar-head.txt
```

こうなった

```yaml
cwlVersion: v1.0
class: CommandLineTool
baseCommand: [head]
arguments: [-n$(inputs.nlines), $(inputs.source)]
inputs:
  - id: source
    type: File
  - id: nlines
    type: int
outputs:
  - id: out
    type: stdout
stdout: $(inputs.source.nameroot)-head.txt
requirements:
  - class: DockerRequirement
    dockerPull: debian:latest # alpine:latest だと動かない例があるので注意！
```


# 拡張方針いろいろ
## 1. ヘルプを追加したい

CWL のリファレンス実装の `cwltool` は、デフォルトで与えられた CWL ファイルの使い方を示す `--help` オプションを提供してくれます。

```console
$ cwltool head.cwl --help
...
usage: head.cwl [-h] --source SOURCE --nlines NLINES [job_order]

positional arguments:
  job_order        Job input json file

optional arguments:
  -h, --help       show this help message and exit
  --source SOURCE
  --nlines NLINES
```

しかしユーザーがほしいのは `head` コマンド自体の `--help` の結果かもしれません。`--help` の結果を出力するためのパラメータを追加してみましょう。

基本方針ですが、前回と同様に `arguments` に追加する方法だとうまくいきません。というのも、`arguments` に書いた部分は、常に実行コマンド上に現れてしまうからです。

今回のように、パラメータの値によって実行コマンド上に引数が現れたり現れなかったりする場合には、`inputBinding` を活用します。

具体的な追加方法は以下になります。

### ヘルプ表示用のパラメータ `show_help` を追加する

```yaml
inputs:
  - id: source
    ...
  - id: nlines
    ...
  - id: show_help
    ...
```

`id` を `help` にしてしまうと、`cwltool` が組み込みで提供する `--help` と衝突してエラーになります。別の名前にしましょう。

### `show_help` の型を追加する

今回のように表示する・しないのような二値には、`boolean` 型を指定します。
`boolean` 型のパラメータは、値が `true` の時のみに実行コマンド中に現れます (`inputBinding` がある場合[^1])。

```yaml
inputs:
    ...
  - id: show_help
    type: boolean
    ...
```

[^1]: `inputBinding` フィールドが存在せず、また `$(...)` などで参照もされていないパラメータは、実行コマンド上に現れないため注意してください。

### `show_help` が `true` の時に表示する引数を追加する

`show_help` が `true` の時に、実行コマンドに `--help` を追加するためには `inputBinding#prefix` フィールドを使用します。

```yaml
inputs:
    ...
  - id: show_help
    type: boolean
    inputBinding:
      prefix: --help
    ...
```

### `default` でデフォルト値を設定する
上記まででヘルプをサポートすることができました。

```console
$ cwltool head.cwl --source manhead.txt --nlines 5 --show_help
...
[job head.cwl] completed success
{
    "out": {
        "location": "file:///Users/tanjo/manhead-head.txt",
        "basename": "manhead-head.txt",
        "class": "File",
        "checksum": "sha1$30c2a20de147871db76e237705ac274277504428",
        "size": 145,
        "path": "/Users/tanjo/manhead-head.txt"
    }
}
Final process status is success
$ cat manhead-head.txt
Usage: head [OPTION]... [FILE]...
Print the first 10 lines of each FILE to standard output.
With more than one FILE, precede each with a header giving the file name.
...
```

しかし、今度はヘルプが必要ない場合にも `show_help` を `false` にわざわざ指定する必要があります[^2]。
`default` フィールドを使うことで、`show_help` を指定しない場合には `false` を暗黙的に指定させることができます。

```yaml
inputs:
    ...
  - id: show_help
    type: boolean
    inputBinding:
      prefix: --help
    default: false
    ...
```

[^2]: しかも `cwltool` には `false` を引数から直接指定する方法が[ない](https://github.com/common-workflow-language/cwltool/issues/1030)…

## 2. 複数ファイルを渡したい

1 で作成したヘルプの出力を確認してみます。

```console
$ cwltool head.cwl --source manhead.txt --nlines 5 --show_help
...
$ cat manhead-head.txt
Usage: head [OPTION]... [FILE]...
...
```

これを見ると、`head` コマンドはファイルを複数受け取ることができるのがわかります。
CWL 側でも複数ファイルを受け取れるように拡張しましょう。

### `source` の型を変更する

`source` の型を、`File` の配列を受け取れるように変更します。
`File` の配列は `File[]` 型で表現できます。

```yaml
inputs:
  - id: source
    type: File[]
  ...
```

### 出力ファイル名をいい感じにする

前回の段階では、出力ファイル名を以下のように指定していました。

```yaml
stdout: $(inputs.source.nameroot)-head.txt
```

しかし `source` の型を `File` から `File[]` に変更してしまったため、このままでは `nameroot` プロパティが使えません。

対策には以下の2つの方法が考えられます。

- 出力ファイル名を決め打ちにする。
    - シンプル！

```yaml
stdout: output-head.txt
```

- 最初に渡したファイル名を元に出力ファイル名を決める
    - 配列パラメータ `source` の `n` 番目の要素は、`$(inputs.arr[n])` という記法で取得できます。`nameroot` プロパティと組み合わせると、`0` 番目の要素の `nameroot` の結果を取得できます。

```yaml
stdout: $(inputs.source[0].nameroot)-head.txt
```

### できあがり！

やったぜ。

```console
$ cwltool head.cwl --nlines 5 --source manhead.txt --source head.cwl
...
[job head.cwl] completed success
{
    "out": {
        "location": "file:///Users/tanjo/manhead-head.txt",
        "basename": "manhead-head.txt",
        "class": "File",
        "checksum": "sha1$c255a9e8f91aeb2ba1dce30caf11d102c89414d6",
        "size": 407,
        "path": "/Users/tanjo/manhead-head.txt"
    }
}
Final process status is success
$ cat manhead-head.txt
==> /var/lib/cwl/stg39c3c00a-51ac-4e1c-924f-6d41d724be0e/manhead.txt <==

HEAD(1)                   BSD General Commands Manual                  HEAD(1)

NAME
     head -- display first lines of a file

==> /var/lib/cwl/stg902fd9f1-d682-4aec-a18a-234962aefe35/head.cwl <==
cwlVersion: v1.0
class: CommandLineTool
baseCommand: [head]
arguments: [-n$(inputs.nlines), $(inputs.source)]
inputs:
```


# その他
## パイプを使ってうまいこと実行してほしい

シェルスクリプトでは、以下のようにパイプで複数のコマンドをつなげることで、中間ファイルを生成させずに複数のコマンドを効率よく実行することができます。

```console
$ cat inputs.txt | head -n5 | sort -nr > output.txt # パイプを使った素敵な処理
```

しかし、前回作成した CWL を使ったワークフローでは、パイプ等を使った効率的な実行ができません。

これを解決するには、ファイル型の入力に対して `streamable: true` を追加します。

```yaml
...
inputs:
  - id: source
    type: File
    streamable: true
   ...
```

こうすることで、CWL 処理系は、`source` がストリーム処理が可能[^3](== パイプが利用可能)であることを検知して、パイプを使って効率的に実行してくれるかもしれません[^4]。

[^3]: 逆にストリーム処理できない例としては、C言語の `fseek` 関数などを用いてファイルのあちこちを書き込んだり読み込んだりするプログラムがあります。
[^4]: 「してくれます」ではなく「してくれるかもしれません」なのは、パイプを使った効率化はあくまで[オプショナル](https://www.commonwl.org/v1.0/CommandLineTool.html#CommandInputParameter)であるためです。

## [Advanced] `show_help` を指定する時には他のパラメータを省略したい

~~`show_help` を指定した時には `source` や `nlines` を省略できるようにしたい、という時には直和型と直積型を利用します。~~
闇への扉(雑に書けない)を開いてしまったので、本項目では省略します。
それでも書く必要がある方は、[ユーザーガイド](https://www.commonwl.org/user_guide/11-records/index.html)を参照してください。

**注意**: この機能を使用すると、柔軟な指定が可能になる代わりに可読性が低下します。本当に必要かどうかをよく考えて使いましょう！

## [Advanced] 渡したファイルを同時に処理してほしい
ツール定義はあくまでツールを一回実行するためのコマンドライン生成や実行を行うためのものなので、その範囲内では並列処理はできません。
複数ファイルを並列処理させるには、`scatter` を用いたワークフロー定義を書く必要があります。

書き方は[ユーザーガイド](https://www.commonwl.org/user_guide/23-scatter-workflow/index.html)を参照してください。

# まとめ

ここまでの項目をマスターすれば、おおよそのツールは CWL 化できるようになると思います。
より深く知りたい方は、以下のリンクを読んでみるといいでしょう。

- 公式の仕様: https://www.commonwl.org/v1.0/
- ユーザーガイド: https://www.commonwl.org/user_guide/index.html