Compare commits
3 Commits
1ed0d9667d
...
2f1a197a91
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f1a197a91 | |||
| d6621f008b | |||
| 9bb1977805 |
+2
-8
@@ -1,8 +1,2 @@
|
|||||||
# Zig build artifacts
|
/.zig-cache/
|
||||||
.zig-cache/
|
/zig-out/
|
||||||
zig-out/
|
|
||||||
|
|
||||||
# Local editor/OS noise
|
|
||||||
*.swp
|
|
||||||
*~
|
|
||||||
.DS_Store
|
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
`pick` is a tiny compiled Zig CLI for selecting TSV fields from stdin. It exists
|
|
||||||
as an agent-safe replacement for the safe subset of `awk '{print $N}'` and
|
|
||||||
`cut -f` when allowlist policy needs argv-obvious behavior.
|
|
||||||
|
|
||||||
## How to build
|
|
||||||
|
|
||||||
```sh
|
|
||||||
zig build
|
|
||||||
```
|
|
||||||
|
|
||||||
## How to test
|
|
||||||
|
|
||||||
```sh
|
|
||||||
zig build test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Constraints
|
|
||||||
|
|
||||||
- One source file: `pick.zig`.
|
|
||||||
- No runtime dependencies, no shell execution, no filesystem reads/writes in the
|
|
||||||
tool.
|
|
||||||
- stdout is TSV/plain records only; diagnostics go to stderr.
|
|
||||||
- No regex, expressions, arithmetic, sorting, paging, JSON, color, or hidden
|
|
||||||
state.
|
|
||||||
- This is an inspection primitive. Do not add mutation or policy workflow.
|
|
||||||
|
|
||||||
## Requirement Governance
|
|
||||||
|
|
||||||
`REQUIREMENTS.md` is the requirement authority for this repository and is checked
|
|
||||||
with `redgate`. Before implementation work, read the applicable requirement row;
|
|
||||||
if none covers the durable behavior, update `REQUIREMENTS.md` first or stop and
|
|
||||||
ask for authority. When adding or changing rows, choose the ring by product
|
|
||||||
foundation, not implementation size: lower rings capture stable kernel
|
|
||||||
obligations and core contracts; higher rings compose on lower-ring obligations
|
|
||||||
and must not weaken, redefine, or bypass them. If one row mixes foundational and
|
|
||||||
optional behavior, split it before implementation.
|
|
||||||
|
|
||||||
If a change alters durable product obligations, acceptance behavior,
|
|
||||||
safety/recovery behavior, public interfaces, or verification duties, update
|
|
||||||
`REQUIREMENTS.md` in the same slice and run the relevant `redgate` checks. For
|
|
||||||
behavior intended to stick, write or update a failing BDD/TDD test, contract
|
|
||||||
test, or executable proof before implementation code; run it and record the RED
|
|
||||||
result. Implement only after the requirement row and RED proof exist. If RED
|
|
||||||
proof would be fake, unsafe, or disproportionate, state that exception before
|
|
||||||
implementation and use the strongest cheaper check.
|
|
||||||
|
|
||||||
Before final handoff, compare the user request and actual diff against
|
|
||||||
`REQUIREMENTS.md`. If there is no requirement impact, the handoff must include
|
|
||||||
exactly `REQUIREMENT IMPACT: none - <specific reason>`. If code, tests, or docs
|
|
||||||
changed but `REQUIREMENTS.md` did not, explicitly justify why no requirement row
|
|
||||||
changed.
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# pick — migrated
|
||||||
|
|
||||||
|
The canonical `pick` agent tool moved to `/opt/repositories/toolset`.
|
||||||
|
|
||||||
|
This repository no longer builds or releases an executable. Its Git history is the migration record; source, requirements, tests, installation, and release authority now live in toolset.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /opt/repositories/toolset
|
||||||
|
zig build install-text -p "$HOME/.ink"
|
||||||
|
```
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# REQUIREMENTS
|
|
||||||
|
|
||||||
## pick
|
|
||||||
0 001 `pick` must be a compiled single-file Zig CLI with no runtime dependencies beyond the OS. [core]
|
|
||||||
0 002 `pick` must read TSV records from stdin and write selected TSV fields to stdout only. [stream]
|
|
||||||
0 003 `pick` must have no filesystem mutation, network access, shell execution, expression language, regex evaluation, hidden config, prompts, color, pager, JSON default, or daemon behavior. [agent-safe]
|
|
||||||
1 004 `pick FIELD...` must select 1-based numeric fields from headerless TSV input and preserve input record order.
|
|
||||||
1 005 `pick --header FIELD...` must resolve field names from the first TSV record, emit the selected header, and then emit selected fields from subsequent records.
|
|
||||||
1 006 Missing fields must emit empty TSV fields rather than failing a pipeline.
|
|
||||||
1 007 Usage errors must exit nonzero and write diagnostics to stderr, never stdout.
|
|
||||||
1 008 `zig build test` must prove numeric selection, header selection, missing fields, help/error stdout-stderr separation, CRLF input tolerance, invalid field rejection, and one forbidden near miss. [verification]
|
|
||||||
1 009 Project smoke tests must run under POSIX `sh` and exercise `pick` through pipe-oriented stdin/stdout/stderr behavior without leaving required generated artifacts. [posix]
|
|
||||||
1 010 `pick` must process stdin incrementally for pipeline-sized streams and must not print noisy diagnostics when downstream consumers close stdout early. [pipeline]
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
pub fn build(b: *std.Build) void {
|
|
||||||
const target = b.standardTargetOptions(.{});
|
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
|
||||||
const exe = b.addExecutable(.{ .name = "pick", .root_module = b.createModule(.{ .root_source_file = b.path("pick.zig"), .target = target, .optimize = optimize }) });
|
|
||||||
b.installArtifact(exe);
|
|
||||||
const run_cmd = b.addRunArtifact(exe);
|
|
||||||
run_cmd.step.dependOn(b.getInstallStep());
|
|
||||||
if (b.args) |args| run_cmd.addArgs(args);
|
|
||||||
b.step("run", "Run pick").dependOn(&run_cmd.step);
|
|
||||||
const smoke = b.addSystemCommand(&.{ "sh", "smoke.sh", "zig-out/bin/pick" });
|
|
||||||
smoke.setCwd(b.path("."));
|
|
||||||
smoke.step.dependOn(b.getInstallStep());
|
|
||||||
b.step("test", "Run smoke test").dependOn(&smoke.step);
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
fn usage(file: std.Io.File, io: std.Io) !void {
|
|
||||||
var buf: [1024]u8 = undefined;
|
|
||||||
var w = file.writer(io, &buf);
|
|
||||||
try w.interface.writeAll(
|
|
||||||
\\usage: pick [--header] FIELD...
|
|
||||||
\\
|
|
||||||
\\Select tab-separated fields from stdin. FIELD is a 1-based column number,
|
|
||||||
\\or a header name when --header is set. Output is TSV/plain records only.
|
|
||||||
\\
|
|
||||||
\\options:
|
|
||||||
\\ --header resolve FIELD names from the first input record
|
|
||||||
\\ --help show this help on stdout and exit 0
|
|
||||||
\\
|
|
||||||
\\stdout: selected records only
|
|
||||||
\\stderr: usage errors and diagnostics only
|
|
||||||
\\exits: 0 on success, nonzero on invalid fields or I/O failure
|
|
||||||
\\
|
|
||||||
\\No regex, expressions, arithmetic, shell, mutation, files, JSON, color,
|
|
||||||
\\prompts, config, network, daemon behavior, or hidden state.
|
|
||||||
\\
|
|
||||||
\\examples:
|
|
||||||
\\ printf 'path\\tsize\\nREADME\\t120\\n' | pick --header path size
|
|
||||||
\\ printf 'a\\tb\\n' | pick 2
|
|
||||||
\\
|
|
||||||
);
|
|
||||||
try w.interface.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn die(io: std.Io, comptime fmt: []const u8, args: anytype) noreturn {
|
|
||||||
var buf: [512]u8 = undefined;
|
|
||||||
var w = std.Io.File.stderr().writer(io, &buf);
|
|
||||||
w.interface.print(fmt, args) catch {};
|
|
||||||
w.interface.flush() catch {};
|
|
||||||
std.process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fieldAt(line: []const u8, index: usize) []const u8 {
|
|
||||||
var it = std.mem.splitScalar(u8, line, '\t');
|
|
||||||
var i: usize = 0;
|
|
||||||
while (it.next()) |field| : (i += 1) {
|
|
||||||
if (i == index) return field;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stripRecordNewlineCr(raw: []const u8) []const u8 {
|
|
||||||
if (raw.len > 0 and raw[raw.len - 1] == '\r') return raw[0 .. raw.len - 1];
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn headerIndex(header: []const u8, name: []const u8, io: std.Io) usize {
|
|
||||||
var it = std.mem.splitScalar(u8, header, '\t');
|
|
||||||
var i: usize = 0;
|
|
||||||
while (it.next()) |field| : (i += 1) {
|
|
||||||
if (std.mem.eql(u8, field, name)) return i;
|
|
||||||
}
|
|
||||||
die(io, "pick: header not found: {s}\n", .{name});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeAll(out: *std.Io.File.Writer, bytes: []const u8) std.Io.File.Writer.Error!void {
|
|
||||||
out.interface.writeAll(bytes) catch |err| switch (err) {
|
|
||||||
error.WriteFailed => return out.err.?,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeByte(out: *std.Io.File.Writer, byte: u8) std.Io.File.Writer.Error!void {
|
|
||||||
out.interface.writeByte(byte) catch |err| switch (err) {
|
|
||||||
error.WriteFailed => return out.err.?,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writePicked(out: *std.Io.File.Writer, line: []const u8, cols: []const usize) !void {
|
|
||||||
for (cols, 0..) |col, i| {
|
|
||||||
if (i > 0) try writeByte(out, '\t');
|
|
||||||
try writeAll(out, fieldAt(line, col));
|
|
||||||
}
|
|
||||||
try writeByte(out, '\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handleLine(
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
io: std.Io,
|
|
||||||
has_header: bool,
|
|
||||||
names: []const []const u8,
|
|
||||||
cols: *std.ArrayList(usize),
|
|
||||||
header_done: *bool,
|
|
||||||
out: *std.Io.File.Writer,
|
|
||||||
raw: []const u8,
|
|
||||||
) !void {
|
|
||||||
const line = stripRecordNewlineCr(raw);
|
|
||||||
if (has_header and !header_done.*) {
|
|
||||||
for (names) |name| try cols.append(allocator, headerIndex(line, name, io));
|
|
||||||
try writePicked(out, line, cols.items);
|
|
||||||
header_done.* = true;
|
|
||||||
} else {
|
|
||||||
try writePicked(out, line, cols.items);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pickStream(
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
io: std.Io,
|
|
||||||
has_header: bool,
|
|
||||||
names: []const []const u8,
|
|
||||||
cols: *std.ArrayList(usize),
|
|
||||||
) !void {
|
|
||||||
var out_buf: [8192]u8 = undefined;
|
|
||||||
var out = std.Io.File.stdout().writer(io, &out_buf);
|
|
||||||
var carry = std.ArrayList(u8).empty;
|
|
||||||
defer carry.deinit(allocator);
|
|
||||||
|
|
||||||
var header_done = !has_header;
|
|
||||||
var buf: [8192]u8 = undefined;
|
|
||||||
while (true) {
|
|
||||||
const n = std.posix.read(std.posix.STDIN_FILENO, &buf) catch |err| die(io, "pick: stdin: {s}\n", .{@errorName(err)});
|
|
||||||
if (n == 0) break;
|
|
||||||
|
|
||||||
var start: usize = 0;
|
|
||||||
for (buf[0..n], 0..) |byte, i| {
|
|
||||||
if (byte != '\n') continue;
|
|
||||||
if (carry.items.len == 0) {
|
|
||||||
try handleLine(allocator, io, has_header, names, cols, &header_done, &out, buf[start..i]);
|
|
||||||
} else {
|
|
||||||
try carry.appendSlice(allocator, buf[start..i]);
|
|
||||||
try handleLine(allocator, io, has_header, names, cols, &header_done, &out, carry.items);
|
|
||||||
carry.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
start = i + 1;
|
|
||||||
}
|
|
||||||
if (start < n) try carry.appendSlice(allocator, buf[start..n]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (carry.items.len > 0) {
|
|
||||||
try handleLine(allocator, io, has_header, names, cols, &header_done, &out, carry.items);
|
|
||||||
} else if (has_header and !header_done) {
|
|
||||||
try handleLine(allocator, io, has_header, names, cols, &header_done, &out, "");
|
|
||||||
}
|
|
||||||
try out.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn main(init: std.process.Init) !void {
|
|
||||||
const allocator = init.gpa;
|
|
||||||
var args = try std.process.Args.Iterator.initAllocator(init.minimal.args, allocator);
|
|
||||||
defer args.deinit();
|
|
||||||
_ = args.skip();
|
|
||||||
|
|
||||||
var has_header = false;
|
|
||||||
var names = std.ArrayList([]const u8).empty;
|
|
||||||
defer names.deinit(allocator);
|
|
||||||
while (args.next()) |arg| {
|
|
||||||
if (std.mem.eql(u8, arg, "--help")) {
|
|
||||||
try usage(std.Io.File.stdout(), init.io);
|
|
||||||
return;
|
|
||||||
} else if (std.mem.eql(u8, arg, "--header")) {
|
|
||||||
has_header = true;
|
|
||||||
} else {
|
|
||||||
try names.append(allocator, arg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (names.items.len == 0) {
|
|
||||||
try usage(std.Io.File.stderr(), init.io);
|
|
||||||
std.process.exit(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
var cols = std.ArrayList(usize).empty;
|
|
||||||
defer cols.deinit(allocator);
|
|
||||||
if (!has_header) {
|
|
||||||
for (names.items) |name| {
|
|
||||||
const n = std.fmt.parseInt(usize, name, 10) catch die(init.io, "pick: invalid field: {s}\n", .{name});
|
|
||||||
if (n == 0) die(init.io, "pick: fields are 1-based\n", .{});
|
|
||||||
try cols.append(allocator, n - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pickStream(allocator, init.io, has_header, names.items, &cols) catch |err| switch (err) {
|
|
||||||
error.BrokenPipe => return,
|
|
||||||
else => die(init.io, "pick: output: {s}\n", .{@errorName(err)}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
tool=${1:-zig-out/bin/pick}
|
|
||||||
tmp=${TMPDIR:-.zig-cache}/pick-smoke.$$
|
|
||||||
mkdir -p "$tmp"
|
|
||||||
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
|
||||||
|
|
||||||
# req: pick/008
|
|
||||||
# req: pick/009
|
|
||||||
# req: pick/010
|
|
||||||
|
|
||||||
assert_eq() {
|
|
||||||
want=$1
|
|
||||||
got=$2
|
|
||||||
cmp "$want" "$got"
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_empty() {
|
|
||||||
file=$1
|
|
||||||
if [ -s "$file" ]; then
|
|
||||||
echo "pick: expected empty file: $file" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_fails() {
|
|
||||||
if "$@"; then
|
|
||||||
echo "pick: command unexpectedly succeeded: $*" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Numeric field selection preserves records and stdout contains only selected TSV.
|
|
||||||
printf 'a\tb\tc\n1\t2\t3\n' | "$tool" 2 > "$tmp/pick-one.out"
|
|
||||||
printf 'b\n2\n' > "$tmp/pick-one.want"
|
|
||||||
assert_eq "$tmp/pick-one.want" "$tmp/pick-one.out"
|
|
||||||
|
|
||||||
# Header selection emits the selected header and selected data columns.
|
|
||||||
printf 'path\tsize\tkind\na\t10\tfile\n' | "$tool" --header path size > "$tmp/pick-header.out"
|
|
||||||
printf 'path\tsize\na\t10\n' > "$tmp/pick-header.want"
|
|
||||||
assert_eq "$tmp/pick-header.want" "$tmp/pick-header.out"
|
|
||||||
|
|
||||||
# Missing fields are empty fields, not pipeline failures.
|
|
||||||
printf 'a\tb\n1\n' | "$tool" 1 2 > "$tmp/pick-missing.out"
|
|
||||||
printf 'a\tb\n1\t\n' > "$tmp/pick-missing.want"
|
|
||||||
assert_eq "$tmp/pick-missing.want" "$tmp/pick-missing.out"
|
|
||||||
|
|
||||||
# CRLF input is accepted and normalized to LF records.
|
|
||||||
printf 'a\tb\r\n1\t2\r\n' | "$tool" 2 > "$tmp/pick-crlf.out"
|
|
||||||
printf 'b\n2\n' > "$tmp/pick-crlf.want"
|
|
||||||
assert_eq "$tmp/pick-crlf.want" "$tmp/pick-crlf.out"
|
|
||||||
|
|
||||||
# A record longer than the internal read buffer still streams correctly across
|
|
||||||
# chunk boundaries without whole-input buffering.
|
|
||||||
: > "$tmp/pick-long.in"
|
|
||||||
i=0
|
|
||||||
while [ "$i" -lt 9000 ]; do
|
|
||||||
printf x >> "$tmp/pick-long.in"
|
|
||||||
i=$((i + 1))
|
|
||||||
done
|
|
||||||
printf '\tb\n' >> "$tmp/pick-long.in"
|
|
||||||
"$tool" 2 < "$tmp/pick-long.in" > "$tmp/pick-long.out"
|
|
||||||
printf 'b\n' > "$tmp/pick-long.want"
|
|
||||||
assert_eq "$tmp/pick-long.want" "$tmp/pick-long.out"
|
|
||||||
|
|
||||||
# Help is stdout-only and successful.
|
|
||||||
"$tool" --help > "$tmp/pick-help.out" 2> "$tmp/pick-help.err"
|
|
||||||
grep -q '^usage: pick' "$tmp/pick-help.out"
|
|
||||||
assert_empty "$tmp/pick-help.err"
|
|
||||||
|
|
||||||
# Usage errors are stderr-only and nonzero.
|
|
||||||
assert_fails "$tool" > "$tmp/pick-noargs.out" 2> "$tmp/pick-noargs.err"
|
|
||||||
assert_empty "$tmp/pick-noargs.out"
|
|
||||||
grep -q '^usage: pick' "$tmp/pick-noargs.err"
|
|
||||||
|
|
||||||
assert_fails "$tool" 0 > "$tmp/pick-zero.out" 2> "$tmp/pick-zero.err"
|
|
||||||
assert_empty "$tmp/pick-zero.out"
|
|
||||||
grep -q 'fields are 1-based' "$tmp/pick-zero.err"
|
|
||||||
|
|
||||||
assert_fails "$tool" nope > "$tmp/pick-invalid.out" 2> "$tmp/pick-invalid.err"
|
|
||||||
assert_empty "$tmp/pick-invalid.out"
|
|
||||||
grep -q 'invalid field' "$tmp/pick-invalid.err"
|
|
||||||
|
|
||||||
# Forbidden near miss: option-looking regex/expression input is rejected as data,
|
|
||||||
# not interpreted as another command language.
|
|
||||||
assert_fails "$tool" --regex '.*' > "$tmp/pick-regex.out" 2> "$tmp/pick-regex.err"
|
|
||||||
assert_empty "$tmp/pick-regex.out"
|
|
||||||
grep -q 'invalid field' "$tmp/pick-regex.err"
|
|
||||||
|
|
||||||
# Head-style consumers may close stdout early; that must not pollute stderr.
|
|
||||||
i=0
|
|
||||||
{
|
|
||||||
while [ "$i" -lt 20000 ]; do
|
|
||||||
printf 'a\tb\n'
|
|
||||||
i=$((i + 1))
|
|
||||||
done
|
|
||||||
} | "$tool" 1 2> "$tmp/pick-head.err" | sed 1q > "$tmp/pick-head.out"
|
|
||||||
printf 'a\n' > "$tmp/pick-head.want"
|
|
||||||
assert_eq "$tmp/pick-head.want" "$tmp/pick-head.out"
|
|
||||||
assert_empty "$tmp/pick-head.err"
|
|
||||||
Reference in New Issue
Block a user