6.4. Arguments

By default every argument in a signature is a positional, in the order it appears. summarize :: [Hit] → [(Str, Int)] takes one argument, so the command takes one token. Save a search first, since the rest of the chapter reuses it:

$ ./sift scan the notes > hits.json
$ ./sift summarize hits.json
[["notes\/todo.txt",2],["notes\/2026\/plan.txt",2]]

What that token may be depends on the argument’s type.

Scalars and strings are read verbatim. An Int, Real, Bool, sized integer or float, or a Str is taken from argv as written — no JSON quoting, no escaping:

$ ./sift scan the notes -c
4

the is the Str pattern and notes is the Str directory.

Numbers need no quoting either, negative ones included: a leading - starts an option only when the next character is a letter. A second small program to show it with, used again later in this section:

calc.py
def add(x, y):
    return x + y


def join(sep, words):
    return sep.join(words)
calc.loc
module calc (add, join)

import root-py

source Py from "calc.py" ("add", "join")

--' Add two numbers
add :: Real -> Real -> Real

--' Join words with a separator
--' @name cat
join ::
  --' the separator
  Str ->
  --' the words to join
  --' @many
  [Str] ->
  Str
$ morloc make -o calc calc.loc
$ ./calc add -4.0 -7
-11

Everything else is a value in a recognized format. Lists, tuples, records, and maps accept either a JSON value inline or a path to a file holding one:

$ ./sift summarize '[{"path":"a.txt","line":1,"text":"x"}]'
[["a.txt",1]]

$ ./sift summarize hits.json
[["notes\/todo.txt",2],["notes\/2026\/plan.txt",2]]

The file’s format is detected from its contents, not its name. JSON, MessagePack, and Morloc’s own binary form (voidstar) are all recognized, so a file produced by an earlier command is read back without saying how it was written:

$ ./sift -f mpk scan the notes > hits.mpk
$ ./sift summarize hits.mpk
[["notes\/todo.txt",2],["notes\/2026\/plan.txt",2]]

Arrow IPC and Parquet are recognized as well when the target type is a Table.

Standard input is a value source too. The token - (or /dev/stdin) reads the argument from standard input, which is what makes two Morloc commands compose in a pipeline:

$ ./sift scan the notes | ./sift summarize -
[["notes\/todo.txt",2],["notes\/2026\/plan.txt",2]]

Only one argument per command may claim stdin; a second - is an error rather than a silent read of zero bytes.

6.4.1. When an argument is wrong

An argument that looks like a path — it contains a /, or ends in a recognized data extension — but does not exist is reported as a missing file rather than parsed as inline data:

$ ./sift summarize nosuch.json
Error: failed to parse argument #0: file 'nosuch.json' not found

A file that exists but does not hold what the type wants is reported against the file:

$ echo 'not json' > bad.json
$ ./sift summarize bad.json
Error: failed to parse argument #0: file 'bad.json': serialization error: JSON parse error: expected ident at line 1 column 2

Failures exit non-zero, so a Morloc command is safe to put in a set -e script or a && chain.

Note

Errors number arguments from zero (argument #0) while --help numbers positionals from one. argument #0 is the argument printed as 1:.

6.4.2. Options, flags, and repeats

An argument becomes an option instead of a positional when you give it a flag name with @arg. An option can be omitted, so it also needs a @default:

  --' Stop after this many hits; 0 means no limit
  --' @arg -m/--max-count
  --' @default 0
  maxCount :: Int

The default is written in JSON, and the compiler insists on it. Drop the @default line from sift.loc and the build stops:

$ morloc make -o sift sift.loc
In sift:scan, argument #3, field maxCount: optional argument -m/--max-count must be given a default value

A Bool is a flag, not an option with a value, so it uses a different pair of directives. @true names the spelling that turns it on, and the default is false:

  --' Match without regard to case
  --' @true -i/--ignore-case
  ignoreCase :: Bool
$ ./sift scan MANUAL notes -i -p
notes/todo.txt:3:write the manual
notes/2026/plan.txt:2:ship the manual

@false is the mirror image: it names the spelling that turns the flag off, and the default becomes true. Giving both declares an on switch and an off switch for the same field. Using @arg on a Bool is rejected, with the alternative spelled out — change ignoreCase’s `@true to @arg and:

$ morloc make -o sift sift.loc
In sift:scan, argument #3, field ignoreCase: a Bool argument cannot use `@arg`. Use `@true <opt>` (default false, the flag turns it on) or `@false <opt>` (default true, the flag turns it off) instead.

Both spellings are delivered, and help shows the flag with the true default it turns off. Adding @false -s/--skip-empty to a reportEmpty field gives:

  -s, --skip-empty              Report each file even when it has no hits
                                type: Bool
                                default: true

@many makes an argument variadic: it consumes the remaining argv tokens and assembles them into a list. It applies to a [a]-typed argument, and as a positional it must be the last one. calc’s `join above declares one:

$ ./calc cat + a b c
"a+b+c"

6.4.3. Naming

@name gives a command a name of its own, independent of the Morloc term. calc exports join and calls the subcommand cat:

$ ./calc -h
Usage: ./calc <nexus_options> <command> <command_options>

Commands:
  add  Add two numbers
  cat  Join words with a separator

General Options:
  -h, --help  Print help; -hh adds details and examples, -hhh adds schemas
              (nexus options: -h @)

Use it when the shell-facing name and the library-facing name want to differ — a name that reads well in a pipeline is not always the name you want to import.

@metavar names an argument. On an option it becomes the placeholder in the help text, replacing the type name:

  --' how many things
  --' @arg -n/--num
  --' @metavar COUNT
  --' @default 0
  Int ->
Optional arguments:
  -n, --num <COUNT>  how many things
                     type: Int [default: 0]

On a positional it labels the slot, beside the index:

$ ./sift scan -h
...
Positional arguments:
  1: PATTERN  The text to search for
              type: Str
              format: literal string
  2:          The directory to search
              type: Str
              format: path to a readable file
...

scan names only its first positional, so the second keeps a bare index and the two labels pad to a common width.

The same name is what the interface is keyed on wherever it is consumed by a program — the property name in the JSON Schema and in the MCP tool definition (key and ciphertext below, from the @metavar KEY and @metavar CIPHERTEXT on the cipher type definitions of Docstrings):

$ ./cipher --json-help | python3 -c "
import json,sys
d=json.load(sys.stdin)
for c in d['commands']:
  print(c['name'], [(a['name'], a['metavar']) for a in c['arguments']])
"
encode [('key', 'KEY'), ('plaintext', 'PLAINTEXT')]
decode [('key', 'KEY'), ('ciphertext', 'CIPHERTEXT')]

An unnamed positional is identified by index alone in both places, which is worth avoiding on anything a model or a script will call.

6.4.4. Ending option parsing

A bare -- ends option parsing: every token after it is a positional, even one that looks like a flag. This is rarely needed, since -4.0 and -7 are already treated as positionals, but it is the way to pass a string that looks like a short option:

$ ./sift scan -- -p notes
[{"path":"notes\/todo.txt","line":4,"text":"use -p for plain output"}]

Note what that costs: after --, the command’s own -p formatter is a positional too, so a search for the literal text -p cannot also ask for plain output.