Skip to content

fix(cli): honor json from config file and MMTREADER_JSON (#96) - #98

Merged
luongnv89 merged 4 commits into
mainfrom
fix/96-config-env-json-ignored
Aug 27, 2026
Merged

luongnv89 merged 4 commits into
mainfrom
fix/96-config-env-json-ignored

Conversation

@luongnv89

Copy link
Copy Markdown
Collaborator

Closes #96

Summary

json = 1 in a config file and MMTREADER_JSON=1 were silently ignored: cli_options_t carried two fields meaning one thing — output_format, which every output decision actually reads, and json, whose only reader was an argument to a verbose debug printf. The config path and the environment path both wrote json alone, so only -j/--json (which wrote both) had any effect. This deletes json, makes output_format the single carrier, and — because acceptance criterion 3 cannot hold otherwise — also fixes the precedence inversion where a --config file beat explicit CLI flags. Both config files now load before the getopt loop under one rule: built-in defaults < ~/.mmtreader.conf < --config file < environment < CLI flags.

Approach

Option 2 — Balanced: unify both config paths and make output_format the single carrier. A new parse_prescan_config_path() resolves the --config value before getopt_long runs (all four spellings, clustered short options, -- terminator, unambiguous long-option abbreviations, last occurrence wins), so both config files are applied through one helper ahead of the option loop, the environment is applied once on top, and the CLI writes last and wins naturally. Because config now precedes the loop, no per-key presence tracking is needed — an unconditional copy overwrites nothing but built-in defaults.

Decision Record

  • Root cause: cli_options_t had two fields for one concept. output_format (cli/parse.h:64) is the only field read behaviorally (mmtReader.c:84, :114 engine_set_output_format, :257); json (cli/parse.h:69) had exactly one reader — an argument to a debug fprintf at mmtReader.c:94. parse_copy_config_fields() and env_apply_int("MMTREADER_JSON", &opts->json) both wrote the dead field, so neither documented path could ever select JSON. Compounding it, parse_reload_custom_config() ran after the getopt loop and re-copied all five shared fields unconditionally with no environment re-application, so a --config file beat explicit CLI flags — the opposite precedence from ~/.mmtreader.conf, which loads before the loop. Neither defect is a regression from [3.5] Split parse_options into layered helpers #67/PR refactor(cli): split parse_options into layered helpers (#67) #91: that refactor extracted the helpers and preserved the ordering verbatim, making the defect legible rather than introducing it.
  • Options considered: Option 1 — Minimal fix (derive output_format from the resolved json flag plus a CLI tri-state); Option 2 — Balanced (unify both config paths, output_format as single carrier); Option 3 — Comprehensive (per-key presence tracking in config_t plus revival of the seven dead config keys).
  • Options rejected: Option 1 — patches only the json symptom while leaving the confirmed config-beats-CLI inversion on no_color, verbose and buffer_mb, so the same class of bug is refilable the day it merges. Option 3 — reviving seven dead config keys and changing the public config_t is required by no acceptance criterion, and the ordering fix in Option 2 already makes presence tracking unnecessary.
  • Selected option: Option 2 — Balanced: unify both config paths and make output_format the single carrier.
  • Residual risk: The --config pre-scan is a hand-rolled mirror of getopt_long; SHORT_OPTS_WITH_ARG is a second literal that must be kept in sync with the getopt optstring by hand. Verified against a 20-case argv matrix (see Test Results). Two pre-existing conditions are deliberately left alone and confirmed unchanged, not regressed: a config-file buffer still bypasses the CLI's 1–10000 bound before the (uint16_t) cast at mmtReader.c:186 (parse_validate_final() never checked buffer_mb on either side, and is still the last call in parse_options()), and a nonexistent --config path is still silently ignored.
  • Design-confirm: auto-selected Option 2 (complexity: M, risk: Medium).
  • Reproduction: printf 'json = 1\n' > "$T/mmt.conf"; ./mmtReader analyze -t smallFlows.pcap --config "$T/mmt.conf" | jq . and MMTREADER_JSON=1 ./mmtReader analyze -t smallFlows.pcap | jq . confirmed red for the stated reason — both emitted the text banner and jq exited 5 (parse error) while the --json control exited 0 → regression tests tests/test_cli.sh (Issue json option from config file and MMTREADER_JSON env var are silently ignored #96 section) and tests/test_parse.c::test_custom_config_json_sets_output_format / test_env_json_sets_output_format / test_custom_config_loses_to_cli_flags. Durability independently proven: the new test_cli.sh run against a binary built from HEAD~1 fails 8 of the new asserts (44/52); it passes 52/52 with the fix.

Analyzed at: fix/96-config-env-json-ignored @ 38c3e88 (2026-08-27)

Changes

File Change
cli/parse.c Added parse_prescan_config_path() + long_opt_lookup() under a Config-file pre-scan banner; replaced parse_load_default_config()/parse_reload_custom_config() with parse_apply_env() + parse_load_config_files(); parse_copy_config_fields() now writes output_format; MMTREADER_JSON mapped explicitly onto OUTPUT_FORMAT_TEXT/JSON so an out-of-range value cannot leave a bogus format; parse_options() reordered to the single precedence rule; the stale "preserved verbatim" comment replaced by the criterion-5 field audit
cli/parse.h Deleted the dead int json;; output_format documented as the single carrier. OUTPUT_FORMAT_* #defines deliberately untouched (they name-collide with core/engine.h's enum; the include order in mmtReader.c is load-bearing)
mmtReader.c Debug printf argument opts.jsonopts.output_format == OUTPUT_FORMAT_JSON; includes not reordered
tests/test_parse.c 5 opts.json asserts retargeted to the live opts.output_format; 2 new helpers and 11 new tests (73 → 120 asserts)
tests/test_cli.sh Strengthened the two exit-code-only JSON checks into real output assertions; new Issue #96 section covering both config paths, the env path, and the CLI-beats-config regressions (35/35 → 52/52)
docs/CONFIG.md One unified Priority Order for both config paths; new "Effective?" columns marking which keys are actually consumed; corrected worked example
docs/TESTING.md, docs/DEVELOPMENT.md, AGENT_ENVIRONMENT.md, README.md Updated hard-coded suite totals and the config/env precedence claim
CHANGELOG.md New Fixed entry for #96

Test Results

  • Unit tests: 299 passed (config 40, anomaly 9, parse 120, wifi 43, flows 38, capture dispatch 37, engine output 7, engine stats 5) — up from 252
  • Integration tests: 52 / 52 passed (tests/test_cli.sh) — up from 35/35; plus 6 / 6 SDK version-gate checks
  • E2e tests: skipped — no e2e framework in this project
  • Build: passed — make clean && make test exits 0, ends with All tests passed!, 0 compiler warnings under the -Wall -Wextra gate, exactly one tolerated skip (SKIP: live capture on 'lo' unavailable — Operation not permitted, root-gated by design)
  • QA cycles: 2 (review returned PASS with 0 blocking issues; one fix cycle cleared 3 note-level findings)
  • Reviewer additionally ran a 20-case --config argv matrix against the built binary: -c path, -cpath, --config path, --config=path, --conf/--c abbreviations and the -qc file cluster all load the config; -- terminates the scan; -x -c CONF -t f.pcap correctly does not treat an option value as the flag; missing values, a directory path, a missing file, an ambiguous --t and a bare - all exit cleanly with no crash or out-of-bounds read

Acceptance Criteria Verification

Criterion Status Evidence
printf 'json = 1\n' > /tmp/j.conf && ./mmtReader analyze -t smallFlows.pcap --config /tmp/j.conf | jq . succeeds (valid JSON) pass Verified red: analyze -t smallFlows.pcap --config <json=1> emitted the text banner and jq exited 5 → fixed → tests/test_cli.sh Issue #96 section + tests/test_parse.c::test_custom_config_json_sets_output_format
MMTREADER_JSON=1 ./mmtReader analyze -t smallFlows.pcap | jq . succeeds (valid JSON) pass Verified red: same text banner, jq exit 5 → fixed → tests/test_cli.sh Issue #96 section + tests/test_parse.c::test_env_json_sets_output_format
Precedence holds: config json = 1 overridden by --text; MMTREADER_JSON=0 overridden by --json pass Verified red on the sibling fields (-b 100 --config <buffer=777> used 777; -C --config <no_color=0> kept 37 ANSI sequences; -v --config <verbose=0> gave 0 DEBUG lines) → fixed → tests/test_parse.c::test_custom_config_loses_to_cli_flags and the --text / MMTREADER_JSON=0 --json cases in tests/test_cli.sh. Post-fix spot check: -b 100 --config <buffer=777> now reports buffer size: 100
tests/test_cli.sh asserts the produced OUTPUT is JSON for both the env-var and config-file paths, not merely exit code 0 pass tests/test_cli.sh — the two exit-code-only checks (env var, and the --json flag itself) now assert output shape, plus a 13-assert Issue #96 section; total 35/35 → 52/52
Same audit applied to quiet, verbose, no_color, buffer_mb — confirm each is read, or report which are dead pass Recorded in the replacement comment in cli/parse.c and in docs/CONFIG.md's "Effective?" columns — see Field audit below
make clean >/dev/null && make test exits 0, 0 failures (1 tolerated root-gated skip) pass Exit 0, All tests passed!, 299 unit asserts + 52/52 CLI + 6/6 SDK, 0 failures, 0 warnings, exactly one SKIP: live capture on 'lo'

Field audit (acceptance criterion 5)

Of the five fields parse_copy_config_fields() writes:

Field Verdict Evidence
json was DEAD for behavior — fixed here Only reader was a debug-printf argument at mmtReader.c:94
quiet LIVE, but live-capture path only mmtReader.c:173, :244, both inside the MODE_LIVE_INTERFACE branch — so -q has no observable effect on analyze (which emits no INFO: lines at all), making the existing tests/test_cli.sh:139 assertion vacuous. Reported, not fixed — out of scope
verbose LIVE and general mmtReader.c:91, :149, :160, :181, :269; no MMTREADER_VERBOSE env var exists (config + CLI only)
no_color LIVE and general mmtReader.c:67
buffer_mb LIVE, but live-capture path only mmtReader.c:174:186 (capture_init is the real consumer); analyze uses pcap_open_offline() and ignores it

Additionally found and reported, not revived (no acceptance criterion asks for them; suitable for a follow-up issue): seven config keys are parsed by config.c but never copied into the options — proto_path, sessions, output_format (fully dead, no reader anywhere), ip_classify, hostname_classify, port_classify, and the per-section [analyze] buffer / [capture] buffer. docs/CONFIG.md now marks these honestly instead of promising behavior that does not exist.

Notes for reviewers

cli_options_t carried two fields meaning one thing: output_format, which
every output decision actually reads, and json, whose only reader was an
argument to a verbose debug printf. The config path
(parse_copy_config_fields) and the environment path (MMTREADER_JSON) both
wrote json alone, so `json = 1` in a config file and MMTREADER_JSON=1 were
silently ignored — only -j/--json, which wrote both, had any effect.

Delete json and make output_format the single carrier. The config copy and
the environment application now write it, and MMTREADER_JSON is mapped
explicitly onto OUTPUT_FORMAT_TEXT/JSON so an out-of-range value cannot
leave a bogus format behind. env_apply_int keeps its contract: an unset or
empty variable still leaves the config value untouched.

Second, deliberate behavior change: a -c/--config file used to be re-read
*after* the getopt loop and re-copied unconditionally, so it beat explicit
CLI flags (`-b 100 --config <buffer=777>` used 777) and skipped the
environment entirely — the opposite precedence from ~/.mmtreader.conf,
which loads before the loop. The comment above the old reload helper
recorded that inversion as intentional and preserved it verbatim; it is
intentional no longer. A new pre-scan finds the --config path before
getopt runs (all four spellings, clustered short options, "--" terminator,
last occurrence wins, unambiguous long-option abbreviations), both config
files are loaded through the same helper before the loop, and the
environment is applied once on top. One rule for both files:

  built-in defaults < ~/.mmtreader.conf < --config file < env < CLI flags

case 'c' still records the path so getopt keeps accepting --config; the
value it records is the same one the pre-scan already resolved.

The replaced comment now carries the audit of all five config-backed
fields: quiet and buffer_mb are live only on the live-capture path,
verbose and no_color are live and general, json was the dead one.
The two checks that covered MMTREADER_JSON=1 and --json asserted only
exit code 0, which the broken build satisfied while emitting the text
table. Both now assert the produced OUTPUT: stdout parses as JSON with an
input_stats key, and the text table is absent.

A new "Issue #96" section covers the config file, which the script never
touched before: json = 1 produces JSON; --text overrides it; --json
overrides MMTREADER_JSON=0; MMTREADER_JSON=5 normalizes to JSON rather
than a bogus format; the environment beats a --config file; and -v, -C
and -b each beat a conflicting --config file. Temp configs go in an
mktemp -d, matching the existing convention.

Against the pre-fix binary 8 of these go red; they pass with the fix.
Unprivileged run: 35/35 -> 52/52 (17 new asserts, all outside the
root-gated live-capture block).

tests/test_parse.c gains ten unit tests for the same behavior at the
parser seam, plus make_bare_home() to isolate a real ~/.mmtreader.conf
and make_test_conf() for an arbitrary -c path (make_test_home hardcodes
the .mmtreader.conf basename). They pin the environment path, the
out-of-range clamp, all four --config spellings, last-occurrence-wins,
--config over the default config, and every field a --config file writes
losing to its CLI flag. 73 -> 118 asserts.
docs/CONFIG.md described a priority order that no longer matched either
config path: it ranked sections above the global block and never said
which of the two config files won. It now states the single rule —
compiled defaults < ~/.mmtreader.conf < --config file < environment <
CLI flags — records that the two files used to have opposite precedence,
and explains that a --config file replaces rather than merges the values
the default file supplied.

Both key tables gain an "Effective?" column. proto_path, sessions,
output_format, ip_classify, hostname_classify, port_classify and the
per-section buffer are parsed and then never read; quiet and buffer only
reach the live-capture path. The worked example promised proto paths,
session counts and a 100 MB capture buffer that the binary never applied,
and implied section keys are scoped to their command when json, quiet,
verbose and no_color share one global slot — both corrected.

Test counts follow the new asserts: 252 -> 297 unit asserts (parse
73 -> 118) and 35/35 -> 52/52 CLI integration checks, across
AGENT_ENVIRONMENT.md, docs/TESTING.md, docs/DEVELOPMENT.md and README.md.
The counts recorded in the historical #72 changelog entry are left frozen.

docs/USER_GUIDE.md is deliberately untouched — open issue #97 targets it.
@luongnv89
luongnv89 merged commit ccbb631 into main Aug 27, 2026
2 checks passed
@luongnv89
luongnv89 deleted the fix/96-config-env-json-ignored branch August 27, 2026 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

json option from config file and MMTREADER_JSON env var are silently ignored

1 participant