Skip to content

Commit ff6c122

Browse files
authored
Fix review follow-ups (#39)
* wip * wip * wip * wip * wip * wip * wip
1 parent ae1a5b1 commit ff6c122

8 files changed

Lines changed: 124 additions & 79 deletions

File tree

src/args.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ pub const Args = struct {
9696
if (std.mem.eql(u8, input, "min")) return .min;
9797
if (std.mem.eql(u8, input, "max")) return .max;
9898
const value = std.fmt.parseInt(usize, input, 10) catch return error.InvalidArgument;
99+
// 0 works but is undocumented, this is fine
99100
if (value == 0) return .auto;
100101
return .{ .chars = value };
101102
}

src/data.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ pub const DataRow = struct {
5454
var slow = false;
5555
var len: usize = 0;
5656
for (row_in, 0..) |field, ii| {
57+
// Intentionally strip cells for nice display
5758
const str = util.strip(u8, field);
5859
row[ii] = str;
5960
if (hasControl(str)) {
@@ -73,6 +74,7 @@ pub const DataRow = struct {
7374
var slow = false;
7475
var len: usize = 0;
7576
for (col_order, 0..) |col, ii| {
77+
// Intentionally strip cells for nice display
7678
const str = util.strip(u8, source[col]);
7779
row[ii] = str;
7880
if (hasControl(str)) slow = true;

src/detect.zig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ pub fn formatFromFilename(path: []const u8) ?InputFormat {
4747
return null;
4848
}
4949

50+
pub fn isSqliteFile(alloc: std.mem.Allocator, filename: ?[]const u8, input: std.fs.File) !bool {
51+
// do we have an actual file?
52+
const path = filename orelse return false;
53+
if (std.mem.eql(u8, path, "-")) return false;
54+
55+
// sample the first few bytes to look for sqlite3 magic
56+
var buf: [32]u8 = undefined;
57+
const n = try input.readAll(&buf);
58+
try input.seekTo(0);
59+
return try detectFormat(alloc, path, buf[0..n]) == .sqlite;
60+
}
61+
5062
//
5163
// testing
5264
//

src/main.zig

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
//
2+
// main entrypoint
13
// Owns process flow, input detection, loading, and top-level CLI behavior.
4+
//
5+
26
pub fn main() !void {
37
var gpa: std.heap.DebugAllocator(.{}) = .init;
48
defer {
@@ -7,28 +11,27 @@ pub fn main() !void {
711
}
812
const alloc = gpa.allocator();
913

10-
if (main0(alloc) catch |err| switch (err) {
11-
error.BrokenPipe, error.WriteFailed => {
12-
std.process.exit(0);
13-
},
14-
else => return err,
15-
}) |fatal| {
16-
defer fatal.deinit(alloc);
17-
try fatal.print();
18-
flushPipe() catch |err| switch (err) {
19-
error.BrokenPipe, error.WriteFailed => std.process.exit(0),
20-
else => return err,
21-
};
22-
std.process.exit(1);
23-
}
24-
flushPipe() catch |err| switch (err) {
25-
error.BrokenPipe, error.WriteFailed => std.process.exit(0),
14+
var exit: u8 = 0;
15+
const fatal = main0(alloc) catch |err| switch (err) {
16+
error.BrokenPipe, error.WriteFailed => null,
2617
else => return err,
2718
};
28-
std.process.exit(0);
19+
if (fatal) |value| {
20+
defer value.deinit(alloc);
21+
try value.print();
22+
exit = 1;
23+
}
24+
25+
util.stdout.flush() catch {};
26+
util.stderr.flush() catch {};
27+
std.process.exit(exit);
2928
}
3029

30+
//
31+
// main0
3132
// Run the CLI and return a printable failure when the command should fail.
33+
//
34+
3235
fn main0(alloc: std.mem.Allocator) !?failure.Failure {
3336
// timer
3437
var total = try std.time.Timer.start();
@@ -102,27 +105,27 @@ fn main0(alloc: std.mem.Allocator) !?failure.Failure {
102105
// input => data rows
103106
var data = load(alloc, config, input) catch |err| {
104107
if (err == error.SqliteInvalidTable) {
105-
const path = config.filename orelse return err;
106-
var db = try sqlite.Sqlite.init(alloc, path);
108+
var db = try sqlite.Sqlite.init(alloc, config.filename.?);
107109
defer db.deinit();
108110
return try failure.Failure.fromSqliteTableError(alloc, config.table, db.tables);
109111
}
110112
return failure.Failure.fromError(err) orelse return err;
111113
};
112-
errdefer data.deinit(alloc);
113114

114115
// plug data headers into config, for validation
115116
config.bind(alloc, data.headers()) catch |err| {
117+
const fatal = try failure.Failure.fromTableError(alloc, err, data.headers());
118+
data.deinit(alloc);
116119
config.deinit(alloc);
117-
return try failure.Failure.fromTableError(alloc, err, data.headers());
120+
return fatal;
118121
};
119122

120123
//
121124
// data => table
122125
//
123126

127+
// Hand off both config and data here; Table.init owns cleanup from this point on.
124128
const table = try Table.init(alloc, config, data);
125-
data = .{ .rows = &.{} };
126129
defer table.deinit();
127130
util.benchmark("table.init", timer.read());
128131

@@ -141,21 +144,23 @@ fn main0(alloc: std.mem.Allocator) !?failure.Failure {
141144
return null;
142145
}
143146

147+
//
148+
// loading input data
149+
//
150+
144151
// Load the configured input into table data, dispatching by detected format.
145152
fn load(alloc: std.mem.Allocator, config: types.Config, input: std.fs.File) !Data {
146153
// typically we read the whole file into memory for processing. That won't
147-
// work if we are using `sqlite3`, though
148-
if (config.filename) |path| {
149-
if (detect.formatFromFilename(path) == .sqlite) {
150-
var db = try sqlite.Sqlite.init(alloc, path);
151-
defer db.deinit();
152-
return try db.load(config.table);
153-
}
154+
// work if we are using `sqlite3`, though.
155+
if (try detect.isSqliteFile(alloc, config.filename, input)) {
156+
var db = try sqlite.Sqlite.init(alloc, config.filename.?);
157+
defer db.deinit();
158+
return try db.load(config.table);
154159
}
155160

156-
const input_bytes = try input.readToEndAlloc(alloc, std.math.maxInt(usize));
157-
defer alloc.free(input_bytes);
158-
return try loadBytes(alloc, config, input_bytes);
161+
const bytes = try input.readToEndAlloc(alloc, std.math.maxInt(usize));
162+
defer alloc.free(bytes);
163+
return try loadBytes(alloc, config, bytes);
159164
}
160165

161166
// Load in-memory bytes into table data using the existing text format loaders.
@@ -166,22 +171,23 @@ fn loadBytes(alloc: std.mem.Allocator, config: types.Config, bytes_in: []const u
166171
bytes = bytes[3..];
167172
}
168173

174+
// sqlite3 and stray --table
169175
const format = try detect.detectFormat(alloc, config.filename, bytes);
170-
171-
// sqlite3 concerns
172176
if (format == .sqlite) return error.SqliteRequiresFile;
173177
if (config.table.len > 0) return error.SqliteTableRequiresSqlite;
178+
179+
// json
174180
if (format == .json) return try json.load(alloc, bytes);
175181

182+
// csv (our default)
176183
var delimiter = config.delimiter;
177184
if (delimiter == 0) delimiter = sniffer.sniff(bytes) orelse ',';
178185
return try csv.load(alloc, bytes, delimiter);
179186
}
180187

181-
fn flushPipe() anyerror!void {
182-
try util.stdout.flush();
183-
try util.stderr.flush();
184-
}
188+
//
189+
// rendering
190+
//
185191

186192
fn renderToPager(alloc: std.mem.Allocator, config: types.Config, table: *Table) !void {
187193
const cmd = std.posix.getenv("PAGER") orelse "less";

src/peek.zig

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,7 @@ fn columnStats(alloc: std.mem.Allocator, table: *Table, c: usize, t: ColumnType)
129129

130130
switch (t) {
131131
.int => {
132-
var values: std.ArrayList(i64) = .empty;
133-
defer values.deinit(alloc);
134-
for (fields.items) |f| try values.append(alloc, try std.fmt.parseInt(i64, f, 10));
135-
const mm = util.minmax(i64, values.items);
132+
const mm = try parseIntMinmax(alloc, fields.items);
136133
return .{
137134
.fill = fill,
138135
.uniq = uniq,
@@ -189,6 +186,18 @@ fn fmtFill(alloc: std.mem.Allocator, fill: usize, nrows: usize) ![]u8 {
189186
return std.fmt.allocPrint(alloc, "{d}%", .{pct});
190187
}
191188

189+
// Parse integer fields into i64 stats, returning null when any value overflows i64.
190+
fn parseIntMinmax(alloc: std.mem.Allocator, fields: []const Field) !?struct { min: i64, max: i64 } {
191+
var values: std.ArrayList(i64) = .empty;
192+
defer values.deinit(alloc);
193+
for (fields) |field| {
194+
const value = std.fmt.parseInt(i64, field, 10) catch continue;
195+
try values.append(alloc, value);
196+
}
197+
if (util.minmax(i64, values.items)) |mm| return .{ .min = mm.min, .max = mm.max };
198+
return null;
199+
}
200+
192201
fn fmtIntValue(alloc: std.mem.Allocator, value: ?i64) ![]u8 {
193202
const num = value orelse return alloc.dupe(u8, dash);
194203
var buf: [32]u8 = undefined;
@@ -265,6 +274,19 @@ test "buildStatsTable reports basic visible stats" {
265274
try test_support.expectEqualRows(&.{ "city", "string", "66%", "2", "6 chars", "7 chars" }, stats.row(2));
266275
}
267276

277+
test "buildStatsTable tolerates oversized ints in peek stats" {
278+
const table = try Table.initCsv(testing.allocator, .{}, "id\n99999999999999999999\n");
279+
defer table.deinit();
280+
281+
const stats = try buildStatsTable(testing.allocator, table);
282+
defer stats.deinit();
283+
284+
try testing.expectEqualStrings("id", stats.row(0)[0]);
285+
try testing.expectEqualStrings("int", stats.row(0)[1]);
286+
try testing.expectEqualStrings("—", stats.row(0)[4]);
287+
try testing.expectEqualStrings("—", stats.row(0)[5]);
288+
}
289+
268290
test "buildStatsTable truncates float min and max to three digits" {
269291
const table = try Table.initCsv(testing.allocator, .{}, "score\n1.23456\n20.9999\n");
270292
defer table.deinit();

src/sqlite.zig

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ pub const Sqlite = struct {
4747
// --table
4848
if (selected_table.len > 0) {
4949
for (self.tables) |table| {
50-
if (std.mem.eql(u8, table, selected_table)) return self.alloc.dupe(u8, table);
50+
if (std.ascii.eqlIgnoreCase(table, selected_table)) {
51+
return self.alloc.dupe(u8, table);
52+
}
5153
}
5254
return error.SqliteInvalidTable;
5355
}

src/util.zig

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ pub fn fileExists(path: []const u8) bool {
2222
return true;
2323
}
2424

25+
// Report whether the file handle supports seeking to the current position.
26+
pub fn isSeekable(file: std.fs.File) bool {
27+
const pos = file.getPos() catch return false;
28+
file.seekTo(pos) catch return false;
29+
return true;
30+
}
31+
2532
// read a single byte from an fd
2633
pub fn readByte(fd: std.posix.fd_t) !u8 {
2734
var buf: [1]u8 = undefined;
@@ -197,25 +204,6 @@ pub fn upperAscii(dest: []u8, src: []const u8) []const u8 {
197204
return dest[0..src.len];
198205
}
199206

200-
// write text truncated to width, using an ellipsis when needed
201-
pub fn truncate(writer: *std.Io.Writer, text: []const u8, stop: usize) !void {
202-
if (stop == 0) return;
203-
204-
var it = std.unicode.Utf8View.init(text) catch {
205-
try writer.writeAll(text[0..@min(text.len, stop)]);
206-
return;
207-
};
208-
var iter = it.iterator();
209-
210-
var used: usize = 0;
211-
while (iter.nextCodepointSlice()) |cp_slice| {
212-
if (used + 1 >= stop) break;
213-
try writer.writeAll(cp_slice);
214-
used += 1;
215-
}
216-
try writer.writeAll("…");
217-
}
218-
219207
//
220208
// misc
221209
//
@@ -277,6 +265,22 @@ test "minmax handles floats" {
277265
try testing.expectEqual(@as(f64, 9.25), got.max);
278266
}
279267

268+
test "isSeekable handles file and pipe" {
269+
var tmp = std.testing.tmpDir(.{});
270+
defer tmp.cleanup();
271+
272+
const file = try tmp.dir.createFile("seekable.txt", .{ .read = true });
273+
defer file.close();
274+
try testing.expect(isSeekable(file));
275+
276+
const pipe_fds = try std.posix.pipe();
277+
defer std.posix.close(pipe_fds[0]);
278+
defer std.posix.close(pipe_fds[1]);
279+
280+
const pipe_file = std.fs.File{ .handle = pipe_fds[0] };
281+
try testing.expect(!isSeekable(pipe_file));
282+
}
283+
280284
test "plural returns the right form" {
281285
try testing.expectEqualStrings("row", plural(1, "row"));
282286
try testing.expectEqualStrings("rows", plural(2, "row"));
@@ -400,22 +404,6 @@ test "lowerAscii" {
400404
try testing.expectEqualStrings("ABC123", upperAscii(&buf, "AbC123"));
401405
}
402406

403-
test "truncate" {
404-
var buf: [256]u8 = undefined;
405-
var writer = std.Io.Writer.fixed(&buf);
406-
407-
try truncate(&writer, "this is too long", 8);
408-
try testing.expectEqualStrings("this is…", writer.buffered());
409-
writer.end = 0;
410-
411-
try truncate(&writer, "éééé", 3);
412-
try testing.expectEqualStrings("éé…", writer.buffered());
413-
writer.end = 0;
414-
415-
try truncate(&writer, "abcdef", 0);
416-
try testing.expectEqualStrings("", writer.buffered());
417-
}
418-
419407
test "sum" {
420408
try testing.expectEqual(@as(usize, 10), sum(usize, &.{ 1, 2, 3, 4 }));
421409
}

testdata/smoke.bats

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,19 @@ setup() {
184184
}
185185

186186
@test "renders named sqlite table" {
187-
run "$TENNIS_BIN" --color=off --width 80 --table players "$REPO_ROOT/testdata/sqlite-single.db"
187+
run "$TENNIS_BIN" --color=off --width 80 --table PLAYERS "$REPO_ROOT/testdata/sqlite-single.db"
188+
[ "$status" -eq 0 ]
189+
[[ "$output" == *"name"* ]]
190+
[[ "$output" == *"score"* ]]
191+
[[ "$output" == *"alice"* ]]
192+
[[ "$output" == *"cara"* ]]
193+
}
194+
195+
@test "detects sqlite by magic bytes for unknown extensions" {
196+
local db
197+
db="$BATS_TEST_TMPDIR/sqlite-single.bin"
198+
cp "$REPO_ROOT/testdata/sqlite-single.db" "$db"
199+
run "$TENNIS_BIN" --color=off --width 80 "$db"
188200
[ "$status" -eq 0 ]
189201
[[ "$output" == *"name"* ]]
190202
[[ "$output" == *"score"* ]]

0 commit comments

Comments
 (0)