6.12. Composing tools
A module that compiles to a CLI is still a module. Nothing about being a command line tool stops another module from importing it, so a toolbox is a module that imports and re-exports.
Here is a second module, unrelated to sift and written in R, that turns
label-count pairs into a bar chart:
histogram <- function(counts){
paste0(
sapply(counts, function(row){
sprintf("%-24s %s", row[[1]], strrep("#", as.integer(row[[2]])))
}),
collapse = "\n"
)
}
module stats (histogram)
import root-r
source R from "stats.R" ("histogram" as histogramR)
histogramR :: [(Str, Int)] -> Str
--' Draw a bar for each label
histogram :: [(Str, Int)] -> Str
histogram = histogramR
A toolbox picks what it wants from each:
--' A little toolbox for reading notes
module tools (scan, summarize, histogram)
import .sift
import .stats
The leading . marks a local file rather than an installed module. Both
modules here are files you built a moment ago, so both take it. A toolbox
assembled from modules you installed names them without the dot — import
sift — and is otherwise identical; that is the more common shape, and the
only reason this chapter uses local files is so you can run it without
installing anything first.
That is the whole toolbox: two imports and an export list, no glue code:
$ morloc make -o tools tools.loc
$ ./tools -h
A little toolbox for reading notes
Usage: ./tools <nexus_options> <command> <command_options>
Commands:
scan Search a directory tree for lines containing a pattern
summarize Count the hits in each file
histogram Draw a bar for each label
General Options:
-h, --help Print help; -hh adds details and examples, -hhh adds schemas
(nexus options: -h @)
Two of those commands run in Python and one in R. The pools start on demand, so
a run that only touches scan never starts the R interpreter, and a pipeline
that touches all three starts each once:
$ ./tools scan the notes | ./tools summarize - | ./tools -p histogram -
notes/todo.txt ##
notes/2026/plan.txt ##
That pipeline is worth a second look. Three processes, two language runtimes,
and no agreement between the stages about a file format: scan writes a
[Hit], summarize reads a [Hit] and writes a [(Str, Int)], histogram
reads a [(Str, Int)]. Each side knows the type, so each side knows how to
read what arrived. Adding a stage means writing a function with the right type,
not a parser.
Subtraction works the same way. A toolbox that lists three of `sift’s five exports is a tool with three commands; nothing of the other two is compiled in. There is no plugin mechanism here because none is needed — the export list is the mechanism.
6.12.1. Grouping commands
A toolbox grows, and a flat list of twenty commands is a bad interface. Group
them with --* annotations in the export list:
--' A little toolbox for reading notes
module tools
--* group: find
--* Search the filesystem
( scan
, scanAll
--* group: report
--* Turn hits into something readable
, summarize
, histogram
)
import .sift
import .stats
A --* group: <name> line opens a group, and the --* lines after it are its
description. Every export listed below it belongs to that group, until the next
group line. Each group becomes a subcommand of its own:
$ ./tools -h
A little toolbox for reading notes
Usage: ./tools <nexus_options> <command> <command_options>
Commands:
find Search the filesystem
report Turn hits into something readable
General Options:
-h, --help Print help; -hh adds details and examples, -hhh adds schemas
(nexus options: -h @)
$ ./tools find -h
Search the filesystem
Usage: ./tools <nexus_options> find <command> <command_options>
Commands:
scan Search a directory tree for lines containing a pattern
scanAll Search for any of several patterns, one per line of a file
General Options:
-h, --help Print help; -hh adds details and examples, -hhh adds schemas
(nexus options: -h @)
and the group name joins the invocation:
$ ./tools find scan the notes | ./tools report summarize -
[["notes\/todo.txt",2],["notes\/2026\/plan.txt",2]]
Grouping is optional per export. Write --* group: with no name to close the
current group; exports after it are ungrouped and appear at the top level
alongside the groups.
6.12.2. Installing
morloc make --install puts the built program on your PATH instead of
leaving it in the current directory:
$ morloc make --install -o sift sift.loc
Installed 'sift' to /opt/morloc/bin/sift # your MORLOC_HOME will differ
morloc list shows what is installed — modules first, then programs:
$ morloc list
Modules:
root 0.7.0 Define type signatures for common functions
...
Programs:
sift 5 commands
...
Add -v to list each program’s commands with their return types.
Installing also regenerates shell completion for every installed program, into
$MORLOC_HOME/completions/. The completions are derived from the same
manifest the help is — command names, group names, and each command’s flags — so they cover the groups of the previous section without any extra
declaration:
$ sed -n '/Installed program: sift/,+9p' $MORLOC_HOME/completions/morloc-completions.bash | tail -2
COMPREPLY=($(compgen -W "scan scanAll summarize total stream" -- "$cur"))
return
Source the one for your shell from your shell’s startup file:
$ source $MORLOC_HOME/completions/morloc-completions.bash # bash
$ source $MORLOC_HOME/completions/_morloc_completions # zsh
The entry points the compiler synthesizes for each output action are marked internal in the manifest, so they are absent from the count and from the completions — the surface you see is the surface the program accepts.