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
-
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.
-
Configurable prefix? Default ! but some tools use \! or shell. A shellEscapePrefix(String) setting could allow customization.
-
Fire CommandExecutionListener? Would be useful for audit logging. The process exit code could be mapped to CommandResult (0 = SUCCESS, non-zero = FAILURE).
-
History integration? !ls would naturally appear in readline history. No special handling needed.
-
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
Summary
Support executing native OS commands from an aesh REPL using the
!prefix, similar tovim,mysql,psql, andgdb.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(\! commandorsystem command)gdb(shell commandor!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)
Interception point
In
ReadlineConsole.processLine(), at the very top — before sub-command mode handling or command registry lookup:Execution
Use
ProcessBuilderwith shell delegation for full shell syntax support (globbing, pipes, env vars):new ProcessBuilder("sh", "-c", command)new ProcessBuilder("cmd", "/c", command)For local terminals,
inheritIO()connects the process directly to the terminal. For remote connections (SSH, WebSocket), pipe stdout/stderr throughConnection.write().Scope
Security considerations
!doesn't grant additional privilegesCommandExecutionListeneris registeredImplementation details
Files to change
Settings.java/SettingsImpl.javaenableShellEscape()setting (default false)SettingsBuilder.javaenableShellEscape(boolean)builder methodAeshConsoleRunner.javaenableShellEscape(boolean)convenience builder methodReadlineConsole.java!prefix check inprocessLine(), addexecuteNativeCommand()methodOpen questions
inheritIO()vs piping throughConnection?inheritIO()is simpler and supports interactive programs (likeless,vim), but only works for local terminals. Piping throughConnection.write()supports remote connections but loses interactivity.Configurable prefix? Default
!but some tools use\!orshell. AshellEscapePrefix(String)setting could allow customization.Fire
CommandExecutionListener? Would be useful for audit logging. The process exit code could be mapped toCommandResult(0 = SUCCESS, non-zero = FAILURE).History integration?
!lswould naturally appear in readline history. No special handling needed.Windows support? Auto-detect platform:
System.getProperty("os.name")to choose betweensh -candcmd /c.Examples
User session: