Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ When `-f` is used, all positional arguments are treated as data files (no positi
| `-f`, `--file <file>` | Read SQL query from file instead of command line |
| `-v`, `--verbose` | Print `Loaded <n> rows in <t>s` to stderr after loading (always on TTY; forced with flag) |
| `-s`, `--silent` | Suppress `Loaded <n> rows in <t>s` and the progress counter from stderr unconditionally. Cannot be combined with `-v`/`--verbose` |
| `--completions <shell>` | Generate shell completion script for `bash`, `zsh`, or `fish` to stdout |
| `-h`, `--help` | Show usage help and exit |
| `-V`, `--version` | Print version and exit |
| `--` | End of options — treat all remaining arguments as files or query |
Expand Down
92 changes: 92 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3027,4 +3027,96 @@ pub fn build(b: *std.Build) void {
});
test_explain_empty_stdin.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_explain_empty_stdin.step);

// ─── --completions integration tests (issue #175) ────────────────────────────

// Integration test 175a: --completions bash generates valid output (contains compete)
const test_completions_bash = b.addSystemCommand(&.{
"bash", "-c",
\\./zig-out/bin/sql-pipe --completions bash | grep -q 'complete -F _sql-pipe sql-pipe'
});
test_completions_bash.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_completions_bash.step);

// Integration test 175b: --completions zsh generates valid output (contains #compdef)
const test_completions_zsh = b.addSystemCommand(&.{
"bash", "-c",
\\./zig-out/bin/sql-pipe --completions zsh | grep -q '#compdef sql-pipe'
});
test_completions_zsh.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_completions_zsh.step);

// Integration test 175c: --completions fish generates valid output (contains complete -c)
const test_completions_fish = b.addSystemCommand(&.{
"bash", "-c",
\\./zig-out/bin/sql-pipe --completions fish | grep -q 'complete -c sql-pipe'
});
test_completions_fish.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_completions_fish.step);

// Integration test 175d: --completions with unknown shell exits 1
const test_completions_invalid = b.addSystemCommand(&.{
"bash", "-c",
\\msg=$(./zig-out/bin/sql-pipe --completions invalid 2>&1 >/dev/null; echo "EXIT:$?")
\\echo "$msg" | grep -q 'unknown shell' && echo "$msg" | grep -q 'EXIT:1'
});
test_completions_invalid.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_completions_invalid.step);

// Integration test 175e: --completions without value exits 1
const test_completions_missing = b.addSystemCommand(&.{
"bash", "-c",
\\./zig-out/bin/sql-pipe --completions 2>&1 >/dev/null; test $? -eq 1
});
test_completions_missing.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_completions_missing.step);

// Integration test 175f: --completions=zsh (equals syntax) works
const test_completions_equals = b.addSystemCommand(&.{
"bash", "-c",
\\./zig-out/bin/sql-pipe --completions=zsh | grep -q '_sql-pipe'
});
test_completions_equals.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_completions_equals.step);

// Integration test 175g: --help contains --completions
const test_completions_help = b.addSystemCommand(&.{
"bash", "-c",
\\./zig-out/bin/sql-pipe --help 2>&1 >/dev/null | grep -q -- '--completions'
});
test_completions_help.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_completions_help.step);

// Integration test 175h: Bash completion script has valid syntax
const test_completions_bash_syntax = b.addSystemCommand(&.{
"bash", "-c",
\\script=$(./zig-out/bin/sql-pipe --completions bash)
\\# Define _init_completion stub and check syntax with bash -n
\\stub='_init_completion() { :; }; complete -F _sql-pipe sql-pipe; '"$script"';'
\\echo "$stub" | bash -n 2>&1 || exit 1
});
test_completions_bash_syntax.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_completions_bash_syntax.step);

// Integration test 175i: Zsh completion script has valid syntax (skip if zsh not available)
const test_completions_zsh_syntax = b.addSystemCommand(&.{
"bash", "-c",
\\script=$(./zig-out/bin/sql-pipe --completions zsh)
\\if command -v zsh >/dev/null 2>&1; then
\\ echo "$script" | zsh -n 2>&1 || exit 1
\\fi
});
test_completions_zsh_syntax.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_completions_zsh_syntax.step);

// Integration test 175j: Fish completion script has valid syntax (skip if fish not available)
const test_completions_fish_syntax = b.addSystemCommand(&.{
"bash", "-c",
\\script=$(./zig-out/bin/sql-pipe --completions fish)
\\if command -v fish >/dev/null 2>&1; then
\\ echo "$script" | fish --no-execute 2>&1 || exit 1
\\fi
});
test_completions_fish_syntax.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_completions_fish_syntax.step);
}
10 changes: 10 additions & 0 deletions docs/sql-pipe.1.scd
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ SYNOPSIS
*sql-pipe* --stats [OPTIONS] [<file>]
*sql-pipe* --schema [OPTIONS] [<file>...]
*sql-pipe* --explain [OPTIONS] [<file>] <query>
*sql-pipe* --completions <shell>

DESCRIPTION
sql-pipe reads CSV, JSON, NDJSON, or XML data from standard input and/or
Expand Down Expand Up @@ -192,6 +193,15 @@ OPTIONS
attribute. Example: *--html-class 'data-table sortable'* produces
*<table class="data-table sortable">*.

*--completions* <shell>
Generate a shell completion script and print it to standard
output. Supported shells: *bash*, *zsh*, *fish*. The script
is self-contained with no external dependencies. Redirect to
the appropriate completions directory for your shell:
*--completions bash > /usr/share/bash-completion/completions/sql-pipe*,
*--completions zsh > /usr/share/zsh/site-functions/_sql-pipe*,
*--completions fish > ~/.config/fish/completions/sql-pipe.fish*.

*-f, --file* <file>
Read the SQL query from <file> instead of the command line. When
this flag is used, all positional arguments are treated as data
Expand Down
26 changes: 26 additions & 0 deletions src/args.zig
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ pub const SqlPipeError = error{
ExplainWithFlags,
MissingNullValue,
MissingHtmlClassValue,
InvalidCompletionsShell,
};

pub const ParsedArgs = struct {
Expand Down Expand Up @@ -202,6 +203,20 @@ pub const SchemaArgs = struct {
type_inference: bool,
};

pub const CompletionsShell = enum {
bash,
zsh,
fish,

pub fn parse(s: []const u8) error{InvalidCompletionsShell}!CompletionsShell {
return std.meta.stringToEnum(CompletionsShell, s) orelse error.InvalidCompletionsShell;
}
};

pub const CompletionsArgs = struct {
shell: CompletionsShell,
};

pub const ArgsResult = union(enum) {
/// Normal execution: run the query.
parsed: ParsedArgs,
Expand All @@ -219,6 +234,8 @@ pub const ArgsResult = union(enum) {
stats: StatsArgs,
/// User requested --schema: print inferred CREATE TABLE DDL.
schema: SchemaArgs,
/// User requested --completions: generate shell completion script.
completions: CompletionsArgs,
};

pub fn printUsage(writer: *std.Io.Writer) !void {
Expand Down Expand Up @@ -279,6 +296,7 @@ pub fn printUsage(writer: *std.Io.Writer) !void {
\\ --null-value <string> Custom NULL representation in output (default: "NULL" for CSV/TSV/table)
\\ --html-class <class> CSS class name for the HTML <table> element (-O html only)
\\ -f, --file <file> Read SQL query from file instead of command line
\\ --completions <shell> Generate shell completion script (bash, zsh, fish)
\\ -h, --help Show this help message and exit
\\ -V, --version Show version and exit
\\
Expand Down Expand Up @@ -512,6 +530,14 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP
html_class = arg["--html-class=".len..];
} else if (std.mem.eql(u8, arg, "--no-table")) {
table_mode = .never;
} else if (std.mem.eql(u8, arg, "--completions")) {
i += 1;
if (i >= args.len) return error.InvalidCompletionsShell;
const shell = CompletionsShell.parse(args[i]) catch return error.InvalidCompletionsShell;
return .{ .completions = CompletionsArgs{ .shell = shell } };
} else if (std.mem.startsWith(u8, arg, "--completions=")) {
const shell = CompletionsShell.parse(arg["--completions=".len..]) catch return error.InvalidCompletionsShell;
return .{ .completions = CompletionsArgs{ .shell = shell } };
} else if (std.mem.eql(u8, arg, "-f") or std.mem.eql(u8, arg, "--file")) {
i += 1;
if (i >= args.len) return error.InvalidQueryFile;
Expand Down
182 changes: 182 additions & 0 deletions src/completions.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
const std = @import("std");
const args = @import("args.zig");

const CompletionsShell = args.CompletionsShell;

pub fn generateCompletions(shell: CompletionsShell, writer: *std.Io.Writer) !void {
switch (shell) {
.bash => try generateBash(writer),
.zsh => try generateZsh(writer),
.fish => try generateFish(writer),
}
}

fn generateBash(writer: *std.Io.Writer) !void {
try writer.writeAll(
\\_sql-pipe() {
\\ local cur prev words cword
\\ _init_completion -s || return
\\
\\ case $prev in
\\ -d|--delimiter)
\\ return
\\ ;;
\\ -I|--input-format)
\\ COMPREPLY=($(compgen -W "csv tsv json ndjson xml" -- "$cur"))
\\ return
\\ ;;
\\ -O|--output-format)
\\ COMPREPLY=($(compgen -W "csv tsv json ndjson xml markdown md html sql" -- "$cur"))
\\ return
\\ ;;
\\ --completions)
\\ COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur"))
\\ return
\\ ;;
\\ --max-rows|--sql-table|--null-value|--html-class|--output|--xml-root|--xml-row|--json-path)
\\ return
\\ ;;
\\ --sample)
\\ COMPREPLY=($(compgen -W "1 5 10 25 50 100 500 1000" -- "$cur"))
\\ return
\\ ;;
\\ -f|--file)
\\ _filedir
\\ return
\\ ;;
\\ esac
\\
\\ if [[ "$cur" == -* ]]; then
\\ COMPREPLY=($(compgen -W '
\\ --delimiter -d
\\ --tsv
\\ --input-format -I
\\ --output-format -O
\\ --json
\\ --sql-table
\\ --no-type-inference
\\ --header -H
\\ --max-rows
\\ --verbose -v
\\ --silent -s
\\ --validate
\\ --columns
\\ --sample
\\ --stats --profile
\\ --schema
\\ --output
\\ --xml-root
\\ --xml-row
\\ --json-path
\\ --disk
\\ --explain
\\ --table --no-table
\\ --null-value
\\ --html-class
\\ --completions
\\ --file -f
\\ --help -h
\\ --version -V
\\ ' -- "$cur"))
\\ else
\\ _filedir
\\ fi
\\}
\\complete -F _sql-pipe sql-pipe
\\
);
}

fn generateZsh(writer: *std.Io.Writer) !void {
try writer.writeAll(
\\#compdef sql-pipe
\\
\\_sql-pipe() {
\\ local -a opts
\\ opts=(
\\ '(-d --delimiter)'{-d+,--delimiter=}'[Input field delimiter]:delimiter:'
\\ '--tsv[Tab-separated input alias]'
\\ '(-I --input-format)'{-I+,--input-format=}'[Input format]:format:(csv tsv json ndjson xml)'
\\ '(-O --output-format)'{-O+,--output-format=}'[Output format]:format:(csv tsv json ndjson xml markdown html sql)'
\\ '--json[Alias for --output-format json]'
\\ '--sql-table=[SQL INSERT table name]:table name:'
\\ '--no-type-inference[Treat all columns as TEXT]'
\\ '(-H --header)'{-H,--header}'[Print column names as first output row]'
\\ '--max-rows=[Row limit]:rows:'
\\ '(-v --verbose)'{-v,--verbose}'[Force row count to stderr]'
\\ '(-s --silent)'{-s,--silent}'[Suppress row count output]'
\\ '--validate[Parse input and print summary]'
\\ '--columns[List column names]'
\\ '--sample::Number of rows:(1 5 10 25 50 100 500 1000)'
\\ '--stats[Compute per-column statistics]'
\\ '--profile[Alias for --stats]'
\\ '--schema[Print inferred CREATE TABLE DDL]'
\\ '--output=[Write results to file]:file:_files'
\\ '--xml-root=[Root element name]:name:'
\\ '--xml-row=[Row element name]:name:'
\\ '--json-path=[Path to JSON array]:path:'
\\ '--disk[Use file-backed temp database]'
\\ '--explain[Print query plan to stderr]'
\\ '--table[Force table output]'
\\ '--no-table[Force CSV output]'
\\ '--null-value=[Custom NULL representation]:string:'
\\ '--html-class=[HTML table CSS class]:class:'
\\ '--completions=[Generate shell completions]:shell:(bash zsh fish)'
\\ '(-f --file)'{-f+,--file=}'[Read SQL query from file]:file:_files'
\\ '(-h --help)'{-h,--help}'[Show help message]'
\\ '(-V --version)'{-V,--version}'[Show version]'
\\ '*:file:_files'
\\ )
\\ _arguments $opts
\\}
\\
\\_sql-pipe
\\
);
}

fn generateFish(writer: *std.Io.Writer) !void {
try writer.writeAll(
\\complete -c sql-pipe -f
\\
\\# Input options
\\complete -c sql-pipe -s d -l delimiter -r -d "Input field delimiter"
\\complete -c sql-pipe -l tsv -d "Alias for --delimiter \\'\\\\t\\'"
\\complete -c sql-pipe -s I -l input-format -r -f -a "csv tsv json ndjson xml" -d "Input format"
\\complete -c sql-pipe -s O -l output-format -r -f -a "csv tsv json ndjson xml markdown html sql" -d "Output format"
\\complete -c sql-pipe -l json -d "Alias for --output-format json"
\\
\\# Query options
\\complete -c sql-pipe -l sql-table -r -d "Target table for SQL INSERT"
\\complete -c sql-pipe -l no-type-inference -d "Treat all columns as TEXT"
\\complete -c sql-pipe -s H -l header -d "Print column names as first output row"
\\complete -c sql-pipe -l max-rows -r -d "Stop if more than N data rows read"
\\complete -c sql-pipe -s v -l verbose -d "Force row count to stderr"
\\complete -c sql-pipe -s s -l silent -d "Suppress row count output"
\\complete -c sql-pipe -l validate -d "Parse input and print summary"
\\complete -c sql-pipe -l columns -d "List column names and exit"
\\complete -c sql-pipe -l sample -r -f -a "1 5 10 25 50 100 500 1000" -d "Print schema and sample rows"
\\complete -c sql-pipe -l stats -d "Compute per-column statistics"
\\complete -c sql-pipe -l profile -d "Alias for --stats"
\\complete -c sql-pipe -l schema -d "Print inferred CREATE TABLE DDL"
\\
\\# Output options
\\complete -c sql-pipe -l output -r -d "Write results to file"
\\complete -c sql-pipe -l xml-root -r -d "Root element name for XML"
\\complete -c sql-pipe -l xml-row -r -d "Row element name for XML"
\\complete -c sql-pipe -l json-path -r -d "Path to JSON array"
\\complete -c sql-pipe -l disk -d "Use file-backed temp database"
\\complete -c sql-pipe -l explain -d "Print query plan to stderr"
\\complete -c sql-pipe -l table -d "Force pretty-printed table output"
\\complete -c sql-pipe -l no-table -d "Force CSV output"
\\complete -c sql-pipe -l null-value -r -d "Custom NULL representation"
\\complete -c sql-pipe -l html-class -r -d "CSS class for HTML table"
\\
\\# Meta options
\\complete -c sql-pipe -l completions -r -f -a "bash zsh fish" -d "Generate shell completions"
\\complete -c sql-pipe -s f -l file -r -d "Read SQL query from file"
\\complete -c sql-pipe -s h -l help -d "Show help message"
\\complete -c sql-pipe -s V -l version -d "Show version"
\\
);
}
Loading
Loading