diff --git a/man/man1/recs-annotate.1 b/man/man1/recs-annotate.1 index f3d96c40..dd36394c 100644 --- a/man/man1/recs-annotate.1 +++ b/man/man1/recs-annotate.1 @@ -1,4 +1,4 @@ -.TH RECS\-ANNOTATE 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-ANNOTATE 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-annotate \- Evaluate an expression on each record and cache the resulting changes by key grouping diff --git a/man/man1/recs-assert.1 b/man/man1/recs-assert.1 index 843e8ae1..505e97e0 100644 --- a/man/man1/recs-assert.1 +++ b/man/man1/recs-assert.1 @@ -1,4 +1,4 @@ -.TH RECS\-ASSERT 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-ASSERT 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-assert \- Asserts that every record in the stream must pass the given expression diff --git a/man/man1/recs-chain.1 b/man/man1/recs-chain.1 index f99be0f4..34a75de6 100644 --- a/man/man1/recs-chain.1 +++ b/man/man1/recs-chain.1 @@ -1,4 +1,4 @@ -.TH RECS\-CHAIN 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-CHAIN 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-chain \- Creates an in\-memory chain of recs operations diff --git a/man/man1/recs-collate.1 b/man/man1/recs-collate.1 index b31c36df..bccd5987 100644 --- a/man/man1/recs-collate.1 +++ b/man/man1/recs-collate.1 @@ -1,4 +1,4 @@ -.TH RECS\-COLLATE 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-COLLATE 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-collate \- Take records, grouped together by \-\-keys, and compute statistics (like average, count, sum, concat, etc diff --git a/man/man1/recs-decollate.1 b/man/man1/recs-decollate.1 index 688e66dd..e1f2ecf2 100644 --- a/man/man1/recs-decollate.1 +++ b/man/man1/recs-decollate.1 @@ -1,4 +1,4 @@ -.TH RECS\-DECOLLATE 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-DECOLLATE 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-decollate \- Reverse of collate: takes a single record and produces multiple records using deaggregators diff --git a/man/man1/recs-delta.1 b/man/man1/recs-delta.1 index 44d8487c..610aac19 100644 --- a/man/man1/recs-delta.1 +++ b/man/man1/recs-delta.1 @@ -1,4 +1,4 @@ -.TH RECS\-DELTA 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-DELTA 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-delta \- Transforms absolute values into deltas between adjacent records diff --git a/man/man1/recs-eval.1 b/man/man1/recs-eval.1 index b7cf1cf0..fef27ee2 100644 --- a/man/man1/recs-eval.1 +++ b/man/man1/recs-eval.1 @@ -1,4 +1,4 @@ -.TH RECS\-EVAL 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-EVAL 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-eval \- Evaluate an expression on each record and print the result as a line of text diff --git a/man/man1/recs-expandjson.1 b/man/man1/recs-expandjson.1 index 532d4691..bca6264f 100644 --- a/man/man1/recs-expandjson.1 +++ b/man/man1/recs-expandjson.1 @@ -1,4 +1,4 @@ -.TH RECS\-EXPANDJSON 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-EXPANDJSON 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-expandjson \- Expand JSON strings embedded in record fields into actual JSON values diff --git a/man/man1/recs-flatten.1 b/man/man1/recs-flatten.1 index 2a0501e9..347d2226 100644 --- a/man/man1/recs-flatten.1 +++ b/man/man1/recs-flatten.1 @@ -1,4 +1,4 @@ -.TH RECS\-FLATTEN 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FLATTEN 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-flatten \- Flatten nested hash/array structures in records into top\-level fields diff --git a/man/man1/recs-fromapache.1 b/man/man1/recs-fromapache.1 index 650fbe62..5a75ac26 100644 --- a/man/man1/recs-fromapache.1 +++ b/man/man1/recs-fromapache.1 @@ -1,4 +1,4 @@ -.TH RECS\-FROMAPACHE 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FROMAPACHE 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-fromapache \- Each line of input (or lines of ) is parsed to produce an output record from Apache access logs diff --git a/man/man1/recs-fromatomfeed.1 b/man/man1/recs-fromatomfeed.1 index ba9b9d38..b9a11bae 100644 --- a/man/man1/recs-fromatomfeed.1 +++ b/man/man1/recs-fromatomfeed.1 @@ -1,4 +1,4 @@ -.TH RECS\-FROMATOMFEED 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FROMATOMFEED 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-fromatomfeed \- Produce records from atom feed entries diff --git a/man/man1/recs-fromcsv.1 b/man/man1/recs-fromcsv.1 index 0e659fb0..3c845ee7 100644 --- a/man/man1/recs-fromcsv.1 +++ b/man/man1/recs-fromcsv.1 @@ -1,4 +1,4 @@ -.TH RECS\-FROMCSV 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FROMCSV 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-fromcsv \- Each line of input (or lines of ) is split on commas to produce an output record diff --git a/man/man1/recs-fromdb.1 b/man/man1/recs-fromdb.1 index ca00119c..08acce5f 100644 --- a/man/man1/recs-fromdb.1 +++ b/man/man1/recs-fromdb.1 @@ -1,4 +1,4 @@ -.TH RECS\-FROMDB 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FROMDB 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-fromdb \- Execute a select statement on a database and create a record stream from the results diff --git a/man/man1/recs-fromjsonarray.1 b/man/man1/recs-fromjsonarray.1 index 2f838ad3..4b5c7005 100644 --- a/man/man1/recs-fromjsonarray.1 +++ b/man/man1/recs-fromjsonarray.1 @@ -1,4 +1,4 @@ -.TH RECS\-FROMJSONARRAY 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FROMJSONARRAY 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-fromjsonarray \- Import JSON objects from within a JSON array diff --git a/man/man1/recs-fromkv.1 b/man/man1/recs-fromkv.1 index dbb5fcd6..92c78886 100644 --- a/man/man1/recs-fromkv.1 +++ b/man/man1/recs-fromkv.1 @@ -1,4 +1,4 @@ -.TH RECS\-FROMKV 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FROMKV 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-fromkv \- Records are generated from character input with the form " diff --git a/man/man1/recs-frommongo.1 b/man/man1/recs-frommongo.1 index e5ef4a56..67987ed7 100644 --- a/man/man1/recs-frommongo.1 +++ b/man/man1/recs-frommongo.1 @@ -1,4 +1,4 @@ -.TH RECS\-FROMMONGO 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FROMMONGO 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-frommongo \- Generate records from a MongoDB query diff --git a/man/man1/recs-frommultire.1 b/man/man1/recs-frommultire.1 index d792b3d8..40f1badc 100644 --- a/man/man1/recs-frommultire.1 +++ b/man/man1/recs-frommultire.1 @@ -1,4 +1,4 @@ -.TH RECS\-FROMMULTIRE 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FROMMULTIRE 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-frommultire \- Match multiple regexes against each line of input (or lines of ) diff --git a/man/man1/recs-fromps.1 b/man/man1/recs-fromps.1 index b771359b..f1650d31 100644 --- a/man/man1/recs-fromps.1 +++ b/man/man1/recs-fromps.1 @@ -1,4 +1,4 @@ -.TH RECS\-FROMPS 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FROMPS 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-fromps \- Generate records from the process table diff --git a/man/man1/recs-fromre.1 b/man/man1/recs-fromre.1 index 77f86be5..1bd026ba 100644 --- a/man/man1/recs-fromre.1 +++ b/man/man1/recs-fromre.1 @@ -1,4 +1,4 @@ -.TH RECS\-FROMRE 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FROMRE 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-fromre \- The regex is matched against each line of input (or lines of ) diff --git a/man/man1/recs-fromsplit.1 b/man/man1/recs-fromsplit.1 index fbcf39d1..4ed3ac8b 100644 --- a/man/man1/recs-fromsplit.1 +++ b/man/man1/recs-fromsplit.1 @@ -1,4 +1,4 @@ -.TH RECS\-FROMSPLIT 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FROMSPLIT 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-fromsplit \- Each line of input (or lines of ) is split on the provided delimiter to produce an output record diff --git a/man/man1/recs-fromtcpdump.1 b/man/man1/recs-fromtcpdump.1 index 133b98fe..fa6c0fa8 100644 --- a/man/man1/recs-fromtcpdump.1 +++ b/man/man1/recs-fromtcpdump.1 @@ -1,4 +1,4 @@ -.TH RECS\-FROMTCPDUMP 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FROMTCPDUMP 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-fromtcpdump \- Runs tcpdump and puts out records, one for each packet diff --git a/man/man1/recs-fromxferlog.1 b/man/man1/recs-fromxferlog.1 index e5fd3912..001f7f1d 100644 --- a/man/man1/recs-fromxferlog.1 +++ b/man/man1/recs-fromxferlog.1 @@ -1,4 +1,4 @@ -.TH RECS\-FROMXFERLOG 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FROMXFERLOG 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-fromxferlog \- Each line of input (or lines of ) is parsed as an FTP transfer log (xferlog format) to produce an output record diff --git a/man/man1/recs-fromxls.1 b/man/man1/recs-fromxls.1 index 99719a34..c865970d 100644 --- a/man/man1/recs-fromxls.1 +++ b/man/man1/recs-fromxls.1 @@ -1,4 +1,4 @@ -.TH RECS\-FROMXLS 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FROMXLS 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-fromxls \- Parse Excel files (xls, xlsx, xlsb, xlsm) into records diff --git a/man/man1/recs-fromxml.1 b/man/man1/recs-fromxml.1 index 67b14e57..0792c9f6 100644 --- a/man/man1/recs-fromxml.1 +++ b/man/man1/recs-fromxml.1 @@ -1,4 +1,4 @@ -.TH RECS\-FROMXML 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-FROMXML 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-fromxml \- Reads either from STDIN or from the specified URIs diff --git a/man/man1/recs-generate.1 b/man/man1/recs-generate.1 index ad7dab96..2931123b 100644 --- a/man/man1/recs-generate.1 +++ b/man/man1/recs-generate.1 @@ -1,4 +1,4 @@ -.TH RECS\-GENERATE 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-GENERATE 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-generate \- Execute an expression for each record to generate new records diff --git a/man/man1/recs-grep.1 b/man/man1/recs-grep.1 index ad86dcfe..19e1dee5 100644 --- a/man/man1/recs-grep.1 +++ b/man/man1/recs-grep.1 @@ -1,4 +1,4 @@ -.TH RECS\-GREP 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-GREP 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-grep \- Filter records where an expression evaluates to true diff --git a/man/man1/recs-join.1 b/man/man1/recs-join.1 index 615f0aff..75965a38 100644 --- a/man/man1/recs-join.1 +++ b/man/man1/recs-join.1 @@ -1,4 +1,4 @@ -.TH RECS\-JOIN 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-JOIN 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-join \- Join two record streams on a key diff --git a/man/man1/recs-multiplex.1 b/man/man1/recs-multiplex.1 index 33fc0c97..50af6993 100644 --- a/man/man1/recs-multiplex.1 +++ b/man/man1/recs-multiplex.1 @@ -1,4 +1,4 @@ -.TH RECS\-MULTIPLEX 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-MULTIPLEX 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-multiplex \- Take records, grouped together by \-\-keys, and run a separate operation instance for each group diff --git a/man/man1/recs-normalizetime.1 b/man/man1/recs-normalizetime.1 index b44f987f..70a25248 100644 --- a/man/man1/recs-normalizetime.1 +++ b/man/man1/recs-normalizetime.1 @@ -1,4 +1,4 @@ -.TH RECS\-NORMALIZETIME 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-NORMALIZETIME 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-normalizetime \- Given a single key field containing a date/time value, construct a normalized version of the value and place it into a field named \'n_\' diff --git a/man/man1/recs-parsedate.1 b/man/man1/recs-parsedate.1 index 67b6b82e..0b5634d0 100644 --- a/man/man1/recs-parsedate.1 +++ b/man/man1/recs-parsedate.1 @@ -1,4 +1,4 @@ -.TH RECS\-PARSEDATE 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-PARSEDATE 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-parsedate \- Parse date/time strings from a field and output them in a normalized format diff --git a/man/man1/recs-sort.1 b/man/man1/recs-sort.1 index 40c10e90..9a534457 100644 --- a/man/man1/recs-sort.1 +++ b/man/man1/recs-sort.1 @@ -1,4 +1,4 @@ -.TH RECS\-SORT 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-SORT 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-sort \- Sort records from input or from files diff --git a/man/man1/recs-stream2table.1 b/man/man1/recs-stream2table.1 index ee44e7f2..1f304f05 100644 --- a/man/man1/recs-stream2table.1 +++ b/man/man1/recs-stream2table.1 @@ -1,4 +1,4 @@ -.TH RECS\-STREAM2TABLE 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-STREAM2TABLE 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-stream2table \- Transforms a list of records, combining records based on a column field diff --git a/man/man1/recs-substream.1 b/man/man1/recs-substream.1 index 6d406ce2..247c4f72 100644 --- a/man/man1/recs-substream.1 +++ b/man/man1/recs-substream.1 @@ -1,4 +1,4 @@ -.TH RECS\-SUBSTREAM 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-SUBSTREAM 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-substream \- Filter to a range of records delimited from when the begin snippet becomes true to when the end snippet becomes true, i diff --git a/man/man1/recs-tochart.1 b/man/man1/recs-tochart.1 index e38fd076..c86003d7 100644 --- a/man/man1/recs-tochart.1 +++ b/man/man1/recs-tochart.1 @@ -1,4 +1,4 @@ -.TH RECS\-TOCHART 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-TOCHART 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-tochart \- Render charts in the terminal from a record stream diff --git a/man/man1/recs-tocsv.1 b/man/man1/recs-tocsv.1 index 7657fe98..2cb04465 100644 --- a/man/man1/recs-tocsv.1 +++ b/man/man1/recs-tocsv.1 @@ -1,4 +1,4 @@ -.TH RECS\-TOCSV 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-TOCSV 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-tocsv \- Outputs records as CSV formatted lines diff --git a/man/man1/recs-todb.1 b/man/man1/recs-todb.1 index b8de6f6b..8c2c1038 100644 --- a/man/man1/recs-todb.1 +++ b/man/man1/recs-todb.1 @@ -1,4 +1,4 @@ -.TH RECS\-TODB 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-TODB 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-todb \- Dumps a stream of input records into a database diff --git a/man/man1/recs-togdgraph.1 b/man/man1/recs-togdgraph.1 index 16a2da85..83afcd2a 100644 --- a/man/man1/recs-togdgraph.1 +++ b/man/man1/recs-togdgraph.1 @@ -1,4 +1,4 @@ -.TH RECS\-TOGDGRAPH 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-TOGDGRAPH 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-togdgraph \- Create a bar, scatter, or line graph diff --git a/man/man1/recs-tognuplot.1 b/man/man1/recs-tognuplot.1 index eef23b47..effb30bd 100644 --- a/man/man1/recs-tognuplot.1 +++ b/man/man1/recs-tognuplot.1 @@ -1,4 +1,4 @@ -.TH RECS\-TOGNUPLOT 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-TOGNUPLOT 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-tognuplot \- Create a graph of points from a record stream using GNU Plot diff --git a/man/man1/recs-tohtml.1 b/man/man1/recs-tohtml.1 index 4b445ab7..7d7f7760 100644 --- a/man/man1/recs-tohtml.1 +++ b/man/man1/recs-tohtml.1 @@ -1,4 +1,4 @@ -.TH RECS\-TOHTML 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-TOHTML 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-tohtml \- Prints out an HTML table for the records from input or from files diff --git a/man/man1/recs-tojsonarray.1 b/man/man1/recs-tojsonarray.1 index 3c711806..2a0a3c29 100644 --- a/man/man1/recs-tojsonarray.1 +++ b/man/man1/recs-tojsonarray.1 @@ -1,4 +1,4 @@ -.TH RECS\-TOJSONARRAY 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-TOJSONARRAY 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-tojsonarray \- Outputs the record stream as a single JSON array diff --git a/man/man1/recs-topn.1 b/man/man1/recs-topn.1 index aa11a042..412cedc2 100644 --- a/man/man1/recs-topn.1 +++ b/man/man1/recs-topn.1 @@ -1,4 +1,4 @@ -.TH RECS\-TOPN 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-TOPN 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-topn \- Output the top N records from the input stream or from files diff --git a/man/man1/recs-toprettyprint.1 b/man/man1/recs-toprettyprint.1 index 9be21c57..2000307a 100644 --- a/man/man1/recs-toprettyprint.1 +++ b/man/man1/recs-toprettyprint.1 @@ -1,4 +1,4 @@ -.TH RECS\-TOPRETTYPRINT 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-TOPRETTYPRINT 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-toprettyprint \- Pretty print records, one key to a line, with a line of dashes separating records diff --git a/man/man1/recs-toptable.1 b/man/man1/recs-toptable.1 index f00a0ea7..8010b146 100644 --- a/man/man1/recs-toptable.1 +++ b/man/man1/recs-toptable.1 @@ -1,4 +1,4 @@ -.TH RECS\-TOPTABLE 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-TOPTABLE 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-toptable \- Creates a multi\-dimensional pivot table with any number of x and y axes diff --git a/man/man1/recs-totable.1 b/man/man1/recs-totable.1 index a4cd55e3..bee8ab6b 100644 --- a/man/man1/recs-totable.1 +++ b/man/man1/recs-totable.1 @@ -1,4 +1,4 @@ -.TH RECS\-TOTABLE 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-TOTABLE 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-totable \- Pretty prints a table of records to the screen diff --git a/man/man1/recs-xform.1 b/man/man1/recs-xform.1 index 9260cd99..249e6089 100644 --- a/man/man1/recs-xform.1 +++ b/man/man1/recs-xform.1 @@ -1,4 +1,4 @@ -.TH RECS\-XFORM 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS\-XFORM 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs\-xform \- Transform records with a JS snippet diff --git a/man/man1/recs.1 b/man/man1/recs.1 index 07a717fd..77ade70b 100644 --- a/man/man1/recs.1 +++ b/man/man1/recs.1 @@ -1,4 +1,4 @@ -.TH RECS 1 "2026-02-23" "recs 0.1.0" "RecordStream Manual" +.TH RECS 1 "2026-02-24" "recs 0.1.0" "RecordStream Manual" .SH NAME recs \- a toolkit for taming JSON record streams diff --git a/src/Executor.ts b/src/Executor.ts index 581fa103..5ed3384b 100644 --- a/src/Executor.ts +++ b/src/Executor.ts @@ -93,22 +93,36 @@ interface CompiledSnippet { * * {{foo/bar}} becomes calls to the __get helper function. * {{foo/bar}} = value becomes calls to the __set helper function. + * {{foo/bar}} += value becomes __set(R, "foo/bar", __get(R, "foo/bar") + value) + * + * The recordVar parameter controls the variable name used in generated code + * (e.g. "r" for JS/Python, "$r" for Perl). */ -export function transformCode(code: string): string { - // Replace {{keyspec}} = value with __set(r, "keyspec", value) - // Negative lookahead (?!=) ensures == and === are not matched as assignment +export function transformCode(code: string, recordVar = "r"): string { + // Step 1: Replace compound assignments like {{ks}} += val + // Operators listed longest-first for correct matching let transformed = code.replace( + /\{\{(.*?)\}\}\s*(\*\*|>>>|>>|<<|\?\?|\/\/|\|\||&&|\+|-|\*|\/|%|&|\||\^)=\s*([^;,\n]+)/g, + (_match, keyspec: string, op: string, value: string) => { + const ks = JSON.stringify(keyspec); + return `__set(${recordVar}, ${ks}, __get(${recordVar}, ${ks}) ${op} ${value.trim()})`; + } + ); + + // Step 2: Replace simple assignments like {{ks}} = val + // Negative lookahead (?!=) ensures == and === are not matched + transformed = transformed.replace( /\{\{(.*?)\}\}\s*=(?!=)\s*([^;,\n]+)/g, (_match, keyspec: string, value: string) => { - return `__set(r, ${JSON.stringify(keyspec)}, ${value.trim()})`; + return `__set(${recordVar}, ${JSON.stringify(keyspec)}, ${value.trim()})`; } ); - // Replace remaining {{keyspec}} with __get(r, "keyspec") + // Step 3: Replace remaining {{keyspec}} reads transformed = transformed.replace( /\{\{(.*?)\}\}/g, (_match, keyspec: string) => { - return `__get(r, ${JSON.stringify(keyspec)})`; + return `__get(${recordVar}, ${JSON.stringify(keyspec)})`; } ); diff --git a/src/snippets/PerlSnippetRunner.ts b/src/snippets/PerlSnippetRunner.ts index ed0c9052..2d20dc7f 100644 --- a/src/snippets/PerlSnippetRunner.ts +++ b/src/snippets/PerlSnippetRunner.ts @@ -17,6 +17,7 @@ import type { SnippetMode, } from "./SnippetRunner.ts"; import { groupResponses } from "./SnippetRunner.ts"; +import { transformCode } from "../Executor.ts"; const SNIPPETS_DIR = dirname(fileURLToPath(import.meta.url)); const RUNNER_DIR = join(SNIPPETS_DIR, "perl"); @@ -28,7 +29,7 @@ export class PerlSnippetRunner implements SnippetRunner { #mode: SnippetMode = "eval"; async init(code: string, context: SnippetContext): Promise { - this.#code = code; + this.#code = transformCode(code, "$r"); this.#mode = context.mode; } diff --git a/src/snippets/PythonSnippetRunner.ts b/src/snippets/PythonSnippetRunner.ts index d48bdd30..2418bc6d 100644 --- a/src/snippets/PythonSnippetRunner.ts +++ b/src/snippets/PythonSnippetRunner.ts @@ -18,6 +18,7 @@ import type { SnippetMode, } from "./SnippetRunner.ts"; import { groupResponses } from "./SnippetRunner.ts"; +import { transformCode } from "../Executor.ts"; const SNIPPETS_DIR = dirname(fileURLToPath(import.meta.url)); const RUNNER_DIR = join(SNIPPETS_DIR, "python"); @@ -29,7 +30,7 @@ export class PythonSnippetRunner implements SnippetRunner { #mode: SnippetMode = "eval"; async init(code: string, context: SnippetContext): Promise { - this.#code = code; + this.#code = transformCode(code, "r"); this.#mode = context.mode; } diff --git a/src/snippets/perl/runner.pl b/src/snippets/perl/runner.pl index 0edb0741..85cfd59a 100644 --- a/src/snippets/perl/runner.pl +++ b/src/snippets/perl/runner.pl @@ -64,6 +64,21 @@ sub write_message { exit 1; } +# ---------------------------------------------------------------- +# __get / __set helpers for {{}} template expansion +# ---------------------------------------------------------------- + +sub __get { + my ($r, $keyspec) = @_; + return RecsSDK::_resolve($r, $keyspec); +} + +sub __set { + my ($r, $keyspec, $value) = @_; + RecsSDK::_set_path($r, $keyspec, $value); + return $value; +} + # ---------------------------------------------------------------- # Compile snippet # ---------------------------------------------------------------- diff --git a/src/snippets/python/runner.py b/src/snippets/python/runner.py index d792856b..71dc02ce 100644 --- a/src/snippets/python/runner.py +++ b/src/snippets/python/runner.py @@ -116,6 +116,13 @@ def emit(rec_or_dict: Any) -> None: f"emit() expects a Record or dict, got {type(rec_or_dict).__name__}" ) + def __get(rec: Record, ks: str) -> Any: + return rec.get("@" + ks) + + def __set(rec: Record, ks: str, value: Any) -> Any: + rec.set("@" + ks, value) + return value + # Build the snippet namespace namespace: dict[str, Any] = { "r": r, @@ -124,6 +131,8 @@ def emit(rec_or_dict: Any) -> None: "filename": "NONE", "emit": emit, "Record": Record, + "__get": __get, + "__set": __set, # Expose builtins for convenience "json": __import__("json"), "re": __import__("re"), diff --git a/tests/Executor.test.ts b/tests/Executor.test.ts index fd834364..4d21dfc7 100644 --- a/tests/Executor.test.ts +++ b/tests/Executor.test.ts @@ -28,6 +28,118 @@ describe("Executor", () => { expect(result).toContain('"a"'); expect(result).toContain('"b"'); }); + + test("transforms {{key}} += value to compound assignment", () => { + const result = transformCode("{{x}} += 2"); + expect(result).toBe('__set(r, "x", __get(r, "x") + 2)'); + }); + + test("transforms {{key}} -= value to compound assignment", () => { + const result = transformCode("{{x}} -= 1"); + expect(result).toBe('__set(r, "x", __get(r, "x") - 1)'); + }); + + test("transforms {{key}} *= value to compound assignment", () => { + const result = transformCode("{{x}} *= 3"); + expect(result).toBe('__set(r, "x", __get(r, "x") * 3)'); + }); + + test("transforms {{key}} /= value to compound assignment", () => { + const result = transformCode("{{x}} /= 2"); + expect(result).toBe('__set(r, "x", __get(r, "x") / 2)'); + }); + + test("transforms {{key}} **= value to compound assignment", () => { + const result = transformCode("{{x}} **= 2"); + expect(result).toBe('__set(r, "x", __get(r, "x") ** 2)'); + }); + + test("transforms {{key}} ||= value to compound assignment", () => { + const result = transformCode("{{x}} ||= 5"); + expect(result).toBe('__set(r, "x", __get(r, "x") || 5)'); + }); + + test("transforms {{key}} &&= value to compound assignment", () => { + const result = transformCode("{{x}} &&= 5"); + expect(result).toBe('__set(r, "x", __get(r, "x") && 5)'); + }); + + test("transforms {{key}} ??= value to compound assignment", () => { + const result = transformCode("{{x}} ??= 5"); + expect(result).toBe('__set(r, "x", __get(r, "x") ?? 5)'); + }); + + test("transforms {{key}} >>= value to compound assignment", () => { + const result = transformCode("{{x}} >>= 1"); + expect(result).toBe('__set(r, "x", __get(r, "x") >> 1)'); + }); + + test("transforms {{key}} >>>= value to compound assignment", () => { + const result = transformCode("{{x}} >>>= 1"); + expect(result).toBe('__set(r, "x", __get(r, "x") >>> 1)'); + }); + + test("transforms {{key}} <<= value to compound assignment", () => { + const result = transformCode("{{x}} <<= 1"); + expect(result).toBe('__set(r, "x", __get(r, "x") << 1)'); + }); + + test("transforms {{key}} |= value to compound assignment", () => { + const result = transformCode("{{x}} |= 3"); + expect(result).toBe('__set(r, "x", __get(r, "x") | 3)'); + }); + + test("transforms {{key}} &= value to compound assignment", () => { + const result = transformCode("{{x}} &= 3"); + expect(result).toBe('__set(r, "x", __get(r, "x") & 3)'); + }); + + test("transforms {{key}} ^= value to compound assignment", () => { + const result = transformCode("{{x}} ^= 3"); + expect(result).toBe('__set(r, "x", __get(r, "x") ^ 3)'); + }); + + test("transforms {{key}} //= value to compound assignment", () => { + const result = transformCode("{{x}} //= 10"); + expect(result).toBe('__set(r, "x", __get(r, "x") // 10)'); + }); + + test("transforms {{key}} %= value to compound assignment", () => { + const result = transformCode("{{x}} %= 3"); + expect(result).toBe('__set(r, "x", __get(r, "x") % 3)'); + }); + + test("compound assignment with nested keyspec", () => { + const result = transformCode("{{a/b}} += 1"); + expect(result).toBe('__set(r, "a/b", __get(r, "a/b") + 1)'); + }); + + test("uses custom recordVar", () => { + const result = transformCode("{{x}} += 1", "$r"); + expect(result).toBe('__set($r, "x", __get($r, "x") + 1)'); + }); + + test("uses custom recordVar for simple assign", () => { + const result = transformCode("{{x}} = 5", "$r"); + expect(result).toBe('__set($r, "x", 5)'); + }); + + test("uses custom recordVar for read", () => { + const result = transformCode("{{x}}", "$r"); + expect(result).toBe('__get($r, "x")'); + }); + + test("does not match == as assignment", () => { + const result = transformCode("{{x}} == 5"); + expect(result).not.toContain("__set"); + expect(result).toContain("__get"); + }); + + test("does not match === as assignment", () => { + const result = transformCode("{{x}} === 5"); + expect(result).not.toContain("__set"); + expect(result).toContain("__get"); + }); }); describe("executeCode", () => { @@ -62,6 +174,34 @@ describe("Executor", () => { expect(data["new_field"]).toBe("created"); }); + test("evaluates {{}} compound assignment +=", () => { + const executor = new Executor("{{x}} += 10"); + const record = new Record({ x: 5 }); + executor.executeCode(record); + expect(record.dataRef()["x"]).toBe(15); + }); + + test("evaluates {{}} compound assignment -=", () => { + const executor = new Executor("{{x}} -= 3"); + const record = new Record({ x: 10 }); + executor.executeCode(record); + expect(record.dataRef()["x"]).toBe(7); + }); + + test("evaluates {{}} compound assignment *=", () => { + const executor = new Executor("{{x}} *= 4"); + const record = new Record({ x: 3 }); + executor.executeCode(record); + expect(record.dataRef()["x"]).toBe(12); + }); + + test("evaluates {{}} compound assignment **=", () => { + const executor = new Executor("{{x}} **= 3"); + const record = new Record({ x: 2 }); + executor.executeCode(record); + expect(record.dataRef()["x"]).toBe(8); + }); + test("provides $line counter", () => { const executor = new Executor("return $line"); const r = new Record({}); diff --git a/tests/snippets/TemplateExpansion.test.ts b/tests/snippets/TemplateExpansion.test.ts new file mode 100644 index 00000000..82785a05 --- /dev/null +++ b/tests/snippets/TemplateExpansion.test.ts @@ -0,0 +1,157 @@ +/** + * Cross-language {{}} template expansion tests. + * + * Verifies that {{field}} syntax works equivalently across JS, Python, and + * Perl snippet runners — reads, simple assignments, compound assignments, + * and nested key access. + */ + +import { describe, test, expect } from "bun:test"; +import { Record } from "../../src/Record.ts"; +import { JsSnippetRunner } from "../../src/snippets/JsSnippetRunner.ts"; +import { PythonSnippetRunner } from "../../src/snippets/PythonSnippetRunner.ts"; +import { PerlSnippetRunner } from "../../src/snippets/PerlSnippetRunner.ts"; +import type { SnippetRunner } from "../../src/snippets/SnippetRunner.ts"; + +// ── Runner factories ───────────────────────────────────────────── + +const runners: { name: string; create: () => SnippetRunner }[] = [ + { name: "js", create: () => new JsSnippetRunner() }, + { name: "python", create: () => new PythonSnippetRunner() }, + { name: "perl", create: () => new PerlSnippetRunner() }, +]; + +// ── Cross-language tests ───────────────────────────────────────── + +for (const { name, create } of runners) { + describe(`{{}} template expansion [${name}]`, () => { + // ── Reads (grep mode) ────────────────────────────────────── + + test("{{field}} read in grep — passes when true", () => { + const runner = create(); + void runner.init("{{x}} > 5", { mode: "grep" }); + + const results = runner.executeBatch([new Record({ x: 10 })]); + expect(results).toHaveLength(1); + expect(results[0]!.passed).toBe(true); + }); + + test("{{field}} read in grep — fails when false", () => { + const runner = create(); + void runner.init("{{x}} > 5", { mode: "grep" }); + + const results = runner.executeBatch([new Record({ x: 3 })]); + expect(results).toHaveLength(1); + expect(results[0]!.passed).toBe(false); + }); + + // ── Simple assignment (eval mode) ────────────────────────── + + test("{{field}} = value assigns new field", () => { + const runner = create(); + void runner.init("{{y}} = 42", { mode: "eval" }); + + const results = runner.executeBatch([new Record({ x: 1 })]); + expect(results).toHaveLength(1); + expect(results[0]!.record!["y"]).toBe(42); + expect(results[0]!.record!["x"]).toBe(1); + }); + + test("{{field}} = value overwrites existing field", () => { + const runner = create(); + void runner.init("{{x}} = 99", { mode: "eval" }); + + const results = runner.executeBatch([new Record({ x: 1 })]); + expect(results).toHaveLength(1); + expect(results[0]!.record!["x"]).toBe(99); + }); + + // ── Compound assignments (eval mode) ──────────────────────── + + test("{{field}} += value", () => { + const runner = create(); + void runner.init("{{x}} += 10", { mode: "eval" }); + + const results = runner.executeBatch([new Record({ x: 5 })]); + expect(results).toHaveLength(1); + expect(results[0]!.record!["x"]).toBe(15); + }); + + test("{{field}} -= value", () => { + const runner = create(); + void runner.init("{{x}} -= 3", { mode: "eval" }); + + const results = runner.executeBatch([new Record({ x: 10 })]); + expect(results).toHaveLength(1); + expect(results[0]!.record!["x"]).toBe(7); + }); + + test("{{field}} *= value", () => { + const runner = create(); + void runner.init("{{x}} *= 4", { mode: "eval" }); + + const results = runner.executeBatch([new Record({ x: 3 })]); + expect(results).toHaveLength(1); + expect(results[0]!.record!["x"]).toBe(12); + }); + + test("{{field}} **= value (exponentiation)", () => { + const runner = create(); + void runner.init("{{x}} **= 3", { mode: "eval" }); + + const results = runner.executeBatch([new Record({ x: 2 })]); + expect(results).toHaveLength(1); + expect(results[0]!.record!["x"]).toBe(8); + }); + + // ── Nested key access ────────────────────────────────────── + + test("{{a/b}} reads nested key in grep", () => { + const runner = create(); + void runner.init("{{a/b}} > 0", { mode: "grep" }); + + const results = runner.executeBatch([ + new Record({ a: { b: 5 } }), + new Record({ a: { b: -1 } }), + ]); + expect(results).toHaveLength(2); + expect(results[0]!.passed).toBe(true); + expect(results[1]!.passed).toBe(false); + }); + + test("{{a/b}} = value writes nested key", () => { + const runner = create(); + void runner.init("{{a/b}} = 99", { mode: "eval" }); + + const results = runner.executeBatch([new Record({ a: { b: 1 } })]); + expect(results).toHaveLength(1); + expect(results[0]!.record!["a"]).toEqual({ b: 99 }); + }); + + test("{{a/b}} += value compound assigns nested key", () => { + const runner = create(); + void runner.init("{{a/b}} += 10", { mode: "eval" }); + + const results = runner.executeBatch([new Record({ a: { b: 5 } })]); + expect(results).toHaveLength(1); + expect(results[0]!.record!["a"]).toEqual({ b: 15 }); + }); + + // ── Multiple records ─────────────────────────────────────── + + test("{{field}} += value across multiple records", () => { + const runner = create(); + void runner.init("{{x}} += 1", { mode: "eval" }); + + const results = runner.executeBatch([ + new Record({ x: 10 }), + new Record({ x: 20 }), + new Record({ x: 30 }), + ]); + expect(results).toHaveLength(3); + expect(results[0]!.record!["x"]).toBe(11); + expect(results[1]!.record!["x"]).toBe(21); + expect(results[2]!.record!["x"]).toBe(31); + }); + }); +}