Appendix A: Techical Introduction
This appendix contains information for developers and others who would like to know more about how Spring Shell works internally and what its design decisions are.
A.1. Command Registration
Defining a command registration is a first step to introducing the structure of a command and its options and parameters. This is loosely decoupled from what happens later, such as parsing command-line input and running actual target code. Essentially, it is the definition of a command API that is shown to a user.
A.1.1. Commands
A command in a spring-shell
structure is defined as an array of commands. This yields a
structure similar to the following example:
command1 sub1
command2 sub1 subsub1
command2 sub2 subsub1
command2 sub2 subsub2
We do not currently support mapping commands to an explicit parent if sub-commands are defined.
For example, command1 sub1 and command1 sub1 subsub1 cannot both be registered.
|
A.1.2. Interaction Mode
Spring Shell has been designed to work on two modes: interactive (which essentially
is a REPL
where you have an active shell instance throughout a series of commands) and
non-interactive (where commands are executed one by one from a command line).
Differentation between these modes is mostly around limitations about what can be done in each mode. For example, it would not be feasible to show what was a previous stacktrace of a command if the shell is no longer active. Generally, whether the shell is still active dictates the available information.
Also, being on an active REPL
session may provide more information about what the user has been
doing within an active session.
A.1.3. Options
Options can be defined as long and short, where the prefixing is --
and -
, respectively.
The following examples show long and short options:
CommandRegistration.builder()
.withOption()
.longNames("myopt")
.and()
.build();
CommandRegistration.builder()
.withOption()
.shortNames('s')
.and()
.build();
A.1.4. Target
The target defines the execution target of a command. It can be a method in a POJO,
a Consumer
, or a Function
.
Method
Using a Method
in an existing POJO is one way to define a target.
Consider the following class:
public static class CommandPojo {
String command(String arg) {
return arg;
}
}
Given the existing class shown in the preceding listing, you can then register its method:
CommandPojo pojo = new CommandPojo();
CommandRegistration.builder()
.command("command")
.withTarget()
.method(pojo, "command")
.and()
.withOption()
.longNames("arg")
.and()
.build();
Function
Using a Function
as a target gives a lot of flexibility to handle what
happens in a command execution, because you can handle many things manually by using
a CommandContext
given to a Function
. The return type from a Function
is
then what gets printed into the shell as a result. Consider the following example:
CommandRegistration.builder()
.command("command")
.withTarget()
.function(ctx -> {
String arg = ctx.getOptionValue("arg");
return String.format("hi, arg value is '%s'", arg);
})
.and()
.withOption()
.longNames("arg")
.and()
.build();
Consumer
Using a Consumer
is basically the same as using a Function
, with the difference being
that there is no return type. If you need to print something into a shell,
you can get a reference to a Terminal
from a context and print something
through it. Consider the following example:
CommandRegistration.builder()
.command("command")
.withTarget()
.consumer(ctx -> {
String arg = ctx.getOptionValue("arg");
ctx.getTerminal().writer()
.println(String.format("hi, arg value is '%s'", arg));
})
.and()
.withOption()
.longNames("arg")
.and()
.build();
A.2. Command Parser
Before a command can be executed, we need to parse the command and whatever options the user may have provided. Parsing comes between command registration and command execution.
A.3. Command Execution
When command parsing has done its job and command registration has been resolved, command execution does the hard work of running the code.
A.4. Command Context
The CommandContext
interface gives access to a currently running
context. You can use it to get access to options:
String arg = ctx.getOptionValue("arg");
If you need to print something into a shell, you can get a Terminal
and use its writer to print something:
ctx.getTerminal().writer().println("hi");
A.5. Command Catalog
The CommandCatalog
interface defines how command registrations exist in
a shell application. It is possible to dynamically register and de-register
commands, which gives flexibility for use cases where possible commands
come and go, depending on a shell’s state. Consider the following example:
CommandRegistration registration = CommandRegistration.builder().build();
catalog.register(registration);
A.5.1. Command Resolver
You can implement the CommandResolver
interface and define a bean to dynamically
resolve mappings from a command’s name to its CommandRegistration
instances. Consider
the following example:
static class CustomCommandResolver implements CommandResolver {
List<CommandRegistration> registrations = new ArrayList<>();
CustomCommandResolver() {
CommandRegistration resolved = CommandRegistration.builder()
.command("resolve command")
.build();
registrations.add(resolved);
}
@Override
public List<CommandRegistration> resolve() {
return registrations;
}
}
A current limitation of a CommandResolver is that it is used every time commands are resolved.
Thus, we advise not using it if a command resolution call takes a long time, as it would
make the shell feel sluggish.
|
A.5.2. Command Catalog Customizer
You can use the CommandCatalogCustomizer
interface to customize a CommandCatalog
.
Its main use is to modify a catalog. Also, within spring-shell
auto-configuration, this
interface is used to register existing CommandRegistration
beans into a catalog.
Consider the following example:
static class CustomCommandCatalogCustomizer implements CommandCatalogCustomizer {
@Override
public void customize(CommandCatalog commandCatalog) {
CommandRegistration registration = CommandRegistration.builder()
.command("resolve command")
.build();
commandCatalog.register(registration);
}
}
You can create a CommandCatalogCustomizer
as a bean, and Spring Shell handles the rest.
A.6. Theming
Styling in a theming is provided by a use of a AttributedString from JLine
.
Unfortunately styling in JLine
is mostly undocumented but we try to go through
some of its features here.
In JLine
a style spec is a string having a special format. Spec can be given
multiple times if separated by a comma. A spec will either define a color for
foreground, background or its mode. Special format <spec>:=<spec>
allows to
define a default within latter spec if former for some reason is invalid.
If spec contains a colon its former part indicates either foreground or background
and possible values are foreground
, fg
, f
, background
, bg
, b
, foreground-rgb
,
fg-rgb
, f-rgb
, background-rgb
, bg-rgb
or b-rgb
. Without rbg a color value
is name from an allowable colors black
, red
, green
, yellow
, blue
, magenta
,
cyan
or white
. Colors have their short formats k
, r
, g
, y
, b
, m
, c
and w
respectively. If color is prefixed with either !
or bright-
, bright mode is automatically
applied. Prefixing with ~
will resolve from JLine internal bsd color table.
If rgb format is expected and prefixed with either x
or #
a normal
hex format is used.
fg-red
fg-r
fg-rgb:red
fg-rgb:xff3333
fg-rgb:#ff3333
If spec contains special names default
, bold
, faint
, italic
, underline
, blink
,
inverse
, inverse-neg
, inverseneg
, conceal
, crossed-out
, crossedout
or hidden
a style is changed accordingly with an existing color.
bold
bold,fg:red
If spec is a number or numbers separated with semicolon, format is a plain part of an ansi ascii codes.
31
31;1
JLine special mapping format which would resolve spec starting with dot can’t be used as we don’t yet map those into Spring Shell styling names. |
A.7. Search Algorithms
SearchMatch
is an interface to match text with a pattern. Match
results are in a returned value SearchMatchResult
. Match result
contains info about match positions and overall score of a match.
fzf.
A.7.1. Implementations
FuzzyMatchV2Search
Port of fzf FuzzyMatchV2Search algorithm. Does a fast fuzzy search and is good quickly finding paths.
ExactMatchNaive
Port of fzf ExactMatchNaive algorithm. Simple exact match works more accurately if you know what to search.
A.7.2. SearchMatch
Algorithms and default syntax are hidden inside package protected classes
as we don’t want to fully open these until we know API’s are good to go
for longer support. You need to construct SearchMatch
via its
build-in builder.
SearchMatch searchMatch = SearchMatch.builder()
.caseSensitive(false)
.normalize(false)
.forward(true)
.build();
It’s possible to configure case sensitivity, on what direction search happens or if text should be normilized before search happens. Normalization is handy when different languages have sligh variation for same type of characters.
Search algorithm is selected based on a search syntax shown in below table.
Token | Match type | Description |
---|---|---|
|
fuzzy-match |
Items that match |
|
exact-match |
Items that include |
A.7.3. Examples
SearchMatch searchMatch = SearchMatch.builder()
.caseSensitive(false)
.normalize(false)
.forward(true)
.build();
SearchMatchResult result = searchMatch.match("foo bar baz", "fbb");
result.getStart();
// 0 - start position inclusive
result.getEnd();
// 9 - end position exclusive
result.getPositions();
// 0,4,8 - positions, inclusive
result.getScore();
// 112 - score
result.getAlgorithm();
// FuzzyMatchV2SearchMatchAlgorithm - resolved algo