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:
def add(x, y):
return x + y
def join(sep, words):
return sep.join(words)
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.
|
|
Errors number arguments from zero ( |
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.