Skip to content

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.

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:

Terminal window
$ hi Alice
Hello, Alice!

That’s it. hi now works at the prompt just like any built-in command.

Typed parameters aren’t decoration — they drive three behaviours at once:

  1. Coercion. n: int receives an integer, not a string. Pass hi notanint and lash prints 'notanint' is not a valid int before the body ever runs.

  2. 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”).

  3. Auto-generated --help. Doc comments on the fn and its params feed the help renderer:

    Terminal window
    $ hi --help
    hi Say hello to someone by name.
    Arguments:
    name (string)

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.
  • ...args inside a command splices the list back into separate arguments. So gpsup --force-with-lease runs git 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.

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}"
}
}
Terminal window
$ 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"
}

Lash loads plugins in this order, with later layers overriding earlier ones of the same name:

  1. Bundled — compiled into the binary (currently: git.lash).
  2. System/usr/share/lash/plugins/*.lash (for package maintainers).
  3. 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.

Terminal window
lash --list-plugins

prints 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.