-
Notifications
You must be signed in to change notification settings - Fork 1
Shell Commands
JNode's command-line environment provides a Bash-like shell experience, implemented entirely in Java, with a custom command framework for parsing arguments and handling I/O redirection.
Unlike Unix, where commands are typically separate executable binaries launched via fork() and exec(), JNode commands are Java classes executed within the same JVM (often within their own Isolates or Proclets).
The shell subsystem handles interactive input, script execution (e.g., jnode.ini), tab-completion, aliasing, and I/O redirection.
Location: shell/src/shell/org/jnode/shell/
The main interactive loop. It reads input from the user (via CommandShellReader), parses it, and executes it using an interpreter.
JNode supports different interpreters.
-
DefaultInterpreter: Executes command sequences (a && b,a || b,a ; b). No pipelines, no redirection; a bare|is a literal character. -
RedirectingInterpreter: ExtendsDefaultInterpreterwith Unix-style pipelines (|) and I/O redirection (>,<). This is the shell's initial interpreter. -
bjorne: A Bourne-compatible shell script interpreter capable of running more complex shell scripts; has its own tokenizer and AST.
See Shell-Interpreter for the tokenizer, sequence grammar, exit-status rules, and escapeWord round-trip.
Decides how the Java class representing the command is run.
-
DefaultCommandInvoker: Runs the command in the current context. -
ThreadCommandInvoker: Runs the command in a newVmThread. -
ProcletCommandInvoker: Runs the command in a new Proclet (a lightweight isolated context sharing the same address space but with separate standard streams and environment variables).
Commands are implemented by extending AbstractCommand (or implementing Command).
public class MyCommand extends AbstractCommand {
private final StringArgument arg = new StringArgument("name", Argument.MANDATORY, "Your name");
public MyCommand() {
super("Prints a greeting");
registerArguments(arg);
}
public void execute() throws Exception {
PrintWriter out = getOutput().getPrintWriter();
out.println("Hello, " + arg.getValue());
}
}JNode commands do not typically parse String[] args manually. Instead, they register Argument objects (e.g., FileArgument, StringArgument, FlagArgument). The shell's syntax parser automatically validates input against these arguments before execute() is called. This also automatically provides --help text and tab-completion.
For detailed information on how the syntax system works, see Syntax.
Commands must be registered to be accessible from the shell. This is done via Plugin Extension Points.
In a plugin's plugin.xml descriptor:
<extension point="org.jnode.shell.aliases">
<alias name="mycmd" class="com.example.MyCommand"/>
</extension>This tells the AliasManager to map the keyword mycmd to the given class. When the user types mycmd, the shell instantiates the class and invokes it.
Many standard Unix-like commands are implemented in shell/src/shell/org/jnode/shell/command/ and cli/src/.
-
File operations:
ls,cp,mv,rm,cat,mkdir -
System info:
free,ps,threads,gc,dmesg -
Network:
ping,ifconfig,route -
Disk:
mount,fdisk,format
cli/src/commands/org/jnode/command/file/MkdirCommand.java (84 lines) supports the POSIX -p / --parents flag:
private final FileArgument argDir;
private final FlagArgument argParents;
// constructor:
argDir = new FileArgument("directory", Argument.MANDATORY, help_dir); // NONEXISTENT was REMOVED
argParents = new FlagArgument("parents", Argument.OPTIONAL, help_parents);execute() semantics:
- With
-p: if the target already exists and is a directory, return silently (idempotent); if it exists and is not a directory, printerr_exists_not_dirandexit(1); otherwisedir.mkdirs(),exit(1)on failure. - Without
-p: unchanged — an existing path prints"%s already exists%n"andexit(1);dir.mkdir()failure printserr_cant_createandexit(1).
Argument.NONEXISTENT was dropped from argDir because that flag only affects tab completion (Argument.isNonExistent() → FileArgument.complete()); with it set, an existing directory could not be completed. It has no runtime validation effect.
Note that mkdir uses only java.io.File.exists()/isDirectory()/mkdir()/mkdirs() — it does not go through the JNode-native FSDirectory.createDirectoryEntry / AbstractFileSystem.createDirectory SPI (see VFS-Layer). java.io.File itself comes from the external classlib.jar.
Descriptor (cli/descriptors/org.jnode.command.file.xml:339-346):
<syntax alias="mkdir">
<sequence description="create a new directory">
<optionSet>
<option argLabel="parents" shortName="p" longName="parents"/>
</optionSet>
<argument argLabel="directory" description="create a new directory"/>
</sequence>
</syntax>ant is exposed as a shell command (cli/descriptors/org.jnode.command.dev.ant.xml) and needs a source-level override of one Apache Ant class.
cli/src/commands/org/apache/tools/ant/taskdefs/ExecuteJava.java (277 lines) is a byte-for-byte copy of Apache Ant 1.6.3's ExecuteJava with a single added line:
// ExecuteJava.java:131
main.setAccessible(true);Everything else is upstream: setJavaCommand, setClasspath, setSystemProperties, setPermissions, setOutput (deprecated no-op), setTimeout, execute(Project), run(), timeoutOccured, killedProcess, fork.
execute(Project) resolves the main class and its main method, then run() invokes it:
main = target.getMethod("main", param); // :121 succeeds
if (main == null) { throw new BuildException(...); } // :122-125 dead code
if ((main.getModifiers() & Modifier.STATIC) == 0) { ... }
main.setAccessible(true); // :131 THE FIX
...
main.invoke(null, argument); // :201A public static void main(String[]) declared in a package-private class passes both getMethod and the STATIC check, but Method.invoke performs the standard caller→declaring-class accessibility check. ExecuteJava lives in a different classloader/runtime-package, so invoke throws IllegalAccessException; run()'s catch (Throwable) swallows it into caught, and execute() rethrows it wrapped in a BuildException. setAccessible(true) suppresses the check.
Regression tests (shell/src/test/org/jnode/test/shell/ExecuteJavaPackagePrivateTest.java) pin both halves: one asserts the IllegalAccessException without setAccessible, the other asserts the side-effect system property is set with it. Fixtures PackagePrivateMain and PublicMain signal via System.setProperty("<FQCN>.ran", "true") rather than stdout. The suite runs on the host JVM (shell/build-tests.xml:62-74, fork="on" haltonfailure="on"), validating standard-JVM reflection semantics, not JNode's VM.
core/descriptors/org.apache.tools.ant.xml was changed in three ways:
<runtime>
<library name="jnode-cli.jar"> <!-- NEW -->
<export name="org.apache.tools.ant.taskdefs.ExecuteJava"/>
</library>
<library name="ant.jar">
...
<exclude name="org.apache.tools.ant.taskdefs.ExecuteJava"/> <!-- NEW -->
</library>
</runtime>
<permission class="java.lang.RuntimePermission" name="accessDeclaredMembers"/> <!-- NEW -->
<permission class="java.lang.reflect.ReflectPermission" name="suppressAccessChecks"/> <!-- NEW -->cli/build.xml:29,56-59 compiles all of cli/src/commands into jnode-cli.jar, so this file is a source-level shadow of the class inside the prebuilt core/lib/ant.jar (which still contains the unpatched 2008-06-27 .class).
Gotcha: <export>/<exclude> are declarative only in this codebase. LibraryModel parses them and Library exposes getExports()/getExcludes(), but nothing consumes them. Real resolution is PluginsClassLoader.findClass (:40-50), which iterates the plugin registry in order and returns the first loader whose containsClass(name) is true, with PluginClassLoaderImpl.findPluginClass (:162-238) checking prerequisite loaders first. The override therefore works because of plugin-list order: all/conf/full-plugin-list.xml:28 includes default-plugin-list.xml, which places the org.jnode.command.* plugins sharing jnode-cli.jar at lines 73-80, well before org.apache.tools.ant at full-plugin-list.xml:46. See Plugin-Descriptor-Schema and Plugin-System.
The two new <permission> elements are exactly what Method.setAccessible(true) needs. They are correctness-by-declaration today, because JNodeSecurityManagerSettings.ENABLED = false (core/src/template/org/jnode/security/JNodeSecurityManagerSettings.java:29) compiles enforcement out. Separately, JNodeSecurityManager.defaultCheckPermission (:41-46) unconditionally forbids setSecurityManager, so the ant descriptors' <permission ... name="setSecurityManager" actions="*"/> can never take effect.
Because all commands run in the same JVM, System.out and System.in cannot be safely redirected globally. Instead, commands use getInput(), getOutput(), and getError() provided by AbstractCommand. The Proclet system manages these streams per-thread-group, allowing CommandA | CommandB to work correctly without polluting the global System.out.
shell/src/test/org/jnode/shell/harness/TestHarness.java (386 lines) is the black-box command test runner used by the all-blackbox / bjorne targets in shell/build-tests.xml:40-60 (TestHarness -E -F -v -s <root.dir>). It reads XML testSpec files and dispatches by runMode to ClassTestRunner, CommandTestRunner or ScriptTestRunner (TestHarness.execute :247-259).
TestHarness uses a single fixed path under java.io.tmpdir:
// prepareTmpDir() :150-160
tempDir = new File(System.getProperty("java.io.tmpdir"), "jnodeTestDir");
if (tempDir.isDirectory()) cleanDir(tempDir);
else if (tempDir.isFile()) { tempDir.delete(); tempDir.mkdir(); }
else tempDir.mkdirs();Wiped on entry, so parallel test runs collide.
cleanupTempDir() (:174-184) is new and runs in a finally around the whole spec loop (:123-148), so the directory is removed even on TestsAbandonedException, on diagnose() for a bad spec file, and on unexpected errors:
if (tempDir == null || preserveTempFiles) return;
if (tempDir.isDirectory()) cleanDir(tempDir);
if (!tempDir.delete() && tempDir.exists())
report("Could not remove temporary test directory: " + tempDir);cleanDir(File) (:162-172) also gained a null guard on directory.listFiles() — previously an unreadable directory caused an NPE — and recurses into subdirectories.
preserveTempFiles is now user-settable via --preserveTemp or --keep-temp (:98-99, documented in usage() :222). The old behaviour of implicitly preserving temp files when stopOnFailure tripped was removed: -F and --preserveTemp are orthogonal, and TestHarness.preserveTempFiles() (:370-372) is the single source of truth. TestRunnerBase.cleanup() (:101-114) honours the same flag for per-test file deletion.
<file name="…" directory="true" input="true"/> means set up, don't assert. TestSpecificationParser.parseFile (:160-180) forces the 2-arg FileSpecification(file, isInput) constructor for directory="true", and TestRunnerBase.setup() (:145-161) creates it with f.mkdirs(). TestRunnerBase.checkFiles() (:170-200) asserts only non-input specs — so a spec without input="true" is a positive assertion that the command created it.
@TEMP_DIR@ expansion happens in ScriptTestRunner.run() (:47-78): the property is set from tempDir.getAbsolutePath(), then expand(props, scriptContent, bw, '@') (:110-146) substitutes @name@, turns @@ into @, and throws TestSpecificationException on CR/LF/EOF inside an @…@. The temp script file is deliberately written to java.io.tmpdir, not @TEMP_DIR@, and deleted in cleanup() (:88-94). check(rc) (:80-86) is a non-short-circuiting & of rc, stdout, stderr and file assertions, so one run reports every mismatch.
Example — cli/src/test/org/jnode/test/command/file/mkdir-command-tests.xml:
<testSpec title="nested-create-without-parents-fails" rc="1">
mkdir @TEMP_DIR@/no-parents/a/b/c
<error><![CDATA[Cannot create directory
]]></error>
</testSpec>
<testSpec title="parents-no-error-if-existing">
mkdir @TEMP_DIR@/existing/a
mkdir -p @TEMP_DIR@/existing/a
<file name="existing" directory="true" input="true"/>
<file name="existing/a" directory="true"/>
</testSpec>Because prepareTmpDir wipes the directory at startup, nothing creates the parent of a path a spec expects to be created — hence the explicit input="true" parent fixtures.
Two disjoint test tiers exist in shell: host-JVM JUnit (AllTests.java via all-junit) and host-JVM black-box XML scripts via TestHarness. Neither runs on JNode itself; TestEmu.initEmu (:50-77) picks org.jnode.emu.Emu on the host vs ShellUtils.getCurrentShell() on JNode. See Testing.
The install command provides an interactive shell-based installer:
install [device-name]This launches CommandLineInstaller (via InstallCommand), which:
- Accepts an optional device name argument (e.g.,
install hda0) - Auto-discovers a single JFAT partition if no argument is given
- Falls back to an interactive prompt if multiple or no candidates are found
- Registers two actions:
CopyFilesAction→GrubInstallerAction(files first, then GRUB) - Provides console-based readline I/O via
System.in/System.out - Drives the same action sequence as the boot-time installer but from within a live JNode shell
- Supports
Step.backnavigation duringcollect()for revisiting previous steps
CommandLineInstaller resolves the target device via three strategies:
- CLI argument — passed directly to the constructor
-
Auto-discovery — scans mounted filesystems for a single JFAT partition under
/devices/ - Interactive prompt — asks the user to enter a device name
The resolved device ID and mount point are cached in InputContext so downstream actions (GrubInstallerAction, CopyFilesAction) can read them without re-prompting.
CopyFilesAction runs before GrubInstallerAction so that GRUB's stage1.5 write does not corrupt the freshly written FAT filesystem. The mount point is resolved by filesystem identity (same approach as JGrub.getMountPoint) rather than by path substring matching.
Handles device selection and GRUB installation:
- Reads
DEVICE_IDfrom the action context (set byCommandLineInstaller) - Only prompts for device input if
DEVICE_IDis absent -
execute()installs GRUB stage1/stage1.5/stage2 on the selected device - Guards against no-device-selected with
IllegalStateException
JGrub handles whole-disk device names (no partition suffix):
- If
partitionSuffixis empty, defaultspartitionNumberto 0 - If
parentDeviceNameis empty, uses the device itself as the parent
-
Plugin-System — How commands are registered via
<alias>extensions. - Code-Conventions — Best practices for writing robust commands.
-
Shell-Interpreter — Tokenizer,
&&/||/;sequences, pipelines, redirection,escapeWord. -
Syntax — Declarative
Argumentparsing that runs after aCommandLineis produced. - Testing — JUnit and black-box command test tiers.