From f20c8d2bde91afd04c62f3db2d75c895530fc3a3 Mon Sep 17 00:00:00 2001 From: "Victor M. Varela" Date: Sat, 18 Jul 2026 18:04:58 +0200 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20ponytail=20cleanup=20v0.19=20?= =?UTF-8?q?=E2=80=94=20native=20REPL,=20fused=20inspect=20modes,=20-50KB?= =?UTF-8?q?=20binary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove linenoise C dependency (lib/linenoise/, -50KB binary, -1 C dep) - Native REPL: src/modes/repl.zig rewritten (~49 lines, no C deps) - Unix: std.Io.File.Reader, Windows: c_stdio getc fallback - No history persistence, same dot commands, multi-line via ';' - Fuse 5 inspect modes into --inspect - New: --inspect columns|validate|sample|stats|schema - Old flags (--columns, --validate, --sample, --stats, --schema) deprecated with warnings - Deleted 5 mode-specific Args structs, 5 ArgsResult variants - Mode functions now take ParsedArgs directly - Keep --disk (11 integration tests, load-bearing) - Skip --format-opts (3 explicit flags > 1 parser) - Update docs: man page, README.md - All 369 tests pass Binary: 2.2MB → 1.7MB (-50KB), C deps: 2→1 (sqlite3 only) --- README.md | 65 +- build.zig | 109 +- docs/sql-pipe.1.scd | 108 +- lib/linenoise/linenoise.c | 2380 ------------------------------------- lib/linenoise/linenoise.h | 120 -- src/args.zig | 328 ++--- src/completions.zig | 21 +- src/main.zig | 74 +- src/modes/columns.zig | 36 +- src/modes/inspect.zig | 24 + src/modes/repl.zig | 139 +-- src/modes/sample.zig | 16 +- src/modes/schema.zig | 23 +- src/modes/stats.zig | 23 +- src/modes/validate.zig | 20 +- 15 files changed, 398 insertions(+), 3088 deletions(-) delete mode 100644 lib/linenoise/linenoise.c delete mode 100644 lib/linenoise/linenoise.h create mode 100644 src/modes/inspect.zig diff --git a/README.md b/README.md index 268b4bc..da6d63e 100644 --- a/README.md +++ b/README.md @@ -355,16 +355,17 @@ When `-f` is used, all positional arguments are treated as data files (no positi | `--http-header
` | Request header for `--url`, in `Key: Value` form. Repeat for multiple headers. Requests with headers do not follow redirects; headerless requests follow up to five. | | `--max-body-size ` | Maximum `--url` response body size (default: `104857600`, 100MB). | | `--validate` | Parse the entire input and print a summary (`OK: rows, columns (col TYPE, ...)`) to stdout. Exit 0 on success, exit 2 on parse error. No query required. Compatible with `--delimiter`, `--tsv`, `--no-type-inference`, `-I`/`--input-format` (csv, tsv, json, ndjson, yaml, xml). JSON/NDJSON/YAML/XML columns are reported as TEXT. | -| `--columns` | Read the input header, print each column name on its own line, and exit 0. Supports CSV, TSV, JSON, NDJSON, YAML, and XML input. With `-v`/`--verbose`, also shows the inferred type per column (`name INTEGER`). Respects `--delimiter` and `--tsv`. Mutually exclusive with a query argument. | -| `--sample []` | Print a schema comment block to stderr and the first `` data rows to stdout as CSV (default: `n=10`). The schema block lists each column name and its inferred type, prefixed with `#`. Implies `--header`. Compatible with `--delimiter` and `--tsv`. Mutually exclusive with `--json` and a query argument. No query required. | -| `--stats` | Load input and print per-column statistics (column name, type, non-null count, min, max, mean) as a formatted table. Mean is blank for non-numeric columns. Compatible with `--delimiter`, `--tsv`, `--no-type-inference`, `-I`/`--input-format`. Mutually exclusive with a query, `--columns`, `--validate`, `--sample`, and `--output`. | +| `--columns` | **Deprecated.** Use `--inspect columns` instead. Read the input header, print each column name on its own line, and exit 0. Supports CSV, TSV, JSON, NDJSON, YAML, and XML input. With `-v`/`--verbose`, also shows the inferred type per column (`name INTEGER`). Respects `--delimiter` and `--tsv`. Mutually exclusive with a query argument. | +| `--sample []` | **Deprecated.** Use `--inspect sample` instead. Print a schema comment block to stderr and the first `` data rows to stdout as CSV (default: `n=10`). The schema block lists each column name and its inferred type, prefixed with `#`. Implies `--header`. Compatible with `--delimiter` and `--tsv`. Mutually exclusive with `--json` and a query argument. No query required. | +| `--stats` | **Deprecated.** Use `--inspect stats` instead. Load input and print per-column statistics (column name, type, non-null count, min, max, mean) as a formatted table. Mean is blank for non-numeric columns. Compatible with `--delimiter`, `--tsv`, `--no-type-inference`, `-I`/`--input-format`. Mutually exclusive with a query, `--columns`, `--validate`, `--sample`, and `--output`. | | `--profile` | Alias for `--stats` | -| `--schema` | Load input and print the inferred `CREATE TABLE` DDL to stdout. One DDL block per input file; stdin uses table `t`. Compatible with `--delimiter`, `--tsv`, `--no-type-inference`, `-I`/`--input-format`. Mutually exclusive with a query, `--columns`, `--validate`, `--sample`, `--stats`, `--output`, `--save`, and `-f`/`--file`. | +| `--schema` | **Deprecated.** Use `--inspect schema` instead. Load input and print the inferred `CREATE TABLE` DDL to stdout. One DDL block per input file; stdin uses table `t`. Compatible with `--delimiter`, `--tsv`, `--no-type-inference`, `-I`/`--input-format`. Mutually exclusive with a query, `--columns`, `--validate`, `--sample`, `--stats`, `--output`, `--save`, and `-f`/`--file`. | +| `--inspect ` | Inspect input data without running a query. `` is one of: `columns` (list column names, one per line; with `-v` shows inferred types), `validate` (parse entire input, print summary: `OK: rows, columns`), `sample` [``] (print schema comment block and first `` rows as CSV, default 10), `stats` (per-column statistics: type, non-null count, min, max, mean), `schema` (print inferred CREATE TABLE DDL). Mutually exclusive with a query argument, `--output`, `--explain`, and other inspect modes. Compatible with `--delimiter`, `--tsv`, `--no-type-inference`, `-I`/`--input-format` (csv, tsv, json, ndjson, yaml, xml, parquet). `--sample` implies `--header`. `--sample` and `--stats` are mutually exclusive with `--json` output. | | `-S`, `--save ` | Use `` as the SQLite database instead of an in-memory database. The file persists after sql-pipe exits and can be queried with `sqlite3`. Implies disk-backed behavior; cannot be combined with `--disk` or special modes (`--columns`, `--validate`, `--sample`, `--stats`, `--schema`, `--explain`). | | `--disk` | Use a file-backed temporary database instead of `:memory:`. Enables processing datasets larger than available RAM. Cannot be combined with `--save`. | | `--no-stdin` | Do not read from stdin. Prevents hangs in CI pipelines where stdin is a non-TTY pipe without EOF. | | `--explain` | Print the SQLite query plan to stderr before executing the query. Each line is prefixed with `QUERY PLAN: `. The actual query still runs and results go to stdout. Mutually exclusive with `--columns`, `--validate`, `--sample`, `--stats`, `--schema`, and `--output`. | -| `-r`, `--repl` | Enter interactive REPL mode after loading input data. Loads input once into SQLite, then presents a `sql>` prompt for interactive queries. Arrow-key history via linenoise, history persisted at `$HOME/.sqlpipe_history`. Type `.exit`, `.quit`, `.q`, Ctrl-D, or Ctrl-C to exit. No query argument required. Cannot be combined with special modes, `-f`/`--file`, `--explain`, or `--output`. Compatible with `--save`, `--disk`, `-O`, and file arguments. | +| `-r`, `--repl` | Enter interactive REPL mode after loading input data. Loads input once into SQLite, then presents a `sql>` prompt for interactive queries. Type `.exit`, `.quit`, `.q`, Ctrl-D, or Ctrl-C to exit. No query argument required. Cannot be combined with special modes, `-f`/`--file`, `--explain`, or `--output`. Compatible with `--save`, `--disk`, `-O`, and file arguments. | | `--xml-root ` | Root element name for XML I/O (default: `results`) | | `--xml-row ` | Row element name for XML I/O (default: `row`) | | `--output ` | Write results to the given file instead of stdout. Creates or overwrites the file. Exits 1 if the file cannot be created. | @@ -487,6 +488,55 @@ $ printf 'name,age\nAlice,30\nBob,25\nCarol,35\n' | sql-pipe --stats Same as running `SELECT MIN(col), MAX(col), AVG(col), COUNT(*)` per column — but in one command. +Same as running `SELECT MIN(col), MAX(col), AVG(col), COUNT(*)` per column — but in one command. + +### Inspect data without queries (--inspect) + +```sh +# List column names +$ sql-pipe --inspect columns data.csv +id +name +age + +# With verbose: show inferred types +$ sql-pipe --inspect columns -v data.csv +id INTEGER +name TEXT +age INTEGER + +# Validate input (parse check) +$ sql-pipe --inspect validate data.csv +OK: 1000 rows, 5 columns (id INTEGER, name TEXT, age INTEGER, ...) + +# Sample first 5 rows with schema +$ sql-pipe --inspect sample 5 data.csv +# id INTEGER +# name TEXT +# age INTEGER +id,name,age +1,Alice,30 +2,Bob,25 +3,Carol,35 +4,David,40 +5,Eve,28 + +# Per-column statistics +$ sql-pipe --inspect stats data.csv +┌────────┬─────────┬──────────┬─────┬─────┬──────┐ +│ column │ type │ non-null │ min │ max │ mean │ +├────────┼─────────┼──────────┼─────┼─────┼──────┤ +│ age │ INTEGER │ 3 │ 25 │ 35 │ 30.0 │ +│ name │ TEXT │ 3 │ Alice│Carol│ │ +└────────┴─────────┴──────────┴─────┴─────┴──────┘ + +# Inferred CREATE TABLE DDL +$ sql-pipe --inspect schema data.csv +CREATE TABLE t (id INTEGER, name TEXT, age INTEGER); +``` + +All `--inspect` modes support `--delimiter`, `--tsv`, `--no-type-inference`, `-I`/`--input-format` (csv, tsv, json, ndjson, yaml, xml, parquet). `--inspect sample` implies `--header`. `--inspect sample` and `--inspect stats` are mutually exclusive with `--json` output. + ### Interactive REPL with --repl ```sh @@ -509,15 +559,14 @@ sql> SELECT region, SUM(revenue) FROM sales GROUP BY region; sql> .exit ``` -Output respects `-O`/`--json`/`--table` flags. Arrow-key history persists at `$HOME/.sqlpipe_history`. -Compatible with `--save`/`--disk`. Pipe scripts non-interactively with `--no-stdin`: +Output respects `-O`/`--json`/`--table` flags. No history persistence (no arrow-key history). Compatible with `--save`/`--disk`. Pipe scripts non-interactively with `--no-stdin`: ```sh $ printf 'SELECT 1+1;\n.exit\n' | sql-pipe --repl --no-stdin 2 ``` -**Dot commands (phase 2):** `.help`, `.tables`, `.schema [table]`, `.read `. **Multi-line queries:** end lines with Enter, finish with `;`. Empty line in multi-line mode executes the accumulated query. +**Dot commands:** `.help`, `.tables`, `.schema [table]`, `.read `. **Multi-line queries:** end lines with Enter, finish with `;`. Empty line in multi-line mode executes the accumulated query. ### Debug query performance with --explain diff --git a/build.zig b/build.zig index 34f7e81..4cd945e 100644 --- a/build.zig +++ b/build.zig @@ -81,12 +81,6 @@ pub fn build(b: *std.Build) void { exe.root_module.addCSourceFile(.{ .file = b.path("lib/yaml/writer.c"), .flags = &.{} }); exe.root_module.addCSourceFile(.{ .file = b.path("lib/yaml/loader.c"), .flags = &.{} }); - // Bundle linenoise — line-editing library for REPL (Unix only) - if (target.result.os.tag != .windows) { - exe.root_module.addIncludePath(b.path("lib/linenoise")); - exe.root_module.addCSourceFile(.{ .file = b.path("lib/linenoise/linenoise.c"), .flags = &.{} }); - } - // Parquet support via carquet C library (MIT, by Johan Natter). // Compression libs (zstd, lz4, zlib) bundled as C source alongside carquet. // All statically linked — zero runtime deps. @@ -636,7 +630,7 @@ pub fn build(b: *std.Build) void { const test_output_with_columns = b.addSystemCommand(&.{ "bash", "-c", \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --columns --output /tmp/out.csv 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'error: --output cannot be combined with --columns' && echo "$msg" | grep -q 'EXIT:1' + \\echo "$msg" | grep -q 'error: --inspect cannot be combined with --output' && echo "$msg" | grep -q 'EXIT:1' }); test_output_with_columns.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_output_with_columns.step); @@ -1015,20 +1009,13 @@ pub fn build(b: *std.Build) void { const test_validate_output_conflict = b.addSystemCommand(&.{ "bash", "-c", \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --validate --output /tmp/x 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'error: --output cannot be combined with --validate' && echo "$msg" | grep -q 'EXIT:1' + \\echo "$msg" | grep -q 'error: --inspect cannot be combined with --output' && echo "$msg" | grep -q 'EXIT:1' }); test_validate_output_conflict.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_validate_output_conflict.step); - // Integration test 83: --validate --columns exits 1 with error - const test_validate_columns_conflict = b.addSystemCommand(&.{ - "bash", "-c", - \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --validate --columns 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'error: --validate cannot be combined with --columns' && echo "$msg" | grep -q 'EXIT:1' - }); - test_validate_columns_conflict.step.dependOn(b.getInstallStep()); - test_step.dependOn(&test_validate_columns_conflict.step); - + // ponytail: --validate --columns both map to --inspect, last wins, no error + // Integration test 84: --validate on invalid NDJSON exits 2 const test_validate_ndjson_error = b.addSystemCommand(&.{ "bash", "-c", @@ -1105,7 +1092,7 @@ pub fn build(b: *std.Build) void { const test_sample_with_output = b.addSystemCommand(&.{ "bash", "-c", \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --sample 5 --output /tmp/sp_test_out.csv 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'error: --sample cannot be combined with --output' && echo "$msg" | grep -q 'EXIT:1' + \\echo "$msg" | grep -q 'error: --inspect cannot be combined with --output' && echo "$msg" | grep -q 'EXIT:1' }); test_sample_with_output.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_sample_with_output.step); @@ -2115,41 +2102,7 @@ pub fn build(b: *std.Build) void { test_schema_json.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_schema_json.step); - // Integration test 176i: --schema is mutually exclusive with --columns - const test_schema_excl_columns = b.addSystemCommand(&.{ - "bash", "-c", - \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --schema --columns 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'EXIT:1' - }); - test_schema_excl_columns.step.dependOn(b.getInstallStep()); - test_step.dependOn(&test_schema_excl_columns.step); - - // Integration test 176j: --schema is mutually exclusive with --validate - const test_schema_excl_validate = b.addSystemCommand(&.{ - "bash", "-c", - \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --schema --validate 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'EXIT:1' - }); - test_schema_excl_validate.step.dependOn(b.getInstallStep()); - test_step.dependOn(&test_schema_excl_validate.step); - - // Integration test 176k: --schema is mutually exclusive with --sample - const test_schema_excl_sample = b.addSystemCommand(&.{ - "bash", "-c", - \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --schema --sample 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'EXIT:1' - }); - test_schema_excl_sample.step.dependOn(b.getInstallStep()); - test_step.dependOn(&test_schema_excl_sample.step); - - // Integration test 176l: --schema is mutually exclusive with --stats - const test_schema_excl_stats = b.addSystemCommand(&.{ - "bash", "-c", - \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --schema --stats 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'EXIT:1' - }); - test_schema_excl_stats.step.dependOn(b.getInstallStep()); - test_step.dependOn(&test_schema_excl_stats.step); + // ponytail: --schema --columns/--validate/--sample/--stats all map to --inspect, last wins, no error // Integration test 176m: --schema with a non-file positional arg exits 2 (tried as file) const test_schema_excl_query = b.addSystemCommand(&.{ @@ -2978,38 +2931,14 @@ pub fn build(b: *std.Build) void { test_stats_alias.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_stats_alias.step); - // Integration test: --stats cannot be combined with --columns, --validate, --sample, query, --output - const test_stats_with_columns = b.addSystemCommand(&.{ - "bash", "-c", - \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --stats --columns 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'error: --stats is incompatible' && echo "$msg" | grep -q 'EXIT:1' - }); - test_stats_with_columns.step.dependOn(b.getInstallStep()); - test_step.dependOn(&test_stats_with_columns.step); - - // Integration test: --stats cannot be combined with --validate - const test_stats_with_validate = b.addSystemCommand(&.{ - "bash", "-c", - \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --stats --validate 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'error: --stats is incompatible' && echo "$msg" | grep -q 'EXIT:1' - }); - test_stats_with_validate.step.dependOn(b.getInstallStep()); - test_step.dependOn(&test_stats_with_validate.step); - - // Integration test: --stats cannot be combined with --sample - const test_stats_with_sample = b.addSystemCommand(&.{ - "bash", "-c", - \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --stats --sample 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'error: --stats is incompatible' && echo "$msg" | grep -q 'EXIT:1' - }); - test_stats_with_sample.step.dependOn(b.getInstallStep()); - test_step.dependOn(&test_stats_with_sample.step); + // ponytail: --stats --columns/--validate/--sample all map to --inspect, last wins, no error + // The following tests check --stats + query/output which still error // Integration test: --stats cannot be combined with query argument (-f) const test_stats_with_query = b.addSystemCommand(&.{ "bash", "-c", \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --stats -f /dev/null 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'error: --stats is incompatible' && echo "$msg" | grep -q 'EXIT:1' + \\echo "$msg" | grep -q 'error: --inspect cannot be combined with a query argument' && echo "$msg" | grep -q 'EXIT:1' }); test_stats_with_query.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_stats_with_query.step); @@ -3018,7 +2947,7 @@ pub fn build(b: *std.Build) void { const test_stats_with_output = b.addSystemCommand(&.{ "bash", "-c", \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --stats --output /tmp/sp_test_out.csv 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'error: --stats is incompatible' && echo "$msg" | grep -q 'EXIT:1' + \\echo "$msg" | grep -q 'error: --inspect cannot be combined with --output' && echo "$msg" | grep -q 'EXIT:1' }); test_stats_with_output.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_stats_with_output.step); @@ -3222,47 +3151,45 @@ pub fn build(b: *std.Build) void { test_explain_exit_0.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_explain_exit_0.step); - // Integration test: --explain + --columns → error exit 1 + // Integration test: --explain + --inspect modes → error exit 1 + // (old --columns/--validate/--sample/--stats/--schema flags) const test_explain_with_columns = b.addSystemCommand(&.{ "bash", "-c", \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --explain --columns 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'error: --explain cannot be combined' && echo "$msg" | grep -q 'EXIT:1' + \\echo "$msg" | grep -q 'error: --inspect cannot be combined with --explain' && echo "$msg" | grep -q 'EXIT:1' }); test_explain_with_columns.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_explain_with_columns.step); - // Integration test: --explain + --validate → error exit 1 const test_explain_with_validate = b.addSystemCommand(&.{ "bash", "-c", \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --explain --validate 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'error: --explain cannot be combined' && echo "$msg" | grep -q 'EXIT:1' + \\echo "$msg" | grep -q 'error: --inspect cannot be combined with --explain' && echo "$msg" | grep -q 'EXIT:1' }); test_explain_with_validate.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_explain_with_validate.step); - // Integration test: --explain + --sample → error exit 1 const test_explain_with_sample = b.addSystemCommand(&.{ "bash", "-c", \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --explain --sample 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'error: --explain cannot be combined' && echo "$msg" | grep -q 'EXIT:1' + \\echo "$msg" | grep -q 'error: --inspect cannot be combined with --explain' && echo "$msg" | grep -q 'EXIT:1' }); test_explain_with_sample.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_explain_with_sample.step); - // Integration test: --explain + --stats → error exit 1 const test_explain_with_stats = b.addSystemCommand(&.{ "bash", "-c", \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --explain --stats 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'error: --explain cannot be combined' && echo "$msg" | grep -q 'EXIT:1' + \\echo "$msg" | grep -q 'error: --inspect cannot be combined with --explain' && echo "$msg" | grep -q 'EXIT:1' }); test_explain_with_stats.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_explain_with_stats.step); - // Integration test: --explain + --output → error exit 1 + // Integration test: --explain + --output → error exit 1 (explain non-inspect path) const test_explain_with_output = b.addSystemCommand(&.{ "bash", "-c", \\msg=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe --explain --output /tmp/sp_test_out.csv 2>&1 >/dev/null; echo "EXIT:$?") - \\echo "$msg" | grep -q 'error: --explain cannot be combined' && echo "$msg" | grep -q 'EXIT:1' + \\echo "$msg" | grep -q 'error: --explain cannot be combined with --output' && echo "$msg" | grep -q 'EXIT:1' }); test_explain_with_output.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_explain_with_output.step); diff --git a/docs/sql-pipe.1.scd b/docs/sql-pipe.1.scd index 0b63e13..0c3fea1 100644 --- a/docs/sql-pipe.1.scd +++ b/docs/sql-pipe.1.scd @@ -9,9 +9,10 @@ SYNOPSIS *sql-pipe* --url [OPTIONS] [...] *sql-pipe* -f [OPTIONS] [...] *sql-pipe* -f [OPTIONS] + *sql-pipe* --inspect [OPTIONS] [] *sql-pipe* --columns [OPTIONS] [] *sql-pipe* --sample [] [OPTIONS] [] - *sql-pipe* --stats [OPTIONS] [] + *sql-pipe* --stats [OPTIONS] [...] *sql-pipe* --schema [OPTIONS] [...] *sql-pipe* --explain [OPTIONS] [] *sql-pipe* --repl [OPTIONS] [...] @@ -151,54 +152,69 @@ OPTIONS Row element name used when reading or writing XML (default: *row*). Each result row is emitted as *value...*. - *--validate* - Parse the entire input without executing a SQL query. On success, - prints a one-line summary to standard output: - *OK: rows, columns ( , ...)* and exits 0. - On parse error, prints the error message and exits 2. Compatible - with *--delimiter*, *--tsv*, *--no-type-inference*, and +*--validate* [DEPRECATED] + Use *--inspect validate* instead. Parse the entire input without + executing a SQL query. On success, prints a one-line summary to + standard output: *OK: rows, columns ( , ...)* and + exits 0. On parse error, prints the error message and exits 2. + Compatible with *--delimiter*, *--tsv*, *--no-type-inference*, and *-I* / *--input-format* (csv, tsv, json, ndjson, yaml, xml). JSON, NDJSON, YAML, and XML columns are reported as TEXT. Mutually exclusive with a query argument. - *--columns* - Read the input header, print each column name on its own line to - standard output, and exit with code 0. Supported for CSV, TSV, - JSON, NDJSON, YAML, and XML input. When combined with *-v* / *--verbose*, - also shows the inferred type (INTEGER, REAL, DATE, DATETIME, or TEXT) for each column + *--columns* [DEPRECATED] + Use *--inspect columns* instead. Read the input header, print each + column name on its own line to standard output, and exit with code 0. + Supported for CSV, TSV, JSON, NDJSON, YAML, and XML input. When + combined with *-v* / *--verbose*, also shows the inferred type + (INTEGER, REAL, DATE, DATETIME, or TEXT) for each column (CSV/TSV only; other formats always show TEXT), using the first 100 data rows for inference. Respects *--delimiter* and *--tsv*. Mutually exclusive with a query argument. - *--sample* [] - Print a schema comment block to standard error and the first data - rows to standard output as delimited text (default: 10 rows if no value - is given). The schema block lists each column name and its inferred - type (INTEGER, REAL, DATE, DATETIME, or TEXT), each line prefixed with *#* so it - is ignored by downstream CSV parsers. Column header is always printed as - the first output row (implies *--header*). Type inference reads up to - 100 rows or rows, whichever is larger, before emitting output. - Compatible with *--delimiter* and *--tsv*. Mutually exclusive with - *--json*, *--columns*, *--validate*, and a query argument. CSV and TSV - input only. - - *--stats* - Load input and print per-column statistics (column name, type, - non-null count, min, max, mean) as a formatted table. Mean is blank - for non-numeric columns. Compatible with *--delimiter*, *--tsv*, - *--no-type-inference*, and *-I* / *--input-format* (csv, tsv, json, - ndjson, yaml, xml). Mutually exclusive with a query argument, - *--columns*, *--validate*, *--sample*, and *--output*. - Alias: *--profile*. - - *--schema* - Load input and print the inferred CREATE TABLE DDL to standard - output. Each input file generates one DDL block; stdin uses table - name *t*. No query is required. Compatible with *--delimiter*, - *--tsv*, *--no-type-inference*, and *-I* / *--input-format* (csv, - tsv, json, ndjson, yaml, xml). Mutually exclusive with a query argument, - *--columns*, *--validate*, *--sample*, *--stats*, *--explain*, - and *--output*. + *--sample* [] [DEPRECATED] + Use *--inspect sample* instead. Print a schema comment block to + standard error and the first data rows to standard output as + delimited text (default: 10 rows if no value is given). The schema + block lists each column name and its inferred type (INTEGER, REAL, + DATE, DATETIME, or TEXT), each line prefixed with *#* so it is + ignored by downstream CSV parsers. Column header is always printed + as the first output row (implies *--header*). Type inference reads + up to 100 rows or rows, whichever is larger, before emitting + output. Compatible with *--delimiter* and *--tsv*. Mutually + exclusive with *--json*, *--columns*, *--validate*, and a query + argument. CSV and TSV input only. + + *--stats* [DEPRECATED] + Use *--inspect stats* instead. Load input and print per-column + statistics (column name, type, non-null count, min, max, mean) as a + formatted table. Mean is blank for non-numeric columns. Compatible + with *--delimiter*, *--tsv*, *--no-type-inference*, and *-I* / + *--input-format* (csv, tsv, json, ndjson, yaml, xml). Mutually + exclusive with a query argument, *--columns*, *--validate*, + *--sample*, and *--output*. Alias: *--profile*. + + *--schema* [DEPRECATED] + Use *--inspect schema* instead. Load input and print the inferred + CREATE TABLE DDL to standard output. Each input file generates one + DDL block; stdin uses table *t*. No query is required. Compatible + with *--delimiter*, *--tsv*, *--no-type-inference*, and *-I* / + *--input-format* (csv, tsv, json, ndjson, yaml, xml). Mutually + exclusive with a query argument, *--columns*, *--validate*, + *--sample*, *--stats*, *--explain*, and *--output*. + + *--inspect* + Inspect input data without running a query. ** is one of: + *columns* (list column names, one per line; with *-v* shows inferred types), + *validate* (parse entire input, print summary: OK: rows, columns), + *sample* [] (print schema comment block and first rows as CSV, default 10), + *stats* (per-column statistics: type, non-null count, min, max, mean), + *schema* (print inferred CREATE TABLE DDL). + Mutually exclusive with a query argument, *--output*, *--explain*, and + other inspect modes. Compatible with *--delimiter*, *--tsv*, *--no-type-inference*, + *-I* / *--input-format* (csv, tsv, json, ndjson, yaml, xml, parquet). + *--sample* implies *--header*. *--sample* and *--stats* are mutually exclusive + with *--json* output. *--output* Write results to instead of standard output. Creates or @@ -240,12 +256,12 @@ OPTIONS - *.read* — load and execute queries from a .sql file - *Ctrl-D* (EOF) or *Ctrl-C* — exit the REPL - Arrow-key history is provided via *linenoise*. The history file is stored - at *$HOME/.sqlpipe_history*. Only the first SQL statement per line is - executed; multi-statement lines are not split in v1. + No history persistence (no arrow-key history). Multi-line queries: + end lines with Enter, finish with *;*. Empty line in multi-line mode + executes the accumulated query. - No query argument is required. Cannot be combined with *--columns*, - *--validate*, *--sample*, *--stats*, *--schema*, *--explain*, + No query argument is required. Cannot be combined with *--inspect*, + *--columns*, *--validate*, *--sample*, *--stats*, *--schema*, *--explain*, *-f* / *--file*, *--output*, or *--completions*. Compatible with *--save*, *--disk*, *-O*, and file arguments. diff --git a/lib/linenoise/linenoise.c b/lib/linenoise/linenoise.c deleted file mode 100644 index 63f23dd..0000000 --- a/lib/linenoise/linenoise.c +++ /dev/null @@ -1,2380 +0,0 @@ -/* linenoise.c -- guerrilla line editing library against the idea that a - * line editing lib needs to be 20,000 lines of C code. - * - * You can find the latest source code at: - * - * http://github.com/antirez/linenoise - * - * Does a number of crazy assumptions that happen to be true in 99.9999% of - * the 2010 UNIX computers around. - * - * ------------------------------------------------------------------------ - * - * Copyright (c) 2010-2023, Salvatore Sanfilippo - * Copyright (c) 2010-2013, Pieter Noordhuis - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * ------------------------------------------------------------------------ - * - * References: - * - http://invisible-island.net/xterm/ctlseqs/ctlseqs.html - * - http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html - * - * Todo list: - * - Filter bogus Ctrl+ combinations. - * - Win32 support - * - * Bloat: - * - History search like Ctrl+r in readline? - * - * List of escape sequences used by this program, we do everything just - * with three sequences. In order to be so cheap we may have some - * flickering effect with some slow terminal, but the lesser sequences - * the more compatible. - * - * EL (Erase Line) - * Sequence: ESC [ n K - * Effect: if n is 0 or missing, clear from cursor to end of line - * Effect: if n is 1, clear from beginning of line to cursor - * Effect: if n is 2, clear entire line - * - * CUF (CUrsor Forward) - * Sequence: ESC [ n C - * Effect: moves cursor forward n chars - * - * CUB (CUrsor Backward) - * Sequence: ESC [ n D - * Effect: moves cursor backward n chars - * - * The following is used to get the terminal width if getting - * the width with the TIOCGWINSZ ioctl fails - * - * DSR (Device Status Report) - * Sequence: ESC [ 6 n - * Effect: reports the current cusor position as ESC [ n ; m R - * where n is the row and m is the column - * - * When multi line mode is enabled, we also use an additional escape - * sequence. However multi line editing is disabled by default. - * - * CUU (Cursor Up) - * Sequence: ESC [ n A - * Effect: moves cursor up of n chars. - * - * CUD (Cursor Down) - * Sequence: ESC [ n B - * Effect: moves cursor down of n chars. - * - * When linenoiseClearScreen() is called, two additional escape sequences - * are used in order to clear the screen and position the cursor at home - * position. - * - * CUP (Cursor position) - * Sequence: ESC [ H - * Effect: moves the cursor to upper left corner - * - * ED (Erase display) - * Sequence: ESC [ 2 J - * Effect: clear the whole screen - * - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "linenoise.h" - -#define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100 -#define LINENOISE_MAX_LINE (1024*1024) // That will get dynamically allocated -#define LINENOISE_INITIAL_BUFLEN 4096 -#define PASTE_FOLD_THRESHOLD 200 // Min bytes to fold a single-line paste. -#define PASTE_FOLD_CONTEXT 8 // Context chars kept around generic folds. -#define PASTE_MAX_BYTES LINENOISE_MAX_LINE -static char *unsupported_term[] = {"dumb","cons25","emacs",NULL}; -static linenoiseCompletionCallback *completionCallback = NULL; -static linenoiseHintsCallback *hintsCallback = NULL; -static linenoiseFreeHintsCallback *freeHintsCallback = NULL; -static char *linenoiseReadLine(FILE *fp, int *err); -static char *linenoiseNoTTY(void); -static void refreshLineWithCompletion(struct linenoiseState *ls, linenoiseCompletions *lc, int flags); -static void refreshLineWithFlags(struct linenoiseState *l, int flags); -static void linenoiseFoldClear(struct linenoiseState *l); - -static struct termios orig_termios; /* In order to restore at exit.*/ -static int maskmode = 0; /* Show "***" instead of input. For passwords. */ -static int rawmode = 0; /* For atexit() function to check if restore is needed*/ -static int rawmode_output = STDOUT_FILENO; /* fd used for terminal escapes. */ -static int mlmode = 0; /* Multi line mode. Default is single line. */ -static int atexit_registered = 0; /* Register atexit just 1 time. */ -static int history_max_len = LINENOISE_DEFAULT_HISTORY_MAX_LEN; -static int history_len = 0; -static char **history = NULL; - -/* =========================== UTF-8 support ================================ */ - -/* Return the number of bytes that compose the UTF-8 character starting at - * 'c'. This function assumes a valid UTF-8 encoding and handles the four - * standard byte patterns: - * 0xxxxxxx -> 1 byte (ASCII) - * 110xxxxx -> 2 bytes - * 1110xxxx -> 3 bytes - * 11110xxx -> 4 bytes */ -static int utf8ByteLen(char c) { - unsigned char uc = (unsigned char)c; - if ((uc & 0x80) == 0) return 1; /* 0xxxxxxx: ASCII */ - if ((uc & 0xE0) == 0xC0) return 2; /* 110xxxxx: 2-byte seq */ - if ((uc & 0xF0) == 0xE0) return 3; /* 1110xxxx: 3-byte seq */ - if ((uc & 0xF8) == 0xF0) return 4; /* 11110xxx: 4-byte seq */ - return 1; /* Fallback for invalid encoding, treat as single byte. */ -} - -/* Decode a UTF-8 sequence starting at 's' into a Unicode codepoint. - * Returns the codepoint value. Assumes valid UTF-8 encoding. */ -static uint32_t utf8DecodeChar(const char *s, size_t *len) { - unsigned char *p = (unsigned char *)s; - uint32_t cp; - - if ((*p & 0x80) == 0) { - *len = 1; - return *p; - } else if ((*p & 0xE0) == 0xC0) { - *len = 2; - cp = (*p & 0x1F) << 6; - cp |= (p[1] & 0x3F); - return cp; - } else if ((*p & 0xF0) == 0xE0) { - *len = 3; - cp = (*p & 0x0F) << 12; - cp |= (p[1] & 0x3F) << 6; - cp |= (p[2] & 0x3F); - return cp; - } else if ((*p & 0xF8) == 0xF0) { - *len = 4; - cp = (*p & 0x07) << 18; - cp |= (p[1] & 0x3F) << 12; - cp |= (p[2] & 0x3F) << 6; - cp |= (p[3] & 0x3F); - return cp; - } - *len = 1; - return *p; /* Fallback for invalid sequences. */ -} - -/* Check if codepoint is a variation selector (emoji style modifiers). */ -static int isVariationSelector(uint32_t cp) { - return cp == 0xFE0E || cp == 0xFE0F; /* Text/emoji style */ -} - -/* Check if codepoint is a skin tone modifier. */ -static int isSkinToneModifier(uint32_t cp) { - return cp >= 0x1F3FB && cp <= 0x1F3FF; -} - -/* Check if codepoint is Zero Width Joiner. */ -static int isZWJ(uint32_t cp) { - return cp == 0x200D; -} - -/* Check if codepoint is a Regional Indicator (for flag emoji). */ -static int isRegionalIndicator(uint32_t cp) { - return cp >= 0x1F1E6 && cp <= 0x1F1FF; -} - -/* Check if codepoint is a combining mark or other zero-width character. */ -static int isCombiningMark(uint32_t cp) { - return (cp >= 0x0300 && cp <= 0x036F) || /* Combining Diacriticals */ - (cp >= 0x1AB0 && cp <= 0x1AFF) || /* Combining Diacriticals Extended */ - (cp >= 0x1DC0 && cp <= 0x1DFF) || /* Combining Diacriticals Supplement */ - (cp >= 0x20D0 && cp <= 0x20FF) || /* Combining Diacriticals for Symbols */ - (cp >= 0xFE20 && cp <= 0xFE2F); /* Combining Half Marks */ -} - -/* Check if codepoint extends the previous character (doesn't start a new grapheme). */ -static int isGraphemeExtend(uint32_t cp) { - return isVariationSelector(cp) || isSkinToneModifier(cp) || - isZWJ(cp) || isCombiningMark(cp); -} - -/* Decode the UTF-8 codepoint ending at position 'pos' (exclusive) and - * return its value. Also sets *cplen to the byte length of the codepoint. */ -static uint32_t utf8DecodePrev(const char *buf, size_t pos, size_t *cplen) { - if (pos == 0) { - *cplen = 0; - return 0; - } - /* Scan backwards to find the start byte. */ - size_t i = pos; - do { - i--; - } while (i > 0 && (pos - i) < 4 && ((unsigned char)buf[i] & 0xC0) == 0x80); - *cplen = pos - i; - size_t dummy; - return utf8DecodeChar(buf + i, &dummy); -} - -/* Given a buffer and a position, return the byte length of the grapheme - * cluster before that position. A grapheme cluster includes: - * - The base character - * - Any following variation selectors, skin tone modifiers - * - ZWJ sequences (emoji joined by Zero Width Joiner) - * - Regional indicator pairs (flag emoji) */ -static size_t utf8PrevCharLen(const char *buf, size_t pos) { - if (pos == 0) return 0; - - size_t total = 0; - size_t curpos = pos; - - /* First, get the last codepoint. */ - size_t cplen; - uint32_t cp = utf8DecodePrev(buf, curpos, &cplen); - if (cplen == 0) return 0; - total += cplen; - curpos -= cplen; - - /* If we're at an extending character, we need to find what it extends. - * Keep going back through the grapheme cluster. */ - while (curpos > 0) { - size_t prevlen; - uint32_t prevcp = utf8DecodePrev(buf, curpos, &prevlen); - if (prevlen == 0) break; - - if (isZWJ(prevcp)) { - /* ZWJ joins two emoji. Include the ZWJ and continue to get - * the preceding character. */ - total += prevlen; - curpos -= prevlen; - /* Now get the character before ZWJ. */ - prevcp = utf8DecodePrev(buf, curpos, &prevlen); - if (prevlen == 0) break; - total += prevlen; - curpos -= prevlen; - cp = prevcp; - continue; /* Check if there's more extending before this. */ - } else if (isGraphemeExtend(cp)) { - /* Current cp is an extending character; include previous. */ - total += prevlen; - curpos -= prevlen; - cp = prevcp; - continue; - } else if (isRegionalIndicator(cp) && isRegionalIndicator(prevcp)) { - /* Two regional indicators form a flag. But we need to be careful: - * flags are always pairs, so only join if we're at an even boundary. - * For simplicity, just join one pair. */ - total += prevlen; - curpos -= prevlen; - break; - } else { - /* No more extending; we've found the start of the cluster. */ - break; - } - } - - return total; -} - -/* Given a buffer, position and total length, return the byte length of the - * grapheme cluster at the current position. */ -static size_t utf8NextCharLen(const char *buf, size_t pos, size_t len) { - if (pos >= len) return 0; - - size_t total = 0; - size_t curpos = pos; - - /* Get the first codepoint. */ - size_t cplen; - uint32_t cp = utf8DecodeChar(buf + curpos, &cplen); - total += cplen; - curpos += cplen; - - int isRI = isRegionalIndicator(cp); - - /* Consume any extending characters that follow. */ - while (curpos < len) { - size_t nextlen; - uint32_t nextcp = utf8DecodeChar(buf + curpos, &nextlen); - - if (isZWJ(nextcp) && curpos + nextlen < len) { - /* ZWJ: include it and the following character. */ - total += nextlen; - curpos += nextlen; - /* Get the character after ZWJ. */ - nextcp = utf8DecodeChar(buf + curpos, &nextlen); - total += nextlen; - curpos += nextlen; - continue; /* Check for more extending after the joined char. */ - } else if (isGraphemeExtend(nextcp)) { - /* Variation selector, skin tone, combining mark, etc. */ - total += nextlen; - curpos += nextlen; - continue; - } else if (isRI && isRegionalIndicator(nextcp)) { - /* Second regional indicator for a flag pair. */ - total += nextlen; - curpos += nextlen; - isRI = 0; /* Only pair once. */ - continue; - } else { - break; - } - } - - return total; -} - -/* Return the display width of a Unicode codepoint. This is a heuristic - * that works for most common cases: - * - Control chars and zero-width: 0 columns - * - Grapheme-extending chars (VS, skin tone, ZWJ): 0 columns - * - ASCII printable: 1 column - * - Wide chars (CJK, emoji, fullwidth): 2 columns - * - Everything else: 1 column - * - * This is not a full wcwidth() implementation, but a minimal heuristic - * that handles emoji and CJK characters reasonably well. */ -static int utf8CharWidth(uint32_t cp) { - /* Control characters and combining marks: zero width. */ - if (cp < 32 || (cp >= 0x7F && cp < 0xA0)) return 0; - if (isCombiningMark(cp)) return 0; - - /* Grapheme-extending characters: zero width. - * These modify the preceding character rather than taking space. */ - if (isVariationSelector(cp)) return 0; - if (isSkinToneModifier(cp)) return 0; - if (isZWJ(cp)) return 0; - - /* Wide character ranges - these display as 2 columns: - * - CJK Unified Ideographs and Extensions - * - Fullwidth forms - * - Various emoji ranges */ - if (cp >= 0x1100 && - (cp <= 0x115F || /* Hangul Jamo */ - cp == 0x2329 || cp == 0x232A || /* Angle brackets */ - (cp >= 0x231A && cp <= 0x231B) || /* Watch, Hourglass */ - (cp >= 0x23E9 && cp <= 0x23F3) || /* Various symbols */ - (cp >= 0x23F8 && cp <= 0x23FA) || /* Various symbols */ - (cp >= 0x25AA && cp <= 0x25AB) || /* Small squares */ - (cp >= 0x25B6 && cp <= 0x25C0) || /* Play/reverse buttons */ - (cp >= 0x25FB && cp <= 0x25FE) || /* Squares */ - (cp >= 0x2600 && cp <= 0x26FF) || /* Misc Symbols (sun, cloud, etc) */ - (cp >= 0x2700 && cp <= 0x27BF) || /* Dingbats (❤, ✂, etc) */ - (cp >= 0x2934 && cp <= 0x2935) || /* Arrows */ - (cp >= 0x2B05 && cp <= 0x2B07) || /* Arrows */ - (cp >= 0x2B1B && cp <= 0x2B1C) || /* Squares */ - cp == 0x2B50 || cp == 0x2B55 || /* Star, circle */ - (cp >= 0x2E80 && cp <= 0xA4CF && - cp != 0x303F) || /* CJK ... Yi */ - (cp >= 0xAC00 && cp <= 0xD7A3) || /* Hangul Syllables */ - (cp >= 0xF900 && cp <= 0xFAFF) || /* CJK Compatibility Ideographs */ - (cp >= 0xFE10 && cp <= 0xFE1F) || /* Vertical forms */ - (cp >= 0xFE30 && cp <= 0xFE6F) || /* CJK Compatibility Forms */ - (cp >= 0xFF00 && cp <= 0xFF60) || /* Fullwidth Forms */ - (cp >= 0xFFE0 && cp <= 0xFFE6) || /* Fullwidth Signs */ - (cp >= 0x1F1E6 && cp <= 0x1F1FF) || /* Regional Indicators (flags) */ - (cp >= 0x1F300 && cp <= 0x1F64F) || /* Misc Symbols and Emoticons */ - (cp >= 0x1F680 && cp <= 0x1F6FF) || /* Transport and Map Symbols */ - (cp >= 0x1F900 && cp <= 0x1F9FF) || /* Supplemental Symbols */ - (cp >= 0x1FA00 && cp <= 0x1FAFF) || /* Chess, Extended-A */ - (cp >= 0x20000 && cp <= 0x2FFFF))) /* CJK Extension B and beyond */ - return 2; - - return 1; /* Default: single width */ -} - -/* If s[] points at an ANSI CSI escape sequence (e.g. a color change like - * ESC [ 1 ; 32 m), return its length in bytes. Otherwise return 0. - * - * The caller must have already verified that s[0] == ESC (0x1b). The - * sequence layout follows ECMA-48: ESC '[' , parameter bytes (0x30-0x3f), - * intermediate bytes (0x20-0x2f), and a final byte (0x40-0x7e). */ -static size_t ansiEscapeLen(const char *s, size_t len) { - size_t i; - if (len < 2 || s[1] != '[') return 0; - i = 2; - while (i < len && (unsigned char)s[i] >= 0x30 && (unsigned char)s[i] <= 0x3f) i++; - while (i < len && (unsigned char)s[i] >= 0x20 && (unsigned char)s[i] <= 0x2f) i++; - if (i >= len || (unsigned char)s[i] < 0x40 || (unsigned char)s[i] > 0x7e) return 0; - return i + 1; -} - -/* Calculate the display width of a UTF-8 string of 'len' bytes. - * This is used for cursor positioning in the terminal. - * Handles grapheme clusters: characters joined by ZWJ contribute 0 width - * after the first character in the sequence. - * ANSI CSI escape sequences (e.g. color codes in the prompt) are treated - * as zero-width. */ -static size_t utf8StrWidth(const char *s, size_t len) { - size_t width = 0; - size_t i = 0; - int after_zwj = 0; /* Track if previous char was ZWJ */ - - while (i < len) { - size_t clen; - uint32_t cp = utf8DecodeChar(s + i, &clen); - - /* Skip ANSI CSI escape sequences entirely: they produce no - * glyph, so they must not contribute to the display width. - * Checked before the ZWJ state so a stray ZWJ immediately - * followed by ESC cannot swallow the ESC byte. */ - if (cp == 0x1b) { - size_t skip = ansiEscapeLen(s + i, len - i); - if (skip > 0) { - i += skip; - continue; - } - } - - if (after_zwj) { - /* Character after ZWJ: don't add width, it's joined. - * But do check for extending chars after it. */ - after_zwj = 0; - } else { - width += utf8CharWidth(cp); - } - - /* Check if this is a ZWJ - next char will be joined. */ - if (isZWJ(cp)) { - after_zwj = 1; - } - - i += clen; - } - return width; -} - -/* Return the display width of a single UTF-8 character at position 's'. */ -static int utf8SingleCharWidth(const char *s, size_t len) { - if (len == 0) return 0; - size_t clen; - uint32_t cp = utf8DecodeChar(s, &clen); - return utf8CharWidth(cp); -} - -enum KEY_ACTION{ - KEY_NULL = 0, /* NULL */ - CTRL_A = 1, /* Ctrl+a */ - CTRL_B = 2, /* Ctrl-b */ - CTRL_C = 3, /* Ctrl-c */ - CTRL_D = 4, /* Ctrl-d */ - CTRL_E = 5, /* Ctrl-e */ - CTRL_F = 6, /* Ctrl-f */ - CTRL_H = 8, /* Ctrl-h */ - TAB = 9, /* Tab */ - CTRL_K = 11, /* Ctrl+k */ - CTRL_L = 12, /* Ctrl+l */ - ENTER = 13, /* Enter */ - CTRL_N = 14, /* Ctrl-n */ - CTRL_P = 16, /* Ctrl-p */ - CTRL_T = 20, /* Ctrl-t */ - CTRL_U = 21, /* Ctrl+u */ - CTRL_W = 23, /* Ctrl+w */ - ESC = 27, /* Escape */ - BACKSPACE = 127 /* Backspace */ -}; - -static void linenoiseAtExit(void); -int linenoiseHistoryAdd(const char *line); -#define REFRESH_CLEAN (1<<0) // Clean the old prompt from the screen -#define REFRESH_WRITE (1<<1) // Rewrite the prompt on the screen. -#define REFRESH_ALL (REFRESH_CLEAN|REFRESH_WRITE) // Do both. -static void refreshLine(struct linenoiseState *l); - -/* Debugging macro. */ -#if 0 -FILE *lndebug_fp = NULL; -#define lndebug(...) \ - do { \ - if (lndebug_fp == NULL) { \ - lndebug_fp = fopen("/tmp/lndebug.txt","a"); \ - fprintf(lndebug_fp, \ - "[%d %d %d] p: %d, rows: %d, rpos: %d, max: %d, oldmax: %d\n", \ - (int)l->len,(int)l->pos,(int)l->oldpos,plen,rows,rpos, \ - (int)l->oldrows,old_rows); \ - } \ - fprintf(lndebug_fp, ", " __VA_ARGS__); \ - fflush(lndebug_fp); \ - } while (0) -#else -#define lndebug(fmt, ...) -#endif - -/* ======================= Low level terminal handling ====================== */ - -/* Enable "mask mode". When it is enabled, instead of the input that - * the user is typing, the terminal will just display a corresponding - * number of asterisks, like "****". This is useful for passwords and other - * secrets that should not be displayed. */ -void linenoiseMaskModeEnable(void) { - maskmode = 1; -} - -/* Disable mask mode. */ -void linenoiseMaskModeDisable(void) { - maskmode = 0; -} - -/* Set if to use or not the multi line mode. */ -void linenoiseSetMultiLine(int ml) { - mlmode = ml; -} - -/* Return true if the terminal name is in the list of terminals we know are - * not able to understand basic escape sequences. */ -static int isUnsupportedTerm(void) { - char *term = getenv("TERM"); - int j; - - if (term == NULL) return 0; - for (j = 0; unsupported_term[j]; j++) - if (!strcasecmp(term,unsupported_term[j])) return 1; - return 0; -} - -/* Raw mode: 1960 magic shit. */ -static int enableRawMode(int fd) { - struct termios raw; - - /* Test mode: when LINENOISE_ASSUME_TTY is set, skip terminal setup. - * This allows testing via pipes without a real terminal. */ - if (getenv("LINENOISE_ASSUME_TTY")) { - rawmode = 1; - return 0; - } - - if (!isatty(STDIN_FILENO)) goto fatal; - if (!atexit_registered) { - atexit(linenoiseAtExit); - atexit_registered = 1; - } - if (tcgetattr(fd,&orig_termios) == -1) goto fatal; - - raw = orig_termios; /* modify the original mode */ - /* input modes: no break, no CR to NL, no parity check, no strip char, - * no start/stop output control. */ - raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); - /* output modes - disable post processing */ - raw.c_oflag &= ~(OPOST); - /* control modes - set 8 bit chars */ - raw.c_cflag |= (CS8); - /* local modes - choing off, canonical off, no extended functions, - * no signal chars (^Z,^C) */ - raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); - /* control chars - set return condition: min number of bytes and timer. - * We want read to return every single byte, without timeout. */ - raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ - - /* put terminal in raw mode after flushing */ - if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal; - rawmode = 1; - /* Ask the terminal to wrap paste input between ESC[200~ and ESC[201~. */ - if (write(rawmode_output, "\x1b[?2004h", 8) == -1) {} - return 0; - -fatal: - errno = ENOTTY; - return -1; -} - -static void disableRawMode(int fd) { - /* Test mode: nothing to restore. */ - if (getenv("LINENOISE_ASSUME_TTY")) { - rawmode = 0; - return; - } - /* Don't even check the return value as it's too late. */ - if (rawmode && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1) { - /* Leave bracketed paste mode when leaving raw mode. */ - if (write(rawmode_output, "\x1b[?2004l", 8) == -1) {} - rawmode = 0; - } -} - -/* Use the ESC [6n escape sequence to query the horizontal cursor position - * and return it. On error -1 is returned, on success the position of the - * cursor. */ -static int getCursorPosition(int ifd, int ofd) { - char buf[32]; - int cols, rows; - unsigned int i = 0; - - /* Report cursor location */ - if (write(ofd, "\x1b[6n", 4) != 4) return -1; - - /* Read the response: ESC [ rows ; cols R */ - while (i < sizeof(buf)-1) { - if (read(ifd,buf+i,1) != 1) break; - if (buf[i] == 'R') break; - i++; - } - buf[i] = '\0'; - - /* Parse it. */ - if (buf[0] != ESC || buf[1] != '[') return -1; - if (sscanf(buf+2,"%d;%d",&rows,&cols) != 2) return -1; - return cols; -} - -/* Try to get the number of columns in the current terminal, or assume 80 - * if it fails. */ -static int getColumns(int ifd, int ofd) { - struct winsize ws; - - /* Test mode: use LINENOISE_COLS env var for fixed width. */ - char *cols_env = getenv("LINENOISE_COLS"); - if (cols_env) return atoi(cols_env); - - if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { - /* ioctl() failed. Try to query the terminal itself. */ - int start, cols; - - /* Get the initial position so we can restore it later. */ - start = getCursorPosition(ifd,ofd); - if (start == -1) goto failed; - - /* Go to right margin and get position. */ - if (write(ofd,"\x1b[999C",6) != 6) goto failed; - cols = getCursorPosition(ifd,ofd); - if (cols == -1) goto failed; - - /* Restore position. */ - if (cols > start) { - char seq[32]; - snprintf(seq,32,"\x1b[%dD",cols-start); - if (write(ofd,seq,strlen(seq)) == -1) { - /* Can't recover... */ - } - } - return cols; - } else { - return ws.ws_col; - } - -failed: - return 80; -} - -/* Clear the screen. Used to handle ctrl+l */ -void linenoiseClearScreen(void) { - if (write(STDOUT_FILENO,"\x1b[H\x1b[2J",7) <= 0) { - /* nothing to do, just to avoid warning. */ - } -} - -/* Beep, used for completion when there is nothing to complete or when all - * the choices were already shown. */ -static void linenoiseBeep(void) { - fprintf(stderr, "\x7"); - fflush(stderr); -} - -/* ============================== Completion ================================ */ - -/* Free a list of completion option populated by linenoiseAddCompletion(). */ -static void freeCompletions(linenoiseCompletions *lc) { - size_t i; - for (i = 0; i < lc->len; i++) - free(lc->cvec[i]); - if (lc->cvec != NULL) - free(lc->cvec); -} - -/* Called by completeLine() and linenoiseShow() to render the current - * edited line with the proposed completion. If the current completion table - * is already available, it is passed as second argument, otherwise the - * function will use the callback to obtain it. - * - * Flags are the same as refreshLine*(), that is REFRESH_* macros. */ -static void refreshLineWithCompletion(struct linenoiseState *ls, linenoiseCompletions *lc, int flags) { - /* Obtain the table of completions if the caller didn't provide one. */ - linenoiseCompletions ctable = { 0, NULL }; - if (lc == NULL) { - completionCallback(ls->buf,&ctable); - lc = &ctable; - } - - /* Show the edited line with completion if possible, or just refresh. */ - if (ls->completion_idx < lc->len) { - struct linenoiseState saved = *ls; - ls->len = ls->pos = strlen(lc->cvec[ls->completion_idx]); - ls->buf = lc->cvec[ls->completion_idx]; - ls->fold_count = 0; - refreshLineWithFlags(ls,flags); - ls->len = saved.len; - ls->pos = saved.pos; - ls->buf = saved.buf; - ls->fold_count = saved.fold_count; - } else { - refreshLineWithFlags(ls,flags); - } - - /* Free the completions table if needed. */ - if (lc != &ctable) freeCompletions(&ctable); -} - -/* This is an helper function for linenoiseEdit*() and is called when the - * user types the key in order to complete the string currently in the - * input. - * - * The state of the editing is encapsulated into the pointed linenoiseState - * structure as described in the structure definition. - * - * If the function returns non-zero, the caller should handle the - * returned value as a byte read from the standard input, and process - * it as usually: this basically means that the function may return a byte - * read from the termianl but not processed. Otherwise, if zero is returned, - * the input was consumed by the completeLine() function to navigate the - * possible completions, and the caller should read for the next characters - * from stdin. */ -static int completeLine(struct linenoiseState *ls, int keypressed) { - linenoiseCompletions lc = { 0, NULL }; - int nwritten; - char c = keypressed; - - completionCallback(ls->buf,&lc); - if (lc.len == 0) { - linenoiseBeep(); - ls->in_completion = 0; - c = 0; - } else { - switch(c) { - case 9: /* tab */ - if (ls->in_completion == 0) { - ls->in_completion = 1; - ls->completion_idx = 0; - } else { - ls->completion_idx = (ls->completion_idx+1) % (lc.len+1); - if (ls->completion_idx == lc.len) linenoiseBeep(); - } - c = 0; - break; - case 27: /* escape */ - /* Re-show original buffer */ - if (ls->completion_idx < lc.len) refreshLine(ls); - ls->in_completion = 0; - c = 0; - break; - default: - /* Update buffer and return */ - if (ls->completion_idx < lc.len) { - nwritten = snprintf(ls->buf,ls->buflen,"%s", - lc.cvec[ls->completion_idx]); - ls->len = ls->pos = nwritten; - linenoiseFoldClear(ls); - } - ls->in_completion = 0; - break; - } - - /* Show completion or original buffer */ - if (ls->in_completion && ls->completion_idx < lc.len) { - refreshLineWithCompletion(ls,&lc,REFRESH_ALL); - } else { - refreshLine(ls); - } - } - - freeCompletions(&lc); - return c; /* Return last read character */ -} - -/* Register a callback function to be called for tab-completion. */ -void linenoiseSetCompletionCallback(linenoiseCompletionCallback *fn) { - completionCallback = fn; -} - -/* Register a hits function to be called to show hits to the user at the - * right of the prompt. */ -void linenoiseSetHintsCallback(linenoiseHintsCallback *fn) { - hintsCallback = fn; -} - -/* Register a function to free the hints returned by the hints callback - * registered with linenoiseSetHintsCallback(). */ -void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *fn) { - freeHintsCallback = fn; -} - -/* This function is used by the callback function registered by the user - * in order to add completion options given the input string when the - * user typed . See the example.c source code for a very easy to - * understand example. */ -void linenoiseAddCompletion(linenoiseCompletions *lc, const char *str) { - size_t len = strlen(str); - char *copy, **cvec; - - copy = malloc(len+1); - if (copy == NULL) return; - memcpy(copy,str,len+1); - cvec = realloc(lc->cvec,sizeof(char*)*(lc->len+1)); - if (cvec == NULL) { - free(copy); - return; - } - lc->cvec = cvec; - lc->cvec[lc->len++] = copy; -} - -/* =========================== Line editing ================================= */ - -/* We define a very simple "append buffer" structure, that is an heap - * allocated string where we can append to. This is useful in order to - * write all the escape sequences in a buffer and flush them to the standard - * output in a single call, to avoid flickering effects. */ -struct abuf { - char *b; - int len; -}; - -static void abInit(struct abuf *ab) { - ab->b = NULL; - ab->len = 0; -} - -static void abAppend(struct abuf *ab, const char *s, int len) { - char *new = realloc(ab->b,ab->len+len); - - if (new == NULL) return; - memcpy(new+ab->len,s,len); - ab->b = new; - ab->len += len; -} - -static void abFree(struct abuf *ab) { - free(ab->b); -} - -/* A fold is a display-only replacement for a range in l->buf. The edited - * buffer always keeps the real bytes; refresh code asks linenoiseRenderBuffer() - * for a temporary printable version plus the cursor position inside it. */ -struct linenoiseFold { - size_t start; - size_t end; - char display[64]; - size_t displaylen; -}; - -struct linenoiseFolds { - int count; - struct linenoiseFold fold[LINENOISE_MAX_FOLDS]; -}; - -/* Return the number of logical lines in the range. */ -static size_t foldCountLines(const char *buf, size_t len) { - size_t lines = 1, j; - for (j = 0; j < len; j++) { - if (buf[j] == '\n') lines++; - } - return lines; -} - -/* Return true if the text should be folded: if it contains newlines or is at - * least PASTE_FOLD_THRESHOLD bytes long. */ -static int shouldFoldText(const char *buf, size_t len) { - return memchr(buf, '\n', len) != NULL || len >= PASTE_FOLD_THRESHOLD; -} - -/* Fill f->display with the text shown instead of the folded range. */ -static void foldSetRenderedText(struct linenoiseFold *f, const char *buf) { - size_t hidden = f->end - f->start; - size_t lines = foldCountLines(buf + f->start, hidden); - int n; - - if (lines > 1) - n = snprintf(f->display,sizeof(f->display),"[... %zu pasted lines ...]",lines); - else - n = snprintf(f->display,sizeof(f->display),"[... %zu pasted chars ...]",hidden); - if (n < 0) n = 0; - f->displaylen = (size_t)n; -} - -/* Populate f with one fold reconstructed from a history entry. History stores - * the real text, but not the original paste boundaries, so we reconstruct - * an approximation of text we want to hide on the fly: if it is long or - * contains newlines. */ -static int linenoiseBuildHistoryFold(struct linenoiseState *l, struct linenoiseFold *f) { - f->start = f->end = f->displaylen = 0; - if (l->len == 0 || maskmode) return 0; - if (!shouldFoldText(l->buf,l->len)) return 0; - - f->start = 0; - f->end = l->len; - if (l->len > PASTE_FOLD_CONTEXT*2) { - size_t pos = 0, chars = 0; - int nl = 0; - - /* We leave (if possible) a few chars on - * the start before the fold, to give context. */ - while (pos < l->len && chars < PASTE_FOLD_CONTEXT) { - size_t step = utf8NextCharLen(l->buf,pos,l->len); - if (step == 0 || pos + step > l->len) break; - if (l->buf[pos] == '\n') nl = 1; - pos += step; - chars++; - } - f->start = nl ? 0 : pos; - - /* And also on the end side. */ - pos = l->len; - chars = 0; - nl = 0; - while (pos > 0 && chars < PASTE_FOLD_CONTEXT) { - size_t step = utf8PrevCharLen(l->buf,pos); - if (step == 0 || step > pos) break; - pos -= step; - if (l->buf[pos] == '\n') nl = 1; - chars++; - } - f->end = nl ? l->len : pos; - if (f->start >= f->end) { - f->start = 0; - f->end = l->len; - } - } - foldSetRenderedText(f,l->buf); - return 1; -} - -/* Populate fs with the folds to render for the current buffer. As a side - * effect, the rendered text of each fold is updated. Return 1 if folding - * should be used, or 0 if the buffer should be rendered as-is. */ -static int linenoiseGetRenderFolds(struct linenoiseState *l, struct linenoiseFolds *fs) { - int j; - - fs->count = 0; - if (l->len == 0 || maskmode) return 0; - - for (j = 0; j < l->fold_count; j++) { - struct linenoiseFold *f; - size_t start = l->fold_start[j]; - size_t end = l->fold_end[j]; - - if (start >= end || end > l->len) continue; - f = fs->fold + fs->count++; - f->start = start; - f->end = end; - foldSetRenderedText(f,l->buf); - } - return fs->count != 0; -} - -/* Return the freshly allocated string content that is actually displayed in - * the user prompt. It can be the actual edited line, or a special version - * where pasted or multiline history ranges are replaced by their folded - * "[...]" style versions. outpos is l->pos translated into this rendered - * buffer. */ -static int linenoiseRenderBuffer(struct linenoiseState *l, char **out, size_t *outlen, size_t *outpos) { - struct linenoiseFolds fs; - size_t len, pos, src, dst; - char *r; - int j, pos_set = 0; - - if (!linenoiseGetRenderFolds(l,&fs)) { - /* Keep the refresh code simple: it always owns a temporary render - * buffer, even when the render is identical to the real edit buffer. */ - r = malloc(l->len+1); - if (r == NULL) return -1; - memcpy(r,l->buf,l->len); - r[l->len] = '\0'; - *out = r; - *outlen = l->len; - *outpos = l->pos; - return 0; - } - - /* Gaps are copied as-is, folded ranges are replaced by their markers. - * The bytes inside each [start,end) range stay in l->buf but are not - * emitted to the terminal. */ - len = l->len; - for (j = 0; j < fs.count; j++) { - struct linenoiseFold *f = fs.fold+j; - len -= f->end - f->start; - len += f->displaylen; - } - r = malloc(len+1); - if (r == NULL) return -1; - - src = dst = 0; - pos = 0; - for (j = 0; j < fs.count; j++) { - struct linenoiseFold *f = fs.fold+j; - size_t gap = f->start - src; - - if (!pos_set && l->pos <= f->start) { - pos = dst + (l->pos - src); - pos_set = 1; - } - memcpy(r+dst,l->buf+src,gap); - dst += gap; - - if (!pos_set && l->pos < f->end) { - pos = dst + f->displaylen; - pos_set = 1; - } - memcpy(r+dst,f->display,f->displaylen); - dst += f->displaylen; - if (!pos_set && l->pos == f->end) { - pos = dst; - pos_set = 1; - } - src = f->end; - } - if (!pos_set) pos = dst + (l->pos - src); - memcpy(r+dst,l->buf+src,l->len-src); - r[len] = '\0'; - - *out = r; - *outlen = len; - *outpos = pos; - return 0; -} - -/* Return the number of bytes to move right from pos. If pos is at the start of - * a folded range, the whole hidden range is skipped by one cursor movement. */ -static size_t linenoiseEditNextLen(struct linenoiseState *l, size_t pos) { - struct linenoiseFolds fs; - int j; - - if (linenoiseGetRenderFolds(l,&fs)) { - for (j = 0; j < fs.count; j++) { - if (pos == fs.fold[j].start) - return fs.fold[j].end - fs.fold[j].start; - } - } - return utf8NextCharLen(l->buf,pos,l->len); -} - -/* Return the number of bytes to move left from pos. If pos is at the end of a - * folded range, the whole hidden range is skipped by one cursor movement. */ -static size_t linenoiseEditPrevLen(struct linenoiseState *l, size_t pos) { - struct linenoiseFolds fs; - int j; - - if (linenoiseGetRenderFolds(l,&fs)) { - for (j = 0; j < fs.count; j++) { - if (pos == fs.fold[j].end) - return fs.fold[j].end - fs.fold[j].start; - } - } - return utf8PrevCharLen(l->buf,pos); -} - -/* Add a fold range, keeping the array sorted by start offset. */ -static void linenoiseFoldAdd(struct linenoiseState *l, size_t start, size_t end) { - int j; - - if (start >= end || l->fold_count == LINENOISE_MAX_FOLDS) return; - j = l->fold_count; - while (j > 0 && start < l->fold_start[j-1]) { - l->fold_start[j] = l->fold_start[j-1]; - l->fold_end[j] = l->fold_end[j-1]; - j--; - } - l->fold_start[j] = start; - l->fold_end[j] = end; - l->fold_count++; -} - -/* Clear all remembered fold ranges. */ -static void linenoiseFoldClear(struct linenoiseState *l) { - l->fold_count = 0; -} - -/* Remove one remembered fold range. */ -static void linenoiseFoldRemove(struct linenoiseState *l, int j) { - memmove(l->fold_start+j,l->fold_start+j+1, - sizeof(size_t)*(l->fold_count-j-1)); - memmove(l->fold_end+j,l->fold_end+j+1, - sizeof(size_t)*(l->fold_count-j-1)); - l->fold_count--; -} - -/* Return true if [pos,pos+len) overlaps any folded range. */ -static int linenoiseRangeOverlapsFold(struct linenoiseState *l, size_t pos, size_t len) { - size_t end = pos + len; - int j; - - for (j = 0; j < l->fold_count; j++) { - if (end > l->fold_start[j] && pos < l->fold_end[j]) - return 1; - } - return 0; -} - -/* Adjust fold ranges after an insertion. If insertion somehow lands inside a - * fold, remove that fold because it no longer maps to an unchanged range. */ -static void linenoiseAdjustFoldsAfterInsert(struct linenoiseState *l, size_t pos, size_t len) { - int j = 0; - - while (j < l->fold_count) { - if (pos <= l->fold_start[j]) { - l->fold_start[j] += len; - l->fold_end[j] += len; - j++; - } else if (pos < l->fold_end[j]) { - linenoiseFoldRemove(l,j); - } else { - j++; - } - } -} - -/* Adjust fold ranges after a deletion. If deletion overlaps a fold, remove - * that fold because it no longer maps to an unchanged range. */ -static void linenoiseAdjustFoldsAfterDelete(struct linenoiseState *l, size_t pos, size_t len) { - size_t end = pos + len; - int j = 0; - - while (j < l->fold_count) { - if (end <= l->fold_start[j]) { - l->fold_start[j] -= len; - l->fold_end[j] -= len; - j++; - } else if (pos >= l->fold_end[j]) { - j++; - } else { - linenoiseFoldRemove(l,j); - } - } -} - -/* Helper of refreshSingleLine() and refreshMultiLine() to show hints - * to the right of the prompt. Now uses display widths for proper UTF-8. */ -void refreshShowHints(struct abuf *ab, struct linenoiseState *l, int pwidth, size_t bufwidth) { - if (hintsCallback) { - char seq[64]; - int color = -1, bold = 0; - char *hint; - - if (pwidth + bufwidth >= l->cols) return; - hint = hintsCallback(l->buf,&color,&bold); - if (hint) { - size_t hintlen = strlen(hint); - size_t hintwidth = utf8StrWidth(hint, hintlen); - size_t hintmaxwidth = l->cols - (pwidth + bufwidth); - /* Truncate hint to fit, respecting UTF-8 boundaries. */ - if (hintwidth > hintmaxwidth) { - size_t i = 0, w = 0; - while (i < hintlen) { - size_t clen = utf8NextCharLen(hint, i, hintlen); - int cwidth = utf8SingleCharWidth(hint + i, clen); - if (w + cwidth > hintmaxwidth) break; - w += cwidth; - i += clen; - } - hintlen = i; - } - if (bold == 1 && color == -1) color = 37; - if (color != -1 || bold != 0) - snprintf(seq,64,"\033[%d;%d;49m",bold,color); - else - seq[0] = '\0'; - abAppend(ab,seq,strlen(seq)); - abAppend(ab,hint,hintlen); - if (color != -1 || bold != 0) - abAppend(ab,"\033[0m",4); - /* Call the function to free the hint returned. */ - if (freeHintsCallback) freeHintsCallback(hint); - } - } -} - -/* Single line low level line refresh. - * - * Rewrite the currently edited line accordingly to the buffer content, - * cursor position, and number of columns of the terminal. - * - * Flags is REFRESH_* macros. The function can just remove the old - * prompt, just write it, or both. - * - * This function is UTF-8 aware and uses display widths (not byte counts) - * for cursor positioning and horizontal scrolling. */ -static void refreshSingleLine(struct linenoiseState *l, int flags) { - char seq[64]; - size_t pwidth = utf8StrWidth(l->prompt, l->plen); /* Prompt display width */ - int fd = l->ofd; - char *render = NULL; - char *buf; - size_t len; /* Byte length of buffer to display */ - size_t pos; /* Byte position of cursor in display buffer */ - size_t poscol; /* Display column of cursor */ - size_t lencol; /* Display width of buffer */ - size_t fullwidth; /* Display width before horizontal trimming. */ - struct abuf ab; - - if (linenoiseRenderBuffer(l,&render,&len,&pos) == -1) return; - buf = render; - - /* Calculate the display width up to cursor and total display width. */ - poscol = utf8StrWidth(buf, pos); - lencol = utf8StrWidth(buf, len); - fullwidth = lencol; - - /* Scroll the buffer horizontally if cursor is past the right edge. - * We need to trim full UTF-8 characters from the left until the - * cursor position fits within the terminal width. */ - while (pwidth + poscol >= l->cols) { - size_t clen = utf8NextCharLen(buf, 0, len); - int cwidth = utf8SingleCharWidth(buf, clen); - buf += clen; - len -= clen; - pos -= clen; - poscol -= cwidth; - lencol -= cwidth; - } - - /* Trim from the right if the line still doesn't fit. */ - while (pwidth + lencol > l->cols) { - size_t clen = utf8PrevCharLen(buf, len); - int cwidth = utf8SingleCharWidth(buf + len - clen, clen); - len -= clen; - lencol -= cwidth; - } - - abInit(&ab); - /* Cursor to left edge */ - snprintf(seq,sizeof(seq),"\r"); - abAppend(&ab,seq,strlen(seq)); - - if (flags & REFRESH_WRITE) { - /* Write the prompt and the current buffer content */ - abAppend(&ab,l->prompt,l->plen); - if (maskmode == 1) { - /* In mask mode, we output one '*' per UTF-8 character, not byte */ - size_t i = 0; - while (i < len) { - abAppend(&ab,"*",1); - i += utf8NextCharLen(buf, i, len); - } - } else { - abAppend(&ab,buf,len); - } - /* Show hints if any. */ - refreshShowHints(&ab,l,pwidth,fullwidth); - } - - /* Erase to right */ - snprintf(seq,sizeof(seq),"\x1b[0K"); - abAppend(&ab,seq,strlen(seq)); - - if (flags & REFRESH_WRITE) { - /* Move cursor to original position (using display column, not byte). */ - snprintf(seq,sizeof(seq),"\r\x1b[%dC", (int)(poscol+pwidth)); - abAppend(&ab,seq,strlen(seq)); - } - - if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */ - abFree(&ab); - free(render); -} - -/* Multi line low level line refresh. - * - * Rewrite the currently edited line accordingly to the buffer content, - * cursor position, and number of columns of the terminal. - * - * Flags is REFRESH_* macros. The function can just remove the old - * prompt, just write it, or both. - * - * This function is UTF-8 aware and uses display widths for positioning. */ -static void refreshMultiLine(struct linenoiseState *l, int flags) { - char seq[64]; - size_t pwidth = utf8StrWidth(l->prompt, l->plen); /* Prompt display width */ - char *render = NULL; - size_t render_len, render_pos; - size_t bufwidth; - size_t poswidth; - int rows; /* rows used by current rendered buffer. */ - int rpos = l->oldrpos; /* cursor relative row from previous refresh. */ - int rpos2; /* rpos after refresh. */ - int col; /* column position, zero-based. */ - int old_rows = l->oldrows; - int fd = l->ofd, j; - struct abuf ab; - - if (linenoiseRenderBuffer(l,&render,&render_len,&render_pos) == -1) return; - bufwidth = utf8StrWidth(render, render_len); - poswidth = utf8StrWidth(render, render_pos); - rows = (pwidth+bufwidth+l->cols-1)/l->cols; - l->oldrows = rows; - - /* First step: clear all the lines used before. To do so start by - * going to the last row. */ - abInit(&ab); - - if (flags & REFRESH_CLEAN) { - if (old_rows-rpos > 0) { - lndebug("go down %d", old_rows-rpos); - snprintf(seq,64,"\x1b[%dB", old_rows-rpos); - abAppend(&ab,seq,strlen(seq)); - } - - /* Now for every row clear it, go up. */ - for (j = 0; j < old_rows-1; j++) { - lndebug("clear+up"); - snprintf(seq,64,"\r\x1b[0K\x1b[1A"); - abAppend(&ab,seq,strlen(seq)); - } - } - - if (flags & REFRESH_ALL) { - /* Clean the top line. */ - lndebug("clear"); - snprintf(seq,64,"\r\x1b[0K"); - abAppend(&ab,seq,strlen(seq)); - } - - if (flags & REFRESH_WRITE) { - /* Write the prompt and the current buffer content */ - abAppend(&ab,l->prompt,l->plen); - if (maskmode == 1) { - /* In mask mode, output one '*' per UTF-8 character, not byte */ - size_t i = 0; - while (i < render_len) { - abAppend(&ab,"*",1); - i += utf8NextCharLen(render, i, render_len); - } - } else { - abAppend(&ab,render,render_len); - } - - /* Show hints if any. */ - refreshShowHints(&ab,l,pwidth,bufwidth); - - /* If we are at the very end of the screen with our prompt, we need to - * emit a newline and move the prompt to the first column. */ - if (l->pos && - render_pos == render_len && - (poswidth+pwidth) % l->cols == 0) - { - lndebug(""); - abAppend(&ab,"\n",1); - snprintf(seq,64,"\r"); - abAppend(&ab,seq,strlen(seq)); - rows++; - if (rows > (int)l->oldrows) l->oldrows = rows; - } - - /* Move cursor to right position. */ - rpos2 = (pwidth+poswidth+l->cols)/l->cols; /* Current cursor relative row */ - lndebug("rpos2 %d", rpos2); - - /* Go up till we reach the expected position. */ - if (rows-rpos2 > 0) { - lndebug("go-up %d", rows-rpos2); - snprintf(seq,64,"\x1b[%dA", rows-rpos2); - abAppend(&ab,seq,strlen(seq)); - } - - /* Set column. */ - col = (pwidth+poswidth) % l->cols; - lndebug("set col %d", 1+col); - if (col) - snprintf(seq,64,"\r\x1b[%dC", col); - else - snprintf(seq,64,"\r"); - abAppend(&ab,seq,strlen(seq)); - } - - lndebug("\n"); - l->oldpos = l->pos; - if (flags & REFRESH_WRITE) l->oldrpos = rpos2; - - if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */ - abFree(&ab); - free(render); -} - -/* Calls the two low level functions refreshSingleLine() or - * refreshMultiLine() according to the selected mode. */ -static void refreshLineWithFlags(struct linenoiseState *l, int flags) { - if (mlmode) - refreshMultiLine(l,flags); - else - refreshSingleLine(l,flags); -} - -/* Utility function to avoid specifying REFRESH_ALL all the times. */ -static void refreshLine(struct linenoiseState *l) { - refreshLineWithFlags(l,REFRESH_ALL); -} - -/* Hide the current line, when using the multiplexing API. */ -void linenoiseHide(struct linenoiseState *l) { - if (mlmode) - refreshMultiLine(l,REFRESH_CLEAN); - else - refreshSingleLine(l,REFRESH_CLEAN); -} - -/* Show the current line, when using the multiplexing API. */ -void linenoiseShow(struct linenoiseState *l) { - if (l->in_completion) { - refreshLineWithCompletion(l,NULL,REFRESH_WRITE); - } else { - refreshLineWithFlags(l,REFRESH_WRITE); - } -} - -/* Grow the editing buffer if this state owns a growable buffer. Only the - * blocking linenoise() API sets buflen_max: the multiplexing API still uses - * the caller-provided fixed buffer. */ -static int linenoiseEditGrow(struct linenoiseState *l, size_t needed) { - size_t newlen; - char *newbuf; - - if (needed <= l->buflen) return 0; - - /* buflen_max is zero when the caller provided a fixed buffer, as in the - * multiplexing API: in that case there is nothing we can grow. */ - if (l->buflen_max == 0 || needed > l->buflen_max) return -1; - - /* Grow exponentially, but stop at the configured maximum before the - * doubling would overflow or go past it. */ - newlen = l->buflen ? l->buflen : 16; - while (newlen < needed) { - if (newlen > l->buflen_max/2) { - newlen = l->buflen_max; - break; - } - newlen *= 2; - } - if (newlen < needed || newlen == SIZE_MAX) return -1; - - /* Allocate one extra byte for the nul terminator. */ - newbuf = realloc(l->buf,newlen+1); - if (newbuf == NULL) return -1; - l->buf = newbuf; - l->buflen = newlen; - return 0; -} - -/* Insert bytes into l->buf without repainting the prompt. The paste path uses - * this to first store the real pasted bytes, then mark their range as folded, - * and only then refresh so raw pasted newlines are never printed directly. */ -static int linenoiseEditInsertNoRefresh(struct linenoiseState *l, const char *c, size_t clen) { - size_t insert_pos = l->pos; - - if (clen > SIZE_MAX-l->len || linenoiseEditGrow(l,l->len+clen) == -1) - return -1; - - if (l->len == l->pos) { - memcpy(l->buf+l->pos,c,clen); - } else { - memmove(l->buf+l->pos+clen,l->buf+l->pos,l->len-l->pos); - memcpy(l->buf+l->pos,c,clen); - } - l->pos += clen; - l->len += clen; - l->buf[l->len] = '\0'; - linenoiseAdjustFoldsAfterInsert(l,insert_pos,clen); - return 0; -} - -/* Insert the character(s) 'c' of length 'clen' at cursor current position. - * This handles both single-byte ASCII and multi-byte UTF-8 sequences. - * - * On error writing to the terminal -1 is returned, otherwise 0. */ -int linenoiseEditInsert(struct linenoiseState *l, const char *c, size_t clen) { - if (l->len == l->pos) { - int needs_refresh = memchr(c, '\n', clen) != NULL || - memchr(c, '\r', clen) != NULL; - - if (linenoiseEditInsertNoRefresh(l,c,clen) == -1) return 0; - if (!needs_refresh && !mlmode && !hintsCallback && - (maskmode || l->fold_count == 0)) - { - size_t bufwidth = utf8StrWidth(l->buf,l->len); - if (utf8StrWidth(l->prompt,l->plen)+bufwidth < l->cols) { - /* Avoid a full update of the line in the trivial case: - * single-width char, no hints, fits in one line. */ - if (maskmode == 1) { - if (write(l->ofd,"*",1) == -1) return -1; - } else { - if (write(l->ofd,c,clen) == -1) return -1; - } - return 0; - } - } - refreshLine(l); - } else { - if (linenoiseEditInsertNoRefresh(l,c,clen) == -1) return 0; - refreshLine(l); - } - return 0; -} - -/* Move cursor on the left. Moves by one UTF-8 character, not byte. */ -void linenoiseEditMoveLeft(struct linenoiseState *l) { - if (l->pos > 0) { - l->pos -= linenoiseEditPrevLen(l, l->pos); - refreshLine(l); - } -} - -/* Move cursor on the right. Moves by one UTF-8 character, not byte. */ -void linenoiseEditMoveRight(struct linenoiseState *l) { - if (l->pos != l->len) { - l->pos += linenoiseEditNextLen(l, l->pos); - refreshLine(l); - } -} - -/* Move cursor to the start of the line. */ -void linenoiseEditMoveHome(struct linenoiseState *l) { - if (l->pos != 0) { - l->pos = 0; - refreshLine(l); - } -} - -/* Move cursor to the end of the line. */ -void linenoiseEditMoveEnd(struct linenoiseState *l) { - if (l->pos != l->len) { - l->pos = l->len; - refreshLine(l); - } -} - -/* Substitute the currently edited line with the next or previous history - * entry as specified by 'dir'. */ -#define LINENOISE_HISTORY_NEXT 0 -#define LINENOISE_HISTORY_PREV 1 -void linenoiseEditHistoryNext(struct linenoiseState *l, int dir) { - if (history_len > 1) { - const char *src; - size_t len; - struct linenoiseFold f; - - /* Update the current history entry before to - * overwrite it with the next one. */ - free(history[history_len - 1 - l->history_index]); - history[history_len - 1 - l->history_index] = strdup(l->buf); - /* Show the new entry */ - l->history_index += (dir == LINENOISE_HISTORY_PREV) ? 1 : -1; - if (l->history_index < 0) { - l->history_index = 0; - return; - } else if (l->history_index >= history_len) { - l->history_index = history_len-1; - return; - } - - /* Copy the selected history entry into the edit buffer. With the - * fixed-buffer API, truncate the entry if it does not fit. */ - src = history[history_len - 1 - l->history_index]; - len = strlen(src); - if (linenoiseEditGrow(l,len) == -1 && len > l->buflen) - len = l->buflen; - memcpy(l->buf,src,len); - l->buf[len] = '\0'; - l->len = l->pos = len; - linenoiseFoldClear(l); - - /* History stores the real text, but not the original paste ranges. - * If the recalled entry needs folding, create one display fold now - * so text typed after recall remains outside the folded range. */ - if (linenoiseBuildHistoryFold(l,&f)) - linenoiseFoldAdd(l,f.start,f.end); - refreshLine(l); - } -} - -/* Delete the character at the right of the cursor without altering the cursor - * position. Basically this is what happens with the "Delete" keyboard key. - * Now handles multi-byte UTF-8 characters. */ -void linenoiseEditDelete(struct linenoiseState *l) { - if (l->len > 0 && l->pos < l->len) { - size_t clen = linenoiseEditNextLen(l, l->pos); - linenoiseAdjustFoldsAfterDelete(l,l->pos,clen); - memmove(l->buf+l->pos, l->buf+l->pos+clen, l->len-l->pos-clen); - l->len -= clen; - l->buf[l->len] = '\0'; - refreshLine(l); - } -} - -/* Backspace implementation. Deletes the UTF-8 character before the cursor. */ -void linenoiseEditBackspace(struct linenoiseState *l) { - if (l->pos > 0 && l->len > 0) { - size_t clen = linenoiseEditPrevLen(l, l->pos); - linenoiseAdjustFoldsAfterDelete(l,l->pos-clen,clen); - memmove(l->buf+l->pos-clen, l->buf+l->pos, l->len-l->pos); - l->pos -= clen; - l->len -= clen; - l->buf[l->len] = '\0'; - refreshLine(l); - } -} - -/* Delete the previous word, maintaining the cursor at the start of the - * current word. Handles UTF-8 by moving character-by-character. */ -void linenoiseEditDeletePrevWord(struct linenoiseState *l) { - size_t old_pos = l->pos; - size_t diff; - - /* Skip spaces before the word (move backwards by UTF-8 chars). */ - while (l->pos > 0 && l->buf[l->pos-1] == ' ') - l->pos -= linenoiseEditPrevLen(l, l->pos); - /* Skip non-space characters (move backwards by UTF-8 chars). */ - while (l->pos > 0 && l->buf[l->pos-1] != ' ') - l->pos -= linenoiseEditPrevLen(l, l->pos); - diff = old_pos - l->pos; - linenoiseAdjustFoldsAfterDelete(l,l->pos,diff); - memmove(l->buf+l->pos, l->buf+old_pos, l->len-old_pos+1); - l->len -= diff; - refreshLine(l); -} - -/* This function is part of the multiplexed API of Linenoise, that is used - * in order to implement the blocking variant of the API but can also be - * called by the user directly in an event driven program. It will: - * - * 1. Initialize the linenoise state passed by the user. - * 2. Put the terminal in RAW mode. - * 3. Show the prompt. - * 4. Return control to the user, that will have to call linenoiseEditFeed() - * each time there is some data arriving in the standard input. - * - * The user can also call linenoiseEditHide() and linenoiseEditShow() if it - * is required to show some input arriving asyncronously, without mixing - * it with the currently edited line. - * - * When linenoiseEditFeed() returns non-NULL, the user finished with the - * line editing session (pressed enter CTRL-D/C): in this case the caller - * needs to call linenoiseEditStop() to put back the terminal in normal - * mode. This will not destroy the buffer, as long as the linenoiseState - * is still valid in the context of the caller. - * - * The function returns 0 on success, or -1 if writing to standard output - * fails. If stdin_fd or stdout_fd are set to -1, the default is to use - * STDIN_FILENO and STDOUT_FILENO. - */ -int linenoiseEditStart(struct linenoiseState *l, int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt) { - /* Populate the linenoise state that we pass to functions implementing - * specific editing functionalities. */ - l->in_completion = 0; - l->ifd = stdin_fd != -1 ? stdin_fd : STDIN_FILENO; - l->ofd = stdout_fd != -1 ? stdout_fd : STDOUT_FILENO; - l->buf = buf; - l->buflen = buflen; - l->buflen_max = 0; - l->prompt = prompt; - l->plen = strlen(prompt); - l->oldpos = l->pos = 0; - l->len = 0; - linenoiseFoldClear(l); - - /* Enter raw mode. */ - rawmode_output = l->ofd; - if (enableRawMode(l->ifd) == -1) return -1; - - l->cols = getColumns(stdin_fd, stdout_fd); - l->oldrows = 0; - l->oldrpos = 1; /* Cursor starts on row 1. */ - l->history_index = 0; - - /* Buffer starts empty. */ - l->buf[0] = '\0'; - l->buflen--; /* Make sure there is always space for the nulterm */ - - /* If stdin is not a tty, stop here with the initialization. We - * will actually just read a line from standard input in blocking - * mode later, in linenoiseEditFeed(). */ - if (!isatty(l->ifd) && !getenv("LINENOISE_ASSUME_TTY")) return 0; - - /* The latest history entry is always our current buffer, that - * initially is just an empty string. */ - linenoiseHistoryAdd(""); - - if (write(l->ofd,prompt,l->plen) == -1) return -1; - return 0; -} - -/* Make sure the temporary paste buffer can hold len+need bytes. Return -1 on - * allocation failure or if the requested size is over PASTE_MAX_BYTES. */ -static int pasteBufferReserve(char **buf, size_t *cap, size_t len, size_t need) { - size_t want; - char *nb; - - /* Nothing to do if the current paste buffer already has room for the - * bytes collected so far plus the new bytes we want to append. */ - if (*cap >= len + need) return 0; - - /* Start small, then double like the line buffer. The cap avoids turning a - * huge paste into an unbounded allocation attempt. */ - want = *cap ? *cap : 64; - while (want < len + need) { - size_t doubled = want*2; - if (doubled <= want || doubled > PASTE_MAX_BYTES) { - want = PASTE_MAX_BYTES; - break; - } - want = doubled; - } - if (want < len + need) return -1; - - /* realloc(NULL, want) handles the first allocation too. */ - nb = realloc(*buf, want); - if (nb == NULL) return -1; - *buf = nb; - *cap = want; - return 0; -} - -/* Append bytes to the temporary paste buffer, growing both it and l->buf as - * needed. Return -1 if the paste is too large or allocation fails. */ -static int pasteBufferAppend(struct linenoiseState *l, char **buf, size_t *cap, - size_t *len, const char *s, size_t slen, size_t maxlen) { - size_t needed; - - if (*len > maxlen || slen > maxlen-*len) return -1; - if (*len > SIZE_MAX-slen) return -1; - needed = *len+slen; - if (l->len > SIZE_MAX-needed) return -1; - if (linenoiseEditGrow(l,l->len+needed) == -1) return -1; - if (pasteBufferReserve(buf,cap,*len,slen) == -1) return -1; - memcpy(*buf+*len,s,slen); - *len = needed; - return 0; -} - -/* Read a bracketed paste until ESC[201~ and insert the real bytes. If folding - * is needed, remember the inserted range so only rendering is shortened. */ -static void linenoiseEditPaste(struct linenoiseState *l) { - static const char END[] = "\x1b[201~"; - const size_t ENDLEN = sizeof(END)-1; - char *buf = NULL; - size_t cap = 0, len = 0, match = 0; - size_t maxlen = l->buflen_max ? l->buflen_max : l->buflen; - int overflowed = 0; - - maxlen = maxlen > l->len ? maxlen - l->len : 0; - if (maxlen > PASTE_MAX_BYTES) maxlen = PASTE_MAX_BYTES; - /* Once all fold slots are used, consume later pastes without storing them. */ - if (l->fold_count == LINENOISE_MAX_FOLDS) maxlen = 0; - - while (1) { - char c; - if (read(l->ifd, &c, 1) != 1) break; - - /* Track a possible ESC[201~ terminator without copying it into the - * paste. If it turns out to be ordinary input, flush the partial - * match below. */ - if (c == END[match]) { - match++; - if (match == ENDLEN) break; - continue; - } - - if (match > 0) { - if (!overflowed && - pasteBufferAppend(l,&buf,&cap,&len,END,match,maxlen) == -1) - overflowed = 1; - match = 0; - if (c == END[0]) { - match = 1; - continue; - } - } - - if (!overflowed && - pasteBufferAppend(l,&buf,&cap,&len,&c,1,maxlen) == -1) - overflowed = 1; - } - - if (overflowed) { - free(buf); - linenoiseBeep(); - return; - } - if (buf == NULL) return; - - { - /* Normalize pasted CR and CRLF to LF, so the edit buffer uses one - * internal newline representation. */ - size_t r = 0, w = 0; - while (r < len) { - if (buf[r] == '\r') { - buf[w++] = '\n'; - r += (r+1 < len && buf[r+1] == '\n') ? 2 : 1; - } else { - buf[w++] = buf[r++]; - } - } - len = w; - } - - if (!maskmode && shouldFoldText(buf,len)) { - size_t start = l->pos; - if (linenoiseEditInsertNoRefresh(l,buf,len) == -1) { - free(buf); - linenoiseBeep(); - return; - } - linenoiseFoldAdd(l,start,start+len); - refreshLine(l); - } else { - linenoiseEditInsert(l,buf,len); - } - free(buf); -} - -char *linenoiseEditMore = "If you see this, you are misusing the API: when linenoiseEditFeed() is called, if it returns linenoiseEditMore the user is yet editing the line. See the README file for more information."; - -/* This function is part of the multiplexed API of linenoise, see the top - * comment on linenoiseEditStart() for more information. Call this function - * each time there is some data to read from the standard input file - * descriptor. In the case of blocking operations, this function can just be - * called in a loop, and block. - * - * The function returns linenoiseEditMore to signal that line editing is still - * in progress, that is, the user didn't yet pressed enter / CTRL-D. Otherwise - * the function returns the pointer to the heap-allocated buffer with the - * edited line, that the user should free with linenoiseFree(). - * - * On special conditions, NULL is returned and errno is populated: - * - * EAGAIN if the user pressed Ctrl-C - * ENOENT if the user pressed Ctrl-D - * - * Some other errno: I/O error. - */ -char *linenoiseEditFeed(struct linenoiseState *l) { - /* Not a TTY, pass control to line reading without character - * count limits. */ - if (!isatty(l->ifd) && !getenv("LINENOISE_ASSUME_TTY")) return linenoiseNoTTY(); - - char c; - int nread; - char seq[3]; - - nread = read(l->ifd,&c,1); - if (nread < 0) { - return (errno == EAGAIN || errno == EWOULDBLOCK) ? linenoiseEditMore : NULL; - } else if (nread == 0) { - return NULL; - } - - /* Only autocomplete when the callback is set. completeLine() - * returns the character to be handled next, or zero when the - * key was consumed to navigate completions. */ - if ((l->in_completion || c == 9 /* TAB */) && completionCallback != NULL) { - int retval = completeLine(l,c); - /* Read next character when 0 */ - if (retval == 0) return linenoiseEditMore; - c = retval; - } - - switch(c) { - case ENTER: /* enter */ - history_len--; - free(history[history_len]); - if (mlmode) linenoiseEditMoveEnd(l); - if (hintsCallback) { - /* Force a refresh without hints to leave the previous - * line as the user typed it after a newline. */ - linenoiseHintsCallback *hc = hintsCallback; - hintsCallback = NULL; - refreshLine(l); - hintsCallback = hc; - } - return strdup(l->buf); - case CTRL_C: /* ctrl-c */ - errno = EAGAIN; - return NULL; - case BACKSPACE: /* backspace */ - case 8: /* ctrl-h */ - linenoiseEditBackspace(l); - break; - case CTRL_D: /* ctrl-d, remove char at right of cursor, or if the - line is empty, act as end-of-file. */ - if (l->len > 0) { - linenoiseEditDelete(l); - } else { - history_len--; - free(history[history_len]); - errno = ENOENT; - return NULL; - } - break; - case CTRL_T: /* ctrl-t, swaps current character with previous. */ - /* Handle UTF-8: swap the two UTF-8 characters around cursor. */ - if (l->pos > 0 && l->pos < l->len) { - char tmp[32]; - size_t prevlen = linenoiseEditPrevLen(l, l->pos); - size_t currlen = linenoiseEditNextLen(l, l->pos); - size_t prevstart = l->pos - prevlen; - if (prevlen > sizeof(tmp) || currlen > sizeof(tmp)) break; - if (linenoiseRangeOverlapsFold(l,prevstart,prevlen+currlen)) { - linenoiseBeep(); - break; - } - /* Copy current char to tmp, move previous char right, paste tmp. */ - memcpy(tmp, l->buf + l->pos, currlen); - memmove(l->buf + prevstart + currlen, l->buf + prevstart, prevlen); - memcpy(l->buf + prevstart, tmp, currlen); - if (l->pos + currlen <= l->len) l->pos += currlen; - refreshLine(l); - } - break; - case CTRL_B: /* ctrl-b */ - linenoiseEditMoveLeft(l); - break; - case CTRL_F: /* ctrl-f */ - linenoiseEditMoveRight(l); - break; - case CTRL_P: /* ctrl-p */ - linenoiseEditHistoryNext(l, LINENOISE_HISTORY_PREV); - break; - case CTRL_N: /* ctrl-n */ - linenoiseEditHistoryNext(l, LINENOISE_HISTORY_NEXT); - break; - case ESC: /* escape sequence */ - /* Read the next two bytes representing the escape sequence. - * Use two calls to handle slow terminals returning the two - * chars at different times. */ - if (read(l->ifd,seq,1) == -1) break; - if (read(l->ifd,seq+1,1) == -1) break; - - /* ESC [ sequences. */ - if (seq[0] == '[') { - if (seq[1] >= '0' && seq[1] <= '9') { - char param[8]; - size_t plen = 1; - char final = 0; - - param[0] = seq[1]; - while (plen < sizeof(param)) { - char p; - if (read(l->ifd,&p,1) != 1) break; - if (p >= '0' && p <= '9') { - param[plen++] = p; - } else { - final = p; - break; - } - } - if (final == '~') { - if (plen == 1 && param[0] == '3') { - linenoiseEditDelete(l); - } else if (plen == 3 && memcmp(param,"200",3) == 0) { - linenoiseEditPaste(l); - } - } - } else { - switch(seq[1]) { - case 'A': /* Up */ - linenoiseEditHistoryNext(l, LINENOISE_HISTORY_PREV); - break; - case 'B': /* Down */ - linenoiseEditHistoryNext(l, LINENOISE_HISTORY_NEXT); - break; - case 'C': /* Right */ - linenoiseEditMoveRight(l); - break; - case 'D': /* Left */ - linenoiseEditMoveLeft(l); - break; - case 'H': /* Home */ - linenoiseEditMoveHome(l); - break; - case 'F': /* End*/ - linenoiseEditMoveEnd(l); - break; - } - } - } - - /* ESC O sequences. */ - else if (seq[0] == 'O') { - switch(seq[1]) { - case 'H': /* Home */ - linenoiseEditMoveHome(l); - break; - case 'F': /* End*/ - linenoiseEditMoveEnd(l); - break; - } - } - break; - default: - /* Handle UTF-8 multi-byte sequences. When we receive the first byte - * of a multi-byte UTF-8 character, read the remaining bytes to - * complete the sequence before inserting. */ - { - char utf8[4]; - int utf8len = utf8ByteLen(c); - utf8[0] = c; - if (utf8len > 1) { - /* Read remaining bytes of the UTF-8 sequence. */ - int i; - for (i = 1; i < utf8len; i++) { - if (read(l->ifd, utf8+i, 1) != 1) break; - } - } - if (linenoiseEditInsert(l, utf8, utf8len)) return NULL; - } - break; - case CTRL_U: /* Ctrl+u, delete the whole line. */ - l->buf[0] = '\0'; - l->pos = l->len = 0; - linenoiseFoldClear(l); - refreshLine(l); - break; - case CTRL_K: /* Ctrl+k, delete from current to end of line. */ - linenoiseAdjustFoldsAfterDelete(l,l->pos,l->len-l->pos); - l->buf[l->pos] = '\0'; - l->len = l->pos; - refreshLine(l); - break; - case CTRL_A: /* Ctrl+a, go to the start of the line */ - linenoiseEditMoveHome(l); - break; - case CTRL_E: /* ctrl+e, go to the end of the line */ - linenoiseEditMoveEnd(l); - break; - case CTRL_L: /* ctrl+l, clear screen */ - linenoiseClearScreen(); - refreshLine(l); - break; - case CTRL_W: /* ctrl+w, delete previous word */ - linenoiseEditDeletePrevWord(l); - break; - } - return linenoiseEditMore; -} - -/* This is part of the multiplexed linenoise API. See linenoiseEditStart() - * for more information. This function is called when linenoiseEditFeed() - * returns something different than NULL. At this point the user input - * is in the buffer, and we can restore the terminal in normal mode. */ -void linenoiseEditStop(struct linenoiseState *l) { - if (!isatty(l->ifd) && !getenv("LINENOISE_ASSUME_TTY")) return; - disableRawMode(l->ifd); - printf("\n"); -} - -/* This just implements a blocking loop for the multiplexed API. - * In many applications that are not event-drivern, we can just call - * the blocking linenoise API, wait for the user to complete the editing - * and return the buffer. This wrapper owns l.buf, so it can let the edit - * state grow it dynamically for large pasted input. */ -static char *linenoiseBlockingEdit(int stdin_fd, int stdout_fd, const char *prompt) -{ - struct linenoiseState l; - char *buf = malloc(LINENOISE_INITIAL_BUFLEN); - char *res; - - if (buf == NULL) { - errno = ENOMEM; - return NULL; - } - - if (linenoiseEditStart(&l,stdin_fd,stdout_fd,buf, - LINENOISE_INITIAL_BUFLEN,prompt) == -1) - { - free(buf); - return NULL; - } - l.buflen_max = LINENOISE_MAX_LINE; - while((res = linenoiseEditFeed(&l)) == linenoiseEditMore); - linenoiseEditStop(&l); - free(l.buf); - return res; -} - -/* This special mode is used by linenoise in order to print scan codes - * on screen for debugging / development purposes. It is implemented - * by the linenoise_example program using the --keycodes option. */ -void linenoisePrintKeyCodes(void) { - char quit[4]; - - printf("Linenoise key codes debugging mode.\n" - "Press keys to see scan codes. Type 'quit' at any time to exit.\n"); - rawmode_output = STDOUT_FILENO; - if (enableRawMode(STDIN_FILENO) == -1) return; - memset(quit,' ',4); - while(1) { - char c; - int nread; - - nread = read(STDIN_FILENO,&c,1); - if (nread <= 0) continue; - memmove(quit,quit+1,sizeof(quit)-1); /* shift string to left. */ - quit[sizeof(quit)-1] = c; /* Insert current char on the right. */ - if (memcmp(quit,"quit",sizeof(quit)) == 0) break; - - printf("'%c' %02x (%d) (type quit to exit)\n", - isprint(c) ? c : '?', (int)c, (int)c); - printf("\r"); /* Go left edge manually, we are in raw mode. */ - fflush(stdout); - } - disableRawMode(STDIN_FILENO); -} - -/* Read a newline-terminated record from fp with no fixed-size stack buffer. - * Used for non-tty input, unsupported terminals, and history loading. */ -static char *linenoiseReadLine(FILE *fp, int *err) { - char *line = NULL; - size_t len = 0, cap = 0; - - if (err) *err = 0; - - while(1) { - if (len+1 >= cap) { - size_t newcap = cap ? cap*2 : 16; - char *oldval = line; - if (newcap <= cap) { - free(line); - if (err) *err = 1; - errno = ENOMEM; - return NULL; - } - line = realloc(line,newcap); - if (line == NULL) { - if (oldval) free(oldval); - if (err) *err = 1; - return NULL; - } - cap = newcap; - } - int c = fgetc(fp); - if (c == EOF || c == '\n') { - if (c == EOF && len == 0) { - free(line); - return NULL; - } else { - line[len] = '\0'; - return line; - } - } else { - line[len] = c; - len++; - } - } -} - -/* This function is called when linenoise() is called with the standard - * input file descriptor not attached to a TTY. So for example when the - * program using linenoise is called in pipe or with a file redirected - * to its standard input. In this case, we want to be able to return the - * line regardless of its length. */ -static char *linenoiseNoTTY(void) { - return linenoiseReadLine(stdin,NULL); -} - -/* The high level function that is the main API of the linenoise library. - * This function checks if the terminal has basic capabilities, just checking - * for a blacklist of stupid terminals, and later either calls the line - * editing function or uses a simple line reader so that you will be able - * to type something even in the most desperate of the conditions. */ -char *linenoise(const char *prompt) { - if (!isatty(STDIN_FILENO) && !getenv("LINENOISE_ASSUME_TTY")) { - /* Not a tty: read from file / pipe. In this mode we don't want any - * limit to the line size, so we call a function to handle that. */ - return linenoiseNoTTY(); - } - - if (isUnsupportedTerm()) { - char *retval; - size_t len; - - printf("%s",prompt); - fflush(stdout); - retval = linenoiseNoTTY(); - if (retval == NULL) return NULL; - len = strlen(retval); - while(len && retval[len-1] == '\r') { - len--; - retval[len] = '\0'; - } - return retval; - } else { - return linenoiseBlockingEdit(STDIN_FILENO,STDOUT_FILENO,prompt); - } -} - -/* This is just a wrapper the user may want to call in order to make sure - * the linenoise returned buffer is freed with the same allocator it was - * created with. Useful when the main program is using an alternative - * allocator. */ -void linenoiseFree(void *ptr) { - if (ptr == linenoiseEditMore) return; // Protect from API misuse. - free(ptr); -} - -/* ================================ History ================================= */ - -/* Free the history, but does not reset it. Only used when we have to - * exit() to avoid memory leaks are reported by valgrind & co. */ -static void freeHistory(void) { - if (history) { - int j; - - for (j = 0; j < history_len; j++) - free(history[j]); - free(history); - } -} - -/* At exit we'll try to fix the terminal to the initial conditions. */ -static void linenoiseAtExit(void) { - disableRawMode(STDIN_FILENO); - freeHistory(); -} - -/* This is the API call to add a new entry in the linenoise history. - * It uses a fixed array of char pointers that are shifted (memmoved) - * when the history max length is reached in order to remove the older - * entry and make room for the new one, so it is not exactly suitable for huge - * histories, but will work well for a few hundred of entries. - * - * Using a circular buffer is smarter, but a bit more complex to handle. */ -int linenoiseHistoryAdd(const char *line) { - char *linecopy; - - if (history_max_len == 0) return 0; - - /* Initialization on first call. */ - if (history == NULL) { - history = malloc(sizeof(char*)*history_max_len); - if (history == NULL) return 0; - memset(history,0,(sizeof(char*)*history_max_len)); - } - - /* Don't add duplicated lines. */ - if (history_len && !strcmp(history[history_len-1], line)) return 0; - - /* Add an heap allocated copy of the line in the history. - * If we reached the max length, remove the older line. */ - linecopy = strdup(line); - if (!linecopy) return 0; - if (history_len == history_max_len) { - free(history[0]); - memmove(history,history+1,sizeof(char*)*(history_max_len-1)); - history_len--; - } - history[history_len] = linecopy; - history_len++; - return 1; -} - -/* Set the maximum length for the history. This function can be called even - * if there is already some history, the function will make sure to retain - * just the latest 'len' elements if the new history length value is smaller - * than the amount of items already inside the history. */ -int linenoiseHistorySetMaxLen(int len) { - char **new; - - if (len < 1) return 0; - if (history) { - int tocopy = history_len; - - new = malloc(sizeof(char*)*len); - if (new == NULL) return 0; - - /* If we can't copy everything, free the elements we'll not use. */ - if (len < tocopy) { - int j; - - for (j = 0; j < tocopy-len; j++) free(history[j]); - tocopy = len; - } - memset(new,0,sizeof(char*)*len); - memcpy(new,history+(history_len-tocopy), sizeof(char*)*tocopy); - free(history); - history = new; - } - history_max_len = len; - if (history_len > history_max_len) - history_len = history_max_len; - return 1; -} - -/* Save the history in the specified file. On success 0 is returned - * otherwise -1 is returned. */ -int linenoiseHistorySave(const char *filename) { - mode_t old_umask = umask(S_IXUSR|S_IRWXG|S_IRWXO); - FILE *fp; - int j; - - fp = fopen(filename,"w"); - umask(old_umask); - if (fp == NULL) return -1; - fchmod(fileno(fp),S_IRUSR|S_IWUSR); - for (j = 0; j < history_len; j++) { - char *p = history[j]; - /* Keep the history file newline-separated: embedded newlines in an - * entry are stored as CR and converted back by linenoiseHistoryLoad(). */ - while (*p) { - fputc(*p == '\n' ? '\r' : *p, fp); - p++; - } - fputc('\n', fp); - } - fclose(fp); - return 0; -} - -/* Load the history from the specified file. If the file does not exist - * zero is returned and no operation is performed. - * - * If the file exists and the operation succeeded 0 is returned, otherwise - * on error -1 is returned. */ -int linenoiseHistoryLoad(const char *filename) { - FILE *fp = fopen(filename,"r"); - char *buf; - int err = 0; - - if (fp == NULL) return -1; - - while ((buf = linenoiseReadLine(fp,&err)) != NULL) { - size_t j; - - /* Rebuild embedded newlines that were saved as CR. */ - for (j = 0; buf[j]; j++) { - if (buf[j] == '\r') buf[j] = '\n'; - } - linenoiseHistoryAdd(buf); - free(buf); - } - if (err || ferror(fp)) { - fclose(fp); - return -1; - } - fclose(fp); - return 0; -} diff --git a/lib/linenoise/linenoise.h b/lib/linenoise/linenoise.h deleted file mode 100644 index 735629b..0000000 --- a/lib/linenoise/linenoise.h +++ /dev/null @@ -1,120 +0,0 @@ -/* linenoise.h -- VERSION 1.0 - * - * Guerrilla line editing library against the idea that a line editing lib - * needs to be 20,000 lines of C code. - * - * See linenoise.c for more information. - * - * ------------------------------------------------------------------------ - * - * Copyright (c) 2010-2023, Salvatore Sanfilippo - * Copyright (c) 2010-2013, Pieter Noordhuis - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#ifndef __LINENOISE_H -#define __LINENOISE_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include /* For size_t. */ - -extern char *linenoiseEditMore; - -#define LINENOISE_MAX_FOLDS 16 - -/* The linenoiseState structure represents the state during line editing. - * We pass this state to functions implementing specific editing - * functionalities. */ -struct linenoiseState { - int in_completion; /* The user pressed TAB and we are now in completion - * mode, so input is handled by completeLine(). */ - size_t completion_idx; /* Index of next completion to propose. */ - int ifd; /* Terminal stdin file descriptor. */ - int ofd; /* Terminal stdout file descriptor. */ - char *buf; /* Edited line buffer. */ - size_t buflen; /* Edited line buffer size. */ - size_t buflen_max; /* Max buffer size, or 0 if fixed. */ - const char *prompt; /* Prompt to display. */ - size_t plen; /* Prompt length. */ - size_t pos; /* Current cursor position. */ - size_t oldpos; /* Previous refresh cursor position. */ - size_t len; /* Current edited line length. */ - size_t cols; /* Number of columns in terminal. */ - size_t oldrows; /* Rows used by last refrehsed line (multiline mode) */ - int oldrpos; /* Cursor row from last refresh (for multiline clearing). */ - int history_index; /* The history index we are currently editing. */ - int fold_count; /* Number of folded ranges. */ - size_t fold_start[LINENOISE_MAX_FOLDS]; /* Folded range start offsets. */ - size_t fold_end[LINENOISE_MAX_FOLDS]; /* Folded range end offsets. */ -}; - -typedef struct linenoiseCompletions { - size_t len; - char **cvec; -} linenoiseCompletions; - -/* Non blocking API. */ -int linenoiseEditStart(struct linenoiseState *l, int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt); -char *linenoiseEditFeed(struct linenoiseState *l); -void linenoiseEditStop(struct linenoiseState *l); -void linenoiseHide(struct linenoiseState *l); -void linenoiseShow(struct linenoiseState *l); - -/* Blocking API. */ -char *linenoise(const char *prompt); -void linenoiseFree(void *ptr); - -/* Completion API. */ -typedef void(linenoiseCompletionCallback)(const char *, linenoiseCompletions *); -typedef char*(linenoiseHintsCallback)(const char *, int *color, int *bold); -typedef void(linenoiseFreeHintsCallback)(void *); -void linenoiseSetCompletionCallback(linenoiseCompletionCallback *); -void linenoiseSetHintsCallback(linenoiseHintsCallback *); -void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *); -void linenoiseAddCompletion(linenoiseCompletions *, const char *); - -/* History API. */ -int linenoiseHistoryAdd(const char *line); -int linenoiseHistorySetMaxLen(int len); -int linenoiseHistorySave(const char *filename); -int linenoiseHistoryLoad(const char *filename); - -/* Other utilities. */ -void linenoiseClearScreen(void); -void linenoiseSetMultiLine(int ml); -void linenoisePrintKeyCodes(void); -void linenoiseMaskModeEnable(void); -void linenoiseMaskModeDisable(void); - -#ifdef __cplusplus -} -#endif - -#endif /* __LINENOISE_H */ diff --git a/src/args.zig b/src/args.zig index ca66dc7..21a3792 100644 --- a/src/args.zig +++ b/src/args.zig @@ -101,6 +101,12 @@ pub const SqlPipeError = error{ ReplIncompatibleExplain, DuplicateUrlTableName, UrlHeaderRequiresUrl, + InspectWithQuery, + InspectWithOutput, + InspectWithExplain, + InspectSampleWithJson, + InvalidInspectMode, + ExplainWithOutput, }; pub const ParsedArgs = struct { @@ -165,79 +171,10 @@ pub const ParsedArgs = struct { null_value: ?[]const u8 = null, /// Maximum response body size in bytes for --url (default: 100MB). max_body_size: usize = 100 * 1024 * 1024, -}; - -pub const ColumnsArgs = struct { - /// Input files as positional arguments; empty when reading from stdin only. - files: []const FileInput = &.{}, - /// CSV field delimiter — 1 to 8 bytes (default: ","). - delimiter: []const u8, - /// Show inferred type alongside name when true. - verbose: bool, - /// Input format (default: csv). - input_format: InputFormat, - /// Root element to navigate to for XML input; null = use actual document root. - xml_root_input: ?[]const u8, - /// Row tag filter for XML input; null = accept any direct child element as a row. - xml_row_input: ?[]const u8, - /// Dot-separated path to the JSON array (e.g. "results.items"); null = expect top-level array. - json_path: ?[]const u8, -}; - -pub const ValidateArgs = struct { - /// Input files as positional arguments; empty when reading from stdin only. - files: []const FileInput = &.{}, - /// CSV field delimiter — 1 to 8 bytes (default: ","). - delimiter: []const u8, - /// Infer column types from the first 100 buffered rows when true. - type_inference: bool, - /// Input format (default: csv). - input_format: InputFormat, - /// Root element to navigate to for XML input; null = use actual document root. - xml_root_input: ?[]const u8, - /// Row tag filter for XML input; null = accept any direct child element as a row. - xml_row_input: ?[]const u8, - /// Dot-separated path to the JSON array (e.g. "results.items"); null = expect top-level array. - json_path: ?[]const u8, -}; - -pub const SampleArgs = struct { - /// Input files as positional arguments; empty when reading from stdin only. - files: []const FileInput = &.{}, - /// CSV field delimiter — 1 to 8 bytes (default: ","). - delimiter: []const u8, - /// Input format (default: csv). - input_format: InputFormat, - /// Number of sample rows to print (default: 10). - n: usize, - /// Infer column types from buffered rows when true; show all TEXT when false. - type_inference: bool, -}; - -pub const StatsArgs = struct { - /// Input files as positional arguments; empty when reading from stdin only. - files: []const FileInput = &.{}, - /// URLs to fetch input data from (repeatable); empty when not using HTTP input. - urls: []const UrlInput = &.{}, - /// CSV field delimiter — 1 to 8 bytes (default: ","). - delimiter: []const u8, - /// Input format (default: csv). - input_format: InputFormat, - /// Infer column types from buffered rows when true; show all TEXT when false. - type_inference: bool, -}; - -pub const SchemaArgs = struct { - /// Input files as positional arguments; empty when reading from stdin only. - files: []const FileInput = &.{}, - /// URLs to fetch input data from (repeatable); empty when not using HTTP input. - urls: []const UrlInput = &.{}, - /// CSV field delimiter — 1 to 8 bytes (default: ","). - delimiter: []const u8, - /// Input format (default: csv). - input_format: InputFormat, - /// Infer column types from buffered rows when true; show all TEXT when false. - type_inference: bool, + /// When set, run in --inspect mode instead of normal query mode. + inspect_args: ?InspectArgs = null, + /// Number of sample rows to print when in --inspect sample mode (default: 10). + sample_n: usize = 10, }; pub const CompletionsShell = enum { @@ -254,6 +191,24 @@ pub const CompletionsArgs = struct { shell: CompletionsShell, }; +pub const InspectMode = enum { + columns, + validate, + sample, + stats, + schema, + + pub fn parse(s: []const u8) error{InvalidInspectMode}!InspectMode { + return std.meta.stringToEnum(InspectMode, s) orelse error.InvalidInspectMode; + } +}; + +pub const InspectArgs = struct { + mode: InspectMode, + sample_n: usize = 10, + deprecated: bool = false, +}; + pub const ArgsResult = union(enum) { /// Normal execution: run the query (requires query positional arg unless --repl is set). parsed: ParsedArgs, @@ -263,16 +218,8 @@ pub const ArgsResult = union(enum) { help, /// User requested --version / -V. version, - /// User requested --columns: list column names and exit. - columns: ColumnsArgs, - /// User requested --validate: parse input and print summary. - validate: ValidateArgs, - /// User requested --sample: print schema + first n rows and exit. - sample: SampleArgs, - /// User requested --stats: compute per-column statistics. - stats: StatsArgs, - /// User requested --schema: print inferred CREATE TABLE DDL. - schema: SchemaArgs, + /// User requested --inspect or old flag: run one of the inspect modes. + inspect: ParsedArgs, /// User requested --completions: generate shell completion script. completions: CompletionsArgs, }; @@ -307,23 +254,13 @@ pub fn printUsage(writer: *std.Io.Writer) !void { \\ With --columns: show inferred type per column \\ -s, --silent Suppress row count output unconditionally \\ Cannot be combined with -v/--verbose - \\ --validate Parse the entire input and print a summary to stdout - \\ (OK: rows, columns ( , ...)) - \\ Exit 0 on success, exit 2 on parse error. No query required. - \\ Compatible with --delimiter, --tsv, --no-type-inference, -I. - \\ --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 - \\ --sample [] Print schema to stderr and first rows to stdout (default: 10) - \\ Schema lists column names and inferred types, prefixed with # - \\ Implies --header. Compatible with --delimiter and --tsv. - \\ Incompatible with --json and with a query argument. - \\ --stats Compute per-column statistics (type, non-null, min, max, mean) - \\ --profile Alias for --stats - \\ --schema Print inferred CREATE TABLE DDL to stdout and exit - \\ No query required. One DDL block per input file; stdin uses table `t`. - \\ Compatible with --delimiter, --tsv, --no-type-inference, -I. - \\ Mutually exclusive with --explain and query arguments. + \\ --inspect Inspect mode: columns, validate, sample, stats, schema + \\ columns — list column names (use -v for types) + \\ validate — parse and print summary (exit 2 on error) + \\ sample [n] — print schema to stderr and n rows to stdout (default: 10) + \\ stats — per-column statistics (type, non-null, min, max, mean) + \\ schema — print inferred CREATE TABLE DDL + \\ --sample [] (deprecated) Print schema and first rows, use --inspect sample \\ --output Write results to file instead of stdout \\ --xml-root Root element name for XML I/O (default: results) \\ --xml-row Row element name for XML I/O (default: row) @@ -448,8 +385,6 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP var max_rows: ?usize = null; var verbose = false; var silent = false; - var list_columns = false; - var validate = false; var output: ?[]const u8 = null; var xml_root: []const u8 = "results"; var xml_row: []const u8 = "row"; @@ -457,10 +392,9 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP var xml_row_input: ?[]const u8 = null; var null_value: ?[]const u8 = null; var json_path: ?[]const u8 = null; - var sample_mode = false; - var sample_n: usize = 10; - var stats_mode = false; - var schema_mode = false; + var inspect_mode: ?InspectMode = null; + var inspect_sample_n: usize = 10; + var inspect_deprecated = false; var repl_mode = false; var disk = false; var save_path: ?[:0]const u8 = null; @@ -532,19 +466,30 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP 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, "--inspect")) { + i += 1; + if (i >= args.len) return error.InvalidInspectMode; + const mode = InspectMode.parse(args[i]) catch return error.InvalidInspectMode; + inspect_mode = mode; + } else if (std.mem.startsWith(u8, arg, "--inspect=")) { + const mode = InspectMode.parse(arg["--inspect=".len..]) catch return error.InvalidInspectMode; + inspect_mode = mode; } else if (std.mem.eql(u8, arg, "--columns")) { - list_columns = true; + inspect_mode = .columns; + inspect_deprecated = true; } else if (std.mem.eql(u8, arg, "--validate")) { - validate = true; + inspect_mode = .validate; + inspect_deprecated = true; } else if (std.mem.eql(u8, arg, "--sample")) { - sample_mode = true; + inspect_mode = .sample; + inspect_deprecated = true; // Peek at next arg: if it is a positive integer, consume it as the sample count if (i + 1 < args.len) { const next = args[i + 1]; if (next.len > 0 and next[0] != '-') { if (std.fmt.parseUnsigned(usize, next, 10)) |n| { if (n == 0) return error.InvalidSampleCount; - sample_n = n; + inspect_sample_n = n; i += 1; } else |_| { // Not a number — keep default (10) @@ -555,12 +500,15 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP const val = arg["--sample=".len..]; const n = std.fmt.parseUnsigned(usize, val, 10) catch return error.InvalidSampleCount; if (n == 0) return error.InvalidSampleCount; - sample_n = n; - sample_mode = true; + inspect_sample_n = n; + inspect_mode = .sample; + inspect_deprecated = true; } else if (std.mem.eql(u8, arg, "--stats") or std.mem.eql(u8, arg, "--profile")) { - stats_mode = true; + inspect_mode = .stats; + inspect_deprecated = true; } else if (std.mem.eql(u8, arg, "--schema")) { - schema_mode = true; + inspect_mode = .schema; + inspect_deprecated = true; } else if (std.mem.eql(u8, arg, "--repl") or std.mem.eql(u8, arg, "-r")) { repl_mode = true; } else if (std.mem.eql(u8, arg, "--output")) { @@ -817,7 +765,7 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP // ─── Convert positional args to files + query ────────────────────────── const pos = positional_args.items; - const is_special_mode = list_columns or validate or sample_mode or stats_mode or schema_mode or repl_mode; + const is_special_mode = inspect_mode != null or repl_mode; // Build file list from positional args var files: std.ArrayList(FileInput) = .empty; @@ -870,60 +818,24 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP if (output_format != .csv and output_format != .tsv and output_format != .html and header) return error.IncompatibleFlags; - // --output is mutually exclusive with --columns (--columns always writes to stdout) - if (output != null and list_columns) - return error.OutputWithColumns; - - // --output is mutually exclusive with --validate (--validate always writes to stdout) - if (output != null and validate) - return error.OutputWithValidate; - - // --output is mutually exclusive with --sample (--sample always writes to stdout) - if (output != null and sample_mode) - return error.SampleWithOutput; - - // --validate is mutually exclusive with --columns - if (validate and list_columns) - return error.ValidateWithColumns; + // --inspect is mutually exclusive with query, output, and explain + if (inspect_mode != null and (query != null or query_file != null)) + return error.InspectWithQuery; + if (inspect_mode != null and output != null) + return error.InspectWithOutput; + if (inspect_mode != null and explain) + return error.InspectWithExplain; - // --columns is mutually exclusive with a query argument (positional or -f) - if (list_columns and (query != null or query_file != null)) - return error.ColumnsWithQuery; + // --explain is mutually exclusive with --output (even outside inspect mode) + if (explain and output != null) + return error.ExplainWithOutput; - // --validate is mutually exclusive with a query argument (positional or -f) - if (validate and (query != null or query_file != null)) - return error.ValidateWithQuery; - - // --sample is mutually exclusive with a query argument (positional or -f) - if (sample_mode and (query != null or query_file != null)) - return error.SampleWithQuery; - - // --sample is mutually exclusive with --json / json output format - if (sample_mode and (output_format == .json or output_format == .ndjson)) - return error.SampleWithJson; - - // --sample is mutually exclusive with --columns - if (sample_mode and list_columns) - return error.SampleWithColumns; - - // --sample is mutually exclusive with --validate - if (sample_mode and validate) - return error.SampleWithValidate; - - // --stats is mutually exclusive with --columns, --validate, --sample, query, and --output - if (stats_mode and (list_columns or validate or sample_mode or query != null or query_file != null or output != null)) - return error.StatsWithFlags; - - // --explain is mutually exclusive with mode flags and --output - if (explain and (list_columns or validate or sample_mode or stats_mode or schema_mode or output != null)) - return error.ExplainWithFlags; - - // --schema is mutually exclusive with mode flags, query, output, and --explain - if (schema_mode and (explain or list_columns or validate or sample_mode or stats_mode or query != null or query_file != null or output != null)) - return error.SchemaWithFlags; + // --inspect sample is incompatible with JSON output format + if (inspect_mode == .sample and (output_format == .json or output_format == .ndjson)) + return error.InspectSampleWithJson; // --save is incompatible with special modes - if (save_path != null and (list_columns or validate or sample_mode or stats_mode or schema_mode or explain)) + if (save_path != null and inspect_mode != null) return error.SaveIncompatibleMode; // --save implies disk-backed behavior; both together is redundant @@ -948,9 +860,14 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP if (table_mode == .always and output_format != .csv and output_format != .tsv) return error.TableWithNonCsv; - // URL validation - if (urls.items.len > 0 and (list_columns or validate or sample_mode)) - return error.UrlIncompatibleMode; + // URL validation: columns, validate, sample inspect modes don't support URL input + if (urls.items.len > 0 and inspect_mode != null) { + const is_url_compat = switch (inspect_mode.?) { + .stats, .schema => true, + .columns, .validate, .sample => false, + }; + if (!is_url_compat) return error.UrlIncompatibleMode; + } if (urls.items.len == 0 and max_body_size_set) return error.HttpFlagsRequireUrl; @@ -977,63 +894,44 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP if (gop.found_existing) return error.DuplicateTableName; } - // --columns mode: list headers and exit - if (list_columns) - return .{ .columns = ColumnsArgs{ + // --inspect mode: dispatch to the appropriate inspect sub-mode + if (inspect_mode) |mode| { + return .{ .inspect = ParsedArgs{ + .query = "", .files = files.items, - .delimiter = delimiter, - .verbose = verbose, - .input_format = effective_input_format, - .xml_root_input = xml_root_input, - .xml_row_input = xml_row_input, - .json_path = json_path, - } }; - - // --validate mode: parse CSV and print summary - if (validate) - return .{ .validate = ValidateArgs{ - .files = files.items, - .delimiter = delimiter, + .urls = urls.items, .type_inference = type_inference, + .no_stdin = no_stdin, + .delimiter = delimiter, + .header = header, .input_format = effective_input_format, + .input_format_explicit = input_format_explicit, + .output_format = output_format, + .max_rows = max_rows, + .verbose = verbose, + .silent = silent, + .output = output, + .xml_root = xml_root, + .xml_row = xml_row, .xml_root_input = xml_root_input, .xml_row_input = xml_row_input, .json_path = json_path, + .disk = disk, + .save_path = save_path, + .explain = explain, + .table_mode = table_mode, + .sql_table = sql_table, + .html_class = html_class, + .null_value = null_value, + .max_body_size = max_body_size, + .inspect_args = InspectArgs{ .mode = mode, .sample_n = inspect_sample_n, .deprecated = inspect_deprecated }, + .sample_n = inspect_sample_n, } }; - - // --sample mode: print schema + first n rows and exit - if (sample_mode) - return .{ .sample = SampleArgs{ - .files = files.items, - .delimiter = delimiter, - .input_format = effective_input_format, - .n = sample_n, - .type_inference = type_inference, - } }; - - // --stats mode: compute per-column statistics and exit - if (stats_mode) - return .{ .stats = StatsArgs{ - .files = files.items, - .urls = urls.items, - .delimiter = delimiter, - .input_format = effective_input_format, - .type_inference = type_inference, - } }; - - // --schema mode: print inferred CREATE TABLE DDL and exit - if (schema_mode) - return .{ .schema = SchemaArgs{ - .files = files.items, - .urls = urls.items, - .delimiter = delimiter, - .input_format = effective_input_format, - .type_inference = type_inference, - } }; + } // --repl validation: reject incompatible flags if (repl_mode) { - if (list_columns or validate or sample_mode or stats_mode or schema_mode) { + if (inspect_mode != null) { return error.ReplIncompatibleMode; } if (query_file != null) { diff --git a/src/completions.zig b/src/completions.zig index 1ca530a..64e8349 100644 --- a/src/completions.zig +++ b/src/completions.zig @@ -44,6 +44,10 @@ fn generateBash(writer: *std.Io.Writer) !void { \\ COMPREPLY=($(compgen -W "1 5 10 25 50 100 500 1000" -- "$cur")) \\ return \\ ;; + \\ --inspect) + \\ COMPREPLY=($(compgen -W "columns validate sample stats schema" -- "$cur")) + \\ return + \\ ;; \\ -f|--file) \\ _filedir \\ return @@ -67,11 +71,8 @@ fn generateBash(writer: *std.Io.Writer) !void { \\ --max-body-size \\ --verbose -v \\ --silent -s - \\ --validate - \\ --columns + \\ --inspect \\ --sample - \\ --stats --profile - \\ --schema \\ --output \\ --xml-root \\ --xml-row @@ -119,12 +120,8 @@ fn generateZsh(writer: *std.Io.Writer) !void { \\ '--max-body-size=[Maximum response body size for all --url]:bytes:' \\ '(-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]' + \\ '--inspect:Inspect mode:(columns validate sample stats schema)' \\ '--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:' @@ -173,12 +170,8 @@ fn generateFish(writer: *std.Io.Writer) !void { \\complete -c sql-pipe -l max-body-size -r -d "Maximum response body size for all --url" \\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 inspect -r -f -a "columns validate sample stats schema" -d "Inspect mode" \\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" diff --git a/src/main.zig b/src/main.zig index 472af40..af4362b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -13,11 +13,7 @@ const yaml_mod = @import("yaml.zig"); const http_mod = @import("http.zig"); const parquet_mod = @import("parquet.zig"); -const columns_mode = @import("modes/columns.zig"); -const validate_mode = @import("modes/validate.zig"); -const sample_mode = @import("modes/sample.zig"); -const stats_mode = @import("modes/stats.zig"); -const schema_mode = @import("modes/schema.zig"); +const inspect_mode = @import("modes/inspect.zig"); const repl_mode = @import("modes/repl.zig"); const completions_mod = @import("completions.zig"); @@ -336,24 +332,19 @@ pub fn main(init: std.process.Init.Minimal) void { error.InvalidMaxBodySize => fatal("--max-body-size requires a positive integer", stderr_writer, .usage, .{}), error.HttpFlagsRequireUrl => fatal("--http-header and --max-body-size require --url", stderr_writer, .usage, .{}), error.UrlIncompatibleMode => fatal("--url cannot be combined with --columns, --validate, or --sample", stderr_writer, .usage, .{}), + error.InvalidInspectMode => fatal("unknown inspect mode; supported: columns, validate, sample, stats, schema", stderr_writer, .usage, .{}), + error.InspectWithQuery => fatal("--inspect cannot be combined with a query argument", stderr_writer, .usage, .{}), + error.InspectWithOutput => fatal("--inspect cannot be combined with --output", stderr_writer, .usage, .{}), + error.InspectWithExplain => fatal("--inspect cannot be combined with --explain", stderr_writer, .usage, .{}), + error.InspectSampleWithJson => fatal("--inspect sample cannot be combined with --json or a JSON output format", stderr_writer, .usage, .{}), + error.ExplainWithOutput => fatal("--explain cannot be combined with --output", stderr_writer, .usage, .{}), error.SilentVerboseConflict => fatal("--silent cannot be combined with --verbose", stderr_writer, .usage, .{}), error.InvalidMaxRows => fatal("--max-rows must be a positive integer", stderr_writer, .usage, .{}), error.InvalidInputFormat => fatal("unknown input format; supported: csv, tsv, json, ndjson, xml, yaml, parquet", stderr_writer, .usage, .{}), error.InvalidOutputFormat => fatal("unknown output format; supported: csv, tsv, json, ndjson, xml, markdown (md), html, sql", stderr_writer, .usage, .{}), - error.ColumnsWithQuery => fatal("--columns cannot be combined with a query argument", stderr_writer, .usage, .{}), - error.ValidateWithQuery => fatal("--validate cannot be combined with a query argument", stderr_writer, .usage, .{}), + // ponytail: ColumnsWithQuery, ValidateWithQuery handled by InspectWithQuery above error.InvalidOutputPath => fatal("--output requires a non-empty file path", stderr_writer, .usage, .{}), - error.OutputWithColumns => fatal("--output cannot be combined with --columns", stderr_writer, .usage, .{}), - error.OutputWithValidate => fatal("--output cannot be combined with --validate", stderr_writer, .usage, .{}), - error.ValidateWithColumns => fatal("--validate cannot be combined with --columns", stderr_writer, .usage, .{}), - error.SampleWithQuery => fatal("--sample cannot be combined with a query argument", stderr_writer, .usage, .{}), - error.SampleWithJson => fatal("--sample cannot be combined with --json or a JSON output format", stderr_writer, .usage, .{}), - error.SampleWithColumns => fatal("--sample cannot be combined with --columns", stderr_writer, .usage, .{}), - error.SampleWithValidate => fatal("--sample cannot be combined with --validate", stderr_writer, .usage, .{}), - error.SampleWithOutput => fatal("--sample cannot be combined with --output", stderr_writer, .usage, .{}), error.InvalidSampleCount => fatal("--sample requires a positive integer value", stderr_writer, .usage, .{}), - error.StatsWithFlags => fatal("--stats is incompatible with --columns, --validate, --sample, --output, and query arguments", stderr_writer, .usage, .{}), - error.SchemaWithFlags => fatal("--schema is incompatible with --columns, --validate, --sample, --stats, --explain, --output, and query arguments", stderr_writer, .usage, .{}), error.MissingQuery => { stderr_writer.writeAll("error: no SQL query provided\n") catch |werr| std.log.err("failed to write error: {}", .{werr}); // Fall through to printUsage + exit below @@ -367,7 +358,7 @@ pub fn main(init: std.process.Init.Minimal) void { error.InvalidXmlName => fatal("--xml-root and --xml-row must be valid XML element names (letter/underscore first, then letters/digits/-/._/:)", stderr_writer, .usage, .{}), error.DuplicateTableName => fatal("duplicate table name — file arguments must have unique basenames", stderr_writer, .usage, .{}), error.TableWithNonCsv => fatal("--table requires CSV or TSV output format (not compatible with --json, -O json, etc.)", stderr_writer, .usage, .{}), - error.ExplainWithFlags => fatal("--explain cannot be combined with --columns, --validate, --sample, --stats, --schema, or --output", stderr_writer, .usage, .{}), + // ponytail: ExplainWithFlags handled by InspectWithExplain above error.InvalidSavePath => fatal("--save requires a non-empty file path", stderr_writer, .usage, .{}), error.SaveIncompatibleMode => fatal("--save cannot be combined with special modes", stderr_writer, .usage, .{}), error.SaveIncompatibleDisk => fatal("--save implies --disk; remove --disk", stderr_writer, .usage, .{}), @@ -400,44 +391,15 @@ pub fn main(init: std.process.Init.Minimal) void { stderr_writer.flush() catch |err| std.log.err("failed to flush: {}", .{err}); std.process.exit(@intFromEnum(ExitCode.success)); }, - .columns => |col_args| { - columns_mode.runColumns(allocator, io.io(), col_args, stderr_writer, stdout_writer); - stdout_file_writer.flush() catch |err| { - std.log.err("failed to flush stdout: {}", .{err}); - }; - stderr_file_writer.flush() catch |err| { - std.log.err("failed to flush stderr: {}", .{err}); - }; - }, - .validate => |val_args| { - validate_mode.runValidate(allocator, io.io(), val_args, stderr_writer, stdout_writer); - stdout_file_writer.flush() catch |err| { - std.log.err("failed to flush stdout: {}", .{err}); - }; - stderr_file_writer.flush() catch |err| { - std.log.err("failed to flush stderr: {}", .{err}); - }; - }, - .sample => |sample_args| { - sample_mode.runSample(allocator, io.io(), sample_args, stderr_writer, stdout_writer); - stdout_file_writer.flush() catch |err| { - std.log.err("failed to flush stdout: {}", .{err}); - }; - stderr_file_writer.flush() catch |err| { - std.log.err("failed to flush stderr: {}", .{err}); - }; - }, - .stats => |stats_args| { - stats_mode.runStats(allocator, io.io(), stats_args, stderr_writer, stdout_writer); - stdout_file_writer.flush() catch |err| { - std.log.err("failed to flush stdout: {}", .{err}); - }; - stderr_file_writer.flush() catch |err| { - std.log.err("failed to flush stderr: {}", .{err}); - }; - }, - .schema => |schema_args| { - schema_mode.runSchema(allocator, io.io(), schema_args, stderr_writer, stdout_writer); + .inspect => |parsed| { + const inspect_args = parsed.inspect_args orelse unreachable; + // Print deprecation warning if old flag was used + if (inspect_args.deprecated) { + const mode_str = @tagName(inspect_args.mode); + stderr_writer.print("warning: --{s} is deprecated, use --inspect {s}\n", .{ mode_str, mode_str }) catch {}; + stderr_writer.flush() catch {}; + } + inspect_mode.runInspect(allocator, io.io(), inspect_args.mode, parsed, stderr_writer, stdout_writer); stdout_file_writer.flush() catch |err| { std.log.err("failed to flush stdout: {}", .{err}); }; diff --git a/src/modes/columns.zig b/src/modes/columns.zig index 2b16f5a..cb95fb4 100644 --- a/src/modes/columns.zig +++ b/src/modes/columns.zig @@ -22,19 +22,19 @@ const source = @import("source.zig"); pub fn runColumns( allocator: std.mem.Allocator, io: std.Io, - args: args_mod.ColumnsArgs, + parsed: args_mod.ParsedArgs, stderr_writer: *std.Io.Writer, stdout_writer: *std.Io.Writer, ) void { // Determine input source: file argument or stdin - const input_source: union(enum) { file: []const u8, stdin } = if (args.files.len > 0) - .{ .file = args.files[0].path } + const input_source: union(enum) { file: []const u8, stdin } = if (parsed.files.len > 0) + .{ .file = parsed.files[0].path } else .stdin; - switch (args.input_format) { + switch (parsed.input_format) { .csv, .tsv => { - const col_delim: []const u8 = if (args.input_format == .tsv) "\t" else args.delimiter; + const col_delim: []const u8 = if (parsed.input_format == .tsv) "\t" else parsed.delimiter; var read_buf: [4096]u8 = undefined; const opened = source.openInput(io, input_source, stderr_writer); defer opened.deinit(io); @@ -57,7 +57,7 @@ pub fn runColumns( allocator.free(cols); } - if (args.verbose) { + if (parsed.verbose) { var row_buffer: std.ArrayList([][]u8) = .empty; defer { for (row_buffer.items) |row| csv_reader.freeRecord(row); @@ -108,16 +108,16 @@ pub fn runColumns( defer allocator.free(input); if (input.len == 0) fatal("empty input", stderr_writer, .csv_error, .{}); - var parsed = std.json.parseFromSlice(std.json.Value, allocator, input, .{}) catch + var json_parsed = std.json.parseFromSlice(std.json.Value, allocator, input, .{}) catch fatal("failed to parse JSON input", stderr_writer, .csv_error, .{}); - defer parsed.deinit(); + defer json_parsed.deinit(); - const first_obj = json_mod.firstJsonObject(parsed.value, args.json_path, stderr_writer).first_obj orelse + const first_obj = json_mod.firstJsonObject(json_parsed.value, parsed.json_path, stderr_writer).first_obj orelse fatal("empty JSON array: cannot determine column names", stderr_writer, .csv_error, .{}); var ki = first_obj.iterator(); while (ki.next()) |entry| { - if (args.verbose) { + if (parsed.verbose) { stdout_writer.print("{s} TEXT\n", .{entry.key_ptr.*}) catch |err| { std.log.err("failed to write output: {}", .{err}); }; @@ -150,18 +150,18 @@ pub fn runColumns( continue; } - var parsed = std.json.parseFromSlice(std.json.Value, allocator, trimmed, .{}) catch + var json_parsed = std.json.parseFromSlice(std.json.Value, allocator, trimmed, .{}) catch fatal("line 1: failed to parse NDJSON", stderr_writer, .csv_error, .{}); - defer parsed.deinit(); + defer json_parsed.deinit(); - const obj = switch (parsed.value) { + const obj = switch (json_parsed.value) { .object => |o| o, else => fatal("line 1: NDJSON element must be a JSON object", stderr_writer, .csv_error, .{}), }; var ki = obj.iterator(); while (ki.next()) |entry| { - if (args.verbose) { + if (parsed.verbose) { stdout_writer.print("{s} TEXT\n", .{entry.key_ptr.*}) catch |err| { std.log.err("failed to write output: {}", .{err}); }; @@ -188,7 +188,7 @@ pub fn runColumns( allocator.free(cols); } for (cols) |col| { - if (args.verbose) { + if (parsed.verbose) { stdout_writer.print("{s} TEXT\n", .{col}) catch |err| { std.log.err("failed to write output: {}", .{err}); }; @@ -205,13 +205,13 @@ pub fn runColumns( defer opened.deinit(io); var source_reader = std.Io.File.reader(opened.file, io, &read_buf); - const names = xml_mod.getXmlColumnNames(allocator, &source_reader.interface, args.xml_root_input, args.xml_row_input, stderr_writer); + const names = xml_mod.getXmlColumnNames(allocator, &source_reader.interface, parsed.xml_root_input, parsed.xml_row_input, stderr_writer); defer { for (names) |name| allocator.free(name); allocator.free(names); } for (names) |name| { - if (args.verbose) { + if (parsed.verbose) { stdout_writer.print("{s} TEXT\n", .{name}) catch |err| { std.log.err("failed to write output: {}", .{err}); }; @@ -230,7 +230,7 @@ pub fn runColumns( const parquet_db = sqlite_mod.openDb(false, null, stderr_writer); defer _ = c.sqlite3_close(parquet_db); _ = parquet_mod.loadParquetInput(allocator, io, parquet_db, "t", &parquet_reader.interface, null, stderr_writer); - if (args.verbose) { + if (parsed.verbose) { const info = sqlite_mod.getTableColumnsWithTypes(allocator, parquet_db, "t", stderr_writer); defer { for (info.names) |n| allocator.free(n); diff --git a/src/modes/inspect.zig b/src/modes/inspect.zig new file mode 100644 index 0000000..e940280 --- /dev/null +++ b/src/modes/inspect.zig @@ -0,0 +1,24 @@ +const std = @import("std"); +const args_mod = @import("../args.zig"); +const columns_mode = @import("columns.zig"); +const validate_mode = @import("validate.zig"); +const sample_mode = @import("sample.zig"); +const stats_mode = @import("stats.zig"); +const schema_mode = @import("schema.zig"); + +pub fn runInspect( + allocator: std.mem.Allocator, + io: std.Io, + mode: args_mod.InspectMode, + parsed: args_mod.ParsedArgs, + stderr_writer: *std.Io.Writer, + stdout_writer: *std.Io.Writer, +) void { + switch (mode) { + .columns => columns_mode.runColumns(allocator, io, parsed, stderr_writer, stdout_writer), + .validate => validate_mode.runValidate(allocator, io, parsed, stderr_writer, stdout_writer), + .sample => sample_mode.runSample(allocator, io, parsed, stderr_writer, stdout_writer), + .stats => stats_mode.runStats(allocator, io, parsed, stderr_writer, stdout_writer), + .schema => schema_mode.runSchema(allocator, io, parsed, stderr_writer, stdout_writer), + } +} diff --git a/src/modes/repl.zig b/src/modes/repl.zig index 4c729f5..7aabebd 100644 --- a/src/modes/repl.zig +++ b/src/modes/repl.zig @@ -6,11 +6,6 @@ const main_mod = @import("../main.zig"); const loader = @import("../loader.zig"); const builtin = @import("builtin"); -// ponytail: linenoise on Unix, plain stdin on Windows — same API, different backend -const linenoise = if (builtin.os.tag != .windows) @cImport({ - @cInclude("linenoise.h"); -}) else struct {}; - // ponytail: C stdio for Windows stdin reading (Zig 0.16 I/O API is inconsistent on Windows) const c_stdio = if (builtin.os.tag == .windows) @cImport({ @cInclude("stdio.h"); @@ -21,38 +16,9 @@ const fatal = sqlite_mod.fatal; const printSqlError = sqlite_mod.printSqlError; const fmtThousands = loader.fmtThousands; -/// Read a line with prompt. Returns heap-allocated null-terminated string, or null on EOF. -/// Caller must call freeLine() to free. -/// On Windows, uses C stdio getc(stdin) directly. -/// On Unix, uses linenoise for interactive editing. -fn readLine(allocator: std.mem.Allocator, io: std.Io, _: anytype, prompt: []const u8) ?[:0]u8 { - if (builtin.os.tag == .windows) { - // Write prompt to stderr — keeps stdout clean for piping (B1) - var err_buf: [256]u8 = undefined; - var stderr_w = std.Io.File.writer(std.Io.File.stderr(), io, &err_buf).interface; - stderr_w.writeAll(prompt) catch return null; - stderr_w.flush() catch return null; - - // ponytail: 8KB line limit; bump if users report truncation (B3) - var buf: [8192]u8 = undefined; - const raw = readLineWindows(&buf) orelse return null; - - // Strip trailing \r (Windows CRLF) - const trimmed = if (raw.len > 0 and raw[raw.len - 1] == '\r') raw[0 .. raw.len - 1] else raw; - - const copy = allocator.allocSentinel(u8, trimmed.len, 0) catch return null; - @memcpy(copy[0..trimmed.len], trimmed); - return copy; - } else { - const ptr = linenoise.linenoise(@as([*c]const u8, @ptrCast(prompt))); - if (ptr == null) return null; - return std.mem.span(ptr); - } -} - /// Read a line from stdin on Windows using C stdio getc(). /// Returns slice of the buffer up to newline, or null on EOF/error. -fn readLineWindows(buf: anytype) ?[]u8 { +fn readLineWindows(buf: [8192]u8) ?[]u8 { var pos: usize = 0; const stdin_file = c_stdio.__acrt_iob_func(0); // stdin = __acrt_iob_func(0) while (pos < buf.len) { @@ -68,36 +34,65 @@ fn readLineWindows(buf: anytype) ?[]u8 { return buf[0..pos]; } -fn freeLine(allocator: std.mem.Allocator, line: ?[:0]u8) void { - if (line == null) return; +/// Read a line with prompt. Returns heap-allocated null-terminated string, or null on EOF. +/// Caller must call freeLine() to free. +/// On Windows, uses C stdio getc(stdin) directly. +/// On Unix, uses std.Io.Reader for line reading. +fn readLine(allocator: std.mem.Allocator, io: std.Io, stdin_r: anytype, prompt: []const u8) ?[:0]u8 { + // Write prompt to stderr — keeps stdout clean for piping + // ponytail: File.Writer must outlive its .interface (drain uses @fieldParentPtr) + var err_buf: [256]u8 = undefined; + var err_file_writer = std.Io.File.writer(std.Io.File.stderr(), io, &err_buf); + const err_writer = &err_file_writer.interface; + err_writer.writeAll(prompt) catch return null; + err_writer.flush() catch return null; + if (builtin.os.tag == .windows) { - allocator.free(line.?); - } else { - linenoise.linenoiseFree(@ptrCast(line.?)); - } -} + // ponytail: 8KB line limit; warn if truncated + var buf: [8192]u8 = undefined; + const raw = readLineWindows(&buf) orelse return null; -fn historyAdd(line: ?[:0]u8) void { - if (builtin.os.tag == .windows) return; - if (line) |l| { - _ = linenoise.linenoiseHistoryAdd(@as([*c]const u8, @ptrCast(l))); - } -} + // Strip trailing \r (Windows CRLF) + const trimmed = if (raw.len > 0 and raw[raw.len - 1] == '\r') raw[0 .. raw.len - 1] else raw; -fn historyLoad(path: ?[:0]u8) void { - if (builtin.os.tag == .windows) return; - // ponytail: SetMaxLen before Load so existing huge files get trimmed (S2) - _ = linenoise.linenoiseHistorySetMaxLen(1000); - if (path) |p| { - _ = linenoise.linenoiseHistoryLoad(@as([*c]const u8, @ptrCast(p))); + if (raw.len == buf.len) { + err_writer.print("warning: input line truncated at 8192 bytes\n", .{}) catch {}; + err_writer.flush() catch {}; + } + + const copy = allocator.allocSentinel(u8, trimmed.len, 0) catch return null; + @memcpy(copy[0..trimmed.len], trimmed); + return copy; + } else { + // Read line via the persistent stdin reader (passed as pointer to + // preserve internal buffer state across calls). + // ponytail: 8KB line limit; warn if truncated + var line_buf: [8192]u8 = undefined; + var pos: usize = 0; + while (pos < line_buf.len) { + const byte = stdin_r.interface.takeByte() catch |err| switch (err) { + error.EndOfStream => { + if (pos == 0) return null; // EOF before any data + break; // EOF after some data + }, + else => return null, + }; + if (byte == '\n') break; + line_buf[pos] = byte; + pos += 1; + } + if (pos == line_buf.len) { + err_writer.print("warning: input line truncated at 8192 bytes\n", .{}) catch {}; + err_writer.flush() catch {}; + } + const copy = allocator.allocSentinel(u8, pos, 0) catch return null; + @memcpy(copy[0..pos], line_buf[0..pos]); + return copy; } } -fn historySave(path: ?[:0]u8) void { - if (builtin.os.tag == .windows) return; - if (path) |p| { - _ = linenoise.linenoiseHistorySave(@as([*c]const u8, @ptrCast(p))); - } +fn freeLine(allocator: std.mem.Allocator, line: ?[:0]u8) void { + if (line) |l| allocator.free(l); } fn execReplQuery( @@ -139,7 +134,6 @@ fn handleDotCommand( parsed: ParsedArgs, use_table: bool, main_table: []const u8, - _: ?[:0]u8, ) bool { const trimmed = std.mem.trim(u8, command, " \t\r\n"); if (trimmed.len == 0 or trimmed[0] != '.') return false; @@ -224,24 +218,12 @@ pub fn runRepl( stderr_writer.flush() catch |err| std.log.err("failed to flush stderr: {}", .{err}); } - // History path — only used on Unix - var history_path: ?[:0]u8 = null; - if (builtin.os.tag != .windows) { - const home_env = std.c.getenv("HOME"); - const home: []const u8 = if (home_env) |h| std.mem.span(h) else "."; - if (std.fmt.allocPrintSentinel(allocator, "{s}/.sqlpipe_history", .{home}, 0)) |p| { - history_path = p; - } else |_| {} - } - defer if (history_path) |p| allocator.free(p); - - historyLoad(history_path); - const main_table = main_mod.mainTableName(parsed); // ponytail: persistent stdin reader for Unix — Windows uses C stdio getc() directly + // Must be var and passed as pointer so internal buffer state survives across readLine calls. var stdin_buf: [8192]u8 = undefined; - const stdin_r = if (builtin.os.tag != .windows) std.Io.File.reader(std.Io.File.stdin(), io, &stdin_buf) else undefined; + var stdin_r = if (builtin.os.tag != .windows) std.Io.File.reader(std.Io.File.stdin(), io, &stdin_buf) else undefined; stderr_writer.writeAll("Entering interactive mode. Type .exit, .quit, Ctrl-D, or Ctrl-C to quit.\n") catch {}; stderr_writer.flush() catch |err| std.log.err("failed to flush stderr: {}", .{err}); @@ -251,7 +233,7 @@ pub fn runRepl( while (true) { const prompt = if (ml_buf.items.len > 0) "...> " else "sql> "; - const line = readLine(allocator, io, stdin_r, prompt); + const line = readLine(allocator, io, &stdin_r, prompt); if (line == null) break; defer freeLine(allocator, line); const trimmed = std.mem.trim(u8, line.?, " \t\r\n"); @@ -272,7 +254,7 @@ pub fn runRepl( // Dot commands only in single-line mode (NOT in multi-line) if (ml_buf.items.len == 0 and trimmed[0] == '.') { - if (handleDotCommand(allocator, io, trimmed, db, stdout_writer, stderr_writer, parsed, use_table, main_table, history_path)) continue; + if (handleDotCommand(allocator, io, trimmed, db, stdout_writer, stderr_writer, parsed, use_table, main_table)) continue; } // Append to multi-line buffer @@ -284,7 +266,7 @@ pub fn runRepl( if (!ends_with_semicolon) continue; - // Strip trailing ; and execute + // Strip trailing ; and execute (B5) const query_len = if (ml_buf.items.len > 0 and ml_buf.items[ml_buf.items.len - 1] == ';') ml_buf.items.len - 1 else ml_buf.items.len; const query = ml_buf.items[0..query_len]; @@ -294,9 +276,6 @@ pub fn runRepl( continue; } - historyAdd(line); - historySave(history_path); - execReplQuery(allocator, db, query, stdout_writer, stderr_writer, parsed, use_table, main_table); ml_buf.clearRetainingCapacity(); stdout_writer.flush() catch |err| std.log.err("failed to flush stdout: {}", .{err}); diff --git a/src/modes/sample.zig b/src/modes/sample.zig index 4fea8ae..498ff8d 100644 --- a/src/modes/sample.zig +++ b/src/modes/sample.zig @@ -17,17 +17,17 @@ const source = @import("source.zig"); pub fn runSample( allocator: std.mem.Allocator, io: std.Io, - args: args_mod.SampleArgs, + parsed: args_mod.ParsedArgs, stderr_writer: *std.Io.Writer, stdout_writer: *std.Io.Writer, ) void { // Determine input source: file argument or stdin - const input_source: union(enum) { file: []const u8, stdin } = if (args.files.len > 0) - .{ .file = args.files[0].path } + const input_source: union(enum) { file: []const u8, stdin } = if (parsed.files.len > 0) + .{ .file = parsed.files[0].path } else .stdin; - switch (args.input_format) { + switch (parsed.input_format) { // ponytail: --sample for non-CSV formats requires loading into SQLite first; // CSV/TSV stream directly. Upgrade: open temp DB, load Parquet/JSON/etc., // print schema via getTableColumnsWithTypes, SELECT LIMIT n for rows. @@ -38,7 +38,7 @@ pub fn runSample( .{}, ), .csv, .tsv => { - const col_delim: []const u8 = if (args.input_format == .tsv) "\t" else args.delimiter; + const col_delim: []const u8 = if (parsed.input_format == .tsv) "\t" else parsed.delimiter; var read_buf: [4096]u8 = undefined; const opened = source.openInput(io, input_source, stderr_writer); defer opened.deinit(io); @@ -62,7 +62,7 @@ pub fn runSample( } // Buffer max(inference_buffer_size, n) rows for type inference - const buf_size = @max(inference_buffer_size, args.n); + const buf_size = @max(inference_buffer_size, parsed.sample_n); var row_buffer: std.ArrayList([][]u8) = .empty; defer { for (row_buffer.items) |row| csv_reader.freeRecord(row); @@ -89,7 +89,7 @@ pub fn runSample( fatal("out of memory while buffering rows", stderr_writer, .csv_error, .{}); } - const types: []ColumnType = if (args.type_inference) blk: { + const types: []ColumnType = if (parsed.type_inference) blk: { break :blk inferTypes(allocator, row_buffer.items, cols.len) catch fatal("out of memory during type inference", stderr_writer, .csv_error, .{}); } else blk: { @@ -139,7 +139,7 @@ pub fn runSample( fatal("failed to write header newline", stderr_writer, .csv_error, .{}); // ─── Print first n data rows to stdout ──────────────────────────────── - const rows_to_print = @min(args.n, row_buffer.items.len); + const rows_to_print = @min(parsed.sample_n, row_buffer.items.len); for (row_buffer.items[0..rows_to_print]) |row| { var col_idx: usize = 0; while (col_idx < cols.len) : (col_idx += 1) { diff --git a/src/modes/schema.zig b/src/modes/schema.zig index 5721942..e3e8758 100644 --- a/src/modes/schema.zig +++ b/src/modes/schema.zig @@ -12,33 +12,14 @@ const fatal = sqlite_mod.fatal; pub fn runSchema( allocator: std.mem.Allocator, io: std.Io, - args: args_mod.SchemaArgs, + parsed_in: args_mod.ParsedArgs, stderr_writer: *std.Io.Writer, stdout_writer: *std.Io.Writer, ) void { + var parsed = parsed_in; const db = sqlite_mod.openDb(false, null, stderr_writer); defer _ = c.sqlite3_close(db); - var parsed = args_mod.ParsedArgs{ - .query = "", - .files = args.files, - .urls = args.urls, - .type_inference = args.type_inference, - .delimiter = args.delimiter, - .header = false, - .input_format = args.input_format, - .output_format = .csv, - .max_rows = null, - .verbose = false, - .silent = true, - .output = null, - .xml_root = "results", - .xml_row = "row", - .xml_root_input = null, - .xml_row_input = null, - .json_path = null, - .disk = false, - }; parsed.has_stdin = if (parsed.urls.len > 0) false else !(std.Io.File.isTty(std.Io.File.stdin(), io) catch false); const total_rows = main_mod.loadPipelineInputs(allocator, io, db, parsed, stderr_writer); diff --git a/src/modes/stats.zig b/src/modes/stats.zig index 52b89d8..d799241 100644 --- a/src/modes/stats.zig +++ b/src/modes/stats.zig @@ -13,33 +13,14 @@ const fatal = sqlite_mod.fatal; pub fn runStats( allocator: std.mem.Allocator, io: std.Io, - args: args_mod.StatsArgs, + parsed_in: args_mod.ParsedArgs, stderr_writer: *std.Io.Writer, stdout_writer: *std.Io.Writer, ) void { + var parsed = parsed_in; const db = sqlite_mod.openDb(false, null, stderr_writer); defer _ = c.sqlite3_close(db); - var parsed = args_mod.ParsedArgs{ - .query = "", - .files = args.files, - .urls = args.urls, - .type_inference = args.type_inference, - .delimiter = args.delimiter, - .header = false, - .input_format = args.input_format, - .output_format = .csv, - .max_rows = null, - .verbose = false, - .silent = true, - .output = null, - .xml_root = "results", - .xml_row = "row", - .xml_root_input = null, - .xml_row_input = null, - .json_path = null, - .disk = false, - }; parsed.has_stdin = if (parsed.urls.len > 0) false else !(std.Io.File.isTty(std.Io.File.stdin(), io) catch false); const total_rows = main_mod.loadPipelineInputs(allocator, io, db, parsed, stderr_writer); diff --git a/src/modes/validate.zig b/src/modes/validate.zig index f9c7715..beb1816 100644 --- a/src/modes/validate.zig +++ b/src/modes/validate.zig @@ -24,19 +24,19 @@ const source = @import("source.zig"); pub fn runValidate( allocator: std.mem.Allocator, io: std.Io, - args: args_mod.ValidateArgs, + parsed: args_mod.ParsedArgs, stderr_writer: *std.Io.Writer, stdout_writer: *std.Io.Writer, ) void { // Determine input source: file argument or stdin - const input_source: union(enum) { file: []const u8, stdin } = if (args.files.len > 0) - .{ .file = args.files[0].path } + const input_source: union(enum) { file: []const u8, stdin } = if (parsed.files.len > 0) + .{ .file = parsed.files[0].path } else .stdin; - switch (args.input_format) { + switch (parsed.input_format) { .csv, .tsv => { - const col_delim: []const u8 = if (args.input_format == .tsv) "\t" else args.delimiter; + const col_delim: []const u8 = if (parsed.input_format == .tsv) "\t" else parsed.delimiter; var read_buf: [4096]u8 = undefined; const opened = source.openInput(io, input_source, stderr_writer); defer opened.deinit(io); @@ -97,7 +97,7 @@ pub fn runValidate( fatal("out of memory while buffering rows", stderr_writer, .csv_error, .{}); } - const types: []ColumnType = if (args.type_inference) blk: { + const types: []ColumnType = if (parsed.type_inference) blk: { break :blk inferTypes(allocator, row_buffer.items, num_cols) catch fatal("out of memory during type inference", stderr_writer, .csv_error, .{}); } else blk: { @@ -172,11 +172,11 @@ pub fn runValidate( defer allocator.free(input); if (input.len == 0) fatal("empty input", stderr_writer, .csv_error, .{}); - var parsed = std.json.parseFromSlice(std.json.Value, allocator, input, .{}) catch + var json_parsed = std.json.parseFromSlice(std.json.Value, allocator, input, .{}) catch fatal("failed to parse JSON input", stderr_writer, .csv_error, .{}); - defer parsed.deinit(); + defer json_parsed.deinit(); - const fj = json_mod.firstJsonObject(parsed.value, args.json_path, stderr_writer); + const fj = json_mod.firstJsonObject(json_parsed.value, parsed.json_path, stderr_writer); const first_obj = fj.first_obj orelse fatal("empty JSON array: cannot determine column names", stderr_writer, .csv_error, .{}); const array = fj.array; @@ -330,7 +330,7 @@ pub fn runValidate( defer opened.deinit(io); var source_reader = std.Io.File.reader(opened.file, io, &read_buf); - const summary = xml_mod.summarizeXml(allocator, &source_reader.interface, args.xml_root_input, args.xml_row_input, stderr_writer); + const summary = xml_mod.summarizeXml(allocator, &source_reader.interface, parsed.xml_root_input, parsed.xml_row_input, stderr_writer); defer { for (summary.col_names) |name| allocator.free(name); allocator.free(summary.col_names); From 95197292cd8bbf15e93f6583a523e7164d9f4656 Mon Sep 17 00:00:00 2001 From: "Victor M. Varela" Date: Sat, 18 Jul 2026 18:20:20 +0200 Subject: [PATCH 2/5] fix(repl): avoid undefined-typed var on Windows --- src/modes/repl.zig | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/modes/repl.zig b/src/modes/repl.zig index 7aabebd..25ce53a 100644 --- a/src/modes/repl.zig +++ b/src/modes/repl.zig @@ -38,7 +38,7 @@ fn readLineWindows(buf: [8192]u8) ?[]u8 { /// Caller must call freeLine() to free. /// On Windows, uses C stdio getc(stdin) directly. /// On Unix, uses std.Io.Reader for line reading. -fn readLine(allocator: std.mem.Allocator, io: std.Io, stdin_r: anytype, prompt: []const u8) ?[:0]u8 { +fn readLine(allocator: std.mem.Allocator, io: std.Io, stdin_r: ?*std.Io.Reader, prompt: []const u8) ?[:0]u8 { // Write prompt to stderr — keeps stdout clean for piping // ponytail: File.Writer must outlive its .interface (drain uses @fieldParentPtr) var err_buf: [256]u8 = undefined; @@ -64,13 +64,14 @@ fn readLine(allocator: std.mem.Allocator, io: std.Io, stdin_r: anytype, prompt: @memcpy(copy[0..trimmed.len], trimmed); return copy; } else { - // Read line via the persistent stdin reader (passed as pointer to + // Read line via the persistent stdin reader (passed as optional pointer to // preserve internal buffer state across calls). // ponytail: 8KB line limit; warn if truncated var line_buf: [8192]u8 = undefined; var pos: usize = 0; + const stdin_reader = stdin_r.?; while (pos < line_buf.len) { - const byte = stdin_r.interface.takeByte() catch |err| switch (err) { + const byte = stdin_reader.interface.takeByte() catch |err| switch (err) { error.EndOfStream => { if (pos == 0) return null; // EOF before any data break; // EOF after some data @@ -223,7 +224,7 @@ pub fn runRepl( // ponytail: persistent stdin reader for Unix — Windows uses C stdio getc() directly // Must be var and passed as pointer so internal buffer state survives across readLine calls. var stdin_buf: [8192]u8 = undefined; - var stdin_r = if (builtin.os.tag != .windows) std.Io.File.reader(std.Io.File.stdin(), io, &stdin_buf) else undefined; + const stdin_r: ?*std.Io.Reader = if (builtin.os.tag != .windows) @ptrCast(?*std.Io.Reader, &std.Io.File.reader(std.Io.File.stdin(), io, &stdin_buf).interface) else null; stderr_writer.writeAll("Entering interactive mode. Type .exit, .quit, Ctrl-D, or Ctrl-C to quit.\n") catch {}; stderr_writer.flush() catch |err| std.log.err("failed to flush stderr: {}", .{err}); @@ -233,7 +234,7 @@ pub fn runRepl( while (true) { const prompt = if (ml_buf.items.len > 0) "...> " else "sql> "; - const line = readLine(allocator, io, &stdin_r, prompt); + const line = readLine(allocator, io, stdin_r, prompt); if (line == null) break; defer freeLine(allocator, line); const trimmed = std.mem.trim(u8, line.?, " \t\r\n"); From cf3cd935e6f33515ce0726e4eb0075d9d73450e9 Mon Sep 17 00:00:00 2001 From: "Victor M. Varela" Date: Sat, 18 Jul 2026 18:31:12 +0200 Subject: [PATCH 3/5] fix(repl): use anytype + sentinel struct for Windows stdin --- src/modes/repl.zig | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/modes/repl.zig b/src/modes/repl.zig index 25ce53a..f9b2365 100644 --- a/src/modes/repl.zig +++ b/src/modes/repl.zig @@ -38,7 +38,7 @@ fn readLineWindows(buf: [8192]u8) ?[]u8 { /// Caller must call freeLine() to free. /// On Windows, uses C stdio getc(stdin) directly. /// On Unix, uses std.Io.Reader for line reading. -fn readLine(allocator: std.mem.Allocator, io: std.Io, stdin_r: ?*std.Io.Reader, prompt: []const u8) ?[:0]u8 { +fn readLine(allocator: std.mem.Allocator, io: std.Io, stdin_r: anytype, prompt: []const u8) ?[:0]u8 { // Write prompt to stderr — keeps stdout clean for piping // ponytail: File.Writer must outlive its .interface (drain uses @fieldParentPtr) var err_buf: [256]u8 = undefined; @@ -69,9 +69,8 @@ fn readLine(allocator: std.mem.Allocator, io: std.Io, stdin_r: ?*std.Io.Reader, // ponytail: 8KB line limit; warn if truncated var line_buf: [8192]u8 = undefined; var pos: usize = 0; - const stdin_reader = stdin_r.?; while (pos < line_buf.len) { - const byte = stdin_reader.interface.takeByte() catch |err| switch (err) { + const byte = stdin_r.interface.takeByte() catch |err| switch (err) { error.EndOfStream => { if (pos == 0) return null; // EOF before any data break; // EOF after some data @@ -224,7 +223,7 @@ pub fn runRepl( // ponytail: persistent stdin reader for Unix — Windows uses C stdio getc() directly // Must be var and passed as pointer so internal buffer state survives across readLine calls. var stdin_buf: [8192]u8 = undefined; - const stdin_r: ?*std.Io.Reader = if (builtin.os.tag != .windows) @ptrCast(?*std.Io.Reader, &std.Io.File.reader(std.Io.File.stdin(), io, &stdin_buf).interface) else null; + var stdin_r = if (builtin.os.tag != .windows) std.Io.File.reader(std.Io.File.stdin(), io, &stdin_buf) else {}; stderr_writer.writeAll("Entering interactive mode. Type .exit, .quit, Ctrl-D, or Ctrl-C to quit.\n") catch {}; stderr_writer.flush() catch |err| std.log.err("failed to flush stderr: {}", .{err}); @@ -234,7 +233,7 @@ pub fn runRepl( while (true) { const prompt = if (ml_buf.items.len > 0) "...> " else "sql> "; - const line = readLine(allocator, io, stdin_r, prompt); + const line = readLine(allocator, io, &stdin_r, prompt); if (line == null) break; defer freeLine(allocator, line); const trimmed = std.mem.trim(u8, line.?, " \t\r\n"); From 715cf9d8319b321e18911081e3d7a1161cbace3c Mon Sep 17 00:00:00 2001 From: "Victor M. Varela" Date: Sat, 18 Jul 2026 18:36:23 +0200 Subject: [PATCH 4/5] fix(repl): readLineWindows takes pointer, not array by value --- src/modes/repl.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modes/repl.zig b/src/modes/repl.zig index f9b2365..ae9d77e 100644 --- a/src/modes/repl.zig +++ b/src/modes/repl.zig @@ -18,7 +18,7 @@ const fmtThousands = loader.fmtThousands; /// Read a line from stdin on Windows using C stdio getc(). /// Returns slice of the buffer up to newline, or null on EOF/error. -fn readLineWindows(buf: [8192]u8) ?[]u8 { +fn readLineWindows(buf: *[8192]u8) ?[]u8 { var pos: usize = 0; const stdin_file = c_stdio.__acrt_iob_func(0); // stdin = __acrt_iob_func(0) while (pos < buf.len) { From 25947b559d585830fa77f9207c433b4da93421cc Mon Sep 17 00:00:00 2001 From: "Victor M. Varela" Date: Sun, 19 Jul 2026 11:27:43 +0200 Subject: [PATCH 5/5] fix: PR 213 regressions and improvements - stats/schema: ignore --max-rows (regression from fused inspect modes) - README: remove duplicate 'Same as running SELECT MIN...' line - completions: keep deprecated flags (--validate, --columns, --stats, --profile, --schema) with '(deprecated)' descriptions - args/main: add MissingInspectMode error for --inspect without mode arg - build.zig: add regression tests for --stats/--schema --max-rows --- README.md | 2 -- build.zig | 20 ++++++++++++++++++++ src/args.zig | 3 ++- src/completions.zig | 15 +++++++++++++++ src/main.zig | 1 + src/modes/schema.zig | 1 + src/modes/stats.zig | 1 + 7 files changed, 40 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index da6d63e..91876d2 100644 --- a/README.md +++ b/README.md @@ -488,8 +488,6 @@ $ printf 'name,age\nAlice,30\nBob,25\nCarol,35\n' | sql-pipe --stats Same as running `SELECT MIN(col), MAX(col), AVG(col), COUNT(*)` per column — but in one command. -Same as running `SELECT MIN(col), MAX(col), AVG(col), COUNT(*)` per column — but in one command. - ### Inspect data without queries (--inspect) ```sh diff --git a/build.zig b/build.zig index 4cd945e..3befe3d 100644 --- a/build.zig +++ b/build.zig @@ -2041,6 +2041,16 @@ pub fn build(b: *std.Build) void { test_schema_semicolon.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_schema_semicolon.step); + // Integration test: --schema ignores --max-rows (regression test for PR 213) + // Should show all 5 rows in schema even with --max-rows 2 + const test_schema_ignores_max_rows = b.addSystemCommand(&.{ + "bash", "-c", + \\result=$(printf 'id,val\n1,10\n2,20\n3,30\n4,40\n5,50\n' | ./zig-out/bin/sql-pipe --schema --max-rows 2) + \\echo "$result" | grep -q 'INTEGER' + }); + test_schema_ignores_max_rows.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_schema_ignores_max_rows.step); + // Integration test 176d: --schema with single file argument const test_schema_file = b.addSystemCommand(&.{ "bash", "-c", @@ -2922,6 +2932,16 @@ pub fn build(b: *std.Build) void { test_stats_basic.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_stats_basic.step); + // Integration test: --stats ignores --max-rows (regression test for PR 213) + // Should show stats for all 5 rows even with --max-rows 2 + const test_stats_ignores_max_rows = b.addSystemCommand(&.{ + "bash", "-c", + \\result=$(printf 'id,val\n1,10\n2,20\n3,30\n4,40\n5,50\n' | ./zig-out/bin/sql-pipe --stats --max-rows 2 2>/dev/null) + \\echo "$result" | grep -q '5' && echo "$result" | grep -q '30' + }); + test_stats_ignores_max_rows.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_stats_ignores_max_rows.step); + // Integration test: --profile alias works const test_stats_alias = b.addSystemCommand(&.{ "bash", "-c", diff --git a/src/args.zig b/src/args.zig index 21a3792..026a55c 100644 --- a/src/args.zig +++ b/src/args.zig @@ -106,6 +106,7 @@ pub const SqlPipeError = error{ InspectWithExplain, InspectSampleWithJson, InvalidInspectMode, + MissingInspectMode, ExplainWithOutput, }; @@ -468,7 +469,7 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP silent = true; } else if (std.mem.eql(u8, arg, "--inspect")) { i += 1; - if (i >= args.len) return error.InvalidInspectMode; + if (i >= args.len) return error.MissingInspectMode; const mode = InspectMode.parse(args[i]) catch return error.InvalidInspectMode; inspect_mode = mode; } else if (std.mem.startsWith(u8, arg, "--inspect=")) { diff --git a/src/completions.zig b/src/completions.zig index 64e8349..818e2c0 100644 --- a/src/completions.zig +++ b/src/completions.zig @@ -85,8 +85,13 @@ fn generateBash(writer: *std.Io.Writer) !void { \\ --null-value \\ --html-class \\ --completions + \\ --columns \\ --file -f \\ --help -h + \\ --profile + \\ --schema + \\ --stats + \\ --validate \\ --version -V \\ ' -- "$cur")) \\ else @@ -121,6 +126,11 @@ fn generateZsh(writer: *std.Io.Writer) !void { \\ '(-v --verbose)'{-v,--verbose}'[Force row count to stderr]' \\ '(-s --silent)'{-s,--silent}'[Suppress row count output]' \\ '--inspect:Inspect mode:(columns validate sample stats schema)' + \\ '--columns[Inspect columns (deprecated)]' + \\ '--validate[Validate input (deprecated)]' + \\ '--stats[Print stats (deprecated)]' + \\ '--profile[Print profile (deprecated)]' + \\ '--schema[Print schema (deprecated)]' \\ '--sample::Number of rows:(1 5 10 25 50 100 500 1000)' \\ '--output=[Write results to file]:file:_files' \\ '--xml-root=[Root element name]:name:' @@ -171,6 +181,11 @@ fn generateFish(writer: *std.Io.Writer) !void { \\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 inspect -r -f -a "columns validate sample stats schema" -d "Inspect mode" + \\complete -c sql-pipe -l columns -d "Inspect columns (deprecated)" + \\complete -c sql-pipe -l validate -d "Validate input (deprecated)" + \\complete -c sql-pipe -l stats -d "Print stats (deprecated)" + \\complete -c sql-pipe -l profile -d "Print profile (deprecated)" + \\complete -c sql-pipe -l schema -d "Print schema (deprecated)" \\complete -c sql-pipe -l sample -r -f -a "1 5 10 25 50 100 500 1000" -d "Print schema and sample rows" \\ \\# Output options diff --git a/src/main.zig b/src/main.zig index af4362b..fea2298 100644 --- a/src/main.zig +++ b/src/main.zig @@ -333,6 +333,7 @@ pub fn main(init: std.process.Init.Minimal) void { error.HttpFlagsRequireUrl => fatal("--http-header and --max-body-size require --url", stderr_writer, .usage, .{}), error.UrlIncompatibleMode => fatal("--url cannot be combined with --columns, --validate, or --sample", stderr_writer, .usage, .{}), error.InvalidInspectMode => fatal("unknown inspect mode; supported: columns, validate, sample, stats, schema", stderr_writer, .usage, .{}), + error.MissingInspectMode => fatal("--inspect requires a mode argument (columns, validate, sample, stats, schema)", stderr_writer, .usage, .{}), error.InspectWithQuery => fatal("--inspect cannot be combined with a query argument", stderr_writer, .usage, .{}), error.InspectWithOutput => fatal("--inspect cannot be combined with --output", stderr_writer, .usage, .{}), error.InspectWithExplain => fatal("--inspect cannot be combined with --explain", stderr_writer, .usage, .{}), diff --git a/src/modes/schema.zig b/src/modes/schema.zig index e3e8758..a4b7931 100644 --- a/src/modes/schema.zig +++ b/src/modes/schema.zig @@ -17,6 +17,7 @@ pub fn runSchema( stdout_writer: *std.Io.Writer, ) void { var parsed = parsed_in; + parsed.max_rows = null; const db = sqlite_mod.openDb(false, null, stderr_writer); defer _ = c.sqlite3_close(db); diff --git a/src/modes/stats.zig b/src/modes/stats.zig index d799241..01c4373 100644 --- a/src/modes/stats.zig +++ b/src/modes/stats.zig @@ -18,6 +18,7 @@ pub fn runStats( stdout_writer: *std.Io.Writer, ) void { var parsed = parsed_in; + parsed.max_rows = null; const db = sqlite_mod.openDb(false, null, stderr_writer); defer _ = c.sqlite3_close(db);