Edited at

続・雑に始める CWL!


はじめに

この記事は CWL Advent Calendar の24日目の記事です。

ところで Wikipedia によると、クリスマスイブにはサンタがプレゼントをくれるそうです。


  • (命題) 今日はクリスマスイブ ⇒ 私はサンタクロースからプレゼントをもらえる

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

まだプレゼントを貰っていないので、まだクリスマスイブは始まっていません!

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


前回のつづき

前回は、既存のツールが最低限動くように CWL を書く方法を紹介しました。

この記事では、前回のツール定義を拡張して、よりそれらしい CWL を書く方法を紹介します。

前回はこれが

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

こうなった

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 オプションを提供してくれます。

$ 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 を追加する

inputs:

- id: source
...
- id: nlines
...
- id: show_help
...

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


show_help の型を追加する

今回のように表示する・しないのような二値には、boolean 型を指定します。

boolean 型のパラメータは、値が true の時のみに実行コマンド中に現れます (inputBinding がある場合1)。

inputs:

...
- id: show_help
type: boolean
...


show_helptrue の時に表示する引数を追加する

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

inputs:

...
- id: show_help
type: boolean
inputBinding:
prefix: --help
...


default でデフォルト値を設定する

上記まででヘルプをサポートすることができました。

$ 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_helpfalse にわざわざ指定する必要があります2

default フィールドを使うことで、show_help を指定しない場合には false を暗黙的に指定させることができます。

inputs:

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


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

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

$ 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[] 型で表現できます。

inputs:

- id: source
type: File[]
...


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

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

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

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

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


  • 出力ファイル名を決め打ちにする。


    • シンプル!



stdout: output-head.txt


  • 最初に渡したファイル名を元に出力ファイル名を決める


    • 配列パラメータ sourcen 番目の要素は、$(inputs.arr[n]) という記法で取得できます。nameroot プロパティと組み合わせると、0 番目の要素の nameroot の結果を取得できます。



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


できあがり!

やったぜ。

$ 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:


その他


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

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

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

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

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

...

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

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


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

show_help を指定した時には sourcenlines を省略できるようにしたい、という時には直和型と直積型を利用します。

闇への扉(雑に書けない)を開いてしまったので、本項目では省略します。

それでも書く必要がある方は、ユーザーガイドを参照してください。

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


[Advanced] 渡したファイルを同時に処理してほしい

ツール定義はあくまでツールを一回実行するためのコマンドライン生成や実行を行うためのものなので、その範囲内では並列処理はできません。

複数ファイルを並列処理させるには、scatter を用いたワークフロー定義を書く必要があります。

書き方はユーザーガイドを参照してください。


まとめ

ここまでの項目をマスターすれば、おおよそのツールは CWL 化できるようになると思います。

より深く知りたい方は、以下のリンクを読んでみるといいでしょう。





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



  2. しかも cwltool には false を引数から直接指定する方法がない… 



  3. 逆にストリーム処理できない例としては、C言語の fseek 関数などを用いてファイルのあちこちを書き込んだり読み込んだりするプログラムがあります。 



  4. 「してくれます」ではなく「してくれるかもしれません」なのは、パイプを使った効率化はあくまでオプショナルであるためです。