-
Notifications
You must be signed in to change notification settings - Fork 1
Shell Interpreter
JNode's line-oriented command interpreters: tokenizing,
&&/||/;command sequences, pipelines, redirection, and the sharedescapeWordround-trip.
JNode's shell has three independent interpreters. CommandShell picks one at startup and exposes it through the CommandInterpreter extension point:
-
DefaultInterpreter— single commands only. No pipelines, no redirection. -
RedirectingInterpreter extends DefaultInterpreter— adds pipelines (|) and file redirection (<,>). This is the shell's initial interpreter. -
BjorneInterpreter— a Bourne-compatible script interpreter with its own tokenizer, AST, andListCommandNode; it has supported&&/||/;since long before the Java interpreters did.
// shell/src/shell/org/jnode/shell/CommandShell.java:114-116
public static final String INITIAL_INTERPRETER = "redirecting";
public static final String FALLBACK_INTERPRETER = "default";DefaultInterpreter and RedirectingInterpreter share a single tokenizer, which lives as a nested class inside DefaultInterpreter. That is why a change to the tokenizer's operator rules immediately changes pipeline behavior too, even though the two interpreters have separate parsers.
This page covers the two Java interpreters. The argument-level parsing that runs after an interpreter has produced a CommandLine is a separate subsystem documented in Syntax and Shell-Syntax.
| Class / File | Role |
|---|---|
shell/src/shell/org/jnode/shell/CommandShell.java |
Interactive loop; INITIAL_INTERPRETER = "redirecting", FALLBACK_INTERPRETER = "default"
|
shell/src/shell/org/jnode/shell/DefaultInterpreter.java |
Tokenizer, parseSequence, interpret, escapeWord, completion (parsePartial) |
shell/src/shell/org/jnode/shell/RedirectingInterpreter.java |
Two-level grammar: sequence of pipelines, each a list of commands with </>
|
shell/src/shell/org/jnode/shell/BjorneInterpreter.java |
Independent Bourne-compatible script interpreter |
shell/src/shell/org/jnode/shell/syntax/SyntaxManager.java |
Validates the CommandLine against the command's declared syntax (see Syntax) |
shell/src/test/org/jnode/test/shell/syntax/DefaultInterpreterSequenceTest.java |
19 tests pinning sequence semantics, exit codes, and error propagation |
shell/src/test/org/jnode/test/shell/syntax/DefaultTokenizerTest.java |
20+ tests pinning token types for operator/quote/escape combinations |
RedirectingInterpreter parses a two-level grammar; DefaultInterpreter implements only the outer level.
line := sequence EOF
sequence:= pipeline ( ('&&' | '||' | ';') pipeline )*
pipeline:= command ( '|' command )* # RedirectingInterpreter only
command := name arg* ( '<' file | '>' file )*
Operators are left-associative by construction: both interpreters produce a flat List<...Entry>, not a precedence tree, and the evaluator walks it in order. There is no operator precedence between &&, || and ;.
DefaultInterpreter explicitly does not treat | as an operator — a bare | is a literal character in that interpreter.
// DefaultInterpreter.java:81-89
public static final char AMP_CHAR = '&';
public static final char SEMI_CHAR = ';';
public static final String AND_IF = "&&";
public static final String OR_IF = "||";
public static final String SEMI = ";";
public static final String AMP = "&";// DefaultInterpreter.java:323-331
protected static class SequenceEntry {
public final CommandLine commandLine;
public final String operatorBefore; // null for the first command
}RedirectingInterpreter uses a parallel shape because its iteration unit is a pipeline, not a command:
// RedirectingInterpreter.java:535-546
private static class SequenceDescriptor {
List<CommandDescriptor> pipeline;
String operatorBefore;
}DefaultInterpreter.interpret (DefaultInterpreter.java:194-233):
if (sequence.isEmpty()) return 0;
if (sequence.size() == 1) {
return shell.invoke(sequence.get(0).commandLine, null, null); // exceptions propagate
}
int rc = 0;
for (int i = 0; i < sequence.size(); i++) {
SequenceEntry entry = sequence.get(i);
if (i > 0) {
String op = entry.operatorBefore;
if (AND_IF.equals(op) && rc != 0) continue; // skip
else if (OR_IF.equals(op) && rc == 0) continue; // skip
// ';' always executes
}
try {
rc = shell.invoke(entry.commandLine, null, null);
} catch (ShellControlException ex) {
throw ex; // always rethrown
} catch (ShellException ex) {
shell.diagnose(ex, null);
rc = 1;
}
}
return rc;Key semantics:
| Rule | Behavior |
|---|---|
| Single command | Short-circuits to a bare shell.invoke, so ShellException (unknown command, script abort, test-harness trapException expectations) still propagates |
| Multiple commands | Per-command ShellException is swallowed → shell.diagnose() + rc = 1; the sequence continues |
ShellControlException |
Abstract subclass of ShellException; always rethrown even mid-sequence (used by TimeCommand and script abort) |
| Skipped command | Does not reset rc, so false && true yields 1 and true || false yields 0 (POSIX-correct) |
| Return value | The exit code of the last executed command, not the last parsed |
$? |
Not available. The default interpreters only return rc up the call stack; there is no lastReturnCode equivalent to BjorneContext.getLastReturnCode()
|
RedirectingInterpreter.interpret has the identical shape but delegates each iteration to runSequence(shell, seq.pipeline), where runSequence chooses runCommand for a single command and runPipeline for two or more.
DefaultInterpreter.Tokenizer.tokenize (DefaultInterpreter.java:499-683) produces tokens of type LITERAL=0, STRING=1, CLOSED=2, SPECIAL=4. REDIRECTS_FLAG = 0x01 switches | from literal to operator.
| Input | Tokenizing | Note |
|---|---|---|
a && b, a || b, a ; b
|
&&/||/; as SPECIAL
|
recognized regardless of REDIRECTS_FLAG
|
a|b (no flag) |
one LITERAL a|b
|
escaping beats the || operator rule |
'a&&b', "c||d", 'e;f'
|
STRING token |
quoting makes operator chars literal |
a\&\&b, a||b, a\;b
|
one LITERAL
|
backslash escape |
a | b (no flag) |
| is LITERAL
|
only RedirectingInterpreter sets the flag |
a | b (with flag) |
| is SPECIAL
|
pipeline |
http://x/?a=1&b=2 |
one LITERAL
|
embedded single & stays literal (URL compatibility) |
echo a & echo b |
& is SPECIAL
|
standalone, so the parser can diagnose it |
a &&& b |
&& SPECIAL, then & SPECIAL
|
parser error |
a && b # c && d |
comment truncates the line | tokenizer still emits a zero-length trailing LITERAL
|
The tokenizer is a nested class of DefaultInterpreter, so RedirectingInterpreter reaches it as DefaultInterpreter.Tokenizer and constructs it with REDIRECTS_FLAG.
CommandShell.escapeWord(String) must invert tokenization so that a command's output can be fed back into the shell. Since &, ; and | can now introduce operators, all three are escaped unconditionally:
// DefaultInterpreter.java:371-379 (escapeWord(String, boolean))
case AMP_CHAR:
case SEMI_CHAR:
case PIPE_CHAR:
// '&', ';' and '|' can introduce operators ...
sb.append(ESCAPE_CHAR).append(ch);
break;Previously | was only escaped when escapeRedirects == true, which is the path used by RedirectingInterpreter.escapeWord at RedirectingInterpreter.java:166-168. DefaultInterpreter.escapeWord(String) delegates to escapeWord(word, false) (:334-336).
Callers: CommandShell.escapeWord (:960), CommandCompletions (:79), BjorneContext (:1066), and the black-box test harness harness/CommandTestRunner (:44, :46). DefaultInterpreterSequenceTest.testEscapeWordRoundTrip (:334-345) pins the round-trip.
All raised as ShellSyntaxException:
| Message | Raised when |
|---|---|
Misplaced '<op>': expected a command name |
&&/||/; appears where a command name was expected |
no command after '<op>' |
sequence operator at end of line |
unsupported '&': use '&&' for conditional execution |
standalone & reaches the parser |
unrecognized symbol: '<text>' |
any other SPECIAL token |
no command after '|' |
trailing pipe (RedirectingInterpreter only) |
no filename after '<'/'>' |
redirection with no target (RedirectingInterpreter only) |
misplaced '<tok>' |
special token in an illegal position (RedirectingInterpreter only) |
empty '<special>' file name |
empty redirection target (RedirectingInterpreter only) |
Errors are suppressed while the parser runs in completing mode (tab completion): a trailing operator yields an empty CommandLine rather than an exception.
DefaultInterpreter.parsePartial (DefaultInterpreter.java:147-172) has to double-tokenize because parseSequence consumes the whole line:
Tokenizer tokenizer = new Tokenizer(line);
List<SequenceEntry> sequence = parseSequence(tokenizer, true);
if (sequence.isEmpty()) return new CommandLine("", null);
tokenizer.seek(0); // re-scan from the beginning
CommandLine.Token last = null;
while (tokenizer.hasNext()) {
CommandLine.Token t = tokenizer.next();
if (t.text.length() > 0) last = t; // skip the empty token a #comment leaves behind
}
if (last != null && last.tokenType == SPECIAL
&& (AND_IF.equals(last.text) || OR_IF.equals(last.text) || SEMI.equals(last.text))) {
return new CommandLine("", null); // completing after a trailing operator
}
CommandLine res = sequence.get(sequence.size() - 1).commandLine;
return res == null ? new CommandLine("", null) : res;This requires SymbolSource.seek(int) (DefaultInterpreter.java:708-713). RedirectingInterpreter.parsePartial (:124-129) accumulates SequenceDescriptors instead, and getLastCommand(List<SequenceDescriptor>) (:155-163) walks backwards skipping empty pipelines — it is also used by help().
-
|is a literal inDefaultInterpreter. A bare|only becomesSPECIALwhen theREDIRECTS_FLAGis set, which onlyRedirectingInterpreterdoes. -
Embedded
&stays literal for URL compatibility. Only a standalone&(token buffer empty) is special. This is deliberate and covered byDefaultTokenizerTest.testTokenizerUrlWithAmp. -
&is recognized but rejected. The tokenizer must surface it asSPECIALsointerpretcan produce a helpful message instead of silently treating it as a literal. -
No operator precedence.
a || b && cevaluates strictly left to right, not asa || (b && c). -
A single command still throws. The
sequence.size() == 1short-circuit is load-bearing for the black-box test harness and for script abort; removing it changes observable behavior oftestSingleUnknownCommandRethrowsandtestSingleCommandFailurePropagates. -
setArgumentAnticipatedis only set on the last command of a sequence (DefaultInterpreter.java:313,RedirectingInterpreter.java:288-289). Earlier entries never get it, so "argument expected" completion state is only meaningful for the final command. -
A trailing
#commentleaks an empty argument. The comment token is zero-length but still lands intokenList, so the last command of a sequence gains an empty-string argument.parsePartialworks around it;interpretdoes not. Pre-existing quirk, preserved. -
Sequence operators are recognized even without
REDIRECTS_FLAG. Only|is flag-gated. -
Testability requires subclasses.
parseSequence,Tokenizerand the entry types areprotected/private, soDefaultInterpreterSequenceTestdrives them throughTestableDefault,TestableRedirectingand aStubShell extends TestShell. -
No
$?, no&&in Bjorne-parsed$(( ))expressions. The arithmetic evaluator (BJorne-Arithmetic-Evaluator) has its own tokenizer and is unaffected.
- Shell-Commands — Parent hub for the command framework, invokers, and built-in commands
-
Syntax — Argument-level parsing (
SyntaxManager,MuParser,Argumenttypes) that runs afterinterpret - Shell-Syntax — Declarative argument syntax framework and tab-completion model
- BJorne-Arithmetic-Evaluator — The independent Bourne-compatible interpreter's expression evaluator
- JNode-Serial-Skill — Driving the shell over the serial agent console; notes which operators the interpreter supports
-
Testing — The two host-JVM test tiers: JUnit
AllTests.javaand black-box XML specs viaTestHarness