# 6.7. Input shape

Morloc Manual > Building CLIs | https://morloc-project.github.io/docs/clis/input-shape.html | prev: https://morloc-project.github.io/docs/clis/sum-type-arguments.md | next: https://morloc-project.github.io/docs/clis/reading-stdin.md

The defaults from [Arguments](https://morloc-project.github.io/docs/clis/arguments.md) 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](https://morloc-project.github.io/docs/clis/reading-stdin.md). |

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

```morloc
--' 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.

```console
$ ./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
...
```

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

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

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

```console
$ ./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**

```morloc
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
```

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

```console
$ 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:
> 
> ```console
> $ 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.
