Skip to main content
Nash supports a practical subset of Bash syntax, including pipes, redirections, conditionals, subshells, and expansions.

Simple Commands

A command consists of a name followed by zero or more arguments:
echo hello world
ls /tmp
mkdir -p /home/user/projects
cat file.txt
Nash looks up the command in its built-in table. If not found, you get:
user@nash:/home/user$ nonexistent_cmd
nash: command not found: nonexistent_cmd
Nash has 28 built-in commands. There is no $PATH search for external binaries—everything runs in-memory.

Pipes

Connect the standard output of one command to the standard input of another using |:
echo hello | cat
cat file.txt | grep pattern
ls /tmp | wc -l
echo -e "c\nb\na" | sort | uniq

Chaining Multiple Pipes

cat data.txt | grep error | sort | uniq -c
find /var/log -name "*.log" | head -5 | cat

Implementation

The parser creates a Pipe AST node:
Pipe execution
// src/runtime/executor.rs
Expr::Pipe { left, right } => {
    let left_out = self.eval(left, stdin)?;
    self.eval(right, &left_out.stdout)  // Right side gets left's stdout
}
Filter
echo -e "apple\nbanana\napricot" | grep "^a"
# apple
# apricot
Count matches
cat /var/log/app.log | grep ERROR | wc -l
# 42
Transform and sort
cat names.txt | cut -d: -f1 | sort

Redirections

Control where input comes from and output goes to:

Output Redirection (>)

Overwrite or create a file:
echo hello > /tmp/out.txt
ls /home/user > files.txt
Result: File contains only the new output.

Append Redirection (>>)

Append to a file:
echo first > log.txt
echo second >> log.txt
cat log.txt
# first
# second

Input Redirection (<)

Read from a file:
cat < input.txt
grep pattern < data.txt
wc -l < file.txt

Combined Redirections

cat < input.txt > output.txt
grep error < app.log >> errors.txt

How Redirections Work

Redirect execution
// src/runtime/executor.rs
fn eval_redirect(
    &mut self,
    expr: &Expr,
    file: &Word,
    mode: &RedirectMode,
    stdin: &str,
) -> Result<Output> {
    match mode {
        RedirectMode::Input => {
            let path = self.expand_word(file)?;
            let content = self.ctx.vfs.read_to_string(&abs_path)?;
            self.eval(expr, &content)  // Pass file content as stdin
        }
        RedirectMode::Overwrite => {
            let output = self.eval(expr, stdin)?;
            self.ctx.vfs.write_str(&abs_path, &output.stdout)?;
            Ok(Output { stdout: String::new(), ..output })  // Consumed
        }
        RedirectMode::Append => {
            let output = self.eval(expr, stdin)?;
            self.ctx.vfs.append(&abs_path, output.stdout.as_bytes())?;
            Ok(Output { stdout: String::new(), ..output })
        }
    }
}

Chaining (&&, ||, ;)

AND Operator (&&)

Run the second command only if the first succeeds (exit code 0):
mkdir /tmp/build && cd /tmp/build
test -f config.json && cat config.json
grep pattern file.txt && echo "Found"
Behavior:
user@nash:/home/user$ true && echo "yes"
yes

user@nash:/home/user$ false && echo "yes"
# (no output — second command skipped)

OR Operator (||)

Run the second command only if the first fails (exit code non-zero):
test -f config.json || echo "Config missing"
grep pattern file.txt || echo "No matches"
Behavior:
user@nash:/home/user$ true || echo "fallback"
# (no output — first command succeeded)

user@nash:/home/user$ false || echo "fallback"
fallback

Sequence Operator (;)

Run commands sequentially regardless of success:
echo start ; echo end
mkdir /tmp/test ; cd /tmp/test ; pwd
Behavior:
user@nash:/home/user$ false ; echo "still runs"
still runs

Combining Operators

test -d /tmp/build || mkdir /tmp/build && cd /tmp/build
grep error app.log && echo "Errors found" || echo "No errors"

Implementation

Conditional execution
// src/runtime/executor.rs
Expr::And { left, right } => {
    let left_out = self.eval(left, stdin)?;
    if left_out.is_success() {
        self.eval(right, stdin)  // Only run if left succeeded
    } else {
        Ok(left_out)
    }
}

Expr::Or { left, right } => {
    let left_out = self.eval(left, stdin)?;
    if !left_out.is_success() {
        self.eval(right, stdin)  // Only run if left failed
    } else {
        Ok(left_out)
    }
}

Expr::Sequence { left, right } => {
    let left_out = self.eval(left, stdin)?;
    // Print left output, then run right unconditionally
    print!("{}", left_out.stdout);
    self.eval(right, stdin)
}

Subshells

Group commands with ( ) to run in an isolated environment:
(cd /tmp && ls)
pwd  # Still in original directory
Environment isolation:
user@nash:/home/user$ export VAR=outer

user@nash:/home/user$ (export VAR=inner; echo $VAR)
inner

user@nash:/home/user$ echo $VAR
outer

How Subshells Work

The executor clones the context before evaluation:
Subshell isolation
// src/runtime/executor.rs
Expr::Subshell { expr } => {
    // Save current state
    let saved_cwd = self.ctx.cwd.clone();
    let saved_env = self.ctx.env.clone();
    
    // Execute subshell
    let result = self.eval(expr, stdin);
    
    // Restore state (changes don't escape)
    self.ctx.cwd = saved_cwd;
    self.ctx.env = saved_env;
    
    result
}
Subshells isolate:
  • Environment variable changes (export)
  • Directory changes (cd)
But they do not isolate:
  • VFS file writes (files created in a subshell persist)
  • Exit codes (subshell failure propagates)

Variable Expansion

Substitute variable values with $VAR or ${VAR}:

Basic Expansion

user@nash:/home/user$ echo $HOME
/home/user

user@nash:/home/user$ echo $USER
user

user@nash:/home/user$ export GREETING=hello
user@nash:/home/user$ echo $GREETING world
hello world

Braced Expansion

Use ${VAR} to delimit variable names:
user@nash:/home/user$ export PREFIX=test

user@nash:/home/user$ echo ${PREFIX}_file.txt
test_file.txt

user@nash:/home/user$ echo $PREFIXfile.txt
# Looks for variable "PREFIXfile" (likely empty)

Undefined Variables

Missing variables expand to empty strings:
user@nash:/home/user$ echo $UNDEFINED
# (empty line)

user@nash:/home/user$ echo "Value: $UNDEFINED"
Value: 

Lexer Implementation

The lexer parses $ and creates a WordPart::Variable:
Variable parsing
// src/parser/lexer.rs
fn read_dollar(&mut self) -> Result<WordPart, ParseError> {
    self.advance(); // consume '$'
    
    match self.current_char() {
        '{' => {
            // ${VARNAME}
            self.advance();
            let name = self.read_identifier();
            if self.current_char() == '}' {
                self.advance();
                Ok(WordPart::Variable(name))
            } else {
                Err(ParseError::Unmatched('{'))
            }
        }
        _ => {
            // $VARNAME
            let name = self.read_identifier();
            Ok(WordPart::Variable(name))
        }
    }
}

Command Substitution

Capture command output with $(cmd):
echo "Files: $(ls | wc -l)"
echo "Current dir: $(pwd)"
export BUILD_DATE=$(date)

Nested Substitution

echo "Lines in logs: $(cat $(find /var/log -name '*.log') | wc -l)"

Trailing Newline Removal

Nash trims the final newline (Bash-compatible behavior):
user@nash:/home/user$ echo $(echo hello)
hello
# Not "hello\n"

Implementation

Command substitution
// src/runtime/executor.rs
WordPart::CommandSubst(expr) => {
    let output = self.eval(expr, "")?;
    // Trim trailing newline
    result.push_str(output.stdout.trim_end_matches('\n'));
}

Examples

user@nash:/home/user$ echo "You are in $(pwd)"
You are in /home/user

user@nash:/home/user$ export FILE_COUNT=$(ls | wc -l)
user@nash:/home/user$ echo $FILE_COUNT
4

user@nash:/home/user$ cat $(echo /etc/hostname)
nash

Quoting

Single Quotes (Literal)

Preserve everything literally—no expansions:
user@nash:/home/user$ echo '$HOME is $USER'
$HOME is $USER

user@nash:/home/user$ echo '$(pwd)'
$(pwd)

Double Quotes (Expansion Enabled)

Expand variables and command substitutions:
user@nash:/home/user$ echo "$HOME is $USER"
/home/user is user

user@nash:/home/user$ echo "Current: $(pwd)"
Current: /home/user

Escape Sequences

Use backslash \ to escape special characters:
user@nash:/home/user$ echo \$HOME
$HOME

user@nash:/home/user$ echo "Literal \$VAR"
Literal $VAR
Inside double quotes, \ escapes $, `, ", and \.

Lexer Quoting Logic

Single-quote parsing
// src/parser/lexer.rs
'\'' => {
    self.advance(); // opening '
    while self.pos < self.src.len() && self.current_char() != '\'' {
        literal_buf.push(self.current_char());
        self.advance();
    }
    if self.pos >= self.src.len() {
        return Err(ParseError::Unmatched('\''));
    }
    self.advance(); // closing '
}
Double-quote parsing
'"' => {
    self.advance(); // opening "
    while self.pos < self.src.len() && self.current_char() != '"' {
        if self.current_char() == '$' {
            // Flush literal, read expansion
            if !literal_buf.is_empty() {
                parts.push(WordPart::Literal(std::mem::take(&mut literal_buf)));
            }
            let part = self.read_dollar()?;
            parts.push(part);
        } else if self.current_char() == '\\' {
            self.advance();
            literal_buf.push(self.current_char());
            self.advance();
        } else {
            literal_buf.push(self.current_char());
            self.advance();
        }
    }
    self.advance(); // closing "
}

Comments

Lines starting with # are ignored:
# This is a comment
echo hello  # Inline comment
Nash stops parsing when it sees #:
user@nash:/home/user$ echo hello # this is ignored
hello

Lexer Implementation

Comment handling
// src/parser/lexer.rs
if ch == '#' {
    tokens.push(Token::Eof);  // Treat rest of line as end-of-input
    break;
}
Nash does not support mid-line comments unless preceded by whitespace. echo hello#world would be parsed as a single argument.

Complete Syntax Example

Combining all features:
Example script
#!/usr/bin/env nash

# Setup
export BUILD_DIR=/tmp/build
export SRC_DIR=/src

# Ensure build directory exists
mkdir -p $BUILD_DIR || (echo "Failed to create build dir" && exit 1)

# Copy source files
cp -r $SRC_DIR/* $BUILD_DIR/ && echo "Sources copied"

# Process files
find $BUILD_DIR -name "*.txt" | while read file; do
    cat "$file" | grep pattern >> $BUILD_DIR/results.txt
done

# Generate report
echo "Build completed at $(date)" > $BUILD_DIR/report.txt
echo "Files processed: $(ls $BUILD_DIR | wc -l)" >> $BUILD_DIR/report.txt

# Display summary
cat $BUILD_DIR/report.txt

# Cleanup (runs even if above fails)
test -f $BUILD_DIR/temp.txt && rm $BUILD_DIR/temp.txt ; echo "Done"

Syntax Summary Table

FeatureSyntaxExample
Simple commandcmd argsecho hello
Pipecmd1 | cmd2cat file | grep pattern
Redirect outputcmd > fileecho hello > out.txt
Redirect appendcmd >> fileecho world >> out.txt
Redirect inputcmd < filecat < in.txt
ANDcmd1 && cmd2mkdir dir && cd dir
ORcmd1 || cmd2test -f x || echo missing
Sequencecmd1 ; cmd2echo a ; echo b
Subshell( cmds )(cd /tmp && ls)
Variable$VAR or ${VAR}echo $HOME
Command subst$(cmd)echo $(pwd)
Single quote'text'echo '$HOME'
Double quote"text"echo "$HOME"
Escape\charecho \$VAR
Comment# text# This is a comment

Not Supported

Nash intentionally omits some Bash features:
Not implemented:
  • Background jobs (&)
  • Here-documents (<< EOF)
  • Process substitution (<(cmd))
  • Brace expansion ({a,b,c})
  • Glob patterns in words (*.txt)
  • Arithmetic expansion ($((1 + 1)))
  • Arrays
  • Functions
  • Control structures (if, for, while)
  • Case statements
For missing features, use command chaining and built-in commands creatively, or write a script in a full language and execute it via Nash.

Build docs developers (and LLMs) love