Skip to content
34 changes: 27 additions & 7 deletions .claude/skills/running-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Default to targeted. Only run the full suite for broad changes or as a pre-PR ga
| What changed | Run |
| ------------------------------------------------------------- | ---------------------------------------- |
| A specific functional area (e.g. format handling, log levels) | The matching suite(s) only |
| `logging.sh` core logic (init, output routing, sanitization) | Full suite |
| `logging.sh` core logic (init, output routing, sanitization) | Full suite, then the cross-shell run |
| Any change to `logging.sh` | Cross-shell run (see below) |
| `install.sh` | `test_install` only |
| Config file parsing | `test_config` and `test_config_security` |
| Security-related code | Relevant security suites + full suite |
Expand All @@ -45,6 +46,7 @@ After discovering available suites, match by name:
* Journal integration → `test_journal_logging`
* Sanitisation (ANSI, newlines, paths) → `test_ansi_injection`, `test_unsafe_newlines`, `test_path_traversal`, `test_script_name_sanitization`
* Security hardening → `test_environment_security`, `test_sensitive_data`, `test_config_security`, `test_toctou_protection`
* Behaviour that must match in every shell → `test_shell_compat`, `test_posix_compat`, `test_validator_equivalence`, `test_sanitize_equivalence`
* `install.sh` → `test_install`
* JUnit/CI reporting → `test_junit_output`

Expand All @@ -64,28 +66,45 @@ Or via Make (quieter output):
make test
```

### Cross-shell run (required after any change to `logging.sh`)

`logging.sh` runs in bash 3.2+, bash `--posix`, zsh, dash and busybox ash. A plain `./tests/run_tests.sh` runs the four cross-shell suites under bash only, so a failure that
appears only in dash, busybox or zsh is missed. After any change to `logging.sh`, also run:

```bash
TEST_SHELLS="bash,bash --posix,zsh,dash,busybox sh" ./tests/run_tests.sh test_shell_compat test_validator_equivalence test_sanitize_equivalence test_posix_compat
```

* Commas separate shell commands, so a command can carry flags (`bash --posix`, `busybox sh`).
* A shell that is not installed is skipped. Add `TEST_REQUIRE_SHELLS="zsh dash busybox"` to make a missing shell fail the run, as CI does.
* A skipped shell is not a pass. Report which shells actually ran.
* See [docs/testing.md](../../../docs/testing.md#shell-matrix-suites) for the details of each suite.

### Single suite

```bash
./tests/run_tests.sh <suite_name>
```

`<suite_name>` is the filename without the `test_` prefix **and** without `.sh`:
`<suite_name>` is the filename without `.sh`. It **keeps** the `test_` prefix:

```bash
# Correct
./tests/run_tests.sh log_levels
./tests/run_tests.sh config
./tests/run_tests.sh initialization
./tests/run_tests.sh test_log_levels
./tests/run_tests.sh test_config
./tests/run_tests.sh test_initialization

# Also accepted — runner strips the extension
./tests/run_tests.sh test_log_levels.sh

# Wrong — "Test file not found"
./tests/run_tests.sh log_levels
```

### Multiple suites in one invocation

```bash
./tests/run_tests.sh config config_security runtime_config
./tests/run_tests.sh test_config test_config_security test_runtime_config
```

### JUnit XML output (for CI artefacts)
Expand Down Expand Up @@ -145,6 +164,7 @@ Exit code `0` = all passed. Exit code `1` = at least one failure. Fix all failur
## Mistakes to avoid

* Do **not** run from inside the `tests/` directory — always run from repo root.
* Do **not** pass the full path (`tests/test_config.sh`) — pass the suite name only.
* Do **not** pass the full path (`tests/test_config.sh`) — pass the suite name only, with its `test_` prefix.
* Do **not** skip the discovery step and guess suite names — the list changes as the project grows.
* Do **not** treat a bash-only pass as enough after changing `logging.sh` — run the cross-shell command above.
* `test_helpers.sh` and `test_example.sh` are never valid suite arguments.
57 changes: 39 additions & 18 deletions .claude/skills/security-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ description: >

Apply to any change that touches:

* Message processing or formatting (`_sanitize_log_message`, `_strip_ansi_codes`, format strings)
* Message processing or formatting (`_sanitize_log_message_into`, `_strip_ansi_codes_into`, format strings)
* File path handling (`init_logger`, `_parse_config_file`, `_validate_config_file_path`)
* Configuration parsing (new config keys, value parsing, `_parse_config_file`)
* Journal/`logger` integration (`_find_and_validate_logger`, `_write_to_journal`)
Expand All @@ -25,15 +25,26 @@ Apply to any change that touches:

## Domain 1: Input sanitization (log injection)

Every message written to file or journal passes through `_sanitize_log_message`, which calls
both `_strip_ansi_codes` (strips CSI, OSC, DCS, PM, APC sequences) and newline replacement.
Every message written to file or journal passes through `_sanitize_log_message_into`, which
replaces newline, carriage return and tab and calls `_strip_ansi_codes_into`. That removes every CSI
sequence, OSC/DCS/SOS/PM/APC strings, and other control characters (C0 other than newline,
carriage return and tab; C1; U+2028 and U+2029; bidirectional formatting characters U+202A to
U+202E and U+2066 to U+2069). The `_into` functions set `_LOGGER_TEXT` and avoid a subshell. The
older `_sanitize_log_message` and `_strip_ansi_codes` only print that result, and the message
path does not use them.

These characters are kept on purpose (audit finding INF-01), so do not report them as a gap:
U+200E and U+200F (left-to-right and right-to-left marks), U+061C (Arabic letter mark), U+200B
(zero width space) and U+FEFF (byte order mark). They cannot reverse a line the way the removed
overrides and isolates can.

**What to check:**

* New `_log_message` callers pass user-supplied content as the `$message` argument — not
as part of `$level_name`, which is never sanitized (it comes from internal constants only).
* No new code path writes directly to `$LOG_FILE` without first calling `_sanitize_log_message`.
* If you add a new ANSI sequence type, update `_strip_ansi_codes` **and** add a test in
* No new code path writes directly to `$LOG_FILE` without first calling
`_sanitize_log_message_into`.
* If you add a new ANSI sequence type, update `_strip_ansi_codes_into` **and** add a test in
`test_ansi_injection.sh`.
* Format string changes: `LOG_FORMAT` uses `%d`, `%l`, `%s`, `%m`, `%z` only. Never
interpolate raw user data into the format string itself.
Expand Down Expand Up @@ -73,8 +84,8 @@ immediate validation to minimize the TOCTOU window.
**Test suites for this domain:**

```bash
./tests/run_tests.sh toctou_protection
./tests/run_tests.sh path_traversal
./tests/run_tests.sh test_toctou_protection
./tests/run_tests.sh test_path_traversal
```

---
Expand Down Expand Up @@ -102,8 +113,8 @@ type-specific validation.
**Test suites for this domain:**

```bash
./tests/run_tests.sh config_security
./tests/run_tests.sh config
./tests/run_tests.sh test_config_security
./tests/run_tests.sh test_config
```

---
Expand All @@ -119,13 +130,19 @@ poisoning constants.
* New global constants that should not be user-overridable: unset before assignment, then
declare `readonly`. Follow the existing guard pattern:

```bash
if ! readonly -p 2>/dev/null | grep -q "declare -[^ ]*r[^ ]* MY_CONST="; then
```sh
if ! _is_readonly MY_CONST; then
unset MY_CONST 2>/dev/null || true
readonly MY_CONST="value"
_logger_readonly MY_CONST="value"
fi
```

`_is_readonly` tests a variable by trying to unset it in a subshell, which fails only for a
readonly variable. Do not test with `readonly -p | grep ...`: the output differs between shells,
and a value that contains a newline and text such as `readonly NAME=` can make a variable look
readonly (the PR #147 review showed this). `_logger_readonly` keeps the constant global in zsh
when the file is sourced from inside a function. Do not call `readonly` directly.

* New mutable state variables (like `LOG_FILE`): these must not be `readonly`. Ensure they
are reset to a safe default on each `source` so pre-set environment values cannot persist.
* `LOGGER_FILE_ERROR_REPORTED` and `LOGGER_JOURNAL_ERROR_REPORTED`: explicitly unset on
Expand All @@ -137,7 +154,7 @@ poisoning constants.
**Test suites for this domain:**

```bash
./tests/run_tests.sh environment_security
./tests/run_tests.sh test_environment_security
```

---
Expand Down Expand Up @@ -183,7 +200,7 @@ during interactive sessions regardless of level configuration.
**Test suites for this domain:**

```bash
./tests/run_tests.sh sensitive_data
./tests/run_tests.sh test_sensitive_data
```

---
Expand Down Expand Up @@ -218,10 +235,14 @@ value, and `SCRIPT_NAME` at the end of `init_logger` regardless of source.
Run all security suites together:

```bash
./tests/run_tests.sh ansi_injection unsafe_newlines path_traversal toctou_protection \
config_security environment_security sensitive_data script_name_sanitization
./tests/run_tests.sh test_ansi_injection test_unsafe_newlines test_path_traversal \
test_toctou_protection test_config_security test_environment_security \
test_sensitive_data test_script_name_sanitization
```

The suite arguments need the `test_` prefix. `./tests/run_tests.sh ansi_injection` fails with
"Test file not found".

After any security-related change, always follow with the full suite:

```bash
Expand All @@ -233,8 +254,8 @@ After any security-related change, always follow with the full suite:
## Security checklist before committing

* [ ] New external input is sanitized before being written to file, journal, or console
* [ ] No new code path bypasses `_sanitize_log_message`
* [ ] No new constant is set without unset + readonly guard against env override
* [ ] No new code path bypasses `_sanitize_log_message_into`
* [ ] No new constant is set without an `_is_readonly` guard, unset and `_logger_readonly`
* [ ] New mutable state is reset to safe default on source
* [ ] New file path handling uses noclobber → symlink check → existence check → regular-file check → writable check
* [ ] New config key uses an appropriate typed validator; metacharacter injection is not possible
Expand Down
28 changes: 25 additions & 3 deletions .claude/skills/writing-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,36 @@ if [[ -z "${ZSH_VERSION:-}" ]]; then
fi
```

## Step 6 — Run your suite, then the full suite
## Step 6 — Cover other shells when behaviour must match

`logging.sh` is POSIX sh plus `local` and also runs in bash `--posix`, zsh, dash and busybox ash. The suites run under bash. If the new behaviour must be the same in every shell, add a
scenario instead of (or as well as) a bash test:

* Create `tests/compat/scenarios/<name>.sh`: a short POSIX sh script starting with `# shellcheck shell=sh`, which sources `"$LOGGER_ROOT/logging.sh"` and writes any log file to
`"$SCENARIO_DIR/test.log"`. An optional `# compat-locale: C.UTF-8` header line sets the locale (default `C`).
* No registration is needed. `test_shell_compat` runs every scenario under each shell in `TEST_SHELLS` and compares the results with bash.
* List a shell and scenario that is expected to differ in `tests/compat/known_failures.txt` (`<shell command>|<scenario file name>`). Never add an entry to hide a new regression.
* Do **not** regenerate the golden files in `tests/compat/golden/` from the current `logging.sh`. They record behaviour before the POSIX conversion.
* Helpers for loops over shells are in `tests/test_helpers.sh`: `test_shell_list`, `require_shell`, `run_with_logger_in_shell`, `shell_counts_characters`, `run_with_timeout`.

See [Tests that run under more than one shell](../../../docs/writing-tests.md#tests-that-run-under-more-than-one-shell).

## Step 7 — Run your suite, then the full suite

```bash
# New suite only first
./tests/run_tests.sh <suite_name>
./tests/run_tests.sh test_<feature>

# Then confirm no regressions
./tests/run_tests.sh
```

After any change to `logging.sh`, also run the cross-shell suites (see the `running-tests` skill):

```bash
TEST_SHELLS="bash,bash --posix,zsh,dash,busybox sh" ./tests/run_tests.sh test_shell_compat test_validator_equivalence test_sanitize_equivalence test_posix_compat
```

## Checklist before committing

* [ ] Function called at the bottom of the suite file
Expand All @@ -134,5 +154,7 @@ fi
* [ ] Every assertion uses `|| return`
* [ ] All file I/O uses `$TEST_DIR`
* [ ] `init_logger` called after `start_test`, not at suite top-level
* [ ] Suite passes: `./tests/run_tests.sh <suite_name>`
* [ ] Suite passes: `./tests/run_tests.sh test_<feature>`
* [ ] Full suite still passes: `./tests/run_tests.sh`
* [ ] If `logging.sh` changed: cross-shell run passes with `TEST_SHELLS` (or each skipped shell is reported)
* [ ] If the behaviour must match across shells: scenario added in `tests/compat/scenarios/`
38 changes: 31 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,28 @@

![Logo](assets/images/logo-640x320.png)

A flexible, reusable logging module for Bash scripts that provides standardized logging functionality with various configuration options.
A flexible, reusable logging module for shell scripts that provides standardized logging functionality with various configuration options.
It runs in bash 3.2+, zsh, dash and busybox ash from one file.

> [!NOTE]
> This is a expansion of the originally published [bash_logging GitHub gist](https://gist.github.com/GingerGraham/99af97eed2cd89cd047a2088947a5405) published by [@GingerGraham](https://github.com/GingerGraham).
> **Upgrading from 2.x?** 3.0 is a major release with some behaviour changes. See [Upgrading to 3.0](docs/upgrading-to-3.0.md) before you update.

## Primary artifact

The core deliverable of this repository is **[logging.sh](logging.sh)**. It is intentionally kept
as a single, self-contained (and yes, long) Bash file so you can drop it next to your
scripts and `source` it without any packaging steps. Everything else in the repo - docs,
as a single, self-contained (and yes, long) shell file, written in POSIX sh plus `local`, so you can drop it next to your
scripts and `source` it (or `.` it in a POSIX sh script) without any packaging steps. The file keeps a `#!/usr/bin/env bash` line only so that
running it directly prints its usage message. Everything else in the repo - docs,
demos, and pipeline scripts - exists to support using that file.

> [!NOTE]
> This is an expansion of the originally published
> [bash_logging GitHub gist](https://gist.github.com/GingerGraham/99af97eed2cd89cd047a2088947a5405)
> published by [@GingerGraham](https://github.com/GingerGraham).

## Features

* Runs in bash 3.2+, bash `--posix`, zsh, dash and busybox ash from one file
* Standard syslog log levels (DEBUG, INFO, WARN, ERROR, CRITICAL, etc.)
* Console output with color-coding by severity
* Configurable stdout/stderr output stream split
Expand All @@ -36,7 +44,8 @@ demos, and pipeline scripts - exists to support using that file.
* Runtime configuration changes
* Special handling for sensitive data
* Secure-by-default newline sanitization to prevent log injection
* Secure-by-default ANSI code stripping to prevent terminal manipulation attacks
* Secure-by-default removal of terminal control sequences and other control characters to prevent terminal manipulation attacks
(see [Upgrading to 3.0](docs/upgrading-to-3.0.md#messages-and-control-characters) for the full list)
* Configurable log line length limits for DoS resistance
* TOCTOU race condition protection during log file creation

Expand All @@ -53,12 +62,15 @@ demos, and pipeline scripts - exists to support using that file.
`scripts/check-bash32-compat.sh` rejects bash 4+ syntax and `scripts/check-posix-compat.sh`
rejects syntax that dash does not support. See [Shell Compatibility](docs/shell-compatibility.md)
for the few documented differences between shells (script name detection and how truncation
counts multi-byte characters). Upgrading from 2.x? See [Upgrading to 3.0](docs/upgrading-to-3.0.md).
counts multi-byte characters).

## Quick Start

In a bash or zsh script:

```bash
# Source the logging module (use ". /path/to/logging.sh" in a POSIX sh script)
#!/usr/bin/env bash
# Source the logging module
source /path/to/logging.sh

# Initialize the logger
Expand All @@ -69,6 +81,18 @@ log_info "Application started"
log_error "Something went wrong"
```

In a POSIX sh script (dash, busybox ash, or any `/bin/sh` on Debian, Ubuntu and Alpine), use `.` instead of `source`:

```sh
#!/bin/sh
# Source the logging module
. /path/to/logging.sh

init_logger
log_info "Application started"
log_error "Something went wrong"
```

See [Getting Started](docs/getting-started.md) for detailed installation and basic usage instructions.

## Documentation
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ This minimal dependency surface reduces supply chain risk.
When using bash-logger in your applications:

1. **Never log sensitive data** with standard functions
2. **Set restrictive permissions** on log files (600 or 640)
2. **Set restrictive permissions** on log files. A file that `init_logger` creates is already mode 600. Use 640 and `chgrp` only to share it with a group
3. **Validate user input** before logging
4. **Review journal access** if using systemd integration
5. **Rotate logs** to prevent information accumulation
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Bash Logger Documentation

Welcome to the bash-logger documentation! This directory contains comprehensive guides for using the bash logging module.
Welcome to the bash-logger documentation! This directory contains comprehensive guides for using bash-logger.

## Getting Started

Expand Down
Loading
Loading