6.7. Input shape

The defaults from Arguments cover most arguments: a scalar is read from argv, a compound value is inline JSON or a file. Three directives override that when an argument needs a particular shape.

  • @source says where the bytes come from: inline (the argv token is the value) or file (the argv token is a path and the file’s contents are the value).

  • @form says how the bytes are read: list, bytes, bytes-only, or packet.

  • @check.<kind> states an invariant the argument must satisfy before the command runs. The only kind today is path.

For a list, the same three exist per element as @list.source, @list.form, and @list.check.<kind>, describing what each line of the outer file means.

Which combinations are legal depends on the argument’s wire type. The tables below are the complete set; anything outside them is a compile error, reported against the docstring line that caused it.

Table 1. Non-Str primitives (numeric values and booleans)
Modifier Effect

(none — the only valid case)

argv is the literal value (42, true, 3.14). No modifiers are allowed.

Table 2. Str
Modifier Effect

(default)

argv is the string itself, verbatim.

@check.path r / w / x / rw

argv must be a path satisfying the requested mode. r = exists and is readable; w = writable (an existing writable file, or a non-existent file in a writable directory); x = does not yet exist and the parent directory is writable (exclusive create); rw = exists and is both readable and writable. Mutually exclusive with @source file.

@source file

argv is a path; the file’s contents become the string. One trailing newline is stripped, so it behaves like $(cat file).

@stdin

Makes the positional optional and reads standard input when it is omitted. Implies @check.path r. See Reading a stream from standard input.

Table 3. Arrays of fixed-width scalars ([U8], [I32], [F64], [Bool], …​)
Modifier Effect

(default)

argv is a JSON array ([1,2,3]) or a path to a JSON / MessagePack / packet file.

@form bytes

argv is a path; the file is checked for a Morloc packet header and otherwise read as packed raw bytes.

@form bytes-only

argv is a path; the file is packed raw bytes, with no packet check.

@form packet

argv is a path; the file must be a Morloc packet.

@source inline + @form bytes or bytes-only

Only on [U8]. argv is the literal byte sequence, one byte per character, with \xNN, \n, \t, \r, \0 and \\ recognized.

Table 4. Any list type ([T], including [Str] and [(Int, Str)])
Modifier Effect

@form list

argv is a file (or -) with one element per line, or an inline JSON array. A token whose first byte is [ is parsed as JSON; anything else is a path. Tuple elements accept a JSON array per line, TSV, or CSV.

@form list + @list.source file

Each line of the outer file is a path to a per-element file, each classified on its own (JSON / MessagePack / packet).

@form list + @list.source file + @list.form packet

Each line is a path, and each per-element file must be a Morloc packet.

@form list + @list.source file + @list.form bytes (or bytes-only)

Each line is a path, and each file is read as packed raw bytes. The element type must be an array of fixed-width scalars.

@form list + @list.check.path r (or w / x / rw)

Each line of the outer file must be a path satisfying the requested mode. The element type must be Str.

Table 5. Tuples, records, and other non-list compound types
Modifier Effect

(default, the only valid case)

argv is JSON, or a path to a file holding JSON / MessagePack / a packet. No outer modifiers are allowed.

6.7.1. A worked example

sift’s `scanAll uses two of these. The pattern list is a file with one pattern per line, and the search root must be a directory that exists:

--' Search for any of several patterns, one per line of a file
scanAll ::
  --' A file of patterns, one per line
  --' @form list
  [Str] ->
  --' The directory to search
  --' @check.path r
  Str ->
  Options ->
  <IO> [Hit]

A shaped argument gets a format: line in the help saying what it will accept. So does every Str argument, shaped or not: Str is the one type where argv is genuinely ambiguous, and stating the default reading is cheaper than making a reader infer it from the absence of a line.

$ ./sift scanAll -h
...
Positional arguments:
  1:  A file of patterns, one per line
      type: [Str]
      format: path to text file with one string per line
  2:  The directory to search
      type: Str
      format: path to a readable file
...
$ printf 'milk\nrest\n' > patterns.txt
$ ./sift scanAll patterns.txt notes -p
notes/todo.txt:1:buy milk
notes/2026/plan.txt:3:rest

@form list still accepts inline JSON, so the same command works without a file:

$ ./sift scanAll '["milk","rest"]' notes -c
2

A failing @check is reported before the command runs, naming the check that failed:

$ ./sift scan the nosuchdir
Error: argument #1: check.path: r requires path 'nosuchdir' to exist and be readable

6.7.2. Shape follows the wire form

Shape is classified against an argument’s wire form, not its source-level type name. A type declared with Packable [(a, b)] T crosses the language boundary as a list of pairs, so the CLI treats it as [(a, b)] and every list modifier is available.

Map a b from the standard library is the case you are most likely to meet. Its wire form is [(a, b)], so a Map Str Int argument reads a two-column TSV or CSV exactly as [(Str, Int)] would:

tally.loc
module tally (tally)

import root-py
import map-py

--' Count the entries of a two-column table read as a Map
tally ::
  --' A file with one `key<TAB>value` pair per line
  --' @form list
  Map Str Int -> U64
tally m = size m
$ printf 'apple\t3\nbanana\t7\ncherry\t1\n' > counts.tsv
$ ./tally counts.tsv
3

Commas work as well as tabs, and a JSON array per line is the fallback:

$ printf 'apple,3\nbanana,7\n' > counts.csv
$ ./tally counts.csv
2

$ printf '["apple",3]\n["banana",7]\n' > counts.jsonl
$ ./tally counts.jsonl
2
Note

@form list is headerless. A delimited file read this way is parsed row by row with no header, because tuples have no column names — a header row would be read as data and fail the schema check on the first field. There is no option to skip one:

$ printf 'name\tcount\napple\t3\n' > hdr.tsv
$ ./tally hdr.tsv
Error: failed to parse argument #0: serialization error: JSON parse error: expected value at line 1 column 10
  hint: the first row (`name	count`) looks like a column-name header. `form: list` is headerless -- remove the header row, or declare the argument as `Table` if you need column names.

If you need column names, declare the argument as a Table. The Table loader honors headers and aligns columns by name; @form list is for streams of rows.