Skip to content

Outputs extension

Work in progress

argtype is early-stage and the extension mechanism is still being designed; this extension's surface may change.

Extension name: outputs

Declares that a node produces an output file, and describes that file's path. This is an extension, not part of the core grammar: outputs describe what a program does, not which argv is valid.

Consumers that don't implement outputs ignore .output() annotations entirely and treat the node as if they weren't there. Nothing needs to be declared in frontmatter - the extension is implied by the presence of .output().

Scope and caveats

Output descriptions are advisory, not authoritative. Many tools compute output paths from information argtype cannot see: environment variables, remote filenames, input file contents, file sizes (e.g. zip splitting by size), timestamps. For those, an output template is necessarily incomplete.

Two consequences:

  • A consumer should treat a resolved output path as a best guess, and must tolerate it being wrong, partial, or unresolvable.
  • It is legitimate for an .output() to describe a pattern rather than an exact path (see Output patterns) so downstream tooling can still find produced files without argtype pretending it can compute them.

Because outputs may cross-reference other named nodes, they introduce implicit dependencies between arguments. Outputs are allowed on any node (the chained notation is the point), but a grammar author should prefer outputs whose template is resolvable from a single parse, and should document it when that isn't the case.

.output(...templates)

.output(...templates) declares that a node produces one or more output files. The outputs are associated with the node it is chained on, typically an opt (the outputs exist when the optional is present) or a terminal. Each output is described by a template, optionally named via the label: expr sugar or .name() chained on the template. .output(...) returns the node it was chained on, so further core chaining (or additional .output(...) calls) composes naturally.

argtype
// single named output, label: sugar form
opt("-thumbnail", size: str)
  .output(thumbnail: `{output}_thumb.png`)

// equivalently, .name() chained on the template
opt("-thumbnail", size: str)
  .output(`{output}_thumb.png`.name("thumbnail"))

// multiple outputs declared in one call
opt("-A").output(
  inskull_mask: `{output}_inskull_mask.nii.gz`,
  inskull_mesh: `{output}_inskull_mesh.nii.gz`,
  outskin_mask: `{output}_outskin_mask.nii.gz`,
)

A template is a nameable node: the label: expr form anywhere a method-call argument is accepted desugars to .name("label") on the template (see spec § Sugar DSL). Anonymous outputs are written with no label.

The desugaring happens in place, at the head of the chain, so a later .name() wins:

argtype
.output(report: `{x}.log`.name("summary"))   // the output is named "summary"

That is deliberately the opposite of a node-level label:, which the spec applies last. The difference follows from what each has to do: a node label must beat a .name() that arrived from an alias definition, so it applies after the chain; an output template has no alias to inherit from, so the plain left-to-right rule applies and the nearer spelling wins. Write one or the other, not both.

Documenting an output

An output may carry its own /// doc comment, following the same title / description convention as any other node:

argtype
opt("-A").output(
  /// Inner skull surface mask
  ///
  /// Binary mask of the inner surface of the skull.
  inskull_mask: `{output}_inskull_mask.nii.gz`,
)

The doc describes the produced file, and consumers surface it on the corresponding output field.

Equivalently, chain .title("...") / .description("...") on the template - use these when the text would confuse the /// title split, e.g. a description whose first line begins with # :

argtype
opt("-A").output(
  `{output}_inskull_mask.nii.gz`
    .name("inskull_mask")
    .description("# reserved: literal hash, not a title"),
)

Output templates

Output templates describe the path of a file produced by the tool, using template literal syntax with references to named nodes.

A backslash escapes a template-significant character, so a path that literally contains a brace or backtick is written with \{, \}, \` (and \\ for a literal backslash):

argtype
opt("-o").output(`\{env\}/result.nii.gz`)
// literal path text: "{env}/result.nii.gz" (no interpolation)

Self-reference

{} refers to the value of the node the output is attached to (or its nearest named ancestor):

argtype
opt("-o", output_path: path)
  .output(`{}.png`)
// {} refers to output_path's value

Cross-reference

{name} refers to another named node:

argtype
opt("-thumbnail", size: str)
  .output(`{output}_thumb.png`)
// {output} refers to the node named "output"

When the target's name is not a valid identifier (it was given as a quoted label), quote it inside the braces too, so the reference and the label agree:

argtype
"4d_output": path
  .output(`{"4d_output"}.nii.gz`)
// {"4d_output"} refers to the node labelled "4d_output"

A } inside the quotes is fine, as is a nested {...} inside an operation argument. A literal backtick cannot appear in a name, since a backtick ends the template literal.

Reference operations

Operations can be applied to references inside templates:

argtype
opt("-i", input: path)
  .output(`{input.strip_suffix(".png")}_thumb.png`)
OperationDescription
.strip_suffix(ext)Remove a file extension/suffix from the value
.strip_prefix(pre)Remove a prefix from the value
.or(fallback)Use fallback if the referenced value is absent (e.g. optional param not set)
.basename()Extract the filename from a path, stripping directories

The set of operations is intentionally open; expect additions.

Reference-level fallback

.or(fallback) on a reference provides a fallback when that specific reference cannot be resolved:

argtype
opt("-o", output_prefix: str)
  .output(`{output_prefix.or("output")}_thumb.png`)
// if output_prefix is not set: "output_thumb.png"
// if output_prefix is "photo-01":  "photo-01_thumb.png"

Output-level fallback

.or(fallback) chained on the template itself provides a fallback for the entire template when it cannot be fully resolved:

argtype
opt("-o", output_prefix: str)
  .output(`{output_prefix}_thumb.png`.or("thumb.png"))
// if output_prefix is not set: "thumb.png"
// if output_prefix is "photo-01":  "photo-01_thumb.png"

This is distinct from reference-level fallback. Reference-level .or() (inside a {...} reference) substitutes a single token. Output-level .or() (on the template) replaces the entire resolved path.

Output patterns

This part of the extension is a sketch, not yet specified.

When a tool's output set can't be reduced to a single computed path (split archives, per-frame image sequences, files named from input contents), an output should be expressible as a glob or regular-expression pattern rather than an exact path. Downstream consumers use the pattern to discover produced files after the run, instead of predicting them. The exact surface (.outputGlob(...), a pattern flavor on .output(...), or both) is still open.

Chaining API

typescript
// On any Node (provided by the outputs extension):
interface Node {
  output(...templates: Template[]): Node     // returns the same Node
}

// A Template is a nameable node carrying an output path expression.
// Naming and output-level fallback are methods on the template itself —
// there is no separate OutputNode wrapper.
interface Template {
  name(s: string):      Template            // names the output
  or(fallback: string): Template            // output-level fallback path
}

// Template construction.
// ref() creates a reference to a named node; ref() with no argument is a
// self-reference ({} in the sugar DSL).
ref(name?: string): TemplateRef
interface TemplateRef {
  strip_suffix(ext: string): TemplateRef
  strip_prefix(pre: string): TemplateRef
  basename():                TemplateRef
  or(fallback: string):      TemplateRef
}

// Templates are built with a tagged template literal:
//   t`${ref("output")}_thumb.png`
// or as a plain function:
//   template(ref("output"), "_thumb.png")
t(strings: TemplateStringsArray, ...refs: TemplateRef[]): Template
template(...parts: (string | TemplateRef)[]): Template

Example

argtype
---
exe: "convert"
---

/// ImageMagick Image Converter
convert: seq(
  input: path,
  output: path,
  set(
    /// Output image quality (1-100)
    opt("-quality", quality: int = 80),

    /// Generate a thumbnail
    opt("-thumbnail", size: str)
      .output(thumbnail: `{output}_thumb.png`),

    /// Apply charcoal sketch effect
    opt("-charcoal", radius: float)
      .output(sketch: `{output}_sketch.png`),

    /// Convert to monochrome
    opt("-monochrome")
      .output(monochrome: `{output}_mono.png`),

    /// Strip all metadata
    opt("-strip"),
  ),
)