check_bash is a SHA-256–based answer-checking tool for bash code chunks
in Quarto notebooks. It was developed for
DSC 011: Computing and Statistical Programming
at UC Merced to give students immediate feedback on both the output of
their shell commands and the structure of their code (required commands,
pipeline depth, flags used).
It is the bash companion to the R-based print_and_check / dsc011_check
system used in the same course.
- Features
- Prerequisites
- Installation
- Typical workflow
- Usage
- Quarto / RStudio Integration
- Instructor Key Generation
- All Options
- Just Recipes
- Contributing
- License
- Output checking — hash-verify the stdout of any command or pipeline
- File checking — hash-verify the byte contents of any output file
- Code structure checking — enforce required commands, forbidden commands, required or forbidden flags/options, and exact or bounded pipeline depth
- Combined checking — one call checks both output correctness and code structure, with separate labeled result lines
- Instructor key generation —
--make-keyflag produces hashes to paste into KEY notebooks; no separate tool needed - Cross-platform — auto-detects
sha256sum(Linux/WSL2) andshasum -a 256(macOS); produces identical hashes on both - Whitespace normalization — optional
-nflag for forgiving matching - Injection-safe —
--codestrings are analyzed as text, never executed - Zero runtime dependencies — pure bash, requires only coreutils
Required:
| Tool | macOS | WSL2/Ubuntu |
|---|---|---|
just |
brew install just |
sudo apt install just |
| bash 5+ | brew install bash |
included |
Optional (only needed for just check):
| Tool | macOS | WSL2/Ubuntu |
|---|---|---|
shellcheck |
brew install shellcheck |
sudo apt install shellcheck |
macOS only — one-time system setup:
RStudio launched from the Dock does not see Homebrew's PATH. Fix this once:
echo /opt/homebrew/bin | sudo tee /etc/paths.d/homebrewThen restart your Mac. See Quarto / RStudio Integration for details.
git clone https://github.com/dhard/dsc011-check-bash.git
cd dsc011-check-bash
just install-system # installs to /usr/local/bin, prompts for sudo if neededOr to install without sudo to ~/bin:
just installTo remove a previous install before switching locations:
just uninstall # removes from ~/bin and/or /usr/local/bin
just install-system # then install to systemIf you just want check_bash without cloning the repo:
curl -fsSL https://raw.githubusercontent.com/dhard/dsc011-check-bash/main/install_check_bash.sh | bashThis installs to ~/bin and adds it to your PATH automatically.
For a system install via curl:
curl -fsSL https://raw.githubusercontent.com/dhard/dsc011-check-bash/main/install_check_bash.sh | bash -s -- --systemcheck_bash --version # check_bash 2.2
echo "hello" | check_bash --make-key # prints a 64-char hex string# Install just and clone the repo
brew install just # macOS
sudo apt install just # WSL2/Ubuntu
git clone https://github.com/dhard/dsc011-check-bash.git
cd dsc011-check-bash
# Read the docs in your terminal
glow README.md
# Remove any previous install
just uninstall
# Run the test suite to verify everything works
just test
# Install system-wide
just install-system
check_bash --versionPipe command output directly into check_bash:
some_command | check_bash <HASH>Example:
wc -l /etc/hosts | check_bash "a3f2..."Output:
12 /etc/hosts
✓ CORRECT
Assign output to a variable, then check it. This mirrors the R
answer <- ...; print_and_check(answer, "hash") pattern:
answer=$(some_command)
check_bash "$answer" <HASH>Example:
answer=$(wc -l < /etc/hosts)
check_bash "$answer" "a3f2..."Check the raw byte contents of a file:
check_bash --file <path> <HASH>Example:
grep "Merced" data.csv > results.txt
check_bash --file results.txt "b7c9..."Output:
[file: results.txt]
SHA-256: b7c9...
✓ CORRECT
Check that student code uses required tools and pipeline structure, without checking output. Useful when output is non-deterministic but the approach must be specific:
MY_CODE='cat data.csv | grep "Merced" | wc -l'
check_bash --code "$MY_CODE" \
--requires grep \
--requires wc \
--forbid awk \
--pipeline 3Output:
[code]
cat data.csv | grep "Merced" | wc -l
✓ requires 'grep'
✓ requires 'wc'
✓ forbids 'awk'
✓ pipeline has exactly 3 stage(s)
✓ CORRECT
For multi-line pipelines, use a heredoc:
MY_CODE=$(cat <<'EOF'
find . -name "*.csv" \
| xargs grep "Merced" \
| wc -l
EOF
)
check_bash --code "$MY_CODE" \
--requires find --requires grep --requires wc \
--pipeline 3The recommended pattern for most exercises. The student writes the command
once as MY_CODE — it is both executed and structure-checked in one call:
MY_CODE='ls -la | grep ".csv" | wc -l'
eval "$MY_CODE" | check_bash \
--code "$MY_CODE" \
--requires ls \
--requires grep \
--requires wc \
--forbid cat \
--pipeline 3 \
"<OUTPUT_HASH>"Output (both correct):
4
✓ requires 'ls'
✓ requires 'grep'
✓ requires 'wc'
✓ forbids 'cat'
✓ pipeline has exactly 3 stage(s)
Output: ✓ CORRECT
Code: ✓ CORRECT
Output (correct output, wrong structure):
4
✗ requires 'ls' — not found in code
✓ requires 'grep'
✓ requires 'wc'
✓ forbids 'cat'
✗ pipeline has 2 stage(s), expected exactly 3
Output: ✓ CORRECT
Code: ✗ INCORRECT
Add to the setup chunk of every bash-using notebook:
```{r setup, include=FALSE}
knitr::opts_chunk$set(
echo = TRUE,
cache = TRUE,
warning = FALSE,
message = FALSE,
collapse = FALSE,
comment = "", # removes ## prefix from bash chunk output
error = TRUE # prevents failed check_bash from killing the render
)
# Fix PATH for bash chunks on macOS (safe no-op on Linux/WSL2)
homebrew_paths <- c("/opt/homebrew/bin", "/usr/local/bin")
existing <- homebrew_paths[dir.exists(homebrew_paths)]
if (length(existing) > 0)
Sys.setenv(PATH = paste(c(existing, Sys.getenv("PATH")), collapse = ":"))
```Add this chunk immediately after setup in every bash-using notebook. It gives students a clear, actionable error if the wrong bash is being used:
```{bash bash-check}
if [[ "${BASH_VERSINFO[0]}" -lt 4 ]]; then
echo "ERROR: bash $BASH_VERSION is too old."
echo "Fix: launch RStudio from your terminal: open -a RStudio"
echo "Or: echo /opt/homebrew/bin | sudo tee /etc/paths.d/homebrew"
exit 1
fi
echo "✓ bash $BASH_VERSION — $(which bash)"
```RStudio launched from the Dock inherits a minimal PATH from launchd that
excludes /opt/homebrew/bin. The permanent fix uses /etc/paths.d/, which
macOS's path_helper reads for all processes including GUI apps:
echo /opt/homebrew/bin | sudo tee /etc/paths.d/homebrew
# Then restart your MacThis is different from ~/.zshrc, which only applies to interactive
terminal sessions. After this fix, RStudio launched from the Dock and
your terminal both use the same Homebrew bash 5.
WSL2/Ubuntu users do not need this fix — Ubuntu ships bash 5.x and RStudio Server inherits the correct PATH automatically.
```{bash}
#| label: ex-pipeline
MY_CODE='cat /etc/hosts | grep "local" | wc -l'
eval "$MY_CODE" | check_bash \
--code "$MY_CODE" \
--requires grep \
--requires wc \
--pipeline 3 \
"YOUR_HASH_HERE"
```A complete setup chunk template with all recommended options is in
docs/DSC011_bash_setup_reference.qmd.
--make-key is an instructor-only flag. Students should never need it.
# Output hash (pipe pattern)
echo "expected output" | check_bash --make-key
# Output hash (capture pattern)
answer=$(some_command)
check_bash --make-key "$answer"
# File hash
check_bash --file expected_output.csv --make-key
# Normalized hash (student must also use -n)
some_command | check_bash -n --make-key| Option | Short | Description |
|---|---|---|
--version |
-V |
Print version and exit. |
--normalize |
-n |
Strip and normalize whitespace before hashing. |
--quiet |
-q |
Suppress printing the answer value. |
--file <path> |
-f |
Hash raw bytes of a file (always exact). |
--code <string> |
-c |
Student code string to analyze structurally. |
--requires <cmd> |
-r |
Command that must appear in --code. Repeatable. |
--forbid <cmd> |
-F |
Command that must NOT appear in --code. Repeatable. |
--pipeline <N> |
-p |
Code must have exactly N pipeline stages. |
--pipeline-min <N> |
Pipeline must have at least N stages. | |
--pipeline-max <N> |
Pipeline must have at most N stages. | |
--requires-flag <f> |
Flag/option that must appear (e.g. "-r"). Repeatable. |
|
--forbid-flag <f> |
Flag/option that must NOT appear. Repeatable. | |
--help |
-h |
Show usage information. |
--make-key |
-k |
(Instructor only) Print SHA-256 hash for KEY notebooks. |
From inside the cloned repo:
| Recipe | Description |
|---|---|
just install |
Install to ~/bin (no sudo) |
just install-system |
Install to /usr/local/bin (sudo if needed) |
just uninstall |
Remove from ~/bin and/or /usr/local/bin |
just test |
Run the full test suite (89 tests) |
just check |
Run ShellCheck on all scripts (requires shellcheck) |
just version |
Print current check_bash version |
See CONTRIBUTING.md for how to report bugs, suggest features, and submit pull requests.
MIT License. Copyright (c) 2026 David Ardell. See LICENSE for full text.
Developed for DSC 011: Computing and Statistical Programming, UC Merced.