Advanced Topics
This section covers advanced Æsh features including piping, redirection, aliases, testing, GraalVM native images, and custom command invocation providers.
Piping and Redirection
Æsh supports Unix-style command operators for piping output between commands and redirecting to files.
Supported Operators
| Operator | Symbol | Description |
|---|---|---|
| Pipe | | | Pass output of one command to input of another |
| Redirect Out | > | Write output to file (overwrite) |
| Redirect Append | >> | Append output to file |
| Redirect In | < | Read input from file |
| AND | && | Execute next command if current succeeds |
| OR | || | Execute next command if current fails |
| Sequence | ; | Execute next command unconditionally |
Enabling Operators
Operators are enabled by default. To configure:
Settings settings = SettingsBuilder.builder()
.enableOperatorParser(true) // Enable operator parsing
.setPipe(true) // Enable pipe operator
.setRedirection(true) // Enable redirection operators
.build();Implementing Pipeable Commands
Commands that receive piped input should read from getInputLine():
@CommandDefinition(name = "grep", description = "Filter lines matching pattern")
public class GrepCommand implements Command<CommandInvocation> {
@Argument(description = "Pattern to match")
private String pattern;
@Override
public CommandResult execute(CommandInvocation invocation) throws InterruptedException {
String line;
while ((line = invocation.getInputLine()) != null) {
if (line.contains(pattern)) {
invocation.println(line);
}
}
return CommandResult.SUCCESS;
}
}Example: Command Pipeline
# List files, filter for .java, count lines
ls | grep ".java" | wc -l
# Process file and save output
cat data.txt | transform | sort > output.txt
# Conditional execution
build && test && deploy
# Execute regardless of result
cleanup ; exitChecking the Current Operator
Commands can check which operator is in effect:
@Override
public CommandResult execute(CommandInvocation invocation) {
Operator operator = invocation.getOperator();
if (operator == Operator.PIPE) {
// Output will be piped to another command
// Avoid formatting, output raw data
} else {
// Output is going to terminal
// Use formatting and colors
}
return CommandResult.SUCCESS;
}Command Aliases
Aliases allow users to create shortcuts for frequently used commands.
Enabling Aliases
Settings settings = SettingsBuilder.builder()
.enableAlias(true)
.aliasFile(new File(System.getProperty("user.home"), ".myapp_aliases"))
.persistAlias(true)
.build();Built-in Alias Commands
When aliases are enabled, the following commands become available:
# Create an alias
alias ll='ls -l'
# List all aliases
alias
# Remove an alias
unalias llAdding the Alias Command
Use the extensions library to add alias support:
import org.aesh.extensions.common.AeshAlias;
AeshConsoleRunner.builder()
.command(MyCommand.class)
.command(AeshAlias.class)
.settings(SettingsBuilder.builder()
.enableAlias(true)
.build())
.start();Programmatic Alias Management
// Access alias manager through settings
AliasManager aliasManager = settings.getAliasManager();
// Add alias
aliasManager.addAlias("ll", "ls -l");
// Get alias value
String value = aliasManager.getAlias("ll");
// Remove alias
aliasManager.removeAlias("ll");
// List all aliases
Map<String, String> aliases = aliasManager.getAliases();Testing Commands
Æsh provides excellent support for testing commands using AeshRuntimeRunner.
Basic Command Testing
import org.aesh.AeshRuntimeRunner;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CommandTest {
@Test
void testGreetCommand() {
AeshRuntimeRunner runner = AeshRuntimeRunner.builder()
.command(GreetCommand.class)
.build();
String output = runner.execute("greet --name Alice");
assertTrue(output.contains("Hello, Alice!"));
}
@Test
void testCommandWithDefaultValue() {
AeshRuntimeRunner runner = AeshRuntimeRunner.builder()
.command(GreetCommand.class)
.build();
String output = runner.execute("greet");
assertTrue(output.contains("Hello, World!"));
}
@Test
void testRequiredOptionMissing() {
AeshRuntimeRunner runner = AeshRuntimeRunner.builder()
.command(RequiredOptionCommand.class)
.build();
String output = runner.execute("mycommand");
assertTrue(output.contains("required"));
}
}Testing with Multiple Commands
@Test
void testCommandInteraction() {
AeshRuntimeRunner runner = AeshRuntimeRunner.builder()
.command(SetCommand.class)
.command(GetCommand.class)
.build();
runner.execute("set key1 value1");
String output = runner.execute("get key1");
assertEquals("value1", output.trim());
}Testing Error Handling
@Test
void testInvalidInput() {
AeshRuntimeRunner runner = AeshRuntimeRunner.builder()
.command(CalculateCommand.class)
.build();
String output = runner.execute("calculate --operation divide 10 0");
assertTrue(output.contains("Error: Division by zero"));
}Testing with Custom Configuration
@Test
void testWithCustomSettings() {
Settings settings = SettingsBuilder.builder()
.enableAlias(true)
.build();
AeshRuntimeRunner runner = AeshRuntimeRunner.builder()
.command(MyCommand.class)
.settings(settings)
.build();
// Test with aliases enabled
String output = runner.execute("mycommand");
// ...
}Parameterized Tests
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
class ParameterizedCommandTest {
@ParameterizedTest
@CsvSource({
"add, 5 3, 8",
"subtract, 10 4, 6",
"multiply, 6 7, 42",
"divide, 20 4, 5"
})
void testCalculatorOperations(String operation, String args, String expected) {
AeshRuntimeRunner runner = AeshRuntimeRunner.builder()
.command(CalculateCommand.class)
.build();
String output = runner.execute("calculate -o " + operation + " " + args);
assertTrue(output.contains(expected));
}
}GraalVM Native Image
Æsh applications can be compiled to native executables using GraalVM, providing fast startup and low memory footprint.
Adding GraalVM Support
Add the GraalVM native-image Maven plugin:
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.10.1</version>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<mainClass>com.example.MyApp</mainClass>
<imageName>myapp</imageName>
<buildArgs>
<buildArg>--no-fallback</buildArg>
</buildArgs>
</configuration>
</plugin>Reflection Configuration
Æsh uses reflection for command discovery. Create a reflection configuration file at src/main/resources/META-INF/native-image/reflect-config.json:
[
{
"name": "com.example.MyCommand",
"allDeclaredConstructors": true,
"allDeclaredFields": true,
"allDeclaredMethods": true
},
{
"name": "com.example.AnotherCommand",
"allDeclaredConstructors": true,
"allDeclaredFields": true,
"allDeclaredMethods": true
}
]Resource Configuration
For resources like history or alias files:
{
"resources": {
"includes": [
{"pattern": ".*\\.properties$"},
{"pattern": "META-INF/.*"}
]
}
}Building Native Image
# With Maven
mvn package -Pnative
# Or directly with GraalVM
native-image -jar target/myapp.jar myappRuntime-Only Commands
For applications that use AeshRuntimeRunner:
public class NativeApp {
public static void main(String[] args) {
AeshRuntimeRunner.builder()
.command(MyCommand.class)
.args(args)
.execute();
}
}This pattern works well with native images as it doesn’t require terminal interaction setup.
Working Examples
See the native-runtime example for a complete GraalVM native image setup.
Custom Command Invocation
For dependency injection and custom context, create a custom CommandInvocation implementation.
Defining Custom CommandInvocation
public interface MyCommandInvocation extends CommandInvocation {
// Add custom methods
DatabaseConnection getDatabase();
UserSession getCurrentUser();
ConfigurationService getConfig();
}Implementing the Provider
import org.aesh.command.invocation.CommandInvocationProvider;
import org.aesh.command.invocation.CommandInvocationBuilder;
public class MyCommandInvocationProvider
implements CommandInvocationProvider<MyCommandInvocation> {
private final DatabaseConnection database;
private final UserSession session;
private final ConfigurationService config;
public MyCommandInvocationProvider(
DatabaseConnection database,
UserSession session,
ConfigurationService config) {
this.database = database;
this.session = session;
this.config = config;
}
@Override
public MyCommandInvocation enhanceCommandInvocation(
CommandInvocationBuilder<MyCommandInvocation> builder) {
return new MyCommandInvocationImpl(
builder.shell(),
builder.helpInfo(),
database,
session,
config
);
}
}Command Invocation Implementation
public class MyCommandInvocationImpl implements MyCommandInvocation {
private final Shell shell;
private final String helpInfo;
private final DatabaseConnection database;
private final UserSession session;
private final ConfigurationService config;
public MyCommandInvocationImpl(
Shell shell,
String helpInfo,
DatabaseConnection database,
UserSession session,
ConfigurationService config) {
this.shell = shell;
this.helpInfo = helpInfo;
this.database = database;
this.session = session;
this.config = config;
}
// Implement CommandInvocation methods
@Override
public void print(String msg) {
shell.write(msg);
}
@Override
public void println(String msg) {
shell.writeln(msg);
}
@Override
public Shell getShell() {
return shell;
}
@Override
public void stop() {
// Stop the console
}
@Override
public String getHelpInfo() {
return helpInfo;
}
// Implement custom methods
@Override
public DatabaseConnection getDatabase() {
return database;
}
@Override
public UserSession getCurrentUser() {
return session;
}
@Override
public ConfigurationService getConfig() {
return config;
}
}Using Custom Invocation in Commands
@CommandDefinition(name = "query", description = "Execute database query")
public class QueryCommand implements Command<MyCommandInvocation> {
@Argument(description = "SQL query")
private String query;
@Override
public CommandResult execute(MyCommandInvocation invocation) {
// Access injected dependencies
DatabaseConnection db = invocation.getDatabase();
UserSession user = invocation.getCurrentUser();
// Check permissions
if (!user.hasPermission("query.execute")) {
invocation.println("Permission denied");
return CommandResult.FAILURE;
}
// Execute query
try {
ResultSet results = db.executeQuery(query);
printResults(results, invocation);
return CommandResult.SUCCESS;
} catch (SQLException e) {
invocation.println("Query error: " + e.getMessage());
return CommandResult.FAILURE;
}
}
}Configuring the Provider
DatabaseConnection db = new DatabaseConnection("jdbc:...");
UserSession session = new UserSession("admin");
ConfigurationService config = new ConfigurationService();
MyCommandInvocationProvider provider = new MyCommandInvocationProvider(db, session, config);
Settings settings = SettingsBuilder.builder()
.commandInvocationProvider(provider)
.build();
AeshConsoleRunner.builder()
.settings(settings)
.command(QueryCommand.class)
.start();Environment Variables
Æsh supports environment variable export and substitution.
Enabling Exports
Settings settings = SettingsBuilder.builder()
.enableExport(true)
.exportFile(new File(System.getProperty("user.home"), ".myapp_exports"))
.build();Using Export Command
# Set an environment variable
export MY_VAR=value
# Use in commands
echo $MY_VAR
# List exports
export
# Remove export
unset MY_VARAccessing Exports Programmatically
@Override
public CommandResult execute(CommandInvocation invocation) {
// Access exported variables through configuration
CommandInvocationConfiguration config = invocation.getConfiguration();
// Environment-aware processing
// ...
return CommandResult.SUCCESS;
}Input Validation
Command-Level Validation
import org.aesh.command.validator.CommandValidator;
import org.aesh.command.validator.CommandValidatorException;
public class CopyCommandValidator implements CommandValidator<CopyCommand, CommandInvocation> {
@Override
public void validate(CopyCommand command) throws CommandValidatorException {
if (command.source == null || command.destination == null) {
throw new CommandValidatorException("Source and destination are required");
}
if (command.source.equals(command.destination)) {
throw new CommandValidatorException("Source and destination cannot be the same");
}
if (!new File(command.source).exists()) {
throw new CommandValidatorException("Source file does not exist: " + command.source);
}
}
}
@CommandDefinition(
name = "copy",
description = "Copy files",
validator = CopyCommandValidator.class
)
public class CopyCommand implements Command<CommandInvocation> {
@Argument(description = "Source file")
String source;
@Argument(description = "Destination")
String destination;
@Override
public CommandResult execute(CommandInvocation invocation) {
// Validation has already passed
// Safe to use source and destination
return CommandResult.SUCCESS;
}
}Option-Level Validation
import org.aesh.command.validator.OptionValidator;
import org.aesh.command.validator.OptionValidatorException;
public class PortValidator implements OptionValidator<Integer> {
@Override
public void validate(Integer port) throws OptionValidatorException {
if (port < 1 || port > 65535) {
throw new OptionValidatorException("Port must be between 1 and 65535");
}
if (port < 1024) {
throw new OptionValidatorException("Ports below 1024 require root privileges");
}
}
}
@CommandDefinition(name = "serve", description = "Start server")
public class ServeCommand implements Command<CommandInvocation> {
@Option(shortName = 'p', validator = PortValidator.class, description = "Port number")
private int port = 8080;
@Override
public CommandResult execute(CommandInvocation invocation) {
invocation.println("Starting server on port " + port);
return CommandResult.SUCCESS;
}
}Man Page Generation
Æsh can generate Unix-style man pages from your command definitions.
Adding Man Page Support
@CommandDefinition(
name = "myapp",
description = "My application description",
generateHelp = true
)
public class MyCommand implements Command<CommandInvocation> {
@Option(
shortName = 'v',
hasValue = false,
description = "Enable verbose output. When enabled, the command will print detailed " +
"information about each step of the process."
)
private boolean verbose;
@Option(
shortName = 'o',
description = "Output file path. The file will be created if it doesn't exist."
)
private String output;
@Argument(description = "Input files to process. Multiple files can be specified.")
private List<File> files;
}Programmatic Man Page Generation
import org.aesh.command.man.AeshManGenerator;
// Generate man page content
String manPage = AeshManGenerator.generateManPage(MyCommand.class);
// Write to file
Files.writeString(Path.of("myapp.1"), manPage);Best Practices Summary
Enable piping for composable commands - Make commands work well together.
Use AeshRuntimeRunner for testing - Write comprehensive tests for all commands.
Plan for native images early - Structure code to work with GraalVM restrictions.
Use custom CommandInvocation for DI - Inject dependencies cleanly.
Validate thoroughly - Use both option and command validators.
Document commands well - Detailed descriptions generate better help and man pages.
Support both interactive and non-interactive modes - Enable automation and scripting.