Skip to content

feat: add command aliasing via PATH lookup #3834

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion cmd/limactl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
package main

import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
Expand Down Expand Up @@ -40,7 +42,8 @@ func main() {
}
}
}
if err := newApp().Execute(); err != nil {
rootCmd := newApp()
if err := executeWithPluginSupport(rootCmd, os.Args[1:]); err != nil {
handleExitCoder(err)
logrus.Fatal(err)
}
Expand Down Expand Up @@ -215,6 +218,67 @@ func handleExitCoder(err error) {
}
}

// executeWithPluginSupport handles command execution with plugin support.
func executeWithPluginSupport(rootCmd *cobra.Command, args []string) error {
if len(args) > 0 {
cmd, _, err := rootCmd.Find(args)
if err != nil || cmd == rootCmd {
// Function calls os.Exit() if it found and executed the plugin
runExternalPlugin(rootCmd.Context(), args[0], args[1:])
}
}

rootCmd.SetArgs(args)
return rootCmd.Execute()
}

func runExternalPlugin(ctx context.Context, name string, args []string) {
if ctx == nil {
ctx = context.Background()
}

if err := updatePathEnv(); err != nil {
logrus.Warnf("failed to update PATH environment: %v", err)
// PATH update failure shouldn't prevent plugin execution
}

externalCmd := "limactl-" + name
execPath, err := exec.LookPath(externalCmd)
if err != nil {
return
}

logrus.Debugf("found external command: %s", execPath)

cmd := exec.CommandContext(ctx, execPath, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()

handleExitCoder(cmd.Run())
os.Exit(0) //nolint:revive // it's intentional to call os.Exit in this function
}

func updatePathEnv() error {
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}

binDir := filepath.Dir(exe)
currentPath := os.Getenv("PATH")
newPath := binDir + string(filepath.ListSeparator) + currentPath

if err := os.Setenv("PATH", newPath); err != nil {
return fmt.Errorf("failed to set PATH environment: %w", err)
}

logrus.Debugf("updated PATH to prioritize %s", binDir)

return nil
}

// WrapArgsError annotates cobra args error with some context, so the error message is more user-friendly.
func WrapArgsError(argFn cobra.PositionalArgs) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
Expand Down
55 changes: 55 additions & 0 deletions website/content/en/docs/usage/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,61 @@ Then you can connect directly:
ssh lima-default
```

### Command Aliasing (Plugin System)

Lima supports a plugin-like command aliasing system similar to `git`, `kubectl`, and `docker`. When you run a `limactl` command that doesn't exist, Lima will automatically look for an external program named `limactl-<command>` in your system's PATH.

#### Creating Custom Aliases

To create a custom alias, create an executable script with the name `limactl-<alias>` and place it somewhere in your PATH.

**Example: Creating a `ps` alias for listing instances**

1. Create a script called `limactl-ps`:
```bash
#!/bin/sh
# Show instances in a compact format
limactl list --format table "$@"
```

2. Make it executable and place it in your PATH:
```bash
chmod +x limactl-ps
sudo mv limactl-ps /usr/local/bin/
```

3. Now you can use it:
```bash
limactl ps # Shows instances in table format
limactl ps --quiet # Shows only instance names
```

**Example: Creating an `sh` alias**

```bash
#!/bin/sh
# limactl-sh - Connect to an instance shell
limactl shell "$@"
```

After creating this alias:
```bash
limactl sh default # Equivalent to: limactl shell default
limactl sh myinstance bash # Equivalent to: limactl shell myinstance bash
```

#### How It Works

1. When you run `limactl <unknown-command>`, Lima first tries to find a built-in command
2. If no built-in command is found, Lima searches for `limactl-<unknown-command>` in your PATH
3. If found, Lima executes the external program and passes all remaining arguments to it
4. If not found, Lima shows the standard "unknown command" error

This system allows you to:
- Create personal shortcuts and aliases
- Extend Lima's functionality without modifying the core application
- Share custom commands with your team by distributing scripts

### Shell completion
- To enable bash completion, add `source <(limactl completion bash)` to `~/.bash_profile`.
- To enable zsh completion, see `limactl completion zsh --help`
Loading