Skip to content

Support native OS command execution via ! prefix in REPL #566

Description

@stalep

Summary

Support executing native OS commands from an aesh REPL using the ! prefix, similar to vim, mysql, psql, and gdb.

[myshell]$ !ls -la
total 42
drwxr-xr-x  5 user user 4096 Aug  4 14:00 .
...
[myshell]$ !grep -r "TODO" src/
src/Main.java:// TODO: fix this
[myshell]$ 

Motivation

Many interactive CLI tools built with aesh would benefit from letting users run quick OS commands without leaving the REPL. This is a well-established UX pattern in:

  • vim / neovim (:!command)
  • mysql / psql (\! command or system command)
  • gdb (shell command or !command)
  • less (!command)

Without this, users must suspend/exit the REPL, run their command, then re-enter — breaking flow.

Proposed design

Settings (opt-in, disabled by default)

AeshConsoleRunner.builder()
    .enableShellEscape(true)
    .command(MyCommand.class)
    .start();

// Or via SettingsBuilder
SettingsBuilder.builder()
    .enableShellEscape(true)
    .build();

Interception point

In ReadlineConsole.processLine(), at the very top — before sub-command mode handling or command registry lookup:

if (settings.enableShellEscape() && line.trim().startsWith("!")) {
    String nativeCmd = line.trim().substring(1).trim();
    if (!nativeCmd.isEmpty()) {
        executeNativeCommand(nativeCmd, conn);
    }
    read(conn, readline);
    return;
}

Execution

Use ProcessBuilder with shell delegation for full shell syntax support (globbing, pipes, env vars):

  • Unix/macOS: new ProcessBuilder("sh", "-c", command)
  • Windows: new ProcessBuilder("cmd", "/c", command)

For local terminals, inheritIO() connects the process directly to the terminal. For remote connections (SSH, WebSocket), pipe stdout/stderr through Connection.write().

Scope

  • AeshConsoleRunner / ReadlineConsole only — interactive REPL mode
  • Not AeshRuntimeRunner — non-interactive single-command execution doesn't need shell escape

Security considerations

  • Disabled by default — applications must explicitly opt in
  • Local REPL context — the user already has shell access, so ! doesn't grant additional privileges
  • For remote/server CLIs (SSH, WebSocket), applications should leave shell escape disabled or implement an allowlist via a custom handler
  • Native command executions should be logged if CommandExecutionListener is registered

Implementation details

Files to change

File Change
Settings.java / SettingsImpl.java Add enableShellEscape() setting (default false)
SettingsBuilder.java Add enableShellEscape(boolean) builder method
AeshConsoleRunner.java Add enableShellEscape(boolean) convenience builder method
ReadlineConsole.java Add ! prefix check in processLine(), add executeNativeCommand() method

Open questions

  1. inheritIO() vs piping through Connection? inheritIO() is simpler and supports interactive programs (like less, vim), but only works for local terminals. Piping through Connection.write() supports remote connections but loses interactivity.

  2. Configurable prefix? Default ! but some tools use \! or shell. A shellEscapePrefix(String) setting could allow customization.

  3. Fire CommandExecutionListener? Would be useful for audit logging. The process exit code could be mapped to CommandResult (0 = SUCCESS, non-zero = FAILURE).

  4. History integration? !ls would naturally appear in readline history. No special handling needed.

  5. Windows support? Auto-detect platform: System.getProperty("os.name") to choose between sh -c and cmd /c.

Examples

// Simple REPL with shell escape
AeshConsoleRunner.builder()
    .command(MyCommand.class)
    .enableShellEscape(true)
    .addExitCommand()
    .start();

User session:

[myshell]$ help
Available commands: deploy, status, logs

[myshell]$ !docker ps
CONTAINER ID   IMAGE     STATUS
abc123         myapp     Up 2 hours

[myshell]$ deploy --env staging
Deployed to staging.

[myshell]$ !tail -f /var/log/myapp.log
... (Ctrl-C to return to REPL)

[myshell]$ exit

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions