Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ These principles guide all development decisions:
## Project Structure

Modules use **file-based organization** (not mod.rs):

- `src/lexer.rs` - Tokenization
- `src/parser.rs` - AST construction
- `src/evaluator.rs` - Execution
Expand All @@ -36,4 +37,4 @@ Modules use **file-based organization** (not mod.rs):

## Reference

- **Project README**: [README.md](../../README.md)
- **Project README**: [README.md](../README.md)
74 changes: 74 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: lint build test

on:
pull_request:
branches: [main]

jobs:
dev:
name: dev (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: build dev
run: cargo build --verbose
- name: test dev
run: cargo test --verbose

release:
name: release (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: build release
run: cargo build --release --verbose
- name: test release
run: cargo test --release --verbose
- name: upload release artifacts
uses: actions/upload-artifact@v4
with:
name: release-build-${{ matrix.os }}
path: target/release/

lint:
name: clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: run clippy
run: cargo clippy --all-targets --all-features -- -D warnings

coverage:
name: coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: install tarpaulin
run: cargo install cargo-tarpaulin
- name: run coverage
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
Comment thread
cdprice02 marked this conversation as resolved.
run: |
if [ -n "$COVERALLS_REPO_TOKEN" ]; then
cargo tarpaulin --verbose --all-features --workspace --timeout 180 --coveralls "$COVERALLS_REPO_TOKEN" --fail-under 80
else
cargo tarpaulin --verbose --all-features --workspace --timeout 180 --fail-under 80
fi
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ target/
*.swp
*.swo
*~

# OS
tmp/
55 changes: 55 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project

**ferrish** is an early-stage shell implementation in Rust. Correctness over performance; when a trade-off exists between the two, correctness wins.

## Commands

```bash
# Build
cargo build
cargo build --release

# Test (all)
cargo test

# Test (single integration test by name)
cargo test --test builtin test_name

# Test (single unit test)
cargo test -p ferrish test_name

# Lint (CI enforces no warnings)
cargo clippy --all-targets --all-features -- -D warnings
```

## Architecture

Input flows through: **Shell** → **Parser** → **Executor** → **BuiltIn | Executable**

- `src/shell.rs` — REPL loop; reads input, calls parser, calls executor, handles fatal vs. recoverable errors
- `src/parser.rs` — Splits input into command + args; resolves whether the command is a built-in, a PATH executable, or unrecognized
- `src/executor.rs` — Dispatches to built-in handlers or spawns external processes
- `src/command/builtin.rs` — Implementations of `exit`, `cd`, `echo`, `pwd`, `type`
- `src/command/executable.rs` — Wraps an external binary found on PATH
- `src/error.rs` — `ShellError` enum; distinguishes fatal errors (process spawn/wait failures) from recoverable ones (command not found, bad path)
- `src/io.rs` — `ShellIo` trait with `StandardIo` (real I/O) and `MockIo` (testing); this abstraction is what makes unit tests possible without spawning a process

## Testing Strategy

Two layers:

1. **Unit tests** — embedded in each module via `#[cfg(test)]`, use `MockIo` for I/O
2. **Integration tests** (`tests/`) — exercise the `ferrish::Shell` library via the `ShellTest` harness in `tests/harness.rs`, using `MockIo` for I/O

The `ShellTest` builder in `harness.rs` is the primary integration test interface. It runs the shell in-process, creates an isolated `HOME` via `tempfile`, and captures stdout/stderr. Prefer integration tests for anything user-visible.

## Key Conventions

- **File-based modules** — no `mod.rs`; each module is a standalone `.rs` file
- **Error reporting** — miette for user-facing errors with span context; `thiserror` for internal error types
- **REPL responsiveness** — lex/parse must complete in <100ms for typical input
- **State** — keep shell state (env vars, working dir) centralized and test scoping/isolation explicitly
106 changes: 106 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ is_executable = "1.0"
soft-canonicalize = { version = "0.5.4", features = ["dunce"] }
strum = { version = "0.27.2", features = ["derive"] }
thiserror = "1.0"

[dev-dependencies]
tempfile = "3.24.0"
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# ferrish 🦀

[![build](https://github-actions-badge.deno.dev/cdprice02/ferrish/ci.yml?label=build-release)](https://github.com/cdprice02/ferrish/actions/workflows/ci.yml)
[![test](https://github-actions-badge.deno.dev/cdprice02/ferrish/ci.yml?label=test-release)](https://github.com/cdprice02/ferrish/actions/workflows/ci.yml)
[![coverage](https://coveralls.io/repos/github/cdprice02/ferrish/badge.svg?branch=main)](https://coveralls.io/github/cdprice02/ferrish?branch=main)
[![lint](https://github-actions-badge.deno.dev/cdprice02/ferrish/ci.yml?label=lint)](https://github.com/cdprice02/ferrish/actions/workflows/ci.yml)
<!--
[![crates.io](https://img.shields.io/crates/v/ferrish.svg)](https://crates.io/crates/ferrish)
[![docs.rs](https://docs.rs/ferrish/badge.svg)](https://docs.rs/ferrish)
-->

`ferrish` — a modern, Rust-powered shell focused on safety, performance, and a clean interactive experience.

> ⚠️ **Status:** ferrish is in early development and is not yet ready for daily use.
Expand Down
41 changes: 41 additions & 0 deletions src/arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::{Command, parser::parse_command};

pub type Args = Vec<Arg>;

/// Represents a shell command argument
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Arg {
Literal(Vec<u8>),
Expand All @@ -17,6 +18,12 @@ impl std::fmt::Display for Arg {
}
}

impl From<&str> for Arg {
fn from(val: &str) -> Self {
Arg::from(val.as_bytes())
}
}

impl From<&[u8]> for Arg {
fn from(val: &[u8]) -> Self {
Arg::from(val.to_vec())
Expand Down Expand Up @@ -44,3 +51,37 @@ impl From<&Arg> for PathBuf {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_arg_from_str() {
let arg = Arg::from("hello");
match arg {
Arg::Literal(bytes) => assert_eq!(bytes, b"hello"),
}
}

#[test]
fn test_arg_display() {
let arg = Arg::from("display_test");
assert_eq!(arg.to_string(), "display_test");
}

#[test]
fn test_arg_equality() {
let arg1 = Arg::from("same");
let arg2 = Arg::from("same");
assert_eq!(arg1, arg2);
}

#[test]
fn test_arg_to_pathbuf() {
let arg = Arg::from("/path/to/file");
let pathbuf = PathBuf::from(&arg);
assert_eq!(pathbuf, PathBuf::from("/path/to/file"));
}

}
Loading
Loading