Skip to content

Commit d119ed1

Browse files
Merge pull request #772 from markjschreiber/feat/directory-listing
feat: add Directory listing stdlib functions
2 parents 2e16c74 + 4e503eb commit d119ed1

2 files changed

Lines changed: 294 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Keep the changelog pleasant to read in the text editor:
2121
version 1.3.0
2222
---------------------------
2323

24+
+ Added `list_directory`, `list_directory_recursive`, and `list_subdirectories` standard library functions for listing the contents of a `Directory` as `Array[File]` or `Array[Directory]`, enabling scatter-gather patterns over directory contents.
25+
2426
+ Clarified that relative paths in `File` and `Directory` declarations are resolved relative to the WDL document's parent directory outside the `output` section, and relative to the task's execution directory inside the `output` section. Also clarified that optional files evaluate to `None` in both contexts if the path does not exist.
2527
([#735](https://github.com/openwdl/wdl/pull/735))
2628

SPEC.md

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ Revisions to this specification are made periodically in order to correct errors
164164
- [`glob`](#glob)
165165
- [Non-standard Bash](#non-standard-bash)
166166
- [`size`](#size)
167+
- [`list_directory`](#list_directory)
168+
- [`list_directory_recursive`](#list_directory_recursive)
169+
- [`list_subdirectories`](#list_subdirectories)
167170
- [`stdout`](#stdout)
168171
- [`stderr`](#stderr)
169172
- [`read_string`](#read_string)
@@ -8755,6 +8758,295 @@ Example output:
87558758
</p>
87568759
</details>
87578760

8761+
### `list_directory`
8762+
8763+
*as of version 1.4*
8764+
8765+
```
8766+
Array[File] list_directory(Directory)
8767+
Array[File] list_directory(Directory, String)
8768+
```
8769+
8770+
Returns an array of all files in the top level of the given directory. If the optional glob pattern is provided, only files whose basenames match the pattern are included. Subdirectories are not included in the result; use [`list_subdirectories`](#list_subdirectories) to obtain those.
8771+
8772+
Results are sorted lexicographically by basename to ensure deterministic ordering across platforms and filesystems.
8773+
8774+
When a glob pattern is provided, it follows standard glob matching rules: patterns without a leading `.` do not match hidden files (dotfiles). When no pattern is provided, all files are returned including hidden files.
8775+
8776+
**Parameters**
8777+
8778+
1. `Directory`: The directory whose immediate file contents to list.
8779+
2. `String`: (Optional) A glob pattern to filter results by basename (e.g., `"*.fastq.gz"`).
8780+
8781+
**Returns**: An `Array[File]` containing the files in the top level of the directory that match the pattern. Returns an empty array if the directory contains no matching files.
8782+
8783+
**Restrictions**: This function reads directory contents and may only be called in a context where the directory exists. If the directory is an input to a task or workflow, then it may be called anywhere in that task or workflow.
8784+
8785+
<details>
8786+
<summary>
8787+
Example: list_fastqs_task.wdl
8788+
8789+
```wdl
8790+
version 1.4
8791+
8792+
task list_fastqs {
8793+
input {
8794+
Directory sample_dir
8795+
}
8796+
8797+
Array[File] fastqs = list_directory(sample_dir, "*.fastq.gz")
8798+
8799+
command <<<
8800+
echo "~{sep(' ', fastqs)}"
8801+
>>>
8802+
8803+
output {
8804+
Int num_fastqs = length(fastqs)
8805+
String file_list = read_string(stdout())
8806+
}
8807+
8808+
requirements {
8809+
container: "ubuntu:latest"
8810+
}
8811+
}
8812+
```
8813+
</summary>
8814+
<p>
8815+
Example input:
8816+
8817+
```json
8818+
{
8819+
"list_fastqs.sample_dir": "/path/to/samples"
8820+
}
8821+
```
8822+
8823+
Example output (given the directory contains `a.fastq.gz`, `b.fastq.gz`, and `readme.txt`):
8824+
8825+
```json
8826+
{
8827+
"list_fastqs.num_fastqs": 2,
8828+
"list_fastqs.file_list": "/path/to/samples/a.fastq.gz /path/to/samples/b.fastq.gz"
8829+
}
8830+
```
8831+
</p>
8832+
</details>
8833+
8834+
<details>
8835+
<summary>
8836+
Example: scatter_directory_workflow.wdl
8837+
8838+
```wdl
8839+
version 1.4
8840+
8841+
workflow scatter_directory {
8842+
input {
8843+
Directory input_dir
8844+
}
8845+
8846+
Array[File] all_files = list_directory(input_dir)
8847+
8848+
scatter (f in all_files) {
8849+
call process_file { input: infile = f }
8850+
}
8851+
8852+
output {
8853+
Array[File] results = process_file.outfile
8854+
}
8855+
}
8856+
8857+
task process_file {
8858+
input {
8859+
File infile
8860+
}
8861+
8862+
command <<<
8863+
wc -l ~{infile} > result.txt
8864+
>>>
8865+
8866+
output {
8867+
File outfile = "result.txt"
8868+
}
8869+
8870+
requirements {
8871+
container: "ubuntu:latest"
8872+
}
8873+
}
8874+
```
8875+
</summary>
8876+
<p>
8877+
Example input:
8878+
8879+
```json
8880+
{
8881+
"scatter_directory.input_dir": "/path/to/dir"
8882+
}
8883+
```
8884+
8885+
Example output (given the directory contains files `a.txt` and `b.txt`):
8886+
8887+
```json
8888+
{
8889+
"scatter_directory.results": ["/path/to/output/result.txt", "/path/to/output/result.txt"]
8890+
}
8891+
```
8892+
</p>
8893+
</details>
8894+
8895+
### `list_directory_recursive`
8896+
8897+
*as of version 1.4*
8898+
8899+
```
8900+
Array[File] list_directory_recursive(Directory)
8901+
Array[File] list_directory_recursive(Directory, String)
8902+
```
8903+
8904+
Returns an array of all files at any depth within the given directory (recursive walk). If the optional glob pattern is provided, only files whose basenames match the pattern are included. Subdirectories themselves are not included in the result.
8905+
8906+
Results are sorted lexicographically by their path relative to the input directory.
8907+
8908+
When a glob pattern is provided, it follows standard glob matching rules: patterns without a leading `.` do not match hidden files (dotfiles). When no pattern is provided, all files are returned including hidden files.
8909+
8910+
**Parameters**
8911+
8912+
1. `Directory`: The directory to recursively list.
8913+
2. `String`: (Optional) A glob pattern to filter results by basename (e.g., `"*.bam"`).
8914+
8915+
**Returns**: An `Array[File]` containing all files found recursively within the directory that match the pattern. Returns an empty array if no matching files are found.
8916+
8917+
**Restrictions**: This function reads directory contents and may only be called in a context where the directory exists. If the directory is an input to a task or workflow, then it may be called anywhere in that task or workflow.
8918+
8919+
<details>
8920+
<summary>
8921+
Example: find_bams_task.wdl
8922+
8923+
```wdl
8924+
version 1.4
8925+
8926+
task find_bams {
8927+
input {
8928+
Directory project_dir
8929+
}
8930+
8931+
Array[File] bams = list_directory_recursive(project_dir, "*.bam")
8932+
8933+
command <<<
8934+
for bam in ~{sep(' ', bams)}; do
8935+
samtools index "$bam"
8936+
done
8937+
>>>
8938+
8939+
output {
8940+
Array[File] indices = glob("*.bai")
8941+
}
8942+
8943+
requirements {
8944+
container: "biocontainers/samtools:latest"
8945+
}
8946+
}
8947+
```
8948+
</summary>
8949+
<p>
8950+
Example input:
8951+
8952+
```json
8953+
{
8954+
"find_bams.project_dir": "/data/project"
8955+
}
8956+
```
8957+
8958+
Example output (given the directory tree contains `sample1/aligned.bam` and `sample2/aligned.bam`):
8959+
8960+
```json
8961+
{
8962+
"find_bams.indices": ["/path/to/exec/aligned.bai", "/path/to/exec/aligned.bai"]
8963+
}
8964+
```
8965+
</p>
8966+
</details>
8967+
8968+
### `list_subdirectories`
8969+
8970+
*as of version 1.4*
8971+
8972+
```
8973+
Array[Directory] list_subdirectories(Directory)
8974+
```
8975+
8976+
Returns an array of immediate child directories within the given directory. Only direct children are returned; this function is not recursive.
8977+
8978+
Results are sorted lexicographically by basename to ensure deterministic ordering across platforms and filesystems.
8979+
8980+
**Parameters**
8981+
8982+
1. `Directory`: The parent directory whose immediate subdirectories to list.
8983+
8984+
**Returns**: An `Array[Directory]` containing the immediate subdirectories. Returns an empty array if the directory contains no subdirectories.
8985+
8986+
**Restrictions**: This function reads directory contents and may only be called in a context where the directory exists. If the directory is an input to a task or workflow, then it may be called anywhere in that task or workflow.
8987+
8988+
<details>
8989+
<summary>
8990+
Example: process_samples_workflow.wdl
8991+
8992+
```wdl
8993+
version 1.4
8994+
8995+
workflow process_samples {
8996+
input {
8997+
Directory data_root # contains one subdirectory per sample
8998+
}
8999+
9000+
Array[Directory] sample_dirs = list_subdirectories(data_root)
9001+
9002+
scatter (sample_dir in sample_dirs) {
9003+
Array[File] fastqs = list_directory(sample_dir, "*.fastq.gz")
9004+
call align { input: reads = fastqs }
9005+
}
9006+
9007+
output {
9008+
Array[File] bams = align.bam
9009+
}
9010+
}
9011+
9012+
task align {
9013+
input {
9014+
Array[File] reads
9015+
}
9016+
9017+
command <<<
9018+
bwa mem ref.fa ~{sep(' ', reads)} | samtools sort -o aligned.bam
9019+
>>>
9020+
9021+
output {
9022+
File bam = "aligned.bam"
9023+
}
9024+
9025+
requirements {
9026+
container: "biocontainers/bwa:latest"
9027+
}
9028+
}
9029+
```
9030+
</summary>
9031+
<p>
9032+
Example input:
9033+
9034+
```json
9035+
{
9036+
"process_samples.data_root": "/data/samples"
9037+
}
9038+
```
9039+
9040+
Example output (given `/data/samples` contains subdirectories `sample1/` and `sample2/`, each with FASTQ files):
9041+
9042+
```json
9043+
{
9044+
"process_samples.bams": ["/path/to/output/aligned.bam", "/path/to/output/aligned.bam"]
9045+
}
9046+
```
9047+
</p>
9048+
</details>
9049+
87589050
### `stdout`
87599051

87609052
```

0 commit comments

Comments
 (0)