Build real CLI tools in pure Emacs Lisp. Declare commands, options,
and args in a single clime-app form — clime handles parsing,
--help generation, error messages, and dispatch.
Write an Elisp file using the clime DSL:
;;; greet.el --- A greeting CLI -*- lexical-binding: t; -*-
;;; Code:
(require 'clime)
(clime-app greet
:version "1.0.0"
:help "A friendly greeter."
(clime-option shout ("--shout" "-s") :bool t
:help "SHOUT THE GREETING")
(clime-command hello
:help "Say hello to someone"
(clime-arg name :help "Person to greet")
(clime-handler (ctx)
(clime-let ctx (name shout)
(let ((msg (format "Hello, %s!" name)))
(if shout (upcase msg) msg)))))
(clime-command goodbye
:help "Say goodbye"
(clime-arg name :help "Person to bid farewell")
(clime-handler (ctx)
(clime-let ctx (name)
(format "Goodbye, %s!" name)))))
(provide 'greet)
;;; greet.el ends hereRun quickstart to add the entrypoint boilerplate and shebang:
path/to/clime-make.el quickstart greet.el
./greet.el hello world # => Hello, world!
./greet.el --shout hello world # => HELLO, WORLD!
./greet.el --help
./greet.el hello --helpclime-make.el init prepends a two-line polyglot header that’s valid as
both shell script and Elisp:
#!/bin/sh
":"; CLIME_ARGV0="$0" exec emacs --batch -Q \
-L "$(dirname "$0")" \
--eval "(setq load-file-name \"$0\")" \
--eval "(with-temp-buffer \
(insert-file-contents load-file-name) \
(setq lexical-binding t) \
(goto-char(point-min)) \
(condition-case nil \
(while t(eval(read(current-buffer))t)) \
(end-of-file nil)))" \
-- "$@" # clime-sh!:v1 -*- mode: emacs-lisp; lexical-binding: t; -*-Line 2 is both a shell no-op (":") and the Emacs invocation. It uses
a read/eval loop that explicitly sets lexical-binding to t, since
the ":" prefix on line 2 prevents Emacs from recognizing the -*-
cookie during normal file loading. The cookie is still useful: editors
use it to detect the major mode when opening the file for editing.
The # clime-sh!:vN tag lets init detect and update existing
shebangs without clobbering newer formats. CLIME_ARGV0 preserves
the original script name for usage output.
So ./myapp.el hello world just works from your shell.
- Declarative DSL — one form defines your entire CLI
- Subcommands and nested groups (
myapp repo add) - Auto-generated
--helpand--versionat every level - Boolean flags, count flags (
-vvv), repeatable options (--tag a --tag b) - Negatable flags (
--color / --no-color) with ternary state - Mutually exclusive options (
:mutex) - Option dependencies (
:requires) — coupled or one-way - Paired option groups (
:zip) — matched cardinality with zipped alist output - Collapsed short flags (
-abcexpands to-a -b -c) --flag=valuesyntax- Choices validation (
:choices '("json" "csv")), including lazy:choicesfunctions - Value separator (
:separator ","splits--tags=a,b,cinto a list) - Custom value transforms (
:coerce) and conformers (:conform) - Type coercion (string, integer, number)
- Default values
- Command aliases (
:aliases (i)) - Command aliases for nested paths (
clime-alias-for start (agents start)) - Hidden commands and options (
:hidden t) - Deprecation warnings (
:deprecated) with migration hints - Environment variables with auto-derived or per-option names
- Stdin via
-sentinel - Output formats with accumulation, custom envelopes, and streaming (
clime-output-format) - Epilog text for extra help content
- Category sections in help output (options, commands, inline groups)
- Terminal-width-aware help wrapping
- Rest args (
:nargs :rest) - Invocable groups (group with its own handler)
- Inline groups (
:inline t) — promote children to parent level - Ancestor option propagation — group options available to all descendants
- Setup hook (
:setup) with two-pass parse for config loading - Parameter templates (
clime-defopt,clime-defarg) with:frominheritance clime-params-plistandclime-param— context param accessors- Interactive transient UI (
clime-invoke) — auto-generated menus from app definitions clime-reload— hot-reload all modules during development- Single-file distribution via
bundlecommand
Boolean flags consume no value. Use :bool t (shorthand for :nargs 0):
(clime-option force ("--force" "-f") :bool t
:help "Skip confirmation")Count flags increment on each occurrence:
(clime-option verbose ("-v" "--verbose") :count t
:help "Increase verbosity")
;; -v → 1, -vv → 2, -vvv → 3Repeatable options collect values into a list:
(clime-option tag ("--tag" "-t") :multiple t
:help "Add tag")
;; --tag dev --tag ci → ("dev" "ci")Default values are used when an option isn’t provided:
(clime-option registry ("--registry" "-r")
:help "Registry to use"
:default "default")Split a single value into a list by a delimiter. Implies :multiple t:
(clime-option tags ("--tags" "-t") :separator ","
:help "Comma-separated tags")
;; --tags=dev,ci → ("dev" "ci")This also affects env var parsing: MYAPP_TAGS=dev,ci produces the
same result.
Mark an option as :required t to enforce that it must be provided.
Unlike positional args (which are required by default), options are
optional by default — :required makes them mandatory:
(clime-option token ("--token") :required t
:help "API authentication token")Error: Missing required option --token for myapp deploy
This is especially useful with :env-prefix — the option can be
satisfied via an environment variable instead of the command line:
MYAPP_TOKEN=secret ./myapp.el deploy # --token satisfied via envUse :negatable t to auto-generate --no-X variants for boolean flags.
This implies boolean (no :bool t needed):
(clime-option color ("--color") :negatable t :default t
:help "Colorize output")Help output:
Options: --color / --no-color Colorize output
The --no-X form explicitly sets the param to nil, enabling handlers
to distinguish three states — unset, enabled, and disabled:
(clime-handler (ctx)
(pcase (clime-param ctx 'color 'auto)
('auto (if (isatty) "color" "plain")) ; unset → auto-detect
('t "color") ; --color
(_ "plain"))) ; --no-colorAll long flags on the option get negated variants. Short flags (-c)
and flags already starting with --no- are not double-negated.
Use :mutex to declare options that cannot be used together. Options
sharing the same :mutex symbol are mutually exclusive — at most one
may be set per invocation:
(clime-option format-json ("--json") :bool t :mutex 'output-format)
(clime-option format-csv ("--csv") :bool t :mutex 'output-format)
(clime-option format-text ("--text") :bool t :mutex 'output-format)Using more than one signals an error:
Error: Options --json, --csv are mutually exclusive
Mutex validation runs after env vars are applied, so conflicts from environment variables are caught too. When any member of a mutex group is set (via CLI or env), defaults on sibling options are suppressed.
Use :requires to declare that an option depends on other options.
When the option is set, all required options must also be present:
;; Bidirectional: both must be used together
(clime-option skip ("--skip") :requires '(reason))
(clime-option reason ("--reason") :requires '(skip))
;; One-way: --output-file needs --format, but --format works alone
(clime-option output-file ("--output-file") :requires '(format))
(clime-option format ("--format"))Using --skip without --reason signals an error:
Error: --skip requires --reason
An option can require multiple others: :requires '(a b c) means all
three must be set. Env-var-set values satisfy the requirement;
defaults do not.
Use :zip to declare options that must be used the same number of
times. Values are paired by position and stored as a list of alists
in ctx under the group name. Implies :multiple t:
(clime-option skip ("--skip") :zip 'skip-reason)
(clime-option reason ("--reason") :zip 'skip-reason)myapp --skip SPEC --reason "no spec" --skip TEST --reason "no test"(clime-handler (ctx)
(clime-ctx-get ctx 'skip-reason)
;; => (((skip . "SPEC") (reason . "no spec"))
;; ((skip . "TEST") (reason . "no test")))
;; Individual lists still available:
(clime-ctx-get ctx 'skip) ;; => ("SPEC" "TEST")
(clime-ctx-get ctx 'reason)) ;; => ("no spec" "no test")Unequal counts signal an error:
Error: Zip group options must be used the same number of times: --skip (2), --reason (1)
Combine with :required t on any member to require at least one pair.
Single-character flags can be collapsed: -abc expands to -a -b -c.
This works when all flags in the group are boolean. A trailing
value-taking flag consumes the next argument:
./myapp.el -vvf show 123 # -v -v -f show 123Options accept --flag=value syntax:
./myapp.el show --format=json 123Use -- to stop option parsing — everything after is treated as
positional arguments:
./myapp.el show -- --not-a-flagEach option and arg value flows through a multi-stage pipeline. The stages are ordered so that each step receives the right kind of input:
raw string → :type → :choices → :coerce → env vars → :conform → :mutex → defaults
pass 1 (immediate, per-token) │ pass 2 (after all input collected)
│
:setup hook runs here
- :type — convert the raw string to a typed value. Accepts
'integer,'number, or'string(default). Runs first so that:choicescan compare typed values:(clime-option level ("--level") :type 'integer :choices '(1 2 3))
- :choices — validate against an allowed set. Runs after
:typeso the comparison uses typed values. Accepts a literal list or a function (lazy; deferred to pass 2):(clime-option format ("--format") :choices '("json" "table" "csv") :help "Output format")
Invalid values produce a clear error:
Error: Invalid value "xml" for --format (choose from: json, table, csv) - :coerce — arbitrary transform after validation. Runs after
:choicesso you validate against user-friendly values, then transform for the handler:(clime-option env ("--env") :choices '("dev" "prod") :coerce #'upcase :help "Deploy target") ;; user types "dev", choices validates "dev", handler gets "DEV"
Also useful for standalone transforms:
(clime-option path ("--path") :coerce #'expand-file-name :help "File path (expanded)")
- :conform — pass-2 validation and normalization. Runs after env
vars are applied, so it catches values from both CLI and environment.
Defaults skip conforming — they’re developer-authored and should
already be valid. Must return the conformed value or signal an error:
(clime-option id ("--id") :conform (lambda (val) (unless (string-match-p "^[a-z0-9]\\{7\\}$" val) (error "Invalid ID format: %s" val)) (upcase val)) :help "Resource ID (7-char alphanumeric)")
The key distinction between :coerce and :conform is when they
run: :coerce runs immediately in pass 1 (CLI input only), while
:conform runs in pass 2 (CLI + env var input, after the :setup
hook). Use :coerce for simple transforms, :conform for
cross-source validation.
Set :env-prefix on the app to auto-derive env var names from options:
(clime-app greet
:version "1.0.0"
:help "A friendly greeter."
:env-prefix "GREET"
...)Now --shout can also be set via GREET_SHOUT=1.
For individual options, override the env var name with :env:
(clime-option token ("--token") :env "API_TOKEN"
:help "Auth token")Boolean flags accept 1~/~true~/~yes and 0~/~false~/~no. Multiple
options split on comma: MYAPP_TAGS=dev,ci becomes ("dev" "ci").
Declare positional arguments with clime-arg:
(clime-command show
:help "Show a resource"
(clime-arg id :help "Resource ID")
(clime-handler (ctx)
(clime-let ctx (id)
(format "Showing %s" id))))Arguments are required by default. Use :required nil for optional args.
Collect all remaining positional arguments into a list:
(clime-command run
:help "Run a script"
(clime-arg script :help "Script name")
(clime-arg args :nargs :rest :required nil
:help "Arguments passed to the script")
(clime-handler (ctx)
(clime-let ctx (script args)
(format "Running %s with %s" script args))))Use - as an argument value to read from stdin:
echo "World" | ./greet.el hello -Define subcommands with clime-command:
(clime-command install
:help "Install a package"
(clime-arg name :help "Package name")
(clime-handler (ctx)
(clime-let ctx (name) (format "Installing %s" name))))Commands can have aliases:
(clime-command install
:help "Install a package"
:aliases (i)
...)Now both myapp install foo and myapp i foo work. Aliases are
symbols or strings.
Use clime-alias-for to expose a nested command at a higher level
without duplicating its definition:
(clime-group agents
:help "Manage agents"
(clime-command start
:help "Start an agent"
(clime-arg name :help "Agent name")
(clime-option prompt ("--prompt") :help "Initial prompt")
(clime-handler (ctx) (do-start ctx))))
(clime-group shortcuts :inline t :category "Shortcuts"
(clime-alias-for start (agents start)
:help "Start an agent (shortcut)"))Now myapp start foo works identically to myapp agents start foo.
The alias copies args, options, and handler from the target at init
time. You can override :help, :aliases, :hidden, and
:category on the alias. When :help is omitted, it inherits from
the target.
Use :defaults to preset option values (user can still override):
(clime-alias-for show-csv (report show)
:help "Show report as CSV"
:defaults '((format . "csv")))Use :vals to lock option values (removed from CLI and help):
(clime-alias-for deploy-ci (deploy run)
:help "Deploy to CI"
:vals '((target . "ci")))Organize related commands under a group:
(clime-group repo
:help "Manage repositories"
(clime-command add
:help "Add a repository"
(clime-arg name :help "Repository name")
(clime-arg url :help "Repository URL")
(clime-handler (ctx)
(clime-let ctx (name url)
(format "Added %s → %s" name url))))
(clime-command remove
:help "Remove a repository"
:aliases (rm)
(clime-arg name :help "Repository name")
(clime-handler (ctx)
(clime-let ctx (name) (format "Removed %s" name)))))Mark a group with :inline t to promote its children to the parent
level. Both the group prefix and the short form work:
(clime-group repo
:inline t
:help "Manage repositories"
(clime-command add
:help "Add a repository"
(clime-arg name :help "Repository name")
(clime-handler (ctx)
(clime-let ctx (name) (format "Added %s" name))))
(clime-command remove
:help "Remove a repository"
(clime-arg name :help "Repository name")
(clime-handler (ctx)
(clime-let ctx (name) (format "Removed %s" name)))))./myapp.el add foo # works (promoted)
./myapp.el repo add foo # also works (direct)In --help, inline group children appear at the parent level. The
group name itself is hidden from the command listing. If a parent has
its own child with the same name, the parent’s child takes priority.
A group can have its own :handler that runs when no subcommand is
given:
(clime-group config
:help "View or modify configuration"
(clime-handler (_ctx)
"Configuration:\n registry = default\n timeout = 30s")
(clime-command set
:help "Set a config value"
(clime-arg key :help "Config key")
(clime-arg value :help "New value")
(clime-handler (ctx)
(clime-let ctx (key value)
(format "Set %s = %s" key value)))))myapp config shows the overview; myapp config set key val sets a value.
Hidden commands and options
Mark a command or option as :hidden t to omit it from --help while
keeping it callable:
(clime-command debug
:help "Dump internal state"
:hidden t
(clime-handler (ctx) ...))
(clime-option trace ("--internal-trace") :bool t
:help "Enable tracing" :hidden t)Mark commands or options as :deprecated to emit a warning when used.
The value can be t (generic) or a string (migration hint):
(clime-option old-flag ("--old") :bool t
:deprecated "Use --new instead"
:help "Legacy option")
(clime-command migrate
:deprecated "Use 'upgrade' instead"
:help "Old migration path"
(clime-handler (ctx) ...))Help output shows the deprecation annotation:
Options: --old Legacy option (deprecated: Use --new instead) Commands: migrate Old migration path (deprecated: Use 'upgrade' instead)
At runtime, using a deprecated item prints a warning to stderr:
Warning: --old is deprecated. Use --new instead Warning: migrate is deprecated. Use 'upgrade' instead
Combine :hidden t with :deprecated for silent deprecation: hidden
from help but still warns when used.
--help and -h are recognized at every level. Both orderings work:
./myapp.el install --help # help for install
./myapp.el --help install # same thing
./myapp.el --help # app-level helpUsage errors include a hint:
Error: Missing required argument <name> Try 'myapp install --help' for more information.
Multiline :help strings are truncated to the first line in command
listings, and shown in full in detailed command help.
Help text wraps to the terminal width, auto-detected from the
COLUMNS environment variable (fallback 80, minimum 40). Override
with the clime-help-width variable.
Add free-form text after the auto-generated help with :epilog:
(clime-app myapp
:version "1.0"
:help "My application."
:epilog "Examples:
myapp install foo --tag dev
myapp config set timeout 60"
...)Commands and groups support :epilog too.
Use :category on options, commands, or inline groups to organize
--help into named sections. Items sharing a category appear under
one heading; uncategorized items fall back to “Options:” or “Commands:”.
Options with :category:
(clime-option author ("--author" "-a")
:help "Filter by author"
:category "Filter")
(clime-option since ("--since")
:help "Only after DATE"
:category "Filter")These appear under a “Filter:” section instead of “Options:”.
Commands with :category:
(clime-command search
:help "Search items" :category "Filter")
(clime-command list
:help "List items" :category "Filter")
(clime-command show
:help "Show details") ;; uncategorized → "Commands:"Options and commands with the same :category share a section.
Inline groups with :category apply it to all their children and
options:
(clime-group admin
:inline t :category "Admin"
:help "Administrative commands"
(clime-option admin-token ("--admin-token")
:help "Admin API token")
(clime-command status
:help "Show system status"))This renders as:
Admin: Administrative commands --admin-token VALUE Admin API token status Show system status
Nested inline groups compose category paths — an inner group with
:category "Config" inside an outer :category "Admin" produces
“Admin / Config:” in help.
Declare output formats with clime-output-format. Each format is a
CLI flag that controls how output is encoded. The form derives from
clime-option, so it supports :mutex, :hidden, :category, etc.
(clime-app myapp
:version "1.0"
(clime-output-format json ("--json")
:help "Output as JSON")
...)./myapp.el show --json 123In JSON mode, clime--active-output-format is bound to the format
struct and handler return values are JSON-encoded in a {success, data}
envelope. Handlers calling clime-output per-item get automatic
accumulation — multiple items become a JSON array, a single item is a
bare object. Errors reported via clime-output-error accumulate and
take priority over items and retval at finalize time.
Use :finalize to control the JSON envelope shape:
(clime-output-format json ("--json")
:finalize (lambda (items retval errors)
(cond
(errors `((error . ,(car errors))))
(t (let ((data (or (and items (vconcat items)) retval)))
(when data `((result . ,data) (status . "ok"))))))))The finalize function receives (items retval errors) where items is
the list of accumulated clime-output calls, retval is the handler’s
return value, and errors is a list of error strings (may be nil).
For streaming output, set :streaming t on the format. This bypasses
the accumulator — each clime-output call emits one JSON object per
line immediately:
(clime-output-format json ("--json") :streaming t)Handlers can also call clime-output-stream explicitly to bypass the
accumulator in a non-streaming format.
:json-mode t on clime-app still works as shorthand for
(clime-output-format json ("--json") :help "Output as JSON").
Prefer the explicit form for new code.
Options declared at any level are automatically available to all
descendants. A --verbose flag on the app or on a group can be used
from any child command:
(clime-app myapp
:version "1.0"
(clime-option verbose ("-v" "--verbose") :count t
:help "Increase verbosity")
(clime-group repo
:help "Manage repositories"
(clime-option registry ("--registry") :help "Registry URL")
(clime-command add
:help "Add a repo"
(clime-arg name :help "Repo name")
;; Handler can access verbose (from app) and registry (from group)
(clime-handler (ctx)
(clime-let ctx (name verbose registry)
(format "Adding %s (verbose=%s, registry=%s)" name verbose registry))))))In --help, inherited options appear in a separate “Global Options:”
section.
The :setup hook runs after pass-1 parsing (all values extracted from
argv) but before pass-2 finalization (dynamic :choices validation,
env vars, defaults). This lets apps load configuration that influences
dynamic option values:
(clime-app myapp
:version "1.0"
:env-prefix "MYAPP"
:setup (lambda (app result)
(let ((dir (plist-get (clime-parse-result-params result) 'config-dir)))
(my-load-config (or dir "~/.myapp"))))
(clime-option config-dir ("--config-dir")
:help "Config directory")
(clime-option level ("--level")
:help "Log level"
:choices #'my-configured-levels) ; resolved after setup
(clime-command run
:help "Run the app"
(clime-handler (ctx) "running")))Static :choices (literal lists) are validated immediately in pass 1.
Dynamic :choices (functions) are deferred to pass 2 after the setup
hook has run.
Define reusable option or arg specs with clime-defopt and
clime-defarg, then reference them with :from:
;; Define a template once
(clime-defopt verified-id
:type 'string
:conform #'my-ensure-id
:help "A verified resource ID")
;; Reuse across commands — explicit values override the template
(clime-command copy
:help "Copy a resource"
(clime-option src ("--src") :from verified-id :multiple t)
(clime-option dst ("--dst") :from verified-id)
(clime-handler (ctx) ...))
(clime-command move
:help "Move a resource"
(clime-option src ("--src") :from verified-id)
(clime-option dst ("--dst") :from verified-id)
(clime-handler (ctx) ...))clime-defopt expands to a defvar with a clime--opt- prefix
(e.g. clime--opt-verified-id). The :from keyword handles the
prefixing transparently — you write the bare name in both places.
Templates cannot contain :name or :flags (those are per-instance).
All DSL shorthands (:bool t, :separator) work in templates.
clime-defarg works the same way for positional args:
(clime-defarg file-path
:type 'string
:coerce #'expand-file-name
:help "Path to a file")
(clime-command cat
:help "Print file contents"
(clime-arg path :from file-path)
(clime-handler (ctx) ...))clime-let destructures context params into let-bindings:
(clime-handler (ctx)
(clime-let ctx (name verbose (tags tag))
(format "name=%s verbose=%s tags=%s" name verbose tags)))clime-params-plist converts context params to a keyword plist:
(clime-handler (ctx)
;; All params as keyword plist
(my-function (clime-params-plist ctx))
;; => (:verbose 3 :package "foo" :force t)
;; Selected params only (nil values omitted)
(my-other-function (clime-params-plist ctx 'package 'force)))clime-param gets a param value with a default for absent params.
Unlike clime-ctx-get, it distinguishes between explicitly nil and
absent — useful for negatable flags:
(clime-param ctx 'color 'auto)
;; => t when --color was passed
;; => nil when --no-color was passed
;; => 'auto when neither was passedAny clime-app can be invoked interactively from Emacs via
clime-invoke, which auto-generates a transient.el menu:
(require 'clime-invoke)
(clime-invoke my-app)Groups become nested transient prefixes (press a key to drill down,
q to go back). Options become infixes (switches and value inputs).
Leaf commands show a “Run” action that collects infix values, prompts
for positional args, calls clime-run, and displays output in the
*clime-output* buffer.
Requires the transient package (ships with Emacs 29+).
clime-reload force-reloads all clime modules in dependency order,
including optional modules (clime-invoke, clime-make) if already
loaded. Useful during development to pick up changes without
restarting Emacs.
All command-like forms (clime-command, clime-group, clime-app)
accept: :help (:doc is an alias), :aliases, :hidden,
:deprecated, :epilog, :category.
All parameter forms (clime-option, clime-arg) accept: :type,
:choices, :coerce, :conform, :default, :help,
:deprecated, :from.
| Macro | Purpose | Distinctive arguments |
|---|---|---|
clime-app | Define a CLI application | :version, :env-prefix, :setup |
clime-command | Define a subcommand | (uses shared command slots) |
clime-group | Nest related commands | :inline |
clime-option | Declare a named option | :bool, :count, :multiple, :separator, :negatable, :required, :requires, :mutex, :zip, :env, :category |
clime-arg | Declare a positional argument | :nargs, :required |
clime-defopt | Reusable option template | Any option slot except :name, :flags |
clime-defarg | Reusable arg template | Any arg slot except :name |
clime-alias-for | Alias for a nested command | Path to target command, :defaults, :vals |
clime-output-format | Declare an output format | :finalize, :streaming, :encoder, :error-handler, plus all option slots |
clime-handler | Attach a handler | (ctx) — receives the parse context |
clime-let | Destructure context in a handler | ctx (var1 var2 ...) — binds option/arg values |
:bool t is shorthand for :nargs 0 (boolean flag, DSL only).
See examples/pkm.el for a comprehensive demo covering all major features,
and examples/cloq.el for an org-ql CLI showcasing output formats, aliases,
and JSON mode.
clime-make.el provides four commands:
| Command | Purpose |
|---|---|
init | Add polyglot shebang to make a script executable |
scaffold | Insert ;;; Entrypoint: boilerplate |
quickstart | scaffold + init in one shot (auto env vars) |
bundle | Combine multiple source files into one |
For a single-file app that lives alongside clime:
./clime-make.el init myapp.el
./myapp.el --helpinit prepends a polyglot shebang, sets the executable bit, and adds
-L flags so Emacs can find clime at runtime.
Common options:
# Script in a subdirectory that needs the parent on the load path
./clime-make.el init -R .. examples/myapp.el
# Script's own dir on the load path (for local requires)
./clime-make.el init --self-dir myapp.el
# Bundled/vendored app that doesn't need the clime load path
./clime-make.el init --standalone --self-dir dist/myapp.el
# Set env vars in the shebang
./clime-make.el init --env CLIME_MAIN_APP=myapp dist/myapp.elRe-running init on a file that already has a clime shebang updates
it in place. init refuses to overwrite a non-clime shebang (use
--force) or to downgrade a newer shebang format version.
scaffold detects the clime-app symbol in a file and inserts an
;;; Entrypoint: section with the clime-main-script-p guard:
./clime-make.el scaffold myapp.elBefore:
(clime-app myapp :version "1.0" ...)
(provide 'myapp)
;;; myapp.el ends hereAfter:
(clime-app myapp :version "1.0" ...)
(provide 'myapp)
;;; Entrypoint:
(when (clime-main-script-p 'myapp)
(clime-run-batch myapp))
;;; myapp.el ends hereThe guard symbol comes from (provide 'FEATURE) if present, falling
back to the clime-app symbol. If ;;; Entrypoint: already exists,
scaffold skips without error. If there is no ;;; ... ends here
marker, the entrypoint is appended at the end.
quickstart composes scaffold + init and auto-detects
CLIME_MAIN_APP:
./clime-make.el quickstart myapp.el
./myapp.el --helpThis is the fastest way to go from a source file to a runnable script.
quickstart accepts all of init’s flags (--self-dir, -R, -L,
--standalone, --force, -e):
./clime-make.el quickstart --self-dir -e MY_VAR=hello myapp.elAn explicit -e CLIME_MAIN_APP=custom overrides the auto-detected
value.
For multi-file projects, bundle concatenates source files into one:
./clime-make.el bundle -o dist/myapp.el \
--provide myapp \
src/myapp-core.el src/myapp-commands.el src/myapp.elThe output provides the feature ('myapp) and can be used as a
library via (require 'myapp).
To make the bundle also work as a standalone CLI, pass --main:
./clime-make.el bundle -o dist/myapp.el \
--provide myapp --main myapp-main.el \
src/myapp-core.el src/myapp-commands.el src/myapp.el
./clime-make.el init --standalone --self-dir \
--env CLIME_MAIN_APP=myapp dist/myapp.elThe main file is a short script — it requires the app and runs it:
;;; myapp-main.el --- Entrypoint for myapp -*- lexical-binding: t; -*-
;;; Code:
(require 'myapp)
(clime-run-batch myapp)
;;; myapp-main.el ends hereIts code is wrapped in a clime-main-script-p guard inside the
bundle, so it only executes when CLIME_MAIN_APP matches the feature
name — not when loaded as a library. The --main file must not
contain an ;;; Entrypoint: marker.
Source files can contain an ;;; Entrypoint: section that separates
library code from standalone entrypoint code:
;;; myapp.el --- My app -*- lexical-binding: t; -*-
;;; Code:
(require 'clime)
(clime-app myapp :version "1.0" ...)
(provide 'myapp)
;;; Entrypoint:
(clime-run-batch myapp)
;;; myapp.el ends hereThe bundler extracts only the library section (;;; Code: to
;;; Entrypoint:), stripping the entrypoint automatically. This lets
each source file run standalone during development while bundling
cleanly. Files without the marker are extracted up to
;;; ... ends here as usual.
clime-run-batch is a no-op in interactive Emacs (emits a warning
instead of calling kill-emacs), so loading a script with
(require 'myapp) during development is safe.
If (clime-run-batch ...) appears in a source file’s library section
(above the marker or without one), bundle signals an error — move
it below ;;; Entrypoint:.
myapp/
vendor/clime/ # clime source (git clone or submodule)
src/
myapp-core.el # library code
myapp-commands.el # commands
myapp.el # app definition + (provide 'myapp)
myapp-main.el # standalone entrypoint
dist/
myapp.el # bundled output (single file)
Makefile
A minimal Makefile:
CLIME := vendor/clime/clime-make.el
SRCS := src/myapp-core.el src/myapp-commands.el src/myapp.el
dist/myapp.el: $(SRCS) myapp-main.el
$(CLIME) bundle -o $@ --provide myapp --main myapp-main.el $(SRCS)
$(CLIME) init --standalone --self-dir --env CLIME_MAIN_APP=myapp $@# Clone directly
git clone https://github.com/cosmicz/clime vendor/clime
# Or as a submodule
git submodule add https://github.com/cosmicz/clime vendor/climeThen set up your app file:
vendor/clime/clime-make.el quickstart myapp.elThis inserts the entrypoint boilerplate, prepends a polyglot shebang, and makes the file executable.
(package-vc-install "https://github.com/cosmicz/clime")Build a single-file bundle and copy it into your project:
cd clime/
make dist
cp dist/clime.el /path/to/myapp/Your app just needs (require 'clime) and -L pointing to the
directory containing the bundled file.
clime draws inspiration from several projects:
- commander.el — command-line parsing for Emacs Lisp with a declarative API
- clingon — a feature-rich Common Lisp command-line parser with subcommands, options, and completions
- orgstrap — self-bootstrapping Org files with embedded Elisp, a different take on making Emacs code executable
make test # run tests (SELECT=pattern to filter)
make lint # byte-compile warnings + checkdoc
make compile # byte-compile
make dist # build single-file dist/clime.el
make clean # remove build artifactsMIT