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: "
}
]
}
]
}
- 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
- 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.
eat will be used if it is installed, otherwise fallback to built-in term
, for displaying the command output buffer and start process asynchronously, becasuse it is blazingly fast.
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 build
Manually 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": "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 test
cli-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",
"china-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-folder
and 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-container
will 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.