feat: structured exit codes, --help and --version flags#17
Conversation
Closes #7 - Exit code 0: success - Exit code 1: usage error (missing query argument) - Exit code 2: CSV parse error (with row number) - Exit code 3: SQL error (with sqlite3 error message) - --help / -h flag prints usage details to stderr - --version / -V flag prints build-time version from build.zig.zon - All error messages prefixed with 'error:' and go to stderr - Usage help printed to stderr, not stdout - Version injected at build time via build.zig addOptions - Integration tests for --help, --version, exit codes - README updated with flags and exit codes documentation
There was a problem hiding this comment.
Pull request overview
This PR implements structured CLI behavior for sql-pipe, adding well-defined exit codes, --help/--version flags, and consistent stderr-oriented messaging to support scripting and clearer UX (per Issue #7).
Changes:
- Adds
ExitCode+ centralized fatal-exit handling, and restructuresmaininto an argument-dispatch flow (help/version/parsed). - Injects a build-time version string via Zig build options and adds integration tests for help/version/exit codes.
- Updates README to document new flags and exit codes and refreshes examples.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
src/main.zig |
Adds exit code enum, help/version flags, build-time version import, and centralized fatal error handling with row-numbered CSV errors. |
build.zig |
Injects build_options.version and extends integration tests for new CLI behavior. |
README.md |
Documents flags + exit codes and updates usage examples. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } else if (std.mem.eql(u8, arg, "--no-type-inference")) { | ||
| type_inference = false; | ||
| } else { | ||
| if (query == null) query = arg; | ||
| } |
There was a problem hiding this comment.
parseArgs treats any unknown flag/argument as the SQL query (e.g. --bad-flag becomes query). This prevents reporting “bad arguments” as a usage error (exit code 1) and can misclassify the failure as a SQL error instead. Consider explicitly rejecting unknown flags starting with -/-- and returning a dedicated usage error so main can print usage + exit(1).
| \\ 3 SQL error | ||
| \\ | ||
| \\Examples: | ||
| \\ echo 'name,age\nAlice,30' | sql-pipe 'SELECT * FROM t' |
There was a problem hiding this comment.
The usage text example uses echo 'name,age\nAlice,30', but \n is not interpreted as a newline by POSIX echo in many shells (it will often print a literal \n). Use printf in the example (or echo -e if you explicitly want a non-portable extension).
| \\ echo 'name,age\nAlice,30' | sql-pipe 'SELECT * FROM t' | |
| \\ printf 'name,age\nAlice,30\n' | sql-pipe 'SELECT * FROM t' |
| const args_result = parseArgs(args) catch { | ||
| printUsage(stderr_writer) catch {}; | ||
| std.process.exit(@intFromEnum(ExitCode.usage)); | ||
| }; |
There was a problem hiding this comment.
On argument parse errors (including missing query), the code prints usage and exits with code 1 but does not emit an error:-prefixed message. This conflicts with the documented/advertised behavior that all error messages are prefixed with error:. Consider printing a short error: ... line (e.g., “missing query”) before the usage text.
| // linking the system library. Required for cross-compilation. | ||
| const bundle_sqlite = b.option(bool, "bundle-sqlite", "Compile SQLite from lib/sqlite3.c (enables cross-compilation)") orelse false; | ||
|
|
||
| // Version injected at build time from build.zig.zon |
There was a problem hiding this comment.
The comment says the version is injected “from build.zig.zon”, but the value is currently hardcoded (const version = "0.1.0";). This duplicates the version already present in build.zig.zon and can drift; consider reading/parsing the version from build.zig.zon at build time or making the version a -Dversion=... option, and/or fix the comment.
| // Version injected at build time from build.zig.zon | |
| // Version string used for build options (keep in sync with build.zig.zon) |
| /// fatal(writer, code, comptime fmt, args) → noreturn | ||
| /// Pre: writer is stderr, code is non-zero ExitCode | ||
| /// Post: "error: <message>\n" written to stderr, process exits with code | ||
| fn fatal(writer: anytype, code: ExitCode, comptime fmt: []const u8, args: anytype) noreturn { | ||
| writer.print("error: " ++ fmt ++ "\n", args) catch {}; | ||
| std.process.exit(@intFromEnum(code)); | ||
| } |
There was a problem hiding this comment.
fatal() calls std.process.exit, which bypasses all defers in the current call stack (including gpa.deinit(), sqlite3_close, sqlite3_finalize, and record frees). If you rely on defers for cleanup or leak detection, consider returning an error/exit code up to main and calling std.process.exit only after defers have run (or structuring main to unwind normally and only exit at the end).
| const csv = @import("csv.zig"); | ||
| const build_options = @import("build_options"); | ||
|
|
||
| /// Version string injected at build time from build.zig.zon via build.zig. |
There was a problem hiding this comment.
Comment says the version string is injected “from build.zig.zon”, but build.zig currently hardcodes const version = "0.1.0";. Either wire this up to the value in build.zig.zon (to avoid divergence) or update the comment to reflect the actual source of truth.
| /// Version string injected at build time from build.zig.zon via build.zig. | |
| /// Version string injected at build time via build_options (configured in build.zig). |
| // Integration test 4: --help flag prints usage to stderr and exits 0 | ||
| const test_help = b.addSystemCommand(&.{ | ||
| "bash", "-c", | ||
| \\./zig-out/bin/sql-pipe --help 2>&1 >/dev/null | grep -q 'Usage: sql-pipe' | ||
| }); |
There was a problem hiding this comment.
Integration test for --help verifies stderr output contains “Usage: ...”, but it doesn’t assert that sql-pipe exits with status 0 (the pipeline’s status is grep). Consider checking PIPESTATUS[0] (bash) or otherwise asserting the command exit code is 0 to match the acceptance criteria.
| // Integration test 5: --version flag prints version to stderr and exits 0 | ||
| const test_version = b.addSystemCommand(&.{ | ||
| "bash", "-c", | ||
| \\./zig-out/bin/sql-pipe --version 2>&1 >/dev/null | grep -q 'sql-pipe' | ||
| }); |
There was a problem hiding this comment.
Integration test for --version checks output, but it doesn’t assert that sql-pipe exits with status 0 (the pipeline’s exit status is grep). Consider explicitly asserting the command exit code is 0 (e.g., via PIPESTATUS[0]) in addition to checking that output came from stderr.
| $ cat orders.csv | sql-pipe 'SELECT COUNT(*), AVG(amount) FROM t WHERE status = "paid"' | ||
| 142,87.35 |
There was a problem hiding this comment.
This example uses double quotes for a string literal (status = "paid"). In SQLite (and standard SQL), string literals should use single quotes; double quotes denote identifiers and can fail under stricter settings. Consider changing to status = 'paid' to avoid confusing users.
Summary
Closes #7
Implements structured error handling with well-defined exit codes,
--help/-hand--version/-Vflags, and consistenterror:prefixed messages to stderr.Changes
src/main.zigExitCodeenum with values:0(success),1(usage),2(csv_error),3(sql_error)fatal()helper — writeserror: <message>\nto stderr and exits with the appropriate code--help/-h— prints detailed usage to stderr, exits 0--version/-V— printssql-pipe 0.1.0to stderr, exits 0parseArgsreturnsArgsResultunion (parsed | help | version) instead of justParsedArgserror: row N: ...)main()no longer returns!void— all errors terminate viastd.process.exitbuild.zig"0.1.0"injected at build time viaaddOptions→ accessible as@import("build_options").version--helpoutput,--versionoutput, exit code 1 (missing query), exit code 3 (SQL error witherror:prefix)README.md--no-type-inference,-h,-V)Acceptance Criteria
--help/-hflag prints usage details--version/-Vflag prints the binary version (injected at build time)error:and go to stderr