LinuxLingo

LinuxLingo Developer Guide


Acknowledgements


Setting Up, Getting Started

Prerequisites:

  1. JDK 17 or above.
  2. Gradle 7.x (wrapper included — use ./gradlew).

Steps:

  1. Fork this repo, then clone the fork onto your computer.

  2. Configure your IDE (IntelliJ IDEA is recommended):
    • Ensure IntelliJ is set to use JDK 17: File → Project Structure → Project SDK.
    • Import the project as a Gradle project
  3. Building the project:
./gradlew build

Running the application:

./gradlew run

Running tests:

./gradlew test

Design

Architecture

The Architecture Diagram below gives a high-level overview of LinuxLingo.

Architecture Diagram

Main components of the architecture:

LinuxLingo (the main class) is in charge of app launch. At startup, it:

The bulk of the app’s work is done by the following components:

Component Responsibility
CLI Handles user I/O (Ui) and top-level command dispatch (MainParser).
Shell Parses and executes shell commands in a simulated Linux environment.
Exam Manages exam sessions — question presentation, answer checking, scoring.
Storage Reads/writes data on the real file system (question banks, VFS snapshots).
VFS In-memory virtual file system that all shell commands operate on.

How the components interact with each other:

The following sequence diagram shows the interactions when the user launches the app in interactive mode and types shell:

Architecture Sequence Diagram


CLI Component

The CLI component consists of two classes: Ui and MainParser.

CLI Class Diagram

Ui is the single point of contact for all user-facing I/O. It wraps Scanner for input and PrintStream for output and error streams. All components use Ui instead of directly calling System.in/System.out/System.err, making testing easier (injectable streams).

MainParser implements the top-level REPL loop. It reads user input and dispatches to one of: shell (enter Shell Simulator), exam (start an exam), exec (one-shot shell command), help, or exit/quit.


Shell Component

The Shell component handles command parsing, execution, and the interactive REPL. It is the largest component in LinuxLingo.

The following class diagram shows the key classes. For clarity, only representative command implementations are shown; the full set of 35 commands follows the same Command interface.

Shell Class Diagram

The Shell component:

Supported commands (35 total, grouped below by user-facing function):

Category Commands
Navigation cd, ls, pwd
File Operations mkdir, touch, rm, cp, mv, cat, echo, diff, tee
Text Processing head, tail, grep, find, wc, sort, uniq
Permissions chmod
Information man, tree, which, whoami, date
Alias & History alias, unalias, history
Environment save, load, reset, envlist, envdelete
Utility help, clear

Exam Component

The Exam component handles question presentation, answer checking, and score tracking.

Exam Class Diagram

The Exam component:


Storage Component

The Storage component handles all real disk I/O operations.

Storage Class Diagram

The Storage component:


VFS (Virtual File System) Component

The VFS component provides an in-memory simulated Linux file system.

VFS Class Diagram

Key design decisions for VFS:

The following object diagram illustrates a concrete VFS state after the user has created a file hello.txt under /home/user:

VFS Object Diagram


Implementation

This section describes some noteworthy details on how certain features are implemented.

Shell Parsing and Execution

The shell parsing pipeline transforms the raw user input string like echo hello | grep h > out.txt into a structured execution plan (ParsedPlan) that the execution engine can act on. Understanding this pipeline is essential before working on any feature that involves parsing or command execution.

Parsing Pipeline Overview: (High Level)

Parsing Pipeline Activity Diagram

The ShellParser.parse() method runs the input through two stages: tokenization, then plan building

Shell Parser Sequence Diagram

Stage 1: Tokenization

The tokenizer reads the input one character at a time using a state machine with three states:

State Behaviour
NORMAL Accumulates characters into tokens; whitespace flushes the current token. Special characters ( \| , >, >>, &&, ;) produce operator tokens. Quote characters switch state.
IN_SINGLE_QUOTE All characters are literal until the closing '
IN_DOUBLE_QUOTE All characters are literal until the closing "

After tokenization, the token list is split into Segment objects at inter-segment operators (PIPE, AND, SEMICOLON , OR). Within each segment, REDIRECT / APPEND / INPUT_REDIRECT tokens consume the next WORD token as the redirect target file.

The parser handles two lookahead cases during tokenization in NORMAL state: || and >> are distinguished from | and > by peeking at the next character before emitting a token.

A lone & character (not followed by another &) is treated as a literal word character rather than an operator, which matches standard shell behaviour.

Note: Single-quoted tokens (e.g. '*.txt') are marked with a \0 prefix by the tokenizer (SINGLE_QUOTE_MARKER). This flag is checked by expandGlobs() and expandVariables() to suppress expansion, matching standard shell behaviour. The marker is stripped before the argument reaches the command.

Stage 2: Plan Building

Once the flat token list is produced, buildPlan() walks through it and groups tokens into Segment objects. Each Segment holds a command name, its arguments, and optional redirect information. Operator tokens (PIPE, AND, SEMICOLON, OR) act as delimiters between segments and are recorded separately in the operators list.

The parser maintains an Expecting flag to handle redirect targets: when a REDIRECT or APPEND token is seen, the very next WORD token is consumed as the redirect file path rather than as a command argument. The same pattern applies to INPUT_REDIRECT (<).

The result is a ParsedPlan with the following invariant, enforced by an assertion:

operators.size() is always exactly segments.size() - 1

This means a plan with three segments always has exactly two operators connecting them, making the execution engine’s iteration straightforward.

Class Diagram

ParsedPlan Class Diagram

Object Diagram (example instance)

ParsedPlan Object Diagram

Execution engine (ShellSession.runPlan())

All parsing ultimately feeds into runPlan(), the core method that iterates the ParsedPlan and chains commands together. It is worth understanding its structure because most enhancements to the shell (new operators, alias resolution, input redirect) are implemented here.

The engine tracks two pieces of state across iterations: pipedStdin (the stdout of the previous command, forwarded when the operator was PIPE) and lastExitCode (used to evaluate && and || conditions). The loop processes one Segment per iteration:

runPlan() Sequence Diagram:

Run Plan Sequence Diagram

Key Behaviour

Exit propagation: The shouldExit() flag on CommandResult causes runPlan() to set
running = false and break the loop immediately.


Execution walkthrough: echo hello | grep h > output.txt

Stage 1 (Tokenization): produces the flat token list:

# Value Type
1 echo WORD
2 hello WORD
3 \| PIPE
4 grep WORD
5 h WORD
6 > REDIRECT
7 output.txt WORD

Stage 2 (Plan building): splits on the PIPE and consumes output.txt as a redirect target, yielding a ParsedPlan with:

Stage 3 (Execution): (runPlan()): the sequence diagram below shows how ShellSession orchestrates the two segments. Key points to note:

Echo Grep Pipe Execution Sequence


Operator semantics:

Operator Symbol Behavior
PIPE | stdout of segment N becomes stdin of segment N+1
AND && Segment N+1 runs only if segment N succeeded (exit code 0)
OR \|\| Segment N+1 runs only if segment N failed (exit code ≠ 0)
SEMICOLON ; Segment N+1 always runs regardless of exit code

Command Execution with Piping and Redirection

All 35 commands follow the same implementation pattern:

  1. Parse flags and arguments from args[].
  2. Determine input source: file arguments take priority over piped stdin.
  3. Call VFS methods on session.getVfs().
  4. Catch VfsException → return CommandResult.error(...).
  5. Return CommandResult.success(output).

The following activity diagram shows the input resolution logic for commands that support both file arguments and piped stdin (e.g., cat, head, tail, grep, sort, uniq, wc):

Input Resolution Activity Diagram

Input redirection (<) is handled by the execution engine before command execution: if a segment has an inputRedirect file, the engine reads that file’s content from the VFS and passes it as stdin to the command.


Alias Resolution and Variable Expansion

Aliases allow users to define shortcuts for commonly used commands (e.g., alias ll='ls -la'). The alias system is implemented in ShellSession:

Variable expansion supports a limited set of shell variables:

Variable Value
$USER user
$HOME /home/user
$PWD Current working directory
$? Exit code of the last command

Variables inside single-quoted strings are not expanded (single-quoted tokens are marked with a \0 prefix during tokenization). The expandVariablesInString() method scans each argument character-by-character, recognises $ followed by an alphanumeric name or ?, and substitutes the resolved value.


Glob Expansion

Glob patterns (* and ?) in command arguments are expanded against the VFS before the command receives them:

  1. Detection: Each argument is checked for * or ? characters. Single-quoted tokens (marked with a \0 prefix) skip expansion entirely.
  2. Matching: For patterns without a path separator (e.g., *.txt), only immediate children of the current directory are matched. For patterns with path separators (e.g., /home/*.txt), VirtualFileSystem.findByName() searches the specified subtree.
  3. Fallback: If no VFS paths match a glob pattern, the literal pattern is passed through unchanged (standard shell behaviour).

Matching uses VirtualFileSystem.matchesWildcard(), which converts * → .* and ? → . to build a regex.


Command Suggestion (“Did you mean?”)

When a command name is not found in the registry, ShellSession.suggestCommand() computes the Levenshtein edit distance between the mistyped input and every registered command name using dynamic programming. If the closest match has an edit distance ≤ 2, a hint like Did you mean 'ls'? is displayed alongside the error.


VFS Environment Persistence

Users can save and load VFS snapshots through the save, load, reset, envlist, and envdelete commands. The VfsSerializer class handles serialization and deserialization of VFS state to and from a plain-text .env file format.

Design Overview

The persistence system is split into two responsibilities:

This separation means VfsSerializer can be unit-tested independently of the filesystem, and the storage layer can be swapped without touching serialization logic.

Save/Load Flow

When the user runs save myenv, the following sequence occurs:

  1. SaveCommand calls Storage.save(name, vfs, workingDir).
  2. Storage calls VfsSerializer.serialize(vfs, workingDir) to get the .env file content as a String.
  3. Storage writes the string to data/environments/myenv.env.

When the user runs load myenv:

  1. LoadCommand calls Storage.load(name).
  2. Storage reads the file content and calls VfsSerializer.deserialize(content).
  3. VfsSerializer reconstructs a VirtualFileSystem and returns it wrapped in a DeserializedVfs record (which also carries the saved working directory).
  4. LoadCommand replaces the session’s VFS and current directory with the deserialized values.

Save/Load sequence diagram:

Save Load Sequence Diagram

.env File Format

Each environment snapshot is a human-readable text file:

# LinuxLingo Virtual File System Snapshot
# Saved: 2026-03-27T14:30:00
# Working Directory: /home/user
#
# Format: TYPE | PATH | PERMISSIONS | CONTENT

DIR  | /              | rwxr-xr-x
DIR  | /home          | rwxr-xr-x
FILE | /etc/hostname  | rw-r--r-- | linuxlingo

Each non-comment line encodes one VFS node:

Field Description
TYPE DIR or FILE
PATH Absolute path within the VFS
PERMISSIONS 9-character Unix permission string e.g. rwxr-xr-x
CONTENT File content (omitted for directories); special characters escaped

Escaping rules for file content:

Escape sequence Meaning
\n Newline character
\| Literal pipe character (since | is the field delimiter)
\\ Literal backslash

Serialization Algorithm

VfsSerializer.serialize() performs a depth-first walk of the VFS tree (parents before children) using a recursive helper. For each node it emits a TYPE | PATH | PERMISSIONS [| CONTENT] line. File content is passed through escapeContent() before writing to ensure the pipe delimiter is never ambiguous.

Deserialization Algorithm

VfsSerializer.deserialize() reads the file line by line:

  1. Lines starting with # are treated as comments and skipped.
  2. Each remaining line is split on ` ` (space-pipe-space) into at most 4 fields.
  3. The first field determines node type (DIR or FILE).
  4. A Directory or RegularFile is created at the given path with the given permissions.
  5. For FILE nodes, the optional fourth field (content) is unescaped via unescapeContent() before being stored.

If a line is malformed (wrong number of fields, unrecognised type), it is silently skipped to allow forward compatibility with future format extensions.

Design Considerations

Why a plain-text format instead of JSON/binary?

A human-readable format means users can inspect and hand-edit saved environments in a text editor without needing special tools. It also makes the format trivially diffable in version control.

Why is VfsSerializer stateless?

All methods are static. There is no mutable state, which makes the class thread-safe and easy to test: pass in a VirtualFileSystem, get back a String; pass in a String, get back a DeserializedVfs. No setup or teardown required.

Alternative considered — serialising to JSON:

Using a JSON library (e.g., Gson) would have reduced boilerplate but added a dependency and produced a less human-readable output. Given the simple, fixed schema of the VFS, a bespoke text format was preferred.


Tab Completion and Interactive Input

LinuxLingo’s interactive shell provides tab completion for command names and VFS paths, as well as persistent command history (up/down arrow keys). This is implemented using the JLine 3 library via two classes: ShellLineReader and ShellCompleter.

Design Overview

Class Responsibility
ShellLineReader Wraps JLine’s LineReader; manages the terminal lifecycle, history, and graceful fallback to a dumb terminal
ShellCompleter Implements JLine’s Completer interface; supplies command-name and VFS-path candidates

ShellSession creates a ShellLineReader at startup and calls readLine(prompt) on each iteration of the REPL loop. ShellLineReader creates a ShellCompleter internally and registers it with JLine so that pressing Tab triggers completion automatically.

Tab Completion Logic

ShellCompleter.complete() is called by JLine each time the user presses Tab. It inspects the parsed input line to decide which kind of completion to offer:

Path completion detail (completePath):

The partial path typed by the user is split at the last / into a directory part and a name prefix:

Partial input Directory part Name prefix
ho . (cwd) ho
/home/u /home u
/ / `` (empty)

VirtualFileSystem.listDirectory() is called on the directory part, and only children whose names start with the name prefix are included. Hidden files (names starting with .) are suppressed unless the user’s prefix itself starts with ., matching standard shell behaviour. Directories have a trailing / appended to their candidate value so that pressing Tab on a directory name immediately positions the cursor for the next path component.

Command History

ShellLineReader configures JLine with a DefaultHistory instance. History is in-memory only for the current session (not persisted to disk). Users navigate history with the up/down arrow keys. The getHistory() and addToHistory() methods on ShellLineReader are exposed for unit testing without a real terminal.

Terminal Fallback

ShellLineReader.create() first attempts to open a system terminal via TerminalBuilder. If that fails (e.g., in a CI environment or when stdin is not a TTY), it falls back to ShellLineReader.createDumb(), which creates a dumb terminal. In dumb mode, tab completion and arrow-key history are unavailable, but the shell still accepts plain line input via standard Scanner-style reading, ensuring one-shot mode and testing remain unaffected.

Sequence Diagram — Tab Completion

The following diagram shows what happens when a user presses Tab mid-input:

User            ShellLineReader      JLine LineReader     ShellCompleter         VFS
 |  [Tab key]        |                     |                    |                 |
 |---------------->  |                     |                    |                 |
 |                   | readLine(prompt)     |                    |                 |
 |                   |-------------------> |                    |                 |
 |                   |                     | complete(line)     |                 |
 |                   |                     | -----------------> |                 |
 |                   |                     |                    | listDirectory() |
 |                   |                     |                    | --------------> |
 |                   |                     |                    | <-------------- |
 |                   |                     | [candidates]       |                 |
 |                   |                     | <----------------- |                 |
 |  [completions displayed]                |                    |                 |
 | <-----------------                      |                    |                 |

Design Considerations

Why JLine instead of a custom implementation?

JLine handles terminal raw-mode switching, ANSI escape sequences, cross-platform terminal detection, and history navigation — hundreds of edge cases that would be prohibitively expensive to replicate correctly. Using JLine lets the implementation focus on the completion logic specific to LinuxLingo (VFS paths, aliases) rather than terminal I/O plumbing.

Why split ShellLineReader and ShellCompleter?

Separating the two classes allows ShellCompleter to be unit-tested independently of JLine’s terminal infrastructure using the getCommandCompletions() and getPathCompletions() helper methods, which return plain SortedSet<String> without requiring a real terminal or JLine objects.

Alternative considered — reading from System.in directly:

A plain BufferedReader on System.in was used in early iterations. It was replaced by JLine because it provided no completion or history, making the shell significantly less usable. The fallback dumb-terminal path in ShellLineReader preserves this simpler behaviour for non-interactive contexts.


Exam Session Flow

The exam module supports three entry modes and three question types.

Interactive exam flow:

Exam Session Sequence Diagram

In-exam control commands (skip / abort)

During an exam run, the user can control the session without exiting the whole application:

These commands are interpreted centrally by QuestionInteraction, not by individual Question subclasses. This keeps question types focused on domain logic (present() / checkAnswer(...)) while the exam UX policy lives in the interaction layer.

Where the logic lives:

Command handling rules (non-PRAC questions):

  1. If the user types abort (case-insensitive), QuestionInteraction sets an internal abort flag and signals ExamSession to stop iterating.
  2. If the user types quit (case-insensitive) or input is null, the question is recorded as skipped (stored with empty answer and counted as incorrect), and the exam continues.
  3. Otherwise, the answer is passed to Question.checkAnswer(...) and the result is recorded.

This design prevents control-flow logic from being duplicated across modes (interactive vs direct vs random), since the same QuestionInteraction logic is reused.

The following sequence diagram illustrates how quit and abort are handled for non-PRAC questions during an exam run:

Exam session – In-exam control commands (quit / abort)

PRAC question handling:

For practical questions, ExamSession.handlePracQuestion():

  1. Creates a fresh VirtualFileSystem via the vfsFactory supplier.
  2. Creates a temporary ShellSession with this VFS.
  3. Calls tempSession.start() — the user types shell commands.
  4. When the user types exit, the temporary session ends.
  5. Calls PracQuestion.checkVfs(tempVfs) which verifies each Checkpoint (expected path + node type, content, or permissions).

Exam Component — Practical Questions

This section documents the configurable VFS setup and richer checkpoint validation features for practical (PRAC) questions. The goal is to support more realistic, multi-step shell scenarios while keeping the public Exam API stable for the rest of the system.

The feature is implemented via two extensions:

  1. PracQuestion.SetupItem — declarative VFS setup instructions applied before the user starts typing commands.
  2. Extended Checkpoint semantics — new node checks (NOT_EXISTS, CONTENT_EQUALS, PERM) to validate more aspects of the final VFS state.

These changes are internal to the exam module and transparent to callers such as ExamSession and QuestionBank.


1. Class-Level Design

At a high level, the flow for a PRAC question becomes:

  1. ExamSession creates a fresh VirtualFileSystem via vfsFactory.
  2. ExamSession starts a temporary ShellSession over the fresh VFS and lets the user perform the task.
  3. When the user exits the shell, PracQuestion.checkVfs() verifies all Checkpoints against the final VFS state.

The feature is implemented by extending the existing PracQuestion and Checkpoint classes; no new top-level types are introduced.

Key classes (Exam sub-package):

This keeps the public interface between modules unchanged: the CLI, Shell, and Storage components continue to treat the Exam module as a black box that exposes ExamSession and QuestionBank only.


2. Class Diagram (PracQuestion V2)

PracQuestion Class Diagram


3. Sequence Flow — PRAC Question with Setup

PracQuestion Sequence Flow


4. Activity Diagram — Applying Setup Items

PracQuestion Setup Activity Diagram


5. Rationale and Alternatives

Why introduce SetupItem?

Why extend Checkpoint.NodeType?

Alternative 1: Global VFS Templates

One alternative was to define named “VFS templates” elsewhere (e.g., serialized trees stored by the Storage component), and let PracQuestion refer to them by ID.

Alternative 2: Hard-coded Setup per Question

Another option was to provide a dedicated subclass or factory per practical question that mutates the VFS imperatively.


6. Impact on Other Components


7. Extension Points

This design leaves room for future growth:


Question Parsing and Loading

This section explains how question bank files under data/questions/ are converted into in-memory Question objects.

Design overview

At runtime, question data flows through the pipeline below:

  1. QuestionBank.load(Path directory) enumerates .txt files in the questions directory via Storage.listFiles(...).
  2. For each file, QuestionParser.parseFile(Path file) reads the file, converts each valid line into a concrete Question, and returns a List<Question>.
  3. QuestionBank stores the resulting List<Question> under a topic key derived from the filename (e.g., navigation.txt → "navigation").
  4. ExamSession later requests questions by topic via questionBank.getQuestions(topic, count, random).

Data flow:

Question Parsing Activity Diagram


File and line format

Each non-empty, non-comment line in a question bank file is expected to be pipe-delimited with up to 6 fields:

TYPE | DIFFICULTY | QUESTION_TEXT | ANSWER | OPTIONS | EXPLANATION

QuestionParser trims fields and normalises TYPE/DIFFICULTY before constructing the corresponding question object.

Field Meaning Used by
TYPE MCQ, FITB, or PRAC Dispatch to parseMcq / parseFitb / parsePrac
DIFFICULTY EASY, MEDIUM, HARD Stored on Question object; used for display/filtering
QUESTION_TEXT Prompt shown to user Question.present()
ANSWER Correct answer (format depends on type) checkAnswer(...) or Checkpoint list
OPTIONS Type-specific extra data MCQ options / PRAC setup items
EXPLANATION Shown after grading Question.getExplanation()

Type-specific parsing rules

The following diagrams zoom in on the MCQ/FITB-specific parsing and model structure:

MCQ/FITB Line Parsing Activity Diagram

Question Models (MCQ/FITB) Class Diagram

MCQ

Parsing logic (exact implementation)

MCQ lines are handled by QuestionParser.parseMcq(...).

  1. Answer validation
    • The ANSWER field is trimmed.
    • If the trimmed answer is blank, parsing fails with IllegalArgumentException("MCQ answer must not be blank").
  2. Options parsing
    • The OPTIONS field is trimmed and split using the regex:
      • (?=[A-D]:)
    • Each split segment is then trimmed and interpreted as:
      • letter ':' text
    • Only segments where segment.length() >= 2 and segment.charAt(1) == ':' are accepted.
    • The option map key is segment.charAt(0) and the value is segment.substring(2).trim().
  3. Options validation
    • If no valid options are parsed, parsing fails with IllegalArgumentException("MCQ options must not be empty").
  4. Correct answer must exist in options
    • The correct answer character is taken as answer.charAt(0).
    • It is uppercased for the lookup: Character.toUpperCase(correctAnswer).
    • If the resulting letter is not a key in the parsed options map, parsing fails with IllegalArgumentException("MCQ correct answer not found in options").

How invalid MCQ entries are handled

QuestionParser.parseFile(...) catches RuntimeException thrown from parseMcq(...). If parsing fails, the line is skipped (with a logged warning) and the parser continues loading subsequent lines.

Example line:

MCQ | EASY | Which command prints the current directory? | B | A:cd B:pwd C:ls D:dir | 'pwd' stands for ...
FITB

Parsing logic (exact implementation)

FITB lines are handled by QuestionParser.parseFitb(...).

  1. Splitting into accepted answers
    • The raw ANSWER field is split using the regex:
      • (?<!\\)\|
      • i.e., split on | that is not preceded by a backslash (\).
  2. Trimming and unescaping
    • Each split part is processed as:
      • acceptedAnswer.trim().replace("\\|", "|")
    • Empty strings after trimming are ignored.
  3. Accepted-answer list must be non-empty
    • If the resulting List<String> is empty, parsing fails with:
      • IllegalArgumentException("FITB accepted answer list must not be empty").

Authoring notes (important for validation)

How invalid FITB entries are handled

As with MCQ, any RuntimeException thrown while parsing a FITB entry causes the line to be skipped (with a logged warning).


Runtime answer input validation (MCQ/FITB)

This section documents how user input is validated during an exam run. It is separate from question-bank parsing.

The following sequence diagram shows the end-to-end interaction when a user answers an MCQ/FITB question (including reprompts, skip, and abort behavior):

MCQ/FITB Answer Interaction Sequence Diagram

Where it happens

MCQ input validation (askValidatedMcqAnswer)

FITB input validation (askValidatedFitbAnswer)

Grading logic (checkAnswer)

Skip/abort handling

Example line:

FITB | EASY | To print the current directory: ___ | pwd|PWD | | pwd prints the current working directory.
PRAC

Checkpoint examples (illustrative):

PRAC also supports an optional setup list in the OPTIONS field (semicolon-separated), which is parsed into PracQuestion.SetupItem objects for PracQuestion.applySetup(vfs).


Error handling and robustness

QuestionParser is intentionally forgiving:

This matches the project’s goal of allowing question banks to be edited by non-developers without requiring perfect formatting.


Rationale and alternatives

Why a pipe-delimited text format (instead of JSON/YAML)?

Alternatives considered:


Resource Extraction on First Run

ResourceExtractor ensures that bundled question bank files are available on disk.

Resource Extraction Activity Diagram

This design respects user customizations — once the questions directory exists, bundled resources are never overwritten. The five bundled question files cover: file-management, navigation, permissions, piping-redirection, and text-processing.


Appendix A: Product Scope

Target User Profile

Value Proposition

LinuxLingo provides an interactive Linux shell simulator combined with a quiz system, allowing students to:


Appendix B: User Stories

Priority As a … I want to … So that I can …
*** new user see a help menu learn what commands are available
*** student practice basic navigation commands (cd, ls, pwd) become familiar with Linux file system navigation
*** student create and manipulate files and directories learn file management in Linux
*** student take an exam on a specific topic test my knowledge of Linux commands
*** student see my exam score after completing a quiz know how well I understand the material
** student use piping to chain commands understand how data flows between commands
** student use output redirection (>, ») learn how to save command output to files
** student use input redirection (<) learn how to feed file content into commands
** student save my VFS environment continue practicing from where I left off
** student load a previously saved environment restore my practice workspace
** student practice practical questions in a real shell apply my knowledge hands-on
** student use text processing commands (grep, sort, wc) learn data manipulation on the command line
** student change file permissions with chmod understand Linux permission model
** student define command aliases create shortcuts for frequently used commands
** student view my command history recall and re-use previous commands
** student use glob patterns (*.txt) match multiple files at once
** student use conditional execution (&&, ||) understand command chaining logic
** student use shell variables ($USER, $HOME, $PWD) understand how variables work in a shell
** student execute a single shell command without entering the shell simulator quickly test or demonstrate one command
* student take a random question get a quick knowledge check
* student list and delete saved environments manage my saved workspaces
* student abort an exam that I do not want to continue return to the main menu without finishing every question
* student read manual pages with man learn about a command’s usage
* student see a directory tree with tree visualize the file system structure
* student get “Did you mean?” suggestions on typos quickly correct mistyped commands
* student use tab-completion in the shell type commands more efficiently
* student compare files with diff learn to find differences between files
* student use tee to save and display output understand pipeline data capture

Appendix C: Non-Functional Requirements

  1. Portability: Should work on any mainstream OS (Windows, Linux, macOS) with Java 17 or above installed.
  2. Performance: All shell commands should execute in under 100ms. VFS operations should handle file systems with up to 1000 nodes without noticeable lag.
  3. Usability: A user familiar with basic Linux commands should be able to complete the core shell workflow (enter shell, navigate directories, create a file, and inspect output) by following the User Guide.
  4. Reliability: The application should handle all invalid inputs gracefully (no crashes) and provide descriptive error messages.
  5. Testability: Core parsing, command execution, exam logic, and VFS logic should remain testable in isolation through unit tests.
  6. Data Integrity: VFS snapshots saved to disk must be losslessly restorable. Escaping rules must preserve file content containing newlines, pipes, and backslashes.
  7. Safety/Isolation: Shell commands must operate only on the in-memory VFS and must not create, modify, or delete the user’s real files.

Appendix D: Glossary

Term Definition
VFS Virtual File System — an in-memory tree structure simulating a Linux file system. No real files on disk are created or modified by shell commands.
Shell Session An interactive REPL where users type Linux-like commands that operate on the VFS.
Exam Session A quiz session where users answer questions about Linux commands.
MCQ Multiple Choice Question — presents options A/B/C/D; user selects one.
FITB Fill In The Blank — user types a free-form answer checked against accepted answers.
PRAC Practical question — user performs tasks in a temporary shell; VFS state is verified against checkpoints.
Checkpoint An expected condition on a VFS path (existence, type, content, or permissions) used to verify PRAC question answers.
SetupItem A declarative VFS initialization instruction (create directory, create file, set permissions) applied before a PRAC question begins.
REPL Read-Eval-Print Loop — an interactive loop that repeatedly reads user input, executes it, and prints the result.
Segment A single command with its arguments and optional redirect info, part of a ParsedPlan.
ParsedPlan The structured result of parsing a shell input: a list of Segments connected by operators.
Token A smallest meaningful unit produced by the shell tokenizer, such as a word, operator, or redirect symbol.
Environment (.env) A text file storing a serialized VFS snapshot and working directory, saved under data/environments/.
Piping Connecting the stdout of one command to the stdin of the next using the pipe character (|).
Redirection Directing command output to a file (>/>>) or reading input from a file (<).
Glob A wildcard pattern (*, ?) used to match multiple file names in the VFS.
Levenshtein Distance A string-edit distance metric used by command suggestion logic to find the closest valid command to a mistyped input.
Alias A user-defined shortcut for a command name, stored in the shell session.
Question Bank A collection of question files (.txt) organized by topic under data/questions/.

Appendix E: Instructions for Manual Testing

Note: These instructions provide a starting point for testers. Testers are expected to do more exploratory testing.

Launch and Shutdown

  1. Initial launch
    • Ensure Java 17+ is installed.
    • Build the project: ./gradlew shadowJar
    • Run: java -jar build/libs/LinuxLingo.jar
    • Expected: Welcome banner and linuxlingo> prompt are displayed.
  2. Help command
    • Input: help
    • Expected: List of available top-level commands (shell, exam, exec, help, exit) is displayed.
  3. Exit
    • Input: exit (or quit)
    • Expected: Prints “Goodbye!” and application terminates.

Shell Simulator

  1. Entering the shell
    • Input: shell
    • Expected: Welcome message and shell prompt user@linuxlingo:/$ are displayed.
  2. Basic navigation
    • pwd → /
    • cd /home/user → prompt changes to user@linuxlingo:/home/user$
    • cd - → returns to /, prints /
    • cd ~ → navigates to /home/user
    • ls -la → lists files with permissions, types, and sizes
  3. File and directory operations
    • mkdir testdir → no output (success)
    • mkdir -p a/b/c → creates nested directories
    • ls → testdir appears in listing
    • touch testdir/hello.txt → no output (success)
    • echo "Hello World" > testdir/hello.txt then cat testdir/hello.txt → Hello World
    • cp testdir/hello.txt testdir/copy.txt → file copied
    • mv testdir/copy.txt testdir/moved.txt → file renamed
    • rm -r testdir → directory and contents removed
  4. Piping
    • echo "line1" | cat → line1
    • echo "apple" | grep apple → apple
  5. Output redirection
    • echo "test output" > /tmp/out.txt then cat /tmp/out.txt → test output
    • echo "more" >> /tmp/out.txt then cat /tmp/out.txt → test output followed by more
  6. Input redirection
    • echo "hello" > /tmp/in.txt then cat < /tmp/in.txt → hello
    • grep hello < /tmp/in.txt → hello
  7. Conditional execution
    • echo success && echo "also runs" → both success and also runs printed
    • ls /nonexistent && echo "no" → error message only, no is NOT printed
    • ls /nonexistent || echo "fallback" → error message, then fallback printed
    • echo a ; echo b → both a and b printed on separate lines
  8. Text processing
    • echo "hello world" > /tmp/test.txt then grep hello /tmp/test.txt → hello world
    • wc /tmp/test.txt → line, word, and character counts
    • sort / uniq / head / tail — verify expected output on multi-line input
  9. Permissions
    • touch /tmp/secret.txt then chmod 000 /tmp/secret.txt then cat /tmp/secret.txt → Permission denied error
  10. Glob patterns
    • touch a.txt b.txt c.log then ls *.txt → lists a.txt b.txt only
    • ls ?.txt → lists a.txt b.txt (single-character match)
  11. Shell variables
    • echo $USER → user
    • echo $HOME → /home/user
    • echo $PWD → current working directory
  12. Aliases
    • alias ll="ls -la" then ll → same output as ls -la
    • unalias ll then ll → ll: command not found
  13. Command history
    • Run a few commands, then history → numbered list of previous commands
    • history 3 → shows the last 3 commands
    • history -c → clears the command history
  14. Info commands
    • man ls → displays usage info for ls
    • tree → displays directory tree from current directory
    • which ls grep → shows that both commands are available
    • whoami → prints user
    • date +%Y-%m-%d → prints the current date in year-month-day format
  15. Diff and tee
    • echo "a" > /tmp/f1.txt and echo "b" > /tmp/f2.txt then diff /tmp/f1.txt /tmp/f2.txt → shows differences
    • echo "hello" | tee /tmp/tee.txt → prints hello AND writes it to /tmp/tee.txt
  16. Command suggestion
    • mkdi → mkdi: command not found followed by Did you mean 'mkdir'?
    • gre → gre: command not found followed by Did you mean 'grep'?
  17. Exiting the shell
    • exit → returns to linuxlingo> prompt

Environment Management

  1. Save environment
    • Enter shell, create some files/directories.
    • save myenv → environment saved message
    • envlist → myenv appears in the list
  2. Load environment
    • reset → VFS reset to default state
    • load myenv → previously created files/directories are restored
  3. Delete environment
    • envdelete myenv → environment deleted message
    • envlist → myenv no longer appears

Exam Module

  1. Interactive exam
    • Input (at main prompt): exam
    • Expected: List of topics is displayed with question counts.
    • Select a topic by number.
    • Enter number of questions (or press Enter for all).
    • Expected: Questions are presented one at a time with feedback after each.
    • Expected: Final score summary (e.g., Score: 7/10 (70%)).
  2. Direct exam with CLI args
    • Input: exam -t navigation -n 3
    • Expected: 3 questions from the “navigation” topic.
  3. Random question
    • Input: exam -random
    • Expected: One random question is presented.
  4. List topics
    • Input: exam -topics
    • Expected: All available topics listed with question counts.
  5. PRAC question (if available in question bank)
    • When a PRAC question appears, a temporary shell session opens.
    • Perform the required task (e.g., mkdir /home/project).
    • Type exit to submit.
    • Expected: Feedback on whether the VFS matches the expected state.

One-Shot Execution

  1. Basic exec
    • Input (at main prompt): exec "echo hello"
    • Expected: hello is printed.
  2. Exec with saved environment
    • First save an environment in the shell (e.g., save testenv).
    • Input: exec -e testenv "ls"
    • Expected: Directory listing from the saved environment.

Error Handling

  1. Unknown command in shell
    • Input: unknowncmd
    • Expected: unknowncmd: command not found
  2. Invalid path
    • Input: cd /nonexistent/path
    • Expected: cd: No such file or directory: /nonexistent/path
  3. Missing operand
    • Input: grep
    • Expected: grep: missing pattern
  4. Invalid exam topic
    • Input: exam -t nonexistent
    • Expected: Invalid topic selection. followed by available topics list.