diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ShellContextAutoConfiguration.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ShellContextAutoConfiguration.java index 8f58da80..28b04fd4 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ShellContextAutoConfiguration.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ShellContextAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,9 @@ */ package org.springframework.shell.boot; +import org.jline.terminal.Terminal; +import org.jline.terminal.spi.TerminalExt; + import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.shell.context.DefaultShellContext; @@ -24,7 +27,11 @@ import org.springframework.shell.context.ShellContext; public class ShellContextAutoConfiguration { @Bean - public ShellContext shellContext() { - return new DefaultShellContext(); + public ShellContext shellContext(Terminal terminal) { + boolean pty = false; + if (terminal instanceof TerminalExt ext) { + pty = ext.getSystemStream() != null; + } + return new DefaultShellContext(pty); } } diff --git a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/CommandCatalogAutoConfigurationTests.java b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/CommandCatalogAutoConfigurationTests.java index 61a05d2b..5386b805 100644 --- a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/CommandCatalogAutoConfigurationTests.java +++ b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/CommandCatalogAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,8 @@ import static org.assertj.core.api.Assertions.assertThat; public class CommandCatalogAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(CommandCatalogAutoConfiguration.class, ShellContextAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(CommandCatalogAutoConfiguration.class, + JLineShellAutoConfiguration.class, ShellContextAutoConfiguration.class)); @Test void defaultCommandCatalog() { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/Shell.java b/spring-shell-core/src/main/java/org/springframework/shell/Shell.java index d5c8ca63..c5c867cf 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/Shell.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/Shell.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2017-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -238,7 +238,7 @@ public class Shell { CommandExecution execution = CommandExecution.of( argumentResolvers != null ? argumentResolvers.getResolvers() : null, validator, terminal, - conversionService, commandRegistry); + shellContext, conversionService, commandRegistry); List commandExceptionResolvers = commandRegistration.get().getExceptionResolvers(); diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandContext.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandContext.java index bb3679d2..17daa5a3 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandContext.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.jline.terminal.Terminal; import org.springframework.shell.command.CommandParser.CommandParserResult; import org.springframework.shell.command.CommandParser.CommandParserResults; +import org.springframework.shell.context.ShellContext; import org.springframework.util.ObjectUtils; /** @@ -77,6 +78,13 @@ public interface CommandContext { */ Terminal getTerminal(); + /** + * Gets a {@link ShellContext} associated with {@link CommandContext}. + * + * @return a shell context + */ + ShellContext getShellContext(); + /** * Gets an instance of a default {@link CommandContext}. * @@ -87,8 +95,8 @@ public interface CommandContext { * @return a command context */ static CommandContext of(String[] args, CommandParserResults results, Terminal terminal, - CommandRegistration commandRegistration) { - return new DefaultCommandContext(args, results, terminal, commandRegistration); + CommandRegistration commandRegistration, ShellContext shellContext) { + return new DefaultCommandContext(args, results, terminal, commandRegistration, shellContext); } /** @@ -100,13 +108,15 @@ public interface CommandContext { private final CommandParserResults results; private final Terminal terminal; private final CommandRegistration commandRegistration; + private final ShellContext shellContext; DefaultCommandContext(String[] args, CommandParserResults results, Terminal terminal, - CommandRegistration commandRegistration) { + CommandRegistration commandRegistration, ShellContext shellContext) { this.args = args; this.results = results; this.terminal = terminal; this.commandRegistration = commandRegistration; + this.shellContext = shellContext; } @Override @@ -144,6 +154,11 @@ public interface CommandContext { return terminal; } + @Override + public ShellContext getShellContext() { + return shellContext; + } + private Optional find(String name) { return results.results().stream() .filter(r -> { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java index 09d2fa02..34bd7aa2 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,7 @@ import org.springframework.shell.command.CommandRegistration.TargetInfo.TargetTy import org.springframework.shell.command.invocation.InvocableShellMethod; import org.springframework.shell.command.invocation.ShellMethodArgumentResolverComposite; import org.springframework.shell.command.parser.ParserConfig; +import org.springframework.shell.context.ShellContext; import org.springframework.util.ObjectUtils; /** @@ -65,7 +66,7 @@ public interface CommandExecution { * @return default command execution */ public static CommandExecution of(List resolvers) { - return new DefaultCommandExecution(resolvers, null, null, null, null); + return new DefaultCommandExecution(resolvers, null, null, null, null, null); } /** @@ -78,8 +79,8 @@ public interface CommandExecution { * @return default command execution */ public static CommandExecution of(List resolvers, Validator validator, - Terminal terminal, ConversionService conversionService) { - return new DefaultCommandExecution(resolvers, validator, terminal, conversionService, null); + Terminal terminal, ShellContext shellContext, ConversionService conversionService) { + return new DefaultCommandExecution(resolvers, validator, terminal, shellContext, conversionService, null); } /** @@ -92,8 +93,8 @@ public interface CommandExecution { * @return default command execution */ public static CommandExecution of(List resolvers, Validator validator, - Terminal terminal, ConversionService conversionService, CommandCatalog commandCatalog) { - return new DefaultCommandExecution(resolvers, validator, terminal, conversionService, commandCatalog); + Terminal terminal, ShellContext shellContext, ConversionService conversionService, CommandCatalog commandCatalog) { + return new DefaultCommandExecution(resolvers, validator, terminal, shellContext, conversionService, commandCatalog); } /** @@ -104,14 +105,16 @@ public interface CommandExecution { private List resolvers; private Validator validator; private Terminal terminal; + private ShellContext shellContext; private ConversionService conversionService; private CommandCatalog commandCatalog; public DefaultCommandExecution(List resolvers, Validator validator, - Terminal terminal, ConversionService conversionService, CommandCatalog commandCatalog) { + Terminal terminal, ShellContext shellContext, ConversionService conversionService, CommandCatalog commandCatalog) { this.resolvers = resolvers; this.validator = validator; this.terminal = terminal; + this.shellContext = shellContext; this.conversionService = conversionService; this.commandCatalog = commandCatalog; } @@ -175,7 +178,7 @@ public interface CommandExecution { throw new CommandParserExceptionsException("Command parser resulted errors", results.errors()); } - CommandContext ctx = CommandContext.of(args, results, terminal, usedRegistration); + CommandContext ctx = CommandContext.of(args, results, terminal, usedRegistration, shellContext); Object res = null; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/context/DefaultShellContext.java b/spring-shell-core/src/main/java/org/springframework/shell/context/DefaultShellContext.java index 0a47f935..d92e6abc 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/context/DefaultShellContext.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/context/DefaultShellContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,15 @@ import org.springframework.util.Assert; public class DefaultShellContext implements ShellContext { private InteractionMode interactionMode = InteractionMode.ALL; + private final boolean pty; + + public DefaultShellContext() { + this(false); + } + + public DefaultShellContext(boolean pty) { + this.pty = pty; + } @Override public InteractionMode getInteractionMode() { @@ -36,4 +45,9 @@ public class DefaultShellContext implements ShellContext { Assert.notNull(interactionMode, "mode cannot be null"); this.interactionMode = interactionMode; } + + @Override + public boolean hasPty() { + return this.pty; + } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/context/ShellContext.java b/spring-shell-core/src/main/java/org/springframework/shell/context/ShellContext.java index bad55da6..6ea25ad4 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/context/ShellContext.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/context/ShellContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,4 +36,13 @@ public interface ShellContext { * @param interactionMode the interaction mode */ void setInteractionMode(InteractionMode interactionMode); + + /** + * Gets if shell has a proper {@code pty} terminal. Terminal don't have + * {@code pty} in cases where output is piped into a file or terminal is run in + * an ci system where there is no real user interaction. + * + * @return {@code true} if terminal has pty features + */ + boolean hasPty(); } diff --git a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionCustomConversionTests.java b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionCustomConversionTests.java index 4eb8b2f0..64a2edd2 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionCustomConversionTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionCustomConversionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ public class CommandExecutionCustomConversionTests { List resolvers = new ArrayList<>(); resolvers.add(new ArgumentHeaderMethodArgumentResolver(conversionService, null)); resolvers.add(new CommandContextMethodArgumentResolver()); - execution = CommandExecution.of(resolvers, null, null, conversionService, commandCatalog); + execution = CommandExecution.of(resolvers, null, null, null, conversionService, commandCatalog); } @Test diff --git a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionTests.java b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionTests.java index abec3a2e..76951b66 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionTests.java @@ -47,7 +47,7 @@ public class CommandExecutionTests extends AbstractCommandTests { List resolvers = new ArrayList<>(); resolvers.add(new ArgumentHeaderMethodArgumentResolver(conversionService, null)); resolvers.add(new CommandContextMethodArgumentResolver()); - execution = CommandExecution.of(resolvers, null, null, conversionService, commandCatalog); + execution = CommandExecution.of(resolvers, null, null, null, conversionService, commandCatalog); } @Test diff --git a/spring-shell-docs/modules/ROOT/nav.adoc b/spring-shell-docs/modules/ROOT/nav.adoc index 714eacf4..e649dafe 100644 --- a/spring-shell-docs/modules/ROOT/nav.adoc +++ b/spring-shell-docs/modules/ROOT/nav.adoc @@ -87,6 +87,7 @@ *** xref:appendices/techintro/registration.adoc[] *** xref:appendices/techintro/parser.adoc[] *** xref:appendices/techintro/execution.adoc[] +*** xref:appendices/techintro/shellcontext.adoc[] *** xref:appendices/techintro/commandcontext.adoc[] *** xref:appendices/techintro/commandcatalog.adoc[] *** xref:appendices/techintro/theming.adoc[] diff --git a/spring-shell-docs/modules/ROOT/pages/appendices/techintro/shellcontext.adoc b/spring-shell-docs/modules/ROOT/pages/appendices/techintro/shellcontext.adoc new file mode 100644 index 00000000..0d7b771d --- /dev/null +++ b/spring-shell-docs/modules/ROOT/pages/appendices/techintro/shellcontext.adoc @@ -0,0 +1,25 @@ +[[shell-context]] += Shell Context +:page-section-summary-toc: 1 + +ifndef::snippets[:snippets: ../../../../../src/test/java/org/springframework/shell/docs] + +The `ShellContext` interface gives access to a currently running +shell context. You can use it to get access to a generic shell options. + +_InteractionMode_ is telling if shell is running on a _non-interactive_ or +_interactive_ mode. + +[source, java, indent=0] +---- +include::{snippets}/ShellContextSnippets.java[tag=interactionMode] +---- + +_Pty_ is telling if current terminal has a pty features which not a case +when output is piped into file or if terminal is run in a ci system. + +[source, java, indent=0] +---- +include::{snippets}/ShellContextSnippets.java[tag=hasPty] +---- + diff --git a/spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandContextSnippets.java b/spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandContextSnippets.java index 4589b5e6..f88583f6 100644 --- a/spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandContextSnippets.java +++ b/spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandContextSnippets.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ import org.springframework.shell.command.CommandContext; @SuppressWarnings("unused") public class CommandContextSnippets { - CommandContext ctx = CommandContext.of(null, null, null, null); + CommandContext ctx = CommandContext.of(null, null, null, null, null); void dump1() { // tag::snippet1[] diff --git a/spring-shell-docs/src/test/java/org/springframework/shell/docs/ShellContextSnippets.java b/spring-shell-docs/src/test/java/org/springframework/shell/docs/ShellContextSnippets.java new file mode 100644 index 00000000..20a7de4c --- /dev/null +++ b/spring-shell-docs/src/test/java/org/springframework/shell/docs/ShellContextSnippets.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024 the original author or authors. + * + * 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 + * + * https://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.springframework.shell.docs; + +import org.springframework.shell.context.InteractionMode; +import org.springframework.shell.context.ShellContext; + +@SuppressWarnings("unused") +class ShellContextSnippets { + + ShellContext ctx = new ShellContext() { + + @Override + public InteractionMode getInteractionMode() { + return null; + } + + @Override + public void setInteractionMode(InteractionMode interactionMode) { + } + + @Override + public boolean hasPty() { + return false; + } + + }; + + void dump1() { + // tag::hasPty[] + boolean hasPty = ctx.hasPty(); + // end::hasPty[] + } + + void dump2() { + // tag::interactionMode[] + InteractionMode interactionMode = ctx.getInteractionMode(); + // end::interactionMode[] + } + +} diff --git a/spring-shell-samples/spring-shell-sample-e2e/src/main/java/org/springframework/shell/samples/e2e/StdOutTypeCommands.java b/spring-shell-samples/spring-shell-sample-e2e/src/main/java/org/springframework/shell/samples/e2e/StdOutTypeCommands.java new file mode 100644 index 00000000..9275260c --- /dev/null +++ b/spring-shell-samples/spring-shell-sample-e2e/src/main/java/org/springframework/shell/samples/e2e/StdOutTypeCommands.java @@ -0,0 +1,62 @@ +/* + * Copyright 2024 the original author or authors. + * + * 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 + * + * https://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.springframework.shell.samples.e2e; + +import org.jline.terminal.Terminal; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.shell.command.CommandContext; +import org.springframework.shell.command.CommandRegistration; +import org.springframework.shell.command.annotation.Command; +import org.springframework.stereotype.Component; + +public class StdOutTypeCommands { + + @Command(command = BaseE2ECommands.ANNO, group = BaseE2ECommands.GROUP) + public static class Annotation extends BaseE2ECommands { + + @Autowired + Terminal terminal; + + @Command(command = "stdout") + public String testStdoutAnnotation( + CommandContext commandContext + ) { + boolean hasPty = commandContext.getShellContext().hasPty(); + return String.format("hasPty='%s'", hasPty); + } + } + + @Component + public static class Registration extends BaseE2ECommands { + + @Bean + public CommandRegistration testStdoutRegistration() { + return getBuilder() + .command(REG, "stdout") + .group(GROUP) + .withTarget() + .function(ctx -> { + boolean hasPty = ctx.getShellContext().hasPty(); + return String.format("hasPty='%s'", hasPty); + }) + .and() + .build(); + } + } + +}