Darkshell is an experimental, Bash-inspired shell implemented in Rust. The shipped binary is dsh. It runs on Windows, Linux, and macOS, with behavior documented for both Unix-style and Windows consoles.
Status: early / v0. The language is not Bash-compatible. Use it to learn, hack on, or drive simple scripts—not as a full replacement for your daily shell yet.
- Why Darkshell?
- Naming:
dshvs other tools - Features (current)
- Non-goals and limitations
- Build and install
- Running
dsh - Interactive REPL
- Language overview
- Builtins
- Exit status
- Windows vs Unix
- Repository layout
- Testing
- Further reading
- License
- Small, readable codebase in Rust (lexer, parser, AST, interpreter).
- Cross-platform
cd,pwd, paths, and subprocess spawning without relying on a POSIX-only layer for core features. - Familiar surface syntax for anyone who knows Bash:
if/while/for,&&/||, pipes,export, here-style comments, etc.—where implemented.
The binary name dsh is the short form for Darkshell.
On many Linux systems, a different program also called dsh exists (Dancer’s Shell / distributed shell for remote clusters). If you install both, use distinct paths or rename one binary to avoid clashes.
| Area | What works |
|---|---|
| Invocation | REPL (no args), dsh -c '…', script file + args (first arg must be an existing file) |
| Line editing | rustyline-based interactive prompt (TTY-aware styling), persistent history (~/.dsh_history), tab completion (builtins, functions, paths, $VAR) |
| Commands | External programs via std::process::Command, builtins, user-defined functions |
| Composition | ;, &&, ` |
| Redirects | <, >, >>, n>, n>>, 2>&1-style dup — on builtins and externals |
| Variables | export, unset, prefix VAR=value cmd, $VAR, ${VAR}, $?, $$, $1…, $# |
| Control flow | if / then / elif / else / fi, while / do / done, for / in / do / done |
| Functions | name() { … } with positional parameters; return [n] exits the function only |
| Misc | # comments, & background (best-effort), source / ., type, help builtin (dsh help—not Windows CMD help) |
Authoritative semantics for the language v0 are in SPEC.md.
These are intentionally out of scope or incomplete in v0 (see SPEC.md):
- Full Bash parameter expansion (only a small
$…subset). - Command substitution (
$(cmd)or backticks) — rejected at parse time in v0. - Arrays,
select,case, arithmetic(( )), process substitution, here-documents. - Job control (
fg,bg,%1) andset -e. - Pipelines: multi-command pipelines cannot mix builtins (builtins are not supported inside
|chains). Redirects and prefix assignments on pipeline segments are not supported yet for multi-command pipes. - Background
&: runs in a detached way;$?is 0 immediately after starting a background job. Do not expect POSIX job-control semantics (especially on Windows—seeWINDOWS.md).
Requirements: Rust toolchain (2021 edition), e.g. stable via rustup.
cd Rust/darkshell # or your clone path
cargo build --releaseThe binary is target/release/dsh (or target\release\dsh.exe on Windows). Add that directory to your PATH, or copy the binary where you prefer.
cargo install --path .installs dsh from the crate manifest into Cargo’s bin directory (if you use this workflow).
| Mode | Example |
|---|---|
| Interactive REPL | dsh with no script path and a real terminal on stdin (embedded IDEs often fail this—use dsh.exe .\\script.dsh for scripts) |
| One command | dsh -c "echo hello" |
| Script file | dsh script.dsh arg1 arg2 — first arg must exist as a regular file; $0 is the script path, $1… are args |
CLI flags (from dsh --help):
-c COMMAND/--command COMMAND— runCOMMANDand exit (afterexithandling).- Positional args — first arg is a script path; remaining args become
$1,$2, … If the path is missing or not a regular file,dshprints an error and exits.
- Stdin must be a terminal (TTY). Some IDE-integrated shells use a pipe for stdin; in that case
dshwith no script exits with an error instead of hanging. Run scripts with an explicit path to the binary, e.g..\target\release\dsh.exe .\script.dsh. - On a color-capable TTY, the banner and prompt use ANSI styling; plain output is used when stdout/stderr are redirected so scripts stay clean.
- The prompt shows the current working directory (see
PWD/cd). - Leave the shell: run the
exitbuiltin, or press Ctrl+D (end-of-input) at the prompt. - Startup file: if
~/.dshrcexists (or%USERPROFILE%\.dshrcon Windows), it is sourced before the first prompt. - History: command history is saved to
~/.dsh_history(or%USERPROFILE%\.dsh_historyon Windows) across sessions. - Tab completion: builtins, shell functions, file paths, and
$VARnames. - Parse/runtime errors print to stderr and the REPL continues (you are not dropped back to the OS on the first typo).
- Whitespace: spaces and tabs separate words. Inside
{ … }blocks or scripts, newlines can separate commands like;. - Comments:
#starts a comment; the rest of the line is ignored. - Single quotes
'…'— literal; a single'cannot appear inside (use concatenation or double quotes). - Double quotes
"…"— allow $ expansion ($VAR,${VAR},$?, etc.). - Escapes: outside quotes,
\escapes the next character (e.g.\$→ literal$). - Command substitution:
$(…)is not supported; the lexer reports an error. - Line endings: scripts may use
\nor\r\n;\ris treated as whitespace.
;or newline (in blocks): always run the next segment.&&: run the next segment only if the previous exited with status 0.||: run the next segment only if the previous exited non-zero.&&and||have equal precedence, left-to-right (similar to Bash).|connects simple commands: stdout of one process feeds stdin of the next.
Pipeline restrictions (v0):
- Only external commands in multi-stage pipes (no builtins in the middle/end of a pipe chain).
- No redirects or prefix
VAR=valueassignments on individual commands inside a multi-command pipeline yet.
| Form | Meaning |
|---|---|
< word |
stdin (fd 0) from file |
> word |
stdout (fd 1) truncate/create |
>> word |
stdout append |
n> word |
redirect fd n to file (e.g. 2> err.log) |
n>> word |
append fd n |
n>&m |
duplicate fd n to m (e.g. 2>&1) |
Redirects may appear before or after the command words on a simple command. They apply to builtins and externals (e.g. echo hi > out.txt). For 2>&1 without >file, stderr is merged onto stdout.
export NAME=value— sets and exports variables for the shell and child processes.export NAME— marksNAMEas exported (value comes from the shell if already set).exportwith no arguments — lists exported variables in a shell-friendly form.VAR=value command …— setsVARfor that command only (overlay), including during word expansion (FOO=bar echo $FOOprintsbar).- Only exported variables (plus any prefix overlay) are passed to child processes; shell-local variables (e.g.
forloop iterators) do not leak to externals.
if (condition is a list; last pipeline’s status decides truth):
if true; then echo yes; fi
if test -f foo; then echo exists; else echo missing; fiwhile:
while false; do echo never; donefor:
for x in a b c; do echo "$x"; doneWords after in are parsed as words and expanded—no IFS splitting complexity in v0.
greet() { echo "hello $1"; }
greet world$1, $2, … and $# are available in the function body. Use return [n] to leave a function without exiting the shell (exit still leaves the whole shell). End the previous statement with ; before a line that starts with name() on the same line (see script.dsh).
Supported forms include $VAR, ${VAR}, $? (last status), $$, positional $n, and $#. Beyond that, see SPEC.md for what is explicitly not implemented.
| Builtin | Summary |
|---|---|
help [topic] |
Darkshell help. On Windows, plain help would run CMD’s help.exe; help is a builtin so you always get dsh documentation. Use cmd /c help for CMD. |
cd [dir] |
Change directory; default home (HOME or on Windows USERPROFILE / dirs fallback). Updates PWD. |
pwd |
Print current working directory. |
echo words… |
Print arguments separated by spaces, then newline. |
export |
Set/export variables, or list exports when given no args. |
unset names… |
Remove shell variables. |
exit [n] |
Exit the shell with status n (default: last command status). |
return [n] |
Return from the current function with status n (default: last status). |
source file / . file |
Run a script file in the current shell (same as Bash source). |
type name |
Show whether name is a builtin, function, or external command. |
:, true, false |
No-op success, success (0), failure (1). |
Run help or help cd inside dsh for the full builtin text.
- The last command’s status is remembered (as
$?), including builtins and functions; truncated to 8 bits where the OS applies that. - Missing commands or expansion to an empty command name yield a non-zero status / error.
cd/pwd: use Rust’sstd::envAPIs; paths follow the OS.- Ctrl+C: Unix targets forward interrupts to a child process group where possible; Windows uses best-effort console handling. Details:
WINDOWS.md. - Job control / background: not POSIX job control; background
&is best-effort—see that doc before relying on it.
| Path | Role |
|---|---|
src/main.rs |
CLI entry (clap), mode selection (REPL / -c / script file) |
src/lexer.rs, parser.rs, ast.rs |
Front-end |
src/expand.rs |
Word / parameter expansion |
src/interp.rs |
Evaluation: pipelines, builtins, functions, redirects |
src/builtins.rs |
Builtin implementations and help text |
src/repl.rs |
Interactive loop (rustyline) |
src/shell.rs |
Process-wide state: env, cwd, functions, exit |
src/signals.rs |
Interrupt / child tracking (platform-specific pieces) |
src/style.rs |
TTY-aware banner, prompt, and error styling |
tests/integration.rs |
Integration tests via assert_cmd |
examples/ |
Example .dsh scripts (see examples/README.md) |
script.dsh |
Full language tour (legacy entry point at crate root) |
SPEC.md |
Language semantics (v0) |
WINDOWS.md |
Windows-specific behavior notes |
cargo testIntegration tests spawn the dsh binary and assert on stdout/stderr and exit codes (redirects, pipelines, exports, functions, return, etc.).
SPEC.md— formal v0 language sketch (what the parser and runtime aim to implement).WINDOWS.md— signals, job/background notes, paths, line endings.
Dual-licensed under MIT OR Apache-2.0, as specified in Cargo.toml (license = "MIT OR Apache-2.0").
Issues and PRs are welcome. When changing behavior, update SPEC.md (if semantics change) and any affected tests. Keep help output in sync with new builtins so Windows users are not surprised by stale text.