Writing your first `.lash` plugin
A native plugin is just a .lash file in ~/.lash/plugins/. Every fn and type declared at the top level is loaded into the session on startup — no registration, no compilation, no restart of anything other than the session itself.
Minimal example
Section titled “Minimal example”Save this as ~/.lash/plugins/greet.lash:
/// Say hello to someone by name.fn hi(name: string) { echo "Hello, $name!"}Open a new session and try it:
$ hi AliceHello, Alice!That’s it. hi now works at the prompt just like any built-in command.
What the signature gets you
Section titled “What the signature gets you”Typed parameters aren’t decoration — they drive three behaviours at once:
-
Coercion.
n: intreceives an integer, not a string. Passhi notanintand lash prints'notanint' is not a valid intbefore the body ever runs. -
Completion. The LSP and the interactive tab completer know the parameter shape and use it for argument suggestions (see “Defining a custom type for completion”).
-
Auto-generated
--help. Doc comments on the fn and its params feed the help renderer:Terminal window $ hi --helphi — Say hello to someone by name.Arguments:name (string)
Wrapping shell commands
Section titled “Wrapping shell commands”Most useful plugins are thin wrappers. The bundled git plugin looks like:
/// Push the current branch and set upstream to origin.fn gpsup(...args: [string]) { git push --set-upstream origin HEAD ...args}...args: [string]is a rest parameter — every extra positional arg after the fixed ones collects into a list....argsinside a command splices the list back into separate arguments. Sogpsup --force-with-leaserunsgit push --set-upstream origin HEAD --force-with-lease.
The fn’s return value becomes the exit code. By convention, return git’s exit code — which happens automatically since git ... is the last statement in the body.
fn main — turning a script into a CLI
Section titled “fn main — turning a script into a CLI”If a .lash file is run directly (not loaded as a plugin), a top-level fn main(...) acts as the entry point:
/// Resize a directory of images to a target width.fn main(dir: string, width: int) { for img in `ls $dir/*.jpg` { convert $img -resize "${width}x" "${dir}/resized/${img}" }}$ lash resize.lash photos 800$ lash resize.lash photos # error — missing `width`Error: Missing required argument 'width' (int)
resize.lash — Resize a directory of images to a target width.
Arguments: dir (string) width (int)Scripts without an explicit fn main receive their CLI args through an implicit args list:
for f in $args { echo "processing $f"}Layering
Section titled “Layering”Lash loads plugins in this order, with later layers overriding earlier ones of the same name:
- Bundled — compiled into the binary (currently:
git.lash). - System —
/usr/share/lash/plugins/*.lash(for package maintainers). - User —
~/.lash/plugins/*.lash.
Define fn gco(...) in your user plugin and it transparently replaces the bundled one. There is no enable / disable toggle — presence is activation.
Inspecting what’s loaded
Section titled “Inspecting what’s loaded”lash --list-pluginsprints every loaded fn with its signature + first doc line and every type with its methods and cache TTL. This is the fastest way to verify that your plugin parsed cleanly.