Wrap any command-line tool to Emacs commands easily.
CLI2ELI is an Emacs package that generates interactive Emacs functions from command-line tool specifications. It allows seamless integration and execution of external CLI tools within Emacs, enhancing developer workflow and productivity.
Emacs should easily integrate external command-line tools.
This saying captures the essence of Emacs:
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.
{
"$schema": "https://raw.githubusercontent.com/nohzafk/cli2eli/main/cli2eli-schema.json",
"tool": "cli-just",
"cwd": "git-root",
"commands": [
{
"name": "just",
"command": "just",
"arguments": [
{
"name": "$$",
"type": "dynamic-select",
"command": "just -l | grep -v Available",
"prompt": "Select a recieps: "
}
]
}
]
}If you have multiple services defined in docker-compose.yaml, you can select a service and input a command to be executed inside the running service container. For example, select a service and run bash to use the terminal inside the container.
Quick and effective.
{
"$schema": "https://raw.githubusercontent.com/nohzafk/cli2eli/main/cli2eli-schema.json",
"tool": "cli-docker-compose",
"cwd": "git-root",
"commands": [
{
"name": "execute",
"command": "docker compose exec",
"arguments": [
{
"name": "$$",
"type": "dynamic-select",
"command": "docker compose config --services",
"prompt": "Select a service: "
},
{
"name": "$$",
"description": "program"
}
]
}
]
}- Dynamic generation of Emacs interactive functions from JSON specifications
- Utilization of Emacs' completion system for argument input and selection
- Support for various argument types: free text, choices, directory paths, and dynamic selections
- Stdin support: Pipe buffer/region content to commands for text transformation
- Command chaining for complex operations
- Context-aware command execution (e.g., from git root)
- Support running commands locally even when editing remote file in a container
To install CLI2ELI:
- 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 generating the interactive functions, you can directly invoke the commands associated with your external CLI tools in Emacs. Each command will have a unique prefix, as specified in your JSON configuration, ensuring easy access and organization.
By default, cli2eli displays the buffer window at the bottom. You can use the following setting to display it on another side vertically.
(setq cli2eli-output-buffer-display-option #'display-buffer-other-frame)This is particularly useful when you want to grep the content of a command output.
For example, you can set up a command to execute unit tests and then grep the content in the buffer. With hyperbole, you can jump to the problematic file by pressing Alt + RETURN on the line of filename:line.
Both buffers - the hyperbole-created buffer and the cli2eli buffer - support q to quit. This allows you to seamlessly return to the original file buffer.
CLI2ELI supports terminal backends for command output. By default, it auto-detects available backends in this priority order: eat > term.
You can customize the backend via cli2eli-terminal-backend:
;; 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 (Text User Interface) applications like glow, lazygit, htop, etc. When using the eat backend:
Eat Input Modes
By default, CLI2ELI uses semi-char mode which provides a good balance:
- Terminal gets most keys (vim navigation, arrow keys, etc.)
- Emacs keybindings like
C-x,C-c, andM-xare reserved for Emacs
You can customize this via cli2eli-default-eat-mode:
;; Semi-char (default): Terminal keys + Emacs C-x/C-c/M-x
(setq cli2eli-default-eat-mode 'semi-char)
;; Char: All keys sent to terminal (full terminal experience)
(setq cli2eli-default-eat-mode 'char)
;; Emacs: Standard Emacs editing keybindings
(setq cli2eli-default-eat-mode 'emacs)
;; Line: Line-based input mode
(setq cli2eli-default-eat-mode 'line)Automatic Window Resize
TUI applications automatically resize and redraw when the Emacs window configuration changes (e.g., after C-x 1 to delete other windows). This ensures the terminal content remains properly aligned.
Example: Markdown preview with glow
{
"$schema": "https://raw.githubusercontent.com/nohzafk/cli2eli/main/cli2eli-schema.json",
"tool": "cli-markdown",
"cwd": "default",
"commands": [
{
"name": "glow",
"description": "Render current markdown file with glow TUI",
"command": "glow -s dark -t",
"arguments": [
{
"name": "$",
"type": "current-file"
}
]
}
]
}This creates a cli-markdown-glow command that renders the current markdown file in glow's TUI mode with vim-style navigation (j/k to scroll, q to quit).
During software development, especially in containerized environments, developers often find themselves repeatedly executing similar command sequences. For instance:
docker ps | grep <id>
docker stop <container>
docker rm <container>
devcontainer up
devcontainer buildManually typing these commands in a terminal is time-consuming and error-prone, I'm just tired with repeatly typing those commands. CLI2ELI addresses this by:
- Allowing these commands to be executed directly from within Emacs
- Providing an interactive interface for selecting containers or other dynamic values
- Enabling command chaining for complex operations (e.g., stop and remove a container in one step)
For example, with CLI2ELI, a developer could use write a chain command and use M-x devcontainer-delete-container to interactively select and remove a Docker container, all without leaving Emacs or manually constructing the command string.
While not intended to replace the terminal entirely, CLI2ELI significantly streamlines common development tasks by integrating them directly into the Emacs environment, reducing context switching and improving workflow efficiency.
Use json-schema to help to write configuration JSON, add a "$schema" field to the json file.
"$schema": "https://raw.githubusercontent.com/nohzafk/cli2eli/main/cli2eli-schema.json",i.e. using VSCode
{
"tool": "devcontainer",
"cwd": "git-root",
"commands": [
{
"name": "up",
"description": "Create a dev container.",
"command": "devcontainer up",
"arguments": [
{
"name": "--workspace-folder"
}
]
}
]
}This configuration creates a simple wrapper for the devcontainer up command. The cwd set to "git-root" means the command will be executed from the root of the git repository. All arguments are required in this case.
user will be prompt to input a value for --workspace-folder
the default behavior is to append a space between argument and argument value
devcontaienr up --workspace-folder <input-value>
Specify which shell to be used to execute the command, if "shell" field is omitted, default value is /bin/bash.
{
"tool": "mytool",
"shell": "/bin/zsh",
"commands": [
// ... command definitions ...
]
}{
"tool": "configtool",
"commands": [
{
"name": "set",
"command": "configtool set",
"arguments": [
{
"name": "user=$$",
"description": "Set the username"
}
]
}
]
}In this example, the $$ in the argument name will be replaced by the user's input.
This configuration will prompt the user to input the value and the whole argument is combined without a space.
This configuration would allow commands like:
configtool set user=john
The $$ will be replaced with the user's input, maintaining the required "option=value" format.
{
"tool": "docker",
"commands": [
{
"name": "run",
"command": "docker run",
"arguments": [
{
"name": "-it --name",
"description": "Container name"
}
],
"extra_arguments": true
}
]
}The extra_arguments field allows users to input additional arguments when calling the command.
{
"tool": "git",
"commands": [
{
"name": "commit",
"command": "git commit",
"arguments": [
{
"name": "-m",
"description": "Commit message"
},
{
"name": "--author",
"description": "Author of the commit"
}
]
}
]
}
User will be prompted to input value for each argument.
{
"tool": "npm",
"commands": [
{
"name": "run",
"command": "npm run",
"arguments": [
{
"name": "",
"choices": ["build", "test", "start", "lint"]
}
]
}
]
}The choices field provides a predefined list of options for the user to choose from.
{
"tool": "project",
"commands": [
{
"name": "init",
"command": "project-init",
"arguments": [
{
"name": "--path",
"type": "directory",
"description": "Select project directory"
}
]
}
]
}The "type": "directory" specification in the argument prompts the user to select a directory using Emacs' built-in directory selection interface. This is useful for commands that require a directory path as an argument. In this case, the user will be prompted to choose a directory, and the selected path will be passed to the project-init command with the --path argument.
These additional examples showcase more advanced features of CLI2ELI, allowing for greater flexibility in command construction and argument input.
{
"tool": "docker",
"commands": [
{
"name": "stop",
"command": "docker stop",
"arguments": [
{
"name": "",
"type": "dynamic-select",
"command": "docker ps --format '{{.ID}} {{.Names}}'",
"prompt": "Select a container to stop: ",
"transform": "awk '{print $1}'"
}
]
}
]
}The dynamic-select type allows for dynamic generation of choices.
The command field specifies how to generate the list, prompt is the message shown to the user, and transform can modify the selected value.
This is equal to
docker ps --format '{{.ID}} {{.Names}}' | grep <something> | awk '{print $1}'{
"tool": "quick-run",
"commands": [
{
"name": "pytest",
"arguments": [
{
"name": "-s $$",
"type": "current-file"
}
]
}
]
}The current-file type will use (buffer-file-name) to get the path of the current file. This will automatically pass the path of the current file to the argument when the command is executed, without requiring user to input the value.
{
"tool": "quick-run",
"commands": [
{
"name": "pytest",
"arguments": [
{
"name": "-s $$",
"type": "current-file-relative-path"
}
]
}
]
}Similar to the current-file type, current-file-relative-path uses the relative path of the current file relative to the project root.
The stdin property allows you to pipe buffer or region content directly to a command. This is useful for text transformation commands like formatters, converters, or filters.
{
"tool": "cli-transform",
"cwd": "default",
"commands": [
{
"name": "format JSON",
"description": "Format JSON using jq",
"command": "jq '.'",
"stdin": "region"
},
{
"name": "format SQL",
"description": "Format SQL using sqlfmt",
"command": "sqlfmt -",
"stdin": "region"
}
]
}The stdin field accepts two values:
"region": Uses selected text, or entire buffer if no selection"buffer": Always uses entire buffer content
When stdin is set, the command runs synchronously using call-process-region instead of the terminal backend, and output is displayed in the CLI2ELI output buffer with a header line showing the command.
Note: For stdin commands, use "cwd": "default" since they typically don't depend on a git repository context.
{
"tool": "docker",
"commands": [
{
"name": "delete container",
"command": "docker stop",
"arguments": [
{
"name": "",
"type": "dynamic-select",
"command": "docker ps --format '{{.Names}}'",
"prompt": "Select a container: "
}
],
"chain-call": "remove container",
"chain-pass": true
},
{
"name": "remove container",
"command": "docker rm",
"arguments": [
{
"name": "",
"type": "dynamic-select",
"command": "docker ps -a --format '{{.Names}}'",
"prompt": "Select a container to remove: "
}
]
}
]
}The chain-call and chain-pass fields allow for sequential execution of commands. In this example, after stopping a container, it will automatically prompt to remove it. This chaining can be cancelled at any point using Ctrl-g, providing flexibility in the workflow.
In this example, when chain-pass is set to true, the result of the delete container command is passed to the remove container command for selection. This is instead of using the command defined in the remove container argument. As a result, after the container is stopped, running docker ps again won't display the container.
If you don't need to pass on the value, set chain-pass to false or leave this field unset.
{
"$schema": "https://raw.githubusercontent.com/nohzafk/cli2eli/main/cli2eli-schema.json",
"tool": "cli-gleam",
"cwd": "git-root",
"commands": [
{
"name": "test",
"command": "gleam build && gleam test"
},
{
"name": "add",
"command": "gleam add",
"arguments": [{ "name": "$$", "description": "package name" }]
}
]
}This will generate two Emacs commands:
cli-gleam-test, this is equivalent to executegleam build && gleam testcli-gleam-add, when executed you will be asked to input the package name in the minibuffer, this is equivalent to executegleam add <package name>.
{
"$schema": "https://raw.githubusercontent.com/nohzafk/cli2eli/main/cli2eli-schema.json",
"tool": "cli-devcontainer",
"cwd": "git-root",
"commands": [
{
"name": "inspect container",
"command": "docker",
"arguments": [
{
"name": "inspect --type container $$ | jless",
"type": "dynamic-select",
"command": "docker ps",
"prompt": "Select a container: ",
"transform": "awk '{print $1}'"
}
]
}
]
}{
"$schema": "https://raw.githubusercontent.com/nohzafk/cli2eli/main/cli2eli-schema.json",
"tool": "cli-devcontainer",
"cwd": "git-root",
"commands": [
{
"name": "build",
"command": "devcontainer build",
"arguments": [
{
"name": "--workspace-folder",
"choices": ["."]
},
{
"name": "--no-cache=$$",
"description": "Builds the image with `--no-cache`.",
"choices": [false, true]
}
]
},
{
"name": "delete container",
"description": "select a devcontainer, stop and delete it and create a new one.",
"command": "docker",
"arguments": [
{
"name": "stop",
"type": "dynamic-select",
"command": "docker ps | grep -v CONTAINER",
"prompt": "Select a container: ",
"transform": "awk '{print $1}'"
}
],
"chain-call": "remove container",
"chain-pass": true
},
{
"name": "remove container",
"command": "docker",
"arguments": [
{
"name": "rm",
"type": "dynamic-select",
"command": "docker ps",
"prompt": "Select a container: ",
"transform": "awk '{print $1}'"
}
]
}
]
}- execute
cli-devcontailer-build, you will be asked to choice value for option--workspace-folderand option--no-cache - execute
cli-devcontainer-delete-container, you will be asked to select a container from the return result ofdocker ps | -v CONTAINER, the selected line will be passed toawk '{print $1}'and then executedocker stop <value>, after the execution, another interactive functioncli-devcontainer-remove-containerwill be invoked to delete the container.
CLI2ELI is released under the MIT License. Feel free to use, modify, and distribute it as per the license terms.


