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
150 changes: 29 additions & 121 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,143 +1,51 @@
# Prerequisites
*.d
# Build directories
cmake-build-*/
build/

# Object files
*.o
*.ko
*.obj
*.elf

# Linker output
*.ilk
*.map
*.exp

# Precompiled Headers
*.gch
*.pch

# Libraries
*.lib
*.a
*.la
*.lo

# Shared objects (inc. Windows DLLs)
*.dll
*.lib
*.so
*.so.*
*.dylib
*.dll

# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex

# Precompiled headers
*.gch
*.pch

# Debug files
*.dSYM/
*.su
*.idb
*.pdb
*.idb
*.ilk

# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf

# debug information files
*.dwo

# Covers JetBrains IDEs: IntelliJ, GoLand, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# AWS User-specific
.idea/**/aws.xml

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# SonarLint plugin
.idea/sonarlint/
.idea/sonarlint.xml # see https://community.sonarsource.com/t/is-the-file-idea-idea-idea-sonarlint-xml-intended-to-be-under-source-control/121119

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based HTTP Client
.idea/httpRequests
http-client.private.env.json

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

# Apifox Helper cache
.idea/.cache/.Apifox_Helper
.idea/ApifoxUploaderProjectSetting.xml

# Github Copilot persisted session migrations, see: https://github.com/microsoft/copilot-intellij-feedback/issues/712#issuecomment-3322062215
.idea/**/copilot.data.migration.*.xml

# Editor / IDE
*.swp
*.swo
*~
.vscode/
.idea/

# OS
.DS_Store
Thumbs.db

# CMake generated
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
Makefile
CTestTestfile.cmake
compile_commands.json
100 changes: 100 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# CLAUDE.md

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

## Project overview

Trigger is a small POSIX shell written in C (C11), originally adapted from Brennan
Baker's "Write a Shell in C" tutorial and built with CMake. It now supports quoting,
pipelines, I/O redirection, and glob expansion on top of the tutorial's core loop.

## Build, run, test

Build:
cmake -B cmake-build-debug -S .
cmake --build cmake-build-debug

Run the shell:
./cmake-build-debug/shell

Run the full test suite:
./run_tests.sh
# equivalent to: cmake -B/--build, then `cd cmake-build-debug && ctest --output-on-failure`

Run a single test module (CTest test names: InputTests, BuiltinsTests, ExecuteTests,
PipelineTests, GlobTests):
cd cmake-build-debug && ctest -R PipelineTests --output-on-failure

Or run a test binary directly, e.g.:
./cmake-build-debug/test_pipeline

Lint (also enforced in CI as a separate `lint` job, see below):
clang-format --dry-run --Werror $(find src include tests -name '*.c' -o -name '*.h')
cmake -B cmake-build-debug -S . -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
clang-tidy -p cmake-build-debug -warnings-as-errors=* $(find src include -name '*.c')

CI (`.github/workflows/ci.yml`) runs on every PR and push to `main` with two jobs:
`build-and-test` (`./run_tests.sh`) and `lint` (clang-format + clang-tidy, both
warnings-as-errors). Formatting follows `.clang-format` (LLVM base, 4-space indent,
100 col limit); tidy checks are configured in `.clang-tidy`. A tag matching `v*.*.*`
triggers `.github/workflows/release.yml`, which builds a Release binary and attaches
it to a GitHub release.

## Architecture

Data flow: `src/main.c` runs `trigger_loop()`, a read → split → execute → free REPL
loop. Each iteration reads a line, tokenizes it via `trigger_split_line_ex()`
(which also produces a parallel `glob_eligible` array), and hands both to
`trigger_execute()`.

- **Input/lexer** (`src/input.c`, `src/utils/utils.c`): `trigger_read_line()` reads
a raw line via `getline`. `trigger_split_line_ex()` delegates to
`parse_line_with_quotes()` in `src/utils/utils.c`, a hand-written state-machine
tokenizer (`ParserState`: NORMAL / IN_SINGLE_QUOTE / IN_DOUBLE_QUOTE / ESCAPED)
Comment thread
melogtm marked this conversation as resolved.
that handles single/double/backslash quoting and grows token/array buffers
dynamically. This is the most complex and previously-buggy part of the codebase —
trace through `process_character()` in `utils.c` before touching quote-parsing
behavior. On unclosed quotes it returns `NULL` rather than crashing; callers must
handle that. Alongside each token, it fills a parallel `glob_eligible` int array:
a token is glob-ineligible if any part of it came from inside quotes or from a
backslash escape, so `echo "*.c"` and `echo \*.c` are never expanded even though
`echo *.c` is. Downstream code must keep `tokens[i]` and `glob_eligible[i]` in
lockstep — they're always the same length and freed/reallocated together.
- **Glob expansion** (`src/glob.c`): `expand_globs()` runs after tokenizing and
before pipeline/operator parsing. It only expands tokens marked glob-eligible and
containing `*`, `?`, or `[`, using libc `glob()` with `GLOB_NOCHECK` (an unmatched
pattern is passed through literally, shell-style). It rebuilds both the argv and
glob_eligible arrays since one input token can expand to N output tokens.
- **Executor** (`src/execute.c`): `trigger_execute()` expands globs first, then
scans the (glob-eligible-aware) token stream for `|`, `<`, `>`, `>>`. If any are
present as real operators (not inside quotes), it hands off to
`trigger_parse_pipeline()` / `trigger_execute_pipeline()` in `src/pipeline.c`.
Otherwise it checks `args[0]` against the builtins table directly and falls
through to `trigger_launch()` (fork() + execvp() + waitpid()) for external
commands. A bare command with no operators never touches pipeline.c.
- **Pipelines & redirection** (`src/pipeline.c`): `trigger_parse_pipeline()` splits
the token stream on `|` into `PipelineStage` structs (`argv`, `infile`, `outfile`,
`append`), consuming `<`/`>`/`>>` and their filename arguments out of each stage's
argv. `trigger_execute_pipeline()` has a fast path for a single stage with no
redirection (checks builtins, else `trigger_launch()`), a path for a single
builtin stage *with* redirection (saves/restores fds 0/1 around the builtin call
via `dup`/`dup2` since builtins run in-process), and a general N-stage path that
forks one child per stage, wires up `pipe()` fds between adjacent stages, and
waits on all children. In the N-stage path, builtins run inside the forked child
and `exit()` immediately after — only the single-stage paths run a builtin
in-process without forking.
- **Builtins** (`src/builtins.c`): dispatch is two parallel arrays kept in lockstep
by hand — `builtin_str[]` (names: cd, help, exit, pwd, echo, export, unset) and
`builtin_func[]` (matching function pointers), iterated together by index in
`trigger_execute()`, `trigger_execute_pipeline()`, and `trigger_help()`. Adding a
builtin means appending to *both* arrays in the same position — there's no
struct/table enforcing this, so mismatches are easy to introduce.

Headers in `include/` mirror `src/` (`input.h`, `execute.h`, `builtins.h`,
`pipeline.h`, `glob_expand.h`), except `utils/utils.h`, which lives next to its
`.c` file in `src/utils/` rather than in `include/`.

Tests in `tests/` are per-module (`test_input.c`, `test_builtins.c`,
`test_execute.c`, `test_pipeline.c`, `test_glob.c`) and link directly against the
relevant `.c` files rather than a shared static library, using a small custom
assertion framework in `tests/test_framework.h` (no external test dependency).
Loading