Provide pty info

- Enhance ShellContext for pty info
- Enhance CommandContext to include ShellContext
- Fixes #985
This commit is contained in:
Janne Valkealahti
2024-03-10 17:45:29 +00:00
parent 90e032d6ce
commit 831f2cf54b
14 changed files with 217 additions and 26 deletions

View File

@@ -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);
}
}

View File

@@ -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() {

View File

@@ -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<CommandExceptionResolver> commandExceptionResolvers = commandRegistration.get().getExceptionResolvers();

View File

@@ -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<CommandParserResult> find(String name) {
return results.results().stream()
.filter(r -> {

View File

@@ -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<? extends HandlerMethodArgumentResolver> 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<? extends HandlerMethodArgumentResolver> 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<? extends HandlerMethodArgumentResolver> 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<? extends HandlerMethodArgumentResolver> resolvers;
private Validator validator;
private Terminal terminal;
private ShellContext shellContext;
private ConversionService conversionService;
private CommandCatalog commandCatalog;
public DefaultCommandExecution(List<? extends HandlerMethodArgumentResolver> 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;

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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<HandlerMethodArgumentResolver> 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

View File

@@ -47,7 +47,7 @@ public class CommandExecutionTests extends AbstractCommandTests {
List<HandlerMethodArgumentResolver> 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

View File

@@ -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[]

View File

@@ -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]
----

View File

@@ -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[]

View File

@@ -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[]
}
}

View File

@@ -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();
}
}
}