Wrap any command-line tool to Emacs commands easily.
CLI2ELI generates interactive Emacs functions from JSON configuration files describing CLI tools. Write a simple JSON config, get Emacs commands with completion, dynamic selection, and terminal integration.
Emacs should easily integrate external command-line tools.
The core idea of Emacs is to have an expressive digital material and an open-ended, user-extensible set of commands that manipulate that material and can be quickly invoked. Obviously, this model can be fruitfully applied to any digital material, not just plain text. - X
If you already have a justfile, simply wrap it with following config, now you can select a just command to run in Emacs.
{
"tool": "cli-just",
"cwd": "git-root",
"commands": [
{
"name": "just",
"command": "just ${recipe}",
"inputs": {
"recipe": {
"type": "shell",
"command": "just -l | grep -v Available | sed 's/#.*$//g'",
"prompt": "Recipe"
}
}
}
]
}Select a service and input a command to be executed inside the running container.
{
"tool": "cli-docker-compose",
"cwd": "git-root",
"commands": [
{
"name": "execute",
"command": "docker compose exec ${service} ${program}",
"inputs": {
"service": {
"type": "shell",
"command": "docker compose config --services",
"prompt": "Service"
},
"program": { "prompt": "Program" }
}
}
]
}- Dynamic generation of Emacs interactive functions from JSON specifications
- Command templates with
${var}placeholders for readable configs - Built-in variables (
${file},${file-relative},${dir}) for current context - Completion system integration for argument input and selection
- Dynamic selection from shell command output
- Stdin support: Pipe buffer/region content to commands
- Output modes: terminal, buffer display, or in-place text replacement
- Context-aware command execution (e.g., from git root)
- Support for running commands locally even when editing a file in a container
- Clone this repository to your local machine.
- Add the following lines to your Emacs configuration file:
(add-to-list 'load-path "/path/to/CLI2ELI")
(require 'cli2eli)packages.el
(package! cli2eli
:recipe (:host github :repo "nohzafk/cli2eli" :branch "main"))config.el
(use-package! cli2eli
:load-path "~/path/to/local/cli2eli"
(cli2eli-load-tool "~/path/to/config.json"))Use M-x cli2eli-load-tool to select a JSON file to load the configuration. Alternatively, add it to your init file:
(cli2eli-load-tool "~/path/to/config-1.json")
(cli2eli-load-tool "~/path/to/config-2.json")After loading, generated interactive functions are available via M-x. Each function is named <tool>-<command> (e.g., cli-quickrun-just).
Use cli2eli-run to open a scoped picker that lists only CLI2ELI-generated commands, with descriptions shown as annotations. Bind it to a key for quick access:
(global-set-key (kbd "<f7>") #'cli2eli-run)Use json-schema for editor autocompletion when writing configs:
"$schema": "https://raw.githubusercontent.com/nohzafk/cli2eli/main/cli2eli-schema.json",Commands use ${var} placeholders that are resolved from built-in variables or declared inputs:
{
"tool": "cli-gleam",
"cwd": "git-root",
"commands": [
{
"name": "test",
"command": "gleam build && gleam test"
},
{
"name": "add",
"command": "gleam add ${package}",
"inputs": {
"package": { "prompt": "Package name" }
}
}
]
}This generates:
cli-gleam-test- runsgleam build && gleam testcli-gleam-add- prompts for package name, runsgleam add <name>
These are always available in command templates without declaring inputs:
| Variable | Value |
|---|---|
${file} |
Current buffer's absolute file path |
${file-relative} |
File path relative to working directory |
${dir} |
Current buffer's directory |
{
"name": "glow",
"command": "glow -s dark -t ${file}"
}If you declare a built-in name in inputs, your declaration overrides the default (e.g., to prompt for a file path instead of using the current buffer).
Inputs are declared in the inputs object. Each key matches a ${var} in the command template.
Free text input:
"inputs": {
"message": { "prompt": "Commit message" }
}Undeclared template variables default to prompt inputs with the variable name as the prompt.
Pick from a static list:
"inputs": {
"env": { "choices": ["dev", "staging", "prod"], "prompt": "Environment" }
}Pick from shell command output (dynamic selection):
"inputs": {
"container": {
"type": "shell",
"command": "docker ps --format '{{.ID}} {{.Names}}' | awk '{print $1}'",
"prompt": "Container"
}
}The command's stdout lines become completion candidates. Include any filtering/transformation in the shell pipeline itself.
Emacs directory picker:
"inputs": {
"path": { "type": "directory", "prompt": "Project directory" }
}Inputs with "optional": true are removed from the command when left empty:
{
"command": "just ${recipe} ${extra}",
"inputs": {
"recipe": { "type": "shell", "command": "just -l", "prompt": "Recipe" },
"extra": { "prompt": "Extra arguments", "optional": true }
}
}The stdin property pipes buffer or region content to a command:
{
"name": "format SQL",
"command": "sqlfmt -",
"stdin": "region",
"output": "replace"
}stdin accepts:
"region"- selected text, or entire buffer if no selection"buffer"- always entire buffer content
The output property controls where command output goes:
| Mode | Behavior |
|---|---|
terminal |
Run in eat/term terminal (default for non-stdin commands) |
buffer |
Display in read-only output buffer (default for stdin commands) |
replace |
Replace the stdin source text in-place |
replace is the key mode for text transforms - select text, run command, text is replaced:
{
"tool": "cli-transform",
"commands": [
{
"name": "format JSON",
"command": "jq '.'",
"stdin": "region",
"output": "replace"
},
{
"name": "Python dict to JSON",
"command": "python3 -c \"import sys, json, ast; print(json.dumps(ast.literal_eval(sys.stdin.read()), indent=2))\"",
"stdin": "region",
"output": "replace"
}
]
}Set cwd at the tool level:
| Value | Behavior |
|---|---|
| (omitted) | Current buffer's directory |
"default" |
Current buffer's directory |
"git-root" |
Git repository root |
/explicit/path |
Literal path |
{
"tool": "mytool",
"shell": "/bin/zsh",
"commands": [...]
}Default is /bin/bash.
By default, cli2eli displays the output buffer at the bottom. Customize with:
(setq cli2eli-output-buffer-display-option #'display-buffer-other-frame)CLI2ELI supports terminal backends for command output. By default, it auto-detects in priority order: eat > term.
;; Auto-detect (default): eat > term
(setq cli2eli-terminal-backend 'auto)
;; Force a specific backend
(setq cli2eli-terminal-backend 'eat) ; Use eat (recommended)
(setq cli2eli-terminal-backend 'term) ; Use built-in term- eat - Recommended, pure Emacs Lisp with good performance
- term - Built-in fallback, always available
CLI2ELI fully supports TUI applications like glow, lazygit, htop, etc. When using the eat backend:
Eat Input Modes
By default, CLI2ELI uses semi-char mode:
- Terminal gets most keys (vim navigation, arrow keys, etc.)
C-x,C-c, andM-xare reserved for Emacs
(setq cli2eli-default-eat-mode 'semi-char) ; default
(setq cli2eli-default-eat-mode 'char) ; all keys to terminal
(setq cli2eli-default-eat-mode 'emacs) ; standard Emacs editing
(setq cli2eli-default-eat-mode 'line) ; line-based inputAutomatic Window Resize
TUI applications automatically resize when the Emacs window configuration changes.
{
"tool": "cli-docker",
"cwd": "git-root",
"commands": [
{
"name": "inspect container",
"command": "docker inspect --type container ${container} | jless",
"inputs": {
"container": {
"type": "shell",
"command": "docker ps --format '{{.ID}} {{.Names}}' | awk '{print $1}'",
"prompt": "Container"
}
}
}
]
}{
"tool": "cli-devcontainer",
"cwd": "git-root",
"commands": [
{
"name": "build",
"command": "devcontainer build --workspace-folder ${folder} --no-cache=${no_cache}",
"inputs": {
"folder": { "choices": ["."], "prompt": "Workspace folder" },
"no_cache": { "choices": [false, true], "prompt": "No cache" }
}
}
]
}{
"tool": "cli-quickrun",
"cwd": "git-root",
"commands": [
{
"name": "pytest",
"command": "pytest -s ${file-relative}"
}
]
}During software development, especially in containerized environments, developers often find themselves repeatedly executing similar command sequences. CLI2ELI addresses this by:
- Allowing commands to be executed directly from within Emacs
- Providing an interactive interface for selecting containers or other dynamic values
- Enabling text transformation workflows with in-place replacement
While not intended to replace the terminal entirely, CLI2ELI streamlines common development tasks by integrating them into the Emacs environment.
CLI2ELI is released under the MIT License. Feel free to use, modify, and distribute it as per the license terms.

