From 7bbd2906bc833f704d9de7ceba15289cc5a75319 Mon Sep 17 00:00:00 2001 From: "Victor M. Varela" Date: Thu, 7 May 2026 12:20:30 +0200 Subject: [PATCH 1/2] feat: add --silent / -s flag to suppress row count output (#114) --- build.zig | 37 +++++++++++++++++++++++++++++++++++++ src/main.zig | 23 +++++++++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/build.zig b/build.zig index a12ecc8..e24c32c 100644 --- a/build.zig +++ b/build.zig @@ -743,6 +743,43 @@ pub fn build(b: *std.Build) void { test_tsv_quoted_tab.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_tsv_quoted_tab.step); + // Integration test 71: --silent suppresses row count output to stderr + const test_silent = b.addSystemCommand(&.{ + "bash", "-c", + \\out=$(printf 'name,age\nAlice,30\n' | ./zig-out/bin/sql-pipe --silent 'SELECT name FROM t' 2>&1 >/dev/null) + \\test -z "$out" + }); + test_silent.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_silent.step); + + // Integration test 72: -s is an alias for --silent + const test_silent_short = b.addSystemCommand(&.{ + "bash", "-c", + \\result=$(printf 'name,age\nAlice,30\n' | ./zig-out/bin/sql-pipe -s 'SELECT name FROM t') + \\expected=$(printf 'Alice\n') + \\[ "$result" = "$expected" ] + }); + test_silent_short.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_silent_short.step); + + // Integration test 73: --silent combined with --verbose exits 1 with error + const test_silent_verbose_conflict = b.addSystemCommand(&.{ + "bash", "-c", + \\msg=$(printf 'a\n1\n' | ./zig-out/bin/sql-pipe --silent --verbose 'SELECT * FROM t' 2>&1 >/dev/null; echo "EXIT:$?") + \\echo "$msg" | grep -q 'error: --silent cannot be combined with --verbose' && echo "$msg" | grep -q 'EXIT:1' + }); + test_silent_verbose_conflict.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_silent_verbose_conflict.step); + + // Integration test 74: --silent combined with -v exits 1 with error + const test_silent_v_conflict = b.addSystemCommand(&.{ + "bash", "-c", + \\msg=$(printf 'a\n1\n' | ./zig-out/bin/sql-pipe --silent -v 'SELECT * FROM t' 2>&1 >/dev/null; echo "EXIT:$?") + \\echo "$msg" | grep -q 'error: --silent cannot be combined with --verbose' && echo "$msg" | grep -q 'EXIT:1' + }); + test_silent_v_conflict.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_silent_v_conflict.step); + // Unit tests for the RFC 4180 CSV parser (src/csv.zig) const unit_tests = b.addTest(.{ .root_module = b.createModule(.{ diff --git a/src/main.zig b/src/main.zig index a81fcfe..0f48f84 100644 --- a/src/main.zig +++ b/src/main.zig @@ -18,6 +18,7 @@ const SqlPipeError = error{ MissingQuery, InvalidDelimiter, IncompatibleFlags, + SilentVerboseConflict, ColumnsWithQuery, InvalidMaxRows, InvalidInputFormat, @@ -31,7 +32,6 @@ const SqlPipeError = error{ PrepareInsertFailed, BindFailed, StepFailed, - CommitFailed, PrepareQueryFailed, InvalidOutputPath, OutputWithColumns, @@ -85,6 +85,8 @@ const ParsedArgs = struct { /// Print "Loaded rows" to stderr after all rows are inserted when true. /// When false, the message is still shown automatically when stderr is a TTY. verbose: bool, + /// Suppress "Loaded rows" unconditionally. + silent: bool, /// Write results to this file path instead of stdout; null = write to stdout. output: ?[]const u8, }; @@ -134,6 +136,8 @@ fn printUsage(writer: *std.Io.Writer) !void { \\ --max-rows Stop if more than data rows are read (exit 1) \\ -v, --verbose Force row count to stderr (shown automatically on TTY) \\ With --columns: show inferred type per column + \\ -s, --silent Suppress row count output unconditionally + \\ Cannot be combined with -v/--verbose \\ --columns List column names from input header (one per line) and exit \\ Combine with -v/--verbose to include inferred types \\ Cannot be combined with --output or a query argument @@ -212,6 +216,7 @@ fn parseArgs(args: []const [:0]const u8) SqlPipeError!ArgsResult { var max_rows: ?usize = null; var verbose = false; + var silent = false; var list_columns = false; var output: ?[]const u8 = null; @@ -273,6 +278,8 @@ fn parseArgs(args: []const [:0]const u8) SqlPipeError!ArgsResult { if (max_rows.? == 0) return error.InvalidMaxRows; } else if (std.mem.eql(u8, arg, "--verbose") or std.mem.eql(u8, arg, "-v")) { verbose = true; + } else if (std.mem.eql(u8, arg, "--silent") or std.mem.eql(u8, arg, "-s")) { + silent = true; } else if (std.mem.eql(u8, arg, "--columns")) { list_columns = true; } else if (std.mem.eql(u8, arg, "--output")) { @@ -302,6 +309,10 @@ fn parseArgs(args: []const [:0]const u8) SqlPipeError!ArgsResult { if (list_columns and query != null) return error.ColumnsWithQuery; + // --silent and --verbose are mutually exclusive + if (silent and verbose) + return error.SilentVerboseConflict; + // --columns mode: list headers and exit if (list_columns) return .{ .columns = ColumnsArgs{ @@ -319,6 +330,7 @@ fn parseArgs(args: []const [:0]const u8) SqlPipeError!ArgsResult { .output_format = output_format, .max_rows = max_rows, .verbose = verbose, + .silent = silent, .output = output, } }; } @@ -1355,7 +1367,7 @@ fn run( // Print row count and elapsed time to stderr when stderr is a TTY or --verbose is set. const is_tty = std.Io.File.isTty(std.Io.File.stderr(), io) catch false; - if (parsed.verbose or is_tty) { + if (!parsed.silent and (parsed.verbose or is_tty)) { const end_ts = std.Io.Timestamp.now(io, .awake); const elapsed_ns: i96 = end_ts.nanoseconds - start_ts.nanoseconds; const elapsed_ms: u64 = @intCast(@max(@as(i96, 0), @divTrunc(elapsed_ns, std.time.ns_per_ms))); @@ -1407,6 +1419,13 @@ pub fn main(init: std.process.Init.Minimal) void { stderr_writer.flush() catch |ferr| std.log.err("failed to flush: {}", .{ferr}); std.process.exit(@intFromEnum(ExitCode.usage)); }, + error.SilentVerboseConflict => { + stderr_writer.writeAll( + "error: --silent cannot be combined with --verbose\n", + ) catch |werr| std.log.err("failed to write error message: {}", .{werr}); + stderr_writer.flush() catch |ferr| std.log.err("failed to flush: {}", .{ferr}); + std.process.exit(@intFromEnum(ExitCode.usage)); + }, error.InvalidMaxRows => { stderr_writer.writeAll("error: --max-rows must be a positive integer\n") catch |werr| { std.log.err("failed to write error message: {}", .{werr}); From f5dd7c47b45d0c72edc8d0392c24b45676fefc44 Mon Sep 17 00:00:00 2001 From: "Victor M. Varela" Date: Thu, 7 May 2026 12:30:50 +0200 Subject: [PATCH 2/2] docs: add --silent / -s flag to README and man page --- README.md | 3 ++- docs/sql-pipe.1.scd | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dba86b6..2a992b8 100644 --- a/README.md +++ b/README.md @@ -214,10 +214,11 @@ $ cat events.csv \ | `--columns` | Read the CSV header row, print each column name on its own line, and exit 0. With `-v`/`--verbose`, also shows the inferred type per column (`name INTEGER`). Respects `--delimiter` and `--tsv`. Mutually exclusive with a query argument. | | `--output ` | Write results to the given file instead of stdout. Creates or overwrites the file. Exits 1 if the file cannot be created. | | `-v`, `--verbose` | Print `Loaded rows in s` to stderr after loading (always on TTY; forced with flag) | +| `-s`, `--silent` | Suppress `Loaded rows in s` and the progress counter from stderr unconditionally. Cannot be combined with `-v`/`--verbose` | | `-h`, `--help` | Show usage help and exit | | `-V`, `--version` | Print version and exit | -After loading, `sql-pipe` prints `Loaded rows in s` to stderr whenever stderr is a TTY (interactive terminal). The message is suppressed in scripts and pipes to keep them noise-free. Use `-v` / `--verbose` to force it regardless of TTY: +After loading, `sql-pipe` prints `Loaded rows in s` to stderr whenever stderr is a TTY (interactive terminal). The message is suppressed in scripts and pipes to keep them noise-free. Use `-v` / `--verbose` to force it regardless of TTY, or `-s` / `--silent` to suppress it unconditionally (e.g. when stderr is a TTY but you want clean output): ```sh $ cat sales.csv | sql-pipe --verbose 'SELECT region, SUM(revenue) FROM t GROUP BY region' diff --git a/docs/sql-pipe.1.scd b/docs/sql-pipe.1.scd index 466aa6f..0ef350f 100644 --- a/docs/sql-pipe.1.scd +++ b/docs/sql-pipe.1.scd @@ -58,6 +58,18 @@ OPTIONS code 1 and an error message. Use this to guard against accidentally piping extremely large files into memory. + *-v, --verbose* + Print "Loaded rows in s" to standard error after all rows + are inserted. The message is shown automatically when stderr is a + TTY; this flag forces it unconditionally (e.g. in scripts or + pipes). Cannot be combined with *-s* / *--silent*. + + *-s, --silent* + Suppress the "Loaded rows in s" message and the loading + progress counter from standard error unconditionally, even when + stderr is a TTY. Useful for producing clean stderr in interactive + terminals. Cannot be combined with *-v* / *--verbose*. + *--columns* Read the CSV header row, print each column name on its own line to standard output, and exit with code 0. When combined with *-v* /