From eb5641b16b12683ad4351029024f698cc493f04b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:10:57 +0000 Subject: [PATCH 1/4] Initial plan From b70dd09303bfc294b33ab6db0771abac59699893 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:19:42 +0000 Subject: [PATCH 2/4] Add support for environment variables in @Option defaultValue Co-authored-by: stalep <49780+stalep@users.noreply.github.com> --- .../impl/internal/ProcessedOptionBuilder.java | 45 ++- .../EnvironmentVariableDefaultValueTest.java | 264 ++++++++++++++++++ 2 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 aesh/src/test/java/org/aesh/command/populator/EnvironmentVariableDefaultValueTest.java diff --git a/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java b/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java index bb7834881..1dc77e143 100644 --- a/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java +++ b/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java @@ -342,8 +342,51 @@ else if(hasMultipleValues) //if(renderer == null) // renderer = new NullOptionRenderer(); + // Resolve environment variables in default values + List resolvedDefaultValues = resolveEnvironmentVariables(defaultValues); + return new ProcessedOption(shortName, name, description, argument, required, - valueSeparator, askIfNotSet, selectorType, defaultValues, type, fieldName, optionType, converter, + valueSeparator, askIfNotSet, selectorType, resolvedDefaultValues, type, fieldName, optionType, converter, completer, validator, activator, renderer, parser, overrideRequired); } + + /** + * Resolves environment variables in default values. + * If a default value matches the pattern $(ENV_VAR_NAME), it will be replaced + * with the value of the environment variable. + * + * @param values the list of default values + * @return a new list with environment variables resolved + */ + private List resolveEnvironmentVariables(List values) { + List resolved = new ArrayList<>(); + for (String value : values) { + resolved.add(resolveEnvironmentVariable(value)); + } + return resolved; + } + + /** + * Resolves a single environment variable reference. + * If the value matches the pattern $(ENV_VAR_NAME), it will be replaced + * with the value of the environment variable. If the environment variable + * is not set, the original value is returned. + * + * @param value the value to resolve + * @return the resolved value + */ + private String resolveEnvironmentVariable(String value) { + if (value == null) { + return null; + } + // Check if the value matches the pattern $(ENV_VAR_NAME) + if (value.startsWith("$(") && value.endsWith(")")) { + String envVarName = value.substring(2, value.length() - 1); + String envValue = System.getenv(envVarName); + if (envValue != null) { + return envValue; + } + } + return value; + } } diff --git a/aesh/src/test/java/org/aesh/command/populator/EnvironmentVariableDefaultValueTest.java b/aesh/src/test/java/org/aesh/command/populator/EnvironmentVariableDefaultValueTest.java new file mode 100644 index 000000000..2b1ba459b --- /dev/null +++ b/aesh/src/test/java/org/aesh/command/populator/EnvironmentVariableDefaultValueTest.java @@ -0,0 +1,264 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.aesh.command.populator; + +import org.aesh.command.Command; +import org.aesh.command.CommandDefinition; +import org.aesh.command.CommandException; +import org.aesh.command.CommandResult; +import org.aesh.command.impl.container.AeshCommandContainerBuilder; +import org.aesh.command.impl.internal.ProcessedCommandBuilder; +import org.aesh.command.impl.internal.ProcessedOptionBuilder; +import org.aesh.command.impl.parser.CommandLineParser; +import org.aesh.command.impl.parser.CommandLineParserBuilder; +import org.aesh.command.invocation.CommandInvocation; +import org.aesh.command.invocation.InvocationProviders; +import org.aesh.command.option.Option; +import org.aesh.command.impl.activator.AeshCommandActivatorProvider; +import org.aesh.command.impl.activator.AeshOptionActivatorProvider; +import org.aesh.command.impl.completer.AeshCompleterInvocationProvider; +import org.aesh.command.impl.converter.AeshConverterInvocationProvider; +import org.aesh.command.impl.invocation.AeshInvocationProviders; +import org.aesh.command.impl.populator.AeshCommandPopulator; +import org.aesh.command.impl.validator.AeshValidatorInvocationProvider; +import org.aesh.command.settings.SettingsBuilder; +import org.aesh.console.AeshContext; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Test for environment variable resolution in @Option defaultValue. + */ +public class EnvironmentVariableDefaultValueTest { + + private final InvocationProviders invocationProviders = new AeshInvocationProviders( + SettingsBuilder.builder() + .converterInvocationProvider(new AeshConverterInvocationProvider()) + .completerInvocationProvider(new AeshCompleterInvocationProvider()) + .validatorInvocationProvider(new AeshValidatorInvocationProvider()) + .optionActivatorProvider(new AeshOptionActivatorProvider()) + .commandActivatorProvider(new AeshCommandActivatorProvider()).build()); + + /** + * Test that environment variable syntax $(ENV_VAR) in defaultValue is resolved. + * This test uses the PATH environment variable which should be available in most environments. + */ + @Test + public void testEnvironmentVariableResolution() throws Exception { + // Use PATH which should be set in most environments + String pathValue = System.getenv("PATH"); + assertNotNull("PATH environment variable should be set", pathValue); + + TestPopulatorWithEnvVar command = new TestPopulatorWithEnvVar(); + ProcessedCommandBuilder, CommandInvocation> commandBuilder = ProcessedCommandBuilder.builder() + .name("test") + .populator(new AeshCommandPopulator<>(command)) + .description("a simple test"); + + commandBuilder.addOption(ProcessedOptionBuilder.builder() + .name("path") + .description("test path option") + .fieldName("pathOption") + .type(String.class) + .addDefaultValue("$(PATH)") + .build()); + + CommandLineParser parser = CommandLineParserBuilder.builder() + .processedCommand(commandBuilder.create()) + .create(); + + AeshContext aeshContext = SettingsBuilder.builder().build().aeshContext(); + + // Parse without specifying the option, so the default value should be used + parser.parse("test"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, CommandLineParser.Mode.VALIDATE); + + // The default value should be the value of the PATH environment variable + assertEquals(pathValue, command.pathOption); + } + + /** + * Test that when the environment variable doesn't exist, the original value is kept. + */ + @Test + public void testNonExistentEnvironmentVariable() throws Exception { + TestPopulatorWithEnvVar command = new TestPopulatorWithEnvVar(); + ProcessedCommandBuilder, CommandInvocation> commandBuilder = ProcessedCommandBuilder.builder() + .name("test") + .populator(new AeshCommandPopulator<>(command)) + .description("a simple test"); + + commandBuilder.addOption(ProcessedOptionBuilder.builder() + .name("myvar") + .description("test option") + .fieldName("myVarOption") + .type(String.class) + .addDefaultValue("$(NON_EXISTENT_VAR_12345)") + .build()); + + CommandLineParser parser = CommandLineParserBuilder.builder() + .processedCommand(commandBuilder.create()) + .create(); + + AeshContext aeshContext = SettingsBuilder.builder().build().aeshContext(); + + // Parse without specifying the option, so the default value should be used + parser.parse("test"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, CommandLineParser.Mode.VALIDATE); + + // The default value should remain unchanged since the env var doesn't exist + assertEquals("$(NON_EXISTENT_VAR_12345)", command.myVarOption); + } + + /** + * Test that regular default values (not environment variables) are not affected. + */ + @Test + public void testRegularDefaultValue() throws Exception { + TestPopulatorWithEnvVar command = new TestPopulatorWithEnvVar(); + ProcessedCommandBuilder, CommandInvocation> commandBuilder = ProcessedCommandBuilder.builder() + .name("test") + .populator(new AeshCommandPopulator<>(command)) + .description("a simple test"); + + commandBuilder.addOption(ProcessedOptionBuilder.builder() + .name("regular") + .description("test option") + .fieldName("regularOption") + .type(String.class) + .addDefaultValue("regularValue") + .build()); + + CommandLineParser parser = CommandLineParserBuilder.builder() + .processedCommand(commandBuilder.create()) + .create(); + + AeshContext aeshContext = SettingsBuilder.builder().build().aeshContext(); + + // Parse without specifying the option, so the default value should be used + parser.parse("test"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, CommandLineParser.Mode.VALIDATE); + + // The default value should remain unchanged + assertEquals("regularValue", command.regularOption); + } + + /** + * Test with HOME environment variable which is also commonly available. + */ + @Test + public void testHomeEnvironmentVariable() throws Exception { + String homeValue = System.getenv("HOME"); + // Skip test if HOME is not set + if (homeValue == null) { + return; + } + + TestPopulatorWithEnvVar command = new TestPopulatorWithEnvVar(); + ProcessedCommandBuilder, CommandInvocation> commandBuilder = ProcessedCommandBuilder.builder() + .name("test") + .populator(new AeshCommandPopulator<>(command)) + .description("a simple test"); + + commandBuilder.addOption(ProcessedOptionBuilder.builder() + .name("home") + .description("test home option") + .fieldName("homeOption") + .type(String.class) + .addDefaultValue("$(HOME)") + .build()); + + CommandLineParser parser = CommandLineParserBuilder.builder() + .processedCommand(commandBuilder.create()) + .create(); + + AeshContext aeshContext = SettingsBuilder.builder().build().aeshContext(); + + parser.parse("test"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, CommandLineParser.Mode.VALIDATE); + + assertEquals(homeValue, command.homeOption); + } + + /** + * Simple command class for testing with environment variable default values. + */ + public static class TestPopulatorWithEnvVar implements Command { + public String pathOption; + public String myVarOption; + public String regularOption; + public String homeOption; + + @Override + public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { + return CommandResult.SUCCESS; + } + } + + /** + * Test environment variable resolution using the @Option annotation. + * This test uses the USER environment variable which should be available in most Unix environments. + */ + @Test + public void testAnnotationBasedEnvironmentVariable() throws Exception { + String userValue = System.getenv("USER"); + // Skip test if USER is not set + if (userValue == null) { + return; + } + + CommandLineParser parser = new AeshCommandContainerBuilder<>() + .create(new AnnotatedEnvVarCommand()) + .getParser(); + + AnnotatedEnvVarCommand command = (AnnotatedEnvVarCommand) parser.getCommand(); + AeshContext aeshContext = SettingsBuilder.builder().build().aeshContext(); + + // Parse without specifying the option, so the default value should be used + parser.parse("envtest"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), + invocationProviders, aeshContext, CommandLineParser.Mode.VALIDATE); + + // The default value should be the value of the USER environment variable + assertEquals(userValue, command.getUsername()); + } + + /** + * Command class using @Option annotation with environment variable default value. + */ + @CommandDefinition(name = "envtest", description = "a test for environment variables") + public static class AnnotatedEnvVarCommand implements Command { + + @Option(defaultValue = "$(USER)") + private String username; + + public String getUsername() { + return username; + } + + @Override + public CommandResult execute(CommandInvocation commandInvocation) + throws CommandException, InterruptedException { + return CommandResult.SUCCESS; + } + } +} From 65374c6be55652e58c20a3c2ae2ac66159897808 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:22:47 +0000 Subject: [PATCH 3/4] Fix code review feedback: handle null values and empty env var syntax Co-authored-by: stalep <49780+stalep@users.noreply.github.com> --- .../impl/internal/ProcessedOptionBuilder.java | 8 +++-- .../EnvironmentVariableDefaultValueTest.java | 34 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java b/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java index 1dc77e143..8fe7bde02 100644 --- a/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java +++ b/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java @@ -356,9 +356,12 @@ else if(hasMultipleValues) * with the value of the environment variable. * * @param values the list of default values - * @return a new list with environment variables resolved + * @return a new list with environment variables resolved, or the original list if null */ private List resolveEnvironmentVariables(List values) { + if (values == null) { + return null; + } List resolved = new ArrayList<>(); for (String value : values) { resolved.add(resolveEnvironmentVariable(value)); @@ -380,7 +383,8 @@ private String resolveEnvironmentVariable(String value) { return null; } // Check if the value matches the pattern $(ENV_VAR_NAME) - if (value.startsWith("$(") && value.endsWith(")")) { + // Minimum length is 4 for "$(X)" where X is at least one character + if (value.length() > 3 && value.startsWith("$(") && value.endsWith(")")) { String envVarName = value.substring(2, value.length() - 1); String envValue = System.getenv(envVarName); if (envValue != null) { diff --git a/aesh/src/test/java/org/aesh/command/populator/EnvironmentVariableDefaultValueTest.java b/aesh/src/test/java/org/aesh/command/populator/EnvironmentVariableDefaultValueTest.java index 2b1ba459b..3de0f7fc1 100644 --- a/aesh/src/test/java/org/aesh/command/populator/EnvironmentVariableDefaultValueTest.java +++ b/aesh/src/test/java/org/aesh/command/populator/EnvironmentVariableDefaultValueTest.java @@ -129,6 +129,39 @@ public void testNonExistentEnvironmentVariable() throws Exception { assertEquals("$(NON_EXISTENT_VAR_12345)", command.myVarOption); } + /** + * Test that empty environment variable syntax $() is not resolved. + */ + @Test + public void testEmptyEnvironmentVariableSyntax() throws Exception { + TestPopulatorWithEnvVar command = new TestPopulatorWithEnvVar(); + ProcessedCommandBuilder, CommandInvocation> commandBuilder = ProcessedCommandBuilder.builder() + .name("test") + .populator(new AeshCommandPopulator<>(command)) + .description("a simple test"); + + commandBuilder.addOption(ProcessedOptionBuilder.builder() + .name("empty") + .description("test option") + .fieldName("emptyOption") + .type(String.class) + .addDefaultValue("$()") + .build()); + + CommandLineParser parser = CommandLineParserBuilder.builder() + .processedCommand(commandBuilder.create()) + .create(); + + AeshContext aeshContext = SettingsBuilder.builder().build().aeshContext(); + + // Parse without specifying the option, so the default value should be used + parser.parse("test"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, CommandLineParser.Mode.VALIDATE); + + // The default value should remain unchanged since $() is not a valid env var syntax + assertEquals("$()", command.emptyOption); + } + /** * Test that regular default values (not environment variables) are not affected. */ @@ -207,6 +240,7 @@ public static class TestPopulatorWithEnvVar implements Command Date: Sat, 29 Nov 2025 12:50:34 +0000 Subject: [PATCH 4/4] Move env var resolution from ProcessedOptionBuilder to ProcessedOption Co-authored-by: stalep <49780+stalep@users.noreply.github.com> --- .../impl/internal/ProcessedOption.java | 31 +++++++++++- .../impl/internal/ProcessedOptionBuilder.java | 49 +------------------ 2 files changed, 30 insertions(+), 50 deletions(-) diff --git a/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOption.java b/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOption.java index 7011ba164..32fc89461 100644 --- a/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOption.java +++ b/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOption.java @@ -392,7 +392,7 @@ public void injectValueIntoField(Object instance, InvocationProviders invocation if(getValue() != null) field.set(instance, doConvert(getValue(), invocationProviders, instance, aeshContext, doValidation)); else if(defaultValues.size() > 0) { - field.set(instance, doConvert(defaultValues.get(0), invocationProviders, instance, aeshContext, doValidation)); + field.set(instance, doConvert(resolveEnvironmentVariable(defaultValues.get(0)), invocationProviders, instance, aeshContext, doValidation)); } } else if(optionType == OptionType.LIST || optionType == OptionType.ARGUMENTS) { @@ -403,7 +403,7 @@ else if(optionType == OptionType.LIST || optionType == OptionType.ARGUMENTS) { } else if(defaultValues.size() > 0) { for(String in : defaultValues) - tmpSet.add(doConvert(in, invocationProviders, instance, aeshContext, doValidation)); + tmpSet.add(doConvert(resolveEnvironmentVariable(in), invocationProviders, instance, aeshContext, doValidation)); } field.set(instance, tmpSet); @@ -512,6 +512,33 @@ boolean isTypeAssignableByResourcesOrFile() { Path.class.isAssignableFrom(type)); } + /** + * Resolves a single environment variable reference. + * If the value matches the pattern $(ENV_VAR_NAME), it will be replaced + * with the value of the environment variable. If the environment variable + * is not set, the original value is returned. + * + * @param value the value to resolve + * @return the resolved value + */ + private String resolveEnvironmentVariable(String value) { + if (value == null) { + return null; + } + // Check if the value matches the pattern $(ENV_VAR_NAME) + // Minimum length is 4 for "$(X)" where X is at least one character + if (value.length() > 3 && value.startsWith("$(") && value.endsWith(")")) { + String envVarName = value.substring(2, value.length() - 1); + if (!envVarName.isEmpty()) { + String envValue = System.getenv(envVarName); + if (envValue != null) { + return envValue; + } + } + } + return value; + } + @Override public String toString() { return "ProcessedOption{" + diff --git a/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java b/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java index 8fe7bde02..bb7834881 100644 --- a/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java +++ b/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java @@ -342,55 +342,8 @@ else if(hasMultipleValues) //if(renderer == null) // renderer = new NullOptionRenderer(); - // Resolve environment variables in default values - List resolvedDefaultValues = resolveEnvironmentVariables(defaultValues); - return new ProcessedOption(shortName, name, description, argument, required, - valueSeparator, askIfNotSet, selectorType, resolvedDefaultValues, type, fieldName, optionType, converter, + valueSeparator, askIfNotSet, selectorType, defaultValues, type, fieldName, optionType, converter, completer, validator, activator, renderer, parser, overrideRequired); } - - /** - * Resolves environment variables in default values. - * If a default value matches the pattern $(ENV_VAR_NAME), it will be replaced - * with the value of the environment variable. - * - * @param values the list of default values - * @return a new list with environment variables resolved, or the original list if null - */ - private List resolveEnvironmentVariables(List values) { - if (values == null) { - return null; - } - List resolved = new ArrayList<>(); - for (String value : values) { - resolved.add(resolveEnvironmentVariable(value)); - } - return resolved; - } - - /** - * Resolves a single environment variable reference. - * If the value matches the pattern $(ENV_VAR_NAME), it will be replaced - * with the value of the environment variable. If the environment variable - * is not set, the original value is returned. - * - * @param value the value to resolve - * @return the resolved value - */ - private String resolveEnvironmentVariable(String value) { - if (value == null) { - return null; - } - // Check if the value matches the pattern $(ENV_VAR_NAME) - // Minimum length is 4 for "$(X)" where X is at least one character - if (value.length() > 3 && value.startsWith("$(") && value.endsWith(")")) { - String envVarName = value.substring(2, value.length() - 1); - String envValue = System.getenv(envVarName); - if (envValue != null) { - return envValue; - } - } - return value; - } }