File: ./avoid/avoid.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package avoid
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "regexp"
33 "strings"
34 )
35
36 const info = `
37 avoid [options...] [regular expressions...]
38
39 Avoid/ignore lines which match any of the extended-mode regular expressions
40 given. When not given any regex, all empty lines are ignored by default.
41
42 The options are, available both in single and double-dash versions
43
44 -h, -help show this help message
45 -i, -ins match regexes case-insensitively
46 -q, -quiet never emit output: exit code still succeeds/fails
47 `
48
49 func Main() {
50 nerr := 0
51 buffered := false
52 sensitive := true
53 quiet := false
54 args := os.Args[1:]
55
56 for len(args) > 0 {
57 switch args[0] {
58 case `-b`, `--b`, `-buffered`, `--buffered`:
59 buffered = true
60 args = args[1:]
61 continue
62
63 case `-h`, `--h`, `-help`, `--help`:
64 os.Stdout.WriteString(info[1:])
65 return
66
67 case `-i`, `--i`, `-ins`, `--ins`:
68 sensitive = false
69 args = args[1:]
70 continue
71
72 case `-q`, `--q`, `-quiet`, `--quiet`:
73 quiet = true
74 args = args[1:]
75 continue
76 }
77
78 if s := args[0]; s == `-` || s == `--` || !strings.HasPrefix(s, `-`) {
79 break
80 }
81
82 for s := args[0][1:]; len(s) > 0; s = s[1:] {
83 switch s[0] {
84 case 'b':
85 buffered = true
86 continue
87 case 'h':
88 os.Stdout.WriteString(info[1:])
89 return
90 case 'i':
91 sensitive = false
92 continue
93 case 'q':
94 quiet = true
95 continue
96 default:
97 os.Stderr.WriteString(`avoid: unsupported option `)
98 os.Stderr.WriteString(args[0])
99 os.Stderr.WriteString("\n")
100 os.Exit(1)
101 }
102 }
103
104 args = args[1:]
105 }
106
107 if len(args) > 0 && args[0] == `--` {
108 args = args[1:]
109 }
110
111 liveLines := false
112 if !quiet {
113 liveLines = !buffered
114 if !buffered {
115 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
116 liveLines = false
117 }
118 }
119 }
120
121 if len(args) == 0 {
122 args = []string{`^$`}
123 }
124
125 exprs := make([]*regexp.Regexp, 0, len(args))
126
127 for _, src := range args {
128 var err error
129 var exp *regexp.Regexp
130 if !sensitive {
131 exp, err = regexp.Compile(`(?i)` + src)
132 } else {
133 exp, err = regexp.Compile(src)
134 }
135
136 if err != nil {
137 os.Stderr.WriteString(`avoid: `)
138 os.Stderr.WriteString(err.Error())
139 os.Stderr.WriteString("\n")
140 nerr++
141 }
142
143 exprs = append(exprs, exp)
144 }
145
146 if nerr > 0 {
147 os.Exit(1)
148 return
149 }
150
151 var buf []byte
152 sc := bufio.NewScanner(os.Stdin)
153 sc.Buffer(nil, 8*1024*1024*1024)
154 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
155
156 ok := false
157
158 for i := 0; sc.Scan(); i++ {
159 line := sc.Bytes()
160 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
161 line = line[3:]
162 }
163
164 s := line
165 if bytes.IndexByte(s, '\x1b') >= 0 {
166 buf = plain(buf[:0], s)
167 s = buf
168 }
169
170 if !match(s, exprs) {
171 ok = true
172 if quiet {
173 continue
174 }
175
176 bw.Write(line)
177 bw.WriteByte('\n')
178
179 if liveLines && bw.Flush() != nil {
180 break
181 }
182 }
183 }
184
185 if !ok {
186 os.Exit(1)
187 return
188 }
189 }
190
191 func match(what []byte, with []*regexp.Regexp) bool {
192 for _, e := range with {
193 if e.Match(what) {
194 return true
195 }
196 }
197 return false
198 }
199
200 func plain(dst []byte, src []byte) []byte {
201 for len(src) > 0 {
202 i, j := indexEscapeSequence(src)
203 if i < 0 {
204 dst = append(dst, src...)
205 break
206 }
207 if j < 0 {
208 j = len(src)
209 }
210
211 if i > 0 {
212 dst = append(dst, src[:i]...)
213 }
214
215 src = src[j:]
216 }
217
218 return dst
219 }
220
221 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
222 // the multi-byte sequences starting with ESC[; the result is a pair of slice
223 // indices which can be independently negative when either the start/end of
224 // a sequence isn't found; given their fairly-common use, even the hyperlink
225 // ESC]8 sequences are supported
226 func indexEscapeSequence(s []byte) (int, int) {
227 var prev byte
228
229 for i, b := range s {
230 if prev == '\x1b' && b == '[' {
231 j := indexLetter(s[i+1:])
232 if j < 0 {
233 return i, -1
234 }
235 return i - 1, i + 1 + j + 1
236 }
237
238 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
239 j := indexPair(s[i+1:], '\x1b', '\\')
240 if j < 0 {
241 return i, -1
242 }
243 return i - 1, i + 1 + j + 2
244 }
245
246 prev = b
247 }
248
249 return -1, -1
250 }
251
252 func indexLetter(s []byte) int {
253 for i, b := range s {
254 upper := b &^ 32
255 if 'A' <= upper && upper <= 'Z' {
256 return i
257 }
258 }
259
260 return -1
261 }
262
263 func indexPair(s []byte, x byte, y byte) int {
264 var prev byte
265
266 for i, b := range s {
267 if prev == x && b == y && i > 0 {
268 return i
269 }
270 prev = b
271 }
272
273 return -1
274 }
File: ./backsort/backsort.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package backsort
26
27 import (
28 "bufio"
29 "math"
30 "os"
31 "sort"
32 "strconv"
33 "strings"
34 )
35
36 const info = `
37 backsort [options...] [columns/indices...]
38
39 BACKward-SORT (numerically) rows using the columns given, in order. Arguments
40 are matched to columns, first exactly, failing that case-insensitively, and
41 failing even that, as 1-based indices, with negative numbers counting backward
42 from the last column.
43
44 Input is TSV (tab-separated values) lines from the standard input, where the
45 first/header line has all the column names.
46
47 The options are, available both in single and double-dash versions
48
49 -h, -help show this help message
50 `
51
52 func Main() {
53 args := os.Args[1:]
54
55 if len(args) > 0 {
56 switch args[0] {
57 case `-h`, `--h`, `-help`, `--help`:
58 os.Stdout.WriteString(info[1:])
59 return
60 }
61 }
62
63 if len(args) > 0 && args[0] == `--` {
64 args = args[1:]
65 }
66
67 // if len(args) == 0 {
68 // os.Stderr.WriteString(info[1:])
69 // os.Exit(1)
70 // return
71 // }
72
73 var header string
74 var rows []string
75 indices := make([]int, 0, len(args))
76
77 sc := bufio.NewScanner(os.Stdin)
78 sc.Buffer(nil, 8*1024*1024*1024)
79
80 for i := 0; sc.Scan(); i++ {
81 line := sc.Text()
82
83 if i == 0 {
84 if strings.HasPrefix(line, "\xef\xbb\xbf") {
85 line = line[3:]
86 }
87
88 header = line
89
90 for _, s := range args {
91 if j := findColumn(header, s); j >= 0 {
92 indices = append(indices, j)
93 }
94 }
95 continue
96 }
97
98 rows = append(rows, line)
99 }
100
101 sort.SliceStable(rows, func(i, j int) bool {
102 return compareRows(rows[i], rows[j], indices)
103 })
104
105 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
106 defer bw.Flush()
107
108 bw.WriteString(header)
109 if bw.WriteByte('\n') != nil {
110 return
111 }
112
113 for _, s := range rows {
114 bw.WriteString(s)
115 if bw.WriteByte('\n') != nil {
116 return
117 }
118 }
119 }
120
121 // findColumn tries to find the 0-based column-index, given the first/header
122 // row, and a name/integer-index
123 func findColumn(line string, name string) int {
124 // try exact match
125 s := line
126 for i := 0; len(s) > 0; i++ {
127 item, rest, ok := sliceItem(s, 0)
128 if !ok {
129 break
130 }
131 if item == name {
132 return i
133 }
134 s = rest
135 }
136
137 // try case-insensitive match
138 s = line
139 for i := 0; len(s) > 0; i++ {
140 item, rest, ok := sliceItem(s, 0)
141 if !ok {
142 break
143 }
144 if strings.EqualFold(item, name) {
145 return i
146 }
147 s = rest
148 }
149
150 // try a non-zero numeric index
151 i, err := strconv.Atoi(name)
152 if err != nil || i == 0 {
153 return -1
154 }
155
156 // make indices 0-based, and correct negative indices
157 if i > 0 {
158 i--
159 } else if i < 0 {
160 n := countItems(line)
161 i += n
162 }
163
164 // see if numeric index is in range
165 if _, _, ok := sliceItem(line, i); !ok {
166 return -1
167 }
168
169 return i
170 }
171
172 // countItems is 1 more than the number of tabs, or 0 for empty strings
173 func countItems(line string) int {
174 if len(line) == 0 {
175 return 0
176 }
177
178 tabs := 0
179
180 for len(line) > 0 {
181 i := strings.IndexByte(line, '\t')
182 if i < 0 {
183 break
184 }
185
186 line = line[i+1:]
187 tabs++
188 }
189
190 return tabs + 1
191 }
192
193 // sliceItem tries to split a TSV item from the row given, using a 0-based
194 // index, returning both the item and the rest of the TSV line
195 func sliceItem(line string, i int) (item string, rest string, ok bool) {
196 s := line
197 for len(s) > 0 && i > 0 {
198 j := strings.IndexByte(s, '\t')
199 if j < 0 {
200 return ``, ``, false
201 }
202
203 s = s[j+1:]
204 i--
205 }
206
207 if len(s) == 0 || i != 0 {
208 return ``, ``, false
209 }
210
211 j := strings.IndexByte(s, '\t')
212 if j < 0 {
213 return s, ``, true
214 }
215 return s[:j], s[j+1:], true
216 }
217
218 // compareRows reverse-sorts 2 rows numerically, using the indices given
219 func compareRows(x, y string, indices []int) bool {
220 for _, k := range indices {
221 rank := compareItems(x, y, k)
222 if rank == 0 {
223 continue
224 }
225 return rank < 0
226 }
227
228 return false
229 }
230
231 // compareItems picks the same-indexed item from 2 rows, and numerically
232 // compares those picks reverse-numerically
233 func compareItems(x, y string, i int) int {
234 item1, _, got1 := sliceItem(x, i)
235 item2, _, got2 := sliceItem(y, i)
236
237 n1, ok1 := parseNumeric(item1)
238 n2, ok2 := parseNumeric(item2)
239
240 ok1 = ok1 && got1
241 ok2 = ok2 && got2
242
243 if !ok1 && !ok2 {
244 return 0
245 }
246
247 if !ok1 {
248 return -1
249 }
250 if !ok2 {
251 return +1
252 }
253
254 if n1 < n2 {
255 return +1
256 }
257 if n1 > n2 {
258 return -1
259 }
260 return 0
261 }
262
263 // parseNumeric rejects NaNs and Infinities
264 func parseNumeric(s string) (f float64, ok bool) {
265 f, err := strconv.ParseFloat(s, 64)
266 ok = err == nil && !math.IsNaN(f) && !math.IsInf(f, 0)
267 return f, ok
268 }
File: ./base64/base64.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package base64
26
27 import (
28 "bufio"
29 "bytes"
30 "encoding/base64"
31 "errors"
32 "io"
33 "os"
34 )
35
36 const info = `
37 base64 [options...] [files...]
38
39 Encode/decode bytes into/from the base-64 format.
40
41 Options
42
43 -d decode away from base-64, instead of encoding into base-64
44 --help show this help message
45 `
46
47 func Main() {
48 args := os.Args[1:]
49 handle := encode
50
51 for len(args) > 0 {
52 switch args[0] {
53 case `--help`:
54 os.Stderr.WriteString(info[1:])
55 return
56
57 case `-d`:
58 handle = decode
59 args = args[1:]
60 continue
61 }
62
63 break
64 }
65
66 if len(args) > 0 && args[0] == `--` {
67 args = args[1:]
68 }
69
70 if err := run(os.Stdout, args, handle); err != nil && err != io.EOF {
71 os.Stderr.WriteString(`base64: `)
72 os.Stderr.WriteString(err.Error())
73 os.Stderr.WriteString("\n")
74 os.Exit(1)
75 return
76 }
77 }
78
79 type handler func(w io.Writer, r io.Reader) error
80
81 func run(w io.Writer, paths []string, handle handler) error {
82 for _, path := range paths {
83 if err := handleFile(os.Stdout, path, handle); err != nil {
84 return err
85 }
86 }
87
88 if len(paths) == 0 {
89 if err := handle(os.Stdout, os.Stdin); err != nil {
90 return err
91 }
92 }
93
94 return nil
95 }
96
97 func handleFile(w io.Writer, path string, handle handler) error {
98 f, err := os.Open(path)
99 if err != nil {
100 return err
101 }
102 defer f.Close()
103 return handle(w, f)
104 }
105
106 func decode(w io.Writer, r io.Reader) error {
107 return debase64(w, r)
108 }
109
110 func encode(w io.Writer, r io.Reader) error {
111 enc := base64.NewEncoder(base64.StdEncoding, w)
112 _, err := io.Copy(enc, r)
113 enc.Close()
114
115 if err == nil {
116 w.Write([]byte{'\n'})
117 }
118 return err
119 }
120
121 // debase64 decodes base64 chunks explicitly, so decoding errors can be told
122 // apart from output-writing ones
123 func debase64(w io.Writer, r io.Reader) error {
124 br := bufio.NewReaderSize(r, 32*1024)
125 start, err := br.Peek(64)
126 if err != nil && err != io.EOF {
127 return err
128 }
129
130 skip, err := skipIntroDataURI(start)
131 if err != nil {
132 return err
133 }
134
135 if skip > 0 {
136 br.Discard(skip)
137 }
138
139 dec := base64.NewDecoder(base64.StdEncoding, br)
140 _, err = io.Copy(w, dec)
141 return err
142 }
143
144 func skipIntroDataURI(chunk []byte) (skip int, err error) {
145 if bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
146 chunk = chunk[3:]
147 skip += 3
148 }
149
150 if !bytes.HasPrefix(chunk, []byte(`data:`)) {
151 return skip, nil
152 }
153
154 const l = len(`data:,`)
155 if len(chunk) == l && chunk[l-1] == ',' {
156 return l, nil
157 }
158
159 start := chunk
160 if len(start) > 64 {
161 start = start[:64]
162 }
163
164 i := bytes.Index(start, []byte(`;base64,`))
165 if i < 0 {
166 return skip, errors.New(`invalid data URI`)
167 }
168
169 skip += i + len(`;base64,`)
170 return skip, nil
171 }
File: ./bitdump/bitdump.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package bitdump
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "os"
32 "strconv"
33 )
34
35 const info = `
36 bitdump [options...] [filenames...]
37
38
39 Show all bits for all input bytes, starting each output line with the
40 leading byte's offset.
41
42 All (optional) leading options start with either single or double-dash:
43
44 -h, -help show this help message
45 -no-offset, -no-offsets don't start lines with current byte offsets
46 -tab, -tabs separate items with tabs instead of spaces
47 `
48
49 const itemsPerLine = 8
50
51 /*
52 tlp = '(f"{bin(v)[2:]:>08}" for v in range(256))' | lineup 6 |
53 gsub '\t' '`, `' | tlp 'f"\t`{l}`,"'
54 */
55 var bits = [256]string{
56 `00000000`, `00000001`, `00000010`, `00000011`, `00000100`, `00000101`,
57 `00000110`, `00000111`, `00001000`, `00001001`, `00001010`, `00001011`,
58 `00001100`, `00001101`, `00001110`, `00001111`, `00010000`, `00010001`,
59 `00010010`, `00010011`, `00010100`, `00010101`, `00010110`, `00010111`,
60 `00011000`, `00011001`, `00011010`, `00011011`, `00011100`, `00011101`,
61 `00011110`, `00011111`, `00100000`, `00100001`, `00100010`, `00100011`,
62 `00100100`, `00100101`, `00100110`, `00100111`, `00101000`, `00101001`,
63 `00101010`, `00101011`, `00101100`, `00101101`, `00101110`, `00101111`,
64 `00110000`, `00110001`, `00110010`, `00110011`, `00110100`, `00110101`,
65 `00110110`, `00110111`, `00111000`, `00111001`, `00111010`, `00111011`,
66 `00111100`, `00111101`, `00111110`, `00111111`, `01000000`, `01000001`,
67 `01000010`, `01000011`, `01000100`, `01000101`, `01000110`, `01000111`,
68 `01001000`, `01001001`, `01001010`, `01001011`, `01001100`, `01001101`,
69 `01001110`, `01001111`, `01010000`, `01010001`, `01010010`, `01010011`,
70 `01010100`, `01010101`, `01010110`, `01010111`, `01011000`, `01011001`,
71 `01011010`, `01011011`, `01011100`, `01011101`, `01011110`, `01011111`,
72 `01100000`, `01100001`, `01100010`, `01100011`, `01100100`, `01100101`,
73 `01100110`, `01100111`, `01101000`, `01101001`, `01101010`, `01101011`,
74 `01101100`, `01101101`, `01101110`, `01101111`, `01110000`, `01110001`,
75 `01110010`, `01110011`, `01110100`, `01110101`, `01110110`, `01110111`,
76 `01111000`, `01111001`, `01111010`, `01111011`, `01111100`, `01111101`,
77 `01111110`, `01111111`, `10000000`, `10000001`, `10000010`, `10000011`,
78 `10000100`, `10000101`, `10000110`, `10000111`, `10001000`, `10001001`,
79 `10001010`, `10001011`, `10001100`, `10001101`, `10001110`, `10001111`,
80 `10010000`, `10010001`, `10010010`, `10010011`, `10010100`, `10010101`,
81 `10010110`, `10010111`, `10011000`, `10011001`, `10011010`, `10011011`,
82 `10011100`, `10011101`, `10011110`, `10011111`, `10100000`, `10100001`,
83 `10100010`, `10100011`, `10100100`, `10100101`, `10100110`, `10100111`,
84 `10101000`, `10101001`, `10101010`, `10101011`, `10101100`, `10101101`,
85 `10101110`, `10101111`, `10110000`, `10110001`, `10110010`, `10110011`,
86 `10110100`, `10110101`, `10110110`, `10110111`, `10111000`, `10111001`,
87 `10111010`, `10111011`, `10111100`, `10111101`, `10111110`, `10111111`,
88 `11000000`, `11000001`, `11000010`, `11000011`, `11000100`, `11000101`,
89 `11000110`, `11000111`, `11001000`, `11001001`, `11001010`, `11001011`,
90 `11001100`, `11001101`, `11001110`, `11001111`, `11010000`, `11010001`,
91 `11010010`, `11010011`, `11010100`, `11010101`, `11010110`, `11010111`,
92 `11011000`, `11011001`, `11011010`, `11011011`, `11011100`, `11011101`,
93 `11011110`, `11011111`, `11100000`, `11100001`, `11100010`, `11100011`,
94 `11100100`, `11100101`, `11100110`, `11100111`, `11101000`, `11101001`,
95 `11101010`, `11101011`, `11101100`, `11101101`, `11101110`, `11101111`,
96 `11110000`, `11110001`, `11110010`, `11110011`, `11110100`, `11110101`,
97 `11110110`, `11110111`, `11111000`, `11111001`, `11111010`, `11111011`,
98 `11111100`, `11111101`, `11111110`, `11111111`,
99 }
100
101 func Main() {
102 emitOffsets := true
103 separator := byte(' ')
104 args := os.Args[1:]
105
106 for len(args) > 0 {
107 switch args[0] {
108 case `-h`, `--h`, `-help`, `--help`:
109 os.Stdout.WriteString(info[1:])
110 return
111
112 case `-no-offset`, `--no-offset`, `-no-offsets`, `--no-offsets`:
113 emitOffsets = false
114 args = args[1:]
115 continue
116
117 case `-tab`, `--tab`, `-tabs`, `--tabs`:
118 separator = '\t'
119 args = args[1:]
120 continue
121 }
122
123 break
124 }
125
126 if len(args) > 0 && args[0] == `--` {
127 args = args[1:]
128 }
129
130 var p params
131 p.emitOffset = emitNoOffset
132 if emitOffsets {
133 p.emitOffset = emitDecimalOffset
134 }
135 p.separator = separator
136
137 if err := run(os.Stdout, p, args); err != nil && err != io.EOF {
138 os.Stderr.WriteString(`bitdump: `)
139 os.Stderr.WriteString(err.Error())
140 os.Stderr.WriteString("\n")
141 os.Exit(1)
142 return
143 }
144 }
145
146 type params struct {
147 offset *int64
148 emitOffset func(w *bufio.Writer, offset int64, sep byte)
149 separator byte
150 }
151
152 func run(w io.Writer, p params, args []string) error {
153 offset := int64(0)
154 p.offset = &offset
155 bw := bufio.NewWriterSize(w, 32*1024)
156 defer func() {
157 if offset%itemsPerLine == 0 && offset > 0 {
158 bw.WriteByte('\n')
159 }
160 bw.Flush()
161 }()
162
163 if len(args) == 0 {
164 return bitdump(bw, os.Stdin, p)
165 }
166
167 for _, name := range args {
168 if err := handleFile(bw, name, p); err != nil {
169 return err
170 }
171 }
172 return nil
173 }
174
175 func handleFile(w *bufio.Writer, name string, p params) error {
176 if name == `` || name == `-` {
177 return bitdump(w, os.Stdin, p)
178 }
179
180 f, err := os.Open(name)
181 if err != nil {
182 return errors.New(`can't read from file named "` + name + `"`)
183 }
184 defer f.Close()
185
186 return bitdump(w, f, p)
187 }
188
189 func bitdump(w *bufio.Writer, r io.Reader, p params) error {
190 var buf [32 * 1024]byte
191 defer w.Flush()
192
193 for {
194 n, err := r.Read(buf[:])
195 if n < 1 && err == io.EOF {
196 return nil
197 }
198
199 if err != nil && err != io.EOF {
200 return err
201 }
202
203 chunk := buf[:n]
204
205 for len(chunk) >= itemsPerLine {
206 if err := emitChunk(w, chunk[:itemsPerLine], p); err != nil {
207 return err
208 }
209
210 chunk = chunk[itemsPerLine:]
211 *p.offset += itemsPerLine
212 }
213
214 if len(chunk) > 0 {
215 if err := emitChunk(w, chunk, p); err != nil {
216 return err
217 }
218
219 *p.offset += int64(len(chunk))
220 }
221 }
222 }
223
224 func emitDecimalOffset(w *bufio.Writer, offset int64, sep byte) {
225 const pad = `00000000`
226 var str [24]byte
227
228 s := strconv.AppendInt(str[:0], offset, 10)
229 // pad small offsets with leading zeros
230 if len(s) < len(pad) {
231 w.WriteString(pad[len(s):])
232 }
233 w.Write(s)
234 w.WriteByte(sep)
235 }
236
237 func emitNoOffset(w *bufio.Writer, offset int64, sep byte) {
238 // deliberately does nothing
239 }
240
241 func emitChunk(w *bufio.Writer, chunk []byte, p params) error {
242 p.emitOffset(w, *p.offset, p.separator)
243 for i, b := range chunk {
244 if i > 0 {
245 w.WriteByte(p.separator)
246 }
247 w.WriteString(bits[b])
248 }
249
250 if w.WriteByte('\n') != nil {
251 // assume a write error is the consequence of stdout
252 // being closed, perhaps by another app along a pipe
253 return io.EOF
254 }
255 return nil
256 }
File: ./breakdown/breakdown.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package breakdown
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "regexp"
33 )
34
35 const info = `
36 breakdown [options...] [regular expressions...]
37
38 Break each input line into multiple output lines using the first matching
39 regexes given. All regexes are tried in the order given starting from the
40 first, until each line is split completely.
41
42 The options are, available both in single and double-dash versions
43
44 -h, -help show this help message
45 -i, -ins match regexes case-insensitively
46 `
47
48 func Main() {
49 nerr := 0
50 buffered := false
51 sensitive := true
52 args := os.Args[1:]
53
54 for len(args) > 0 {
55 switch args[0] {
56 case `-b`, `--b`, `-buffered`, `--buffered`:
57 buffered = true
58 args = args[1:]
59 continue
60
61 case `-h`, `--h`, `-help`, `--help`:
62 os.Stdout.WriteString(info[1:])
63 return
64
65 case `-i`, `--i`, `-ins`, `--ins`:
66 sensitive = false
67 args = args[1:]
68 continue
69 }
70
71 break
72 }
73
74 if len(args) > 0 && args[0] == `--` {
75 args = args[1:]
76 }
77
78 if len(args) == 0 {
79 os.Stderr.WriteString(info[1:])
80 return
81 }
82
83 liveLines := !buffered
84 if !buffered {
85 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
86 liveLines = false
87 }
88 }
89
90 exprs := make([]*regexp.Regexp, 0, len(args))
91
92 for _, src := range args {
93 var err error
94 var exp *regexp.Regexp
95 if !sensitive {
96 exp, err = regexp.Compile(`(?i)` + src)
97 } else {
98 exp, err = regexp.Compile(src)
99 }
100
101 if err != nil {
102 os.Stderr.WriteString(`breakdown: `)
103 os.Stderr.WriteString(err.Error())
104 os.Stderr.WriteString("\n")
105 nerr++
106 }
107
108 exprs = append(exprs, exp)
109 }
110
111 if nerr > 0 {
112 os.Exit(1)
113 return
114 }
115
116 var buf []byte
117 sc := bufio.NewScanner(os.Stdin)
118 sc.Buffer(nil, 8*1024*1024*1024)
119 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
120
121 for i := 0; sc.Scan(); i++ {
122 line := sc.Bytes()
123 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
124 line = line[3:]
125 }
126
127 s := line
128 if bytes.IndexByte(s, '\x1b') >= 0 {
129 buf = plain(buf[:0], s)
130 s = buf
131 }
132
133 for len(s) > 0 {
134 start, end := findEarliest(s, exprs)
135 if start < 0 {
136 if err := emit(bw, s, liveLines); err != nil {
137 return
138 }
139 break
140 }
141
142 if err := emit(bw, s[:start], liveLines); err != nil {
143 return
144 }
145 s = s[end:]
146 }
147 }
148 }
149
150 func findEarliest(s []byte, exprs []*regexp.Regexp) (start int, end int) {
151 start = -1
152 end = -1
153
154 for _, e := range exprs {
155 m := e.FindIndex(s)
156 if len(m) == 2 && m[0] != m[1] && (m[0] < start || start < 0) {
157 start = m[0]
158 end = m[1]
159 }
160 }
161
162 return start, end
163 }
164
165 func emit(w *bufio.Writer, line []byte, live bool) error {
166 w.Write(line)
167 w.WriteByte('\n')
168
169 if live && w.Flush() != nil {
170 return io.EOF
171 }
172 return nil
173 }
174
175 func plain(dst []byte, src []byte) []byte {
176 for len(src) > 0 {
177 i, j := indexEscapeSequence(src)
178 if i < 0 {
179 dst = append(dst, src...)
180 break
181 }
182 if j < 0 {
183 j = len(src)
184 }
185
186 if i > 0 {
187 dst = append(dst, src[:i]...)
188 }
189
190 src = src[j:]
191 }
192
193 return dst
194 }
195
196 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
197 // the multi-byte sequences starting with ESC[; the result is a pair of slice
198 // indices which can be independently negative when either the start/end of
199 // a sequence isn't found; given their fairly-common use, even the hyperlink
200 // ESC]8 sequences are supported
201 func indexEscapeSequence(s []byte) (int, int) {
202 var prev byte
203
204 for i, b := range s {
205 if prev == '\x1b' && b == '[' {
206 j := indexLetter(s[i+1:])
207 if j < 0 {
208 return i, -1
209 }
210 return i - 1, i + 1 + j + 1
211 }
212
213 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
214 j := indexPair(s[i+1:], '\x1b', '\\')
215 if j < 0 {
216 return i, -1
217 }
218 return i - 1, i + 1 + j + 2
219 }
220
221 prev = b
222 }
223
224 return -1, -1
225 }
226
227 func indexLetter(s []byte) int {
228 for i, b := range s {
229 upper := b &^ 32
230 if 'A' <= upper && upper <= 'Z' {
231 return i
232 }
233 }
234
235 return -1
236 }
237
238 func indexPair(s []byte, x byte, y byte) int {
239 var prev byte
240
241 for i, b := range s {
242 if prev == x && b == y && i > 0 {
243 return i
244 }
245 prev = b
246 }
247
248 return -1
249 }
File: ./bytedump/bytedump.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package bytedump
26
27 import (
28 "bufio"
29 "fmt"
30 "io"
31 "math"
32 "os"
33 "strconv"
34 "strings"
35 )
36
37 const info = `
38 bytedump [options...] [filenames...]
39
40 Show bytes as hexadecimal and ascii on the side.
41
42 Each line shows the starting offset for the bytes shown, 16 of the bytes
43 themselves in base-16 notation, and any ASCII codes when the byte values
44 are in the typical ASCII range.
45
46 The ASCII codes always include 2 rows, which makes the output more 'grep'
47 friendly, since strings up to 32 items can't be accidentally missed. The
48 offsets shown are base-16.
49 `
50
51 const perLine = 16
52
53 // hexSymbols is a direct lookup table combining 2 hex digits with either a
54 // space or a displayable ASCII symbol matching the byte's own ASCII value;
55 // this table was autogenerated by running the command
56 //
57 // seq 0 255 | ./hex-symbols.awk
58 var hexSymbols = [256]string{
59 `00 `, `01 `, `02 `, `03 `, `04 `, `05 `, `06 `, `07 `,
60 `08 `, `09 `, `0a `, `0b `, `0c `, `0d `, `0e `, `0f `,
61 `10 `, `11 `, `12 `, `13 `, `14 `, `15 `, `16 `, `17 `,
62 `18 `, `19 `, `1a `, `1b `, `1c `, `1d `, `1e `, `1f `,
63 `20 `, `21!`, `22"`, `23#`, `24$`, `25%`, `26&`, `27'`,
64 `28(`, `29)`, `2a*`, `2b+`, `2c,`, `2d-`, `2e.`, `2f/`,
65 `300`, `311`, `322`, `333`, `344`, `355`, `366`, `377`,
66 `388`, `399`, `3a:`, `3b;`, `3c<`, `3d=`, `3e>`, `3f?`,
67 `40@`, `41A`, `42B`, `43C`, `44D`, `45E`, `46F`, `47G`,
68 `48H`, `49I`, `4aJ`, `4bK`, `4cL`, `4dM`, `4eN`, `4fO`,
69 `50P`, `51Q`, `52R`, `53S`, `54T`, `55U`, `56V`, `57W`,
70 `58X`, `59Y`, `5aZ`, `5b[`, `5c\`, `5d]`, `5e^`, `5f_`,
71 "60`", `61a`, `62b`, `63c`, `64d`, `65e`, `66f`, `67g`,
72 `68h`, `69i`, `6aj`, `6bk`, `6cl`, `6dm`, `6en`, `6fo`,
73 `70p`, `71q`, `72r`, `73s`, `74t`, `75u`, `76v`, `77w`,
74 `78x`, `79y`, `7az`, `7b{`, `7c|`, `7d}`, `7e~`, `7f `,
75 `80 `, `81 `, `82 `, `83 `, `84 `, `85 `, `86 `, `87 `,
76 `88 `, `89 `, `8a `, `8b `, `8c `, `8d `, `8e `, `8f `,
77 `90 `, `91 `, `92 `, `93 `, `94 `, `95 `, `96 `, `97 `,
78 `98 `, `99 `, `9a `, `9b `, `9c `, `9d `, `9e `, `9f `,
79 `a0 `, `a1 `, `a2 `, `a3 `, `a4 `, `a5 `, `a6 `, `a7 `,
80 `a8 `, `a9 `, `aa `, `ab `, `ac `, `ad `, `ae `, `af `,
81 `b0 `, `b1 `, `b2 `, `b3 `, `b4 `, `b5 `, `b6 `, `b7 `,
82 `b8 `, `b9 `, `ba `, `bb `, `bc `, `bd `, `be `, `bf `,
83 `c0 `, `c1 `, `c2 `, `c3 `, `c4 `, `c5 `, `c6 `, `c7 `,
84 `c8 `, `c9 `, `ca `, `cb `, `cc `, `cd `, `ce `, `cf `,
85 `d0 `, `d1 `, `d2 `, `d3 `, `d4 `, `d5 `, `d6 `, `d7 `,
86 `d8 `, `d9 `, `da `, `db `, `dc `, `dd `, `de `, `df `,
87 `e0 `, `e1 `, `e2 `, `e3 `, `e4 `, `e5 `, `e6 `, `e7 `,
88 `e8 `, `e9 `, `ea `, `eb `, `ec `, `ed `, `ee `, `ef `,
89 `f0 `, `f1 `, `f2 `, `f3 `, `f4 `, `f5 `, `f6 `, `f7 `,
90 `f8 `, `f9 `, `fa `, `fb `, `fc `, `fd `, `fe `, `ff `,
91 }
92
93 func Main() {
94 args := os.Args[1:]
95
96 if len(args) > 0 {
97 switch args[0] {
98 case `-h`, `--h`, `-help`, `--help`:
99 os.Stdout.WriteString(info[1:])
100 return
101 }
102 }
103
104 if len(args) > 0 && args[0] == `--` {
105 args = args[1:]
106 }
107
108 if err := run(args); err != nil {
109 os.Stdout.WriteString(err.Error())
110 os.Stdout.WriteString("\n")
111 os.Exit(1)
112 return
113 }
114 }
115
116 func run(args []string) error {
117 w := bufio.NewWriterSize(os.Stdout, 32*1024)
118 defer w.Flush()
119
120 // with no filenames given, handle stdin and quit
121 if len(args) == 0 {
122 return handle(w, os.Stdin, `<stdin>`, -1)
123 }
124
125 for i, fname := range args {
126 if i > 0 {
127 w.WriteString("\n")
128 w.WriteString("\n")
129 }
130
131 if err := handleFile(w, fname); err != nil {
132 return err
133 }
134 }
135
136 return nil
137 }
138
139 func handleFile(w *bufio.Writer, fname string) error {
140 f, err := os.Open(fname)
141 if err != nil {
142 return err
143 }
144 defer f.Close()
145
146 stat, err := f.Stat()
147 if err != nil {
148 return handle(w, f, fname, -1)
149 }
150
151 fsize := int(stat.Size())
152 return handle(w, f, fname, fsize)
153 }
154
155 // handle shows some messages related to the input and the cmd-line options
156 // used, and then follows them by the hexadecimal byte-view
157 func handle(w *bufio.Writer, r io.Reader, name string, size int) error {
158 owidth10 := -1
159 owidth16 := -1
160 if size > 0 {
161 w10 := math.Log10(float64(size))
162 w10 = math.Max(math.Ceil(w10), 1)
163 w16 := math.Log2(float64(size)) / 4
164 w16 = math.Max(math.Ceil(w16), 1)
165 owidth10 = int(w10)
166 owidth16 = int(w16)
167 }
168
169 if owidth10 < 0 {
170 owidth10 = 8
171 }
172 if owidth16 < 0 {
173 owidth16 = 8
174 }
175
176 rc := rendererConfig{
177 out: w,
178 offsetWidth10: max(owidth10, 8),
179 offsetWidth16: max(owidth16, 8),
180 }
181
182 if size < 0 {
183 fmt.Fprintf(w, "• %s\n", name)
184 } else {
185 const fs = "• %s (%s bytes)\n"
186 fmt.Fprintf(w, fs, name, sprintCommas(size))
187 }
188 w.WriteByte('\n')
189
190 // calling func Read directly can sometimes result in chunks shorter
191 // than the max chunk-size, even when there are plenty of bytes yet
192 // to read; to avoid that, use a buffered-reader to explicitly fill
193 // a slice instead
194 br := bufio.NewReader(r)
195
196 // to show ASCII up to 1 full chunk ahead, 2 chunks are needed
197 cur := make([]byte, 0, perLine)
198 ahead := make([]byte, 0, perLine)
199
200 // the ASCII-panel's wide output requires staying 1 step/chunk behind,
201 // so to speak
202 cur, err := fillChunk(cur[:0], perLine, br)
203 if len(cur) == 0 {
204 if err == io.EOF {
205 err = nil
206 }
207 return err
208 }
209
210 for {
211 ahead, err := fillChunk(ahead[:0], perLine, br)
212 if err != nil && err != io.EOF {
213 return err
214 }
215
216 if len(ahead) == 0 {
217 // done, maybe except for an extra line of output
218 break
219 }
220
221 // show the byte-chunk on its own output line
222 if err := writeChunk(rc, cur, ahead); err != nil {
223 return io.EOF
224 }
225
226 rc.offset += uint(len(cur))
227 cur = cur[:copy(cur, ahead)]
228 }
229
230 // don't forget the last output line
231 if len(cur) > 0 {
232 return writeChunk(rc, cur, nil)
233 }
234 return nil
235 }
236
237 // fillChunk tries to read the number of bytes given, appending them to the
238 // byte-slice given; this func returns an EOF error only when no bytes are
239 // read, which somewhat simplifies error-handling for the func caller
240 func fillChunk(chunk []byte, n int, br *bufio.Reader) ([]byte, error) {
241 // read buffered-bytes up to the max chunk-size
242 for i := 0; i < n; i++ {
243 b, err := br.ReadByte()
244 if err == nil {
245 chunk = append(chunk, b)
246 continue
247 }
248
249 if err == io.EOF && i > 0 {
250 return chunk, nil
251 }
252 return chunk, err
253 }
254
255 // got the full byte-count asked for
256 return chunk, nil
257 }
258
259 // rendererConfig groups several arguments given to any of the rendering funcs
260 type rendererConfig struct {
261 // out is writer to send all output to
262 out *bufio.Writer
263
264 // offset is the byte-offset of the first byte shown on the current output
265 // line: if shown at all, it's shown at the start the line
266 offset uint
267
268 // offsetWidth10 is the max string-width for the base-10 byte-offsets
269 // shown at the start of output lines, and determines those values'
270 // left-padding
271 offsetWidth10 int
272
273 // offsetWidth16 is the max string-width for the base-16 byte-offsets
274 // shown at the start of output lines, and determines those values'
275 // left-padding
276 offsetWidth16 int
277 }
278
279 // loopThousandsGroups comes from my lib/package `mathplus`: that's why it
280 // handles negatives, even though this app only uses it with non-negatives.
281 func loopThousandsGroups(n int, fn func(i, n int)) {
282 // 0 doesn't have a log10
283 if n == 0 {
284 fn(0, 0)
285 return
286 }
287
288 sign := +1
289 if n < 0 {
290 n = -n
291 sign = -1
292 }
293
294 intLog1000 := int(math.Log10(float64(n)) / 3)
295 remBase := int(math.Pow10(3 * intLog1000))
296
297 for i := 0; remBase > 0; i++ {
298 group := (1000 * n) / remBase / 1000
299 fn(i, sign*group)
300 // if original number was negative, ensure only first
301 // group gives a negative input to the callback
302 sign = +1
303
304 n %= remBase
305 remBase /= 1000
306 }
307 }
308
309 // sprintCommas turns the non-negative number given into a readable string,
310 // where digits are grouped-separated by commas
311 func sprintCommas(n int) string {
312 var sb strings.Builder
313 loopThousandsGroups(n, func(i, n int) {
314 if i == 0 {
315 var buf [4]byte
316 sb.Write(strconv.AppendInt(buf[:0], int64(n), 10))
317 return
318 }
319 sb.WriteByte(',')
320 writePad0Sub1000Counter(&sb, uint(n))
321 })
322 return sb.String()
323 }
324
325 // writePad0Sub1000Counter is an alternative to fmt.Fprintf(w, `%03d`, n)
326 func writePad0Sub1000Counter(w io.Writer, n uint) {
327 // precondition is 0...999
328 if n > 999 {
329 w.Write([]byte(`???`))
330 return
331 }
332
333 var buf [3]byte
334 buf[0] = byte(n/100) + '0'
335 n %= 100
336 buf[1] = byte(n/10) + '0'
337 buf[2] = byte(n%10) + '0'
338 w.Write(buf[:])
339 }
340
341 // writeHex is faster than calling fmt.Fprintf(w, `%02x`, b): this
342 // matters because it's called for every byte of input which isn't
343 // all 0s or all 1s
344 func writeHex(w *bufio.Writer, b byte) {
345 const hexDigits = `0123456789abcdef`
346 w.WriteByte(hexDigits[b>>4])
347 w.WriteByte(hexDigits[b&0x0f])
348 }
349
350 // padding is the padding/spacing emitted across each output line
351 const padding = 2
352
353 func writeChunk(rc rendererConfig, first, second []byte) error {
354 w := rc.out
355
356 // start each line with the byte-offset for the 1st item shown on it
357 // writeDecimalCounter(w, rc.offsetWidth10, rc.offset)
358 // w.WriteByte(' ')
359
360 // start each line with the byte-offset for the 1st item shown on it
361 writeHexadecimalCounter(w, rc.offsetWidth16, rc.offset)
362 w.WriteByte(' ')
363
364 for _, b := range first {
365 // fmt.Fprintf(w, ` %02x`, b)
366 //
367 // the commented part above was a performance bottleneck, since
368 // the slow/generic fmt.Fprintf was called for each input byte
369 w.WriteByte(' ')
370 writeHex(w, b)
371 }
372
373 writeASCII(w, first, second, perLine)
374 return w.WriteByte('\n')
375 }
376
377 // writeDecimalCounter just emits a left-padded number
378 func writeDecimalCounter(w *bufio.Writer, width int, n uint) {
379 var buf [24]byte
380 str := strconv.AppendUint(buf[:0], uint64(n), 10)
381 writeSpaces(w, width-len(str))
382 w.Write(str)
383 }
384
385 // writeHexadecimalCounter just emits a zero-padded base-16 number
386 func writeHexadecimalCounter(w *bufio.Writer, width int, n uint) {
387 var buf [24]byte
388 str := strconv.AppendUint(buf[:0], uint64(n), 16)
389 // writeSpaces(w, width-len(str))
390 for i := 0; i < width-len(str); i++ {
391 w.WriteByte('0')
392 }
393 w.Write(str)
394 }
395
396 // writeSpaces bulk-emits the number of spaces given
397 func writeSpaces(w *bufio.Writer, n int) {
398 const spaces = ` `
399 for ; n > len(spaces); n -= len(spaces) {
400 w.WriteString(spaces)
401 }
402 if n > 0 {
403 w.WriteString(spaces[:n])
404 }
405 }
406
407 // writeASCII emits the side-panel showing all ASCII runs for each line
408 func writeASCII(w *bufio.Writer, first, second []byte, perline int) {
409 spaces := padding + 3*(perline-len(first))
410
411 for _, b := range first {
412 if 32 < b && b < 127 {
413 writeSpaces(w, spaces)
414 w.WriteByte(b)
415 spaces = 0
416 } else {
417 spaces++
418 }
419 }
420
421 for _, b := range second {
422 if 32 < b && b < 127 {
423 writeSpaces(w, spaces)
424 w.WriteByte(b)
425 spaces = 0
426 } else {
427 spaces++
428 }
429 }
430 }
File: ./calc/calc.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package calc
26
27 import (
28 "errors"
29 "fmt"
30 "go/ast"
31 "go/parser"
32 "go/token"
33 "io"
34 "math"
35 "math/big"
36 "os"
37 "strings"
38 )
39
40 const info = `
41 ca [options...] [go expressions...]
42
43
44 CAlculate arbitrary-size fractions, using go expressions. For convenience,
45 function names are case-insensitive, numeric literals can have underscores,
46 and square brackets are treated the same as (round) parentheses.
47
48 Treating square brackets as parentheses extends the convenience of unquoted
49 arguments without spaces to also call functions, as parentheses are part of
50 command-line syntax, and would require quoting to pass as part of literal
51 command-line arguments otherwise.
52
53 Functions with only 1-input can also be called as fake-properties, using a
54 dot between the value and the function name: due to a quirk of the syntax,
55 integer literals require 2 dots, instead of 1 dot.
56
57 Several functions are available, along with their aliases:
58
59 abs(x)
60 bits(x)
61 c(n, k) choose, com, comb, combinations
62 ceil(x) ceiling
63 dbin(x, n, p) dbinom
64 den(x) denom, denominator
65 digits(x)
66 f(x) fac, fact, factorial
67 floor(x)
68 isprime(n) prime
69 gcd(x, y) gcf
70 lcm(x, y)
71 num(x) numer, numerator
72 p(n, k) per, perm, permutations
73 pow(x, y) power
74 pow2(x) power2
75 pow10(x) power10
76 rem(x, y) remainder
77 sgn(x) sign
78
79 avg(...) mean
80 max(...)
81 min(...)
82 polyval(x, ...) horner
83
84 Note: when the exponent given to the pow/power functions isn't an integer,
85 the result is a double-precision floating-point approximation.
86
87 All (optional) leading options start with either single or double-dash:
88
89 -d, -decs, -decimals show decimal digits, instead of fractions
90 -h, -help show this help message
91 -v, -verbose show a styled banner for each expression being run
92 `
93
94 type config struct {
95 showFractions bool
96 verbose bool
97 }
98
99 func Main() {
100 args := os.Args[1:]
101
102 var cfg config
103 cfg.verbose = false
104 cfg.showFractions = true
105
106 for len(args) > 0 {
107 switch args[0] {
108 case `-h`, `--h`, `-help`, `--help`:
109 os.Stdout.WriteString(info[1:])
110 return
111
112 case `-d`, `--d`, `-decs`, `--decs`, `-decimals`, `--decimals`:
113 cfg.showFractions = false
114 args = args[1:]
115 continue
116
117 case `-v`, `--v`, `-verbose`, `--verbose`:
118 cfg.verbose = true
119 args = args[1:]
120 continue
121 }
122
123 break
124 }
125
126 if len(args) > 0 && args[0] == `--` {
127 args = args[1:]
128 }
129
130 if len(args) == 0 {
131 os.Stderr.WriteString(info[1:])
132 os.Exit(1)
133 return
134 }
135
136 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
137 os.Stderr.WriteString(`calc: `)
138 os.Stderr.WriteString(err.Error())
139 os.Stderr.WriteString("\n")
140 os.Exit(1)
141 return
142 }
143 }
144
145 func run(w io.Writer, args []string, cfg config) error {
146 for _, src := range args {
147 src = strings.ToLower(src)
148
149 // treat square brackets like parentheses, for convenience
150 src = strings.Replace(src, `[`, `(`, -1)
151 src = strings.Replace(src, `]`, `)`, -1)
152
153 if cfg.verbose {
154 fmt.Fprintf(os.Stderr, "\x1b[7m%s\x1b[0m\n", src)
155 }
156
157 expr, err := parser.ParseExpr(src)
158 if err != nil {
159 return fmt.Errorf("%s: %s", src, err.Error())
160 }
161
162 n, err := eval(expr)
163 if err != nil {
164 return err
165 }
166
167 // only show the numerator, when the denominator is 1; when showing
168 // results as numbers with decimals, all trailing zero decimals are
169 // ignored
170 s := ``
171 if n.IsInt() {
172 s = n.Num().String()
173 } else if cfg.showFractions {
174 s = n.String()
175 } else {
176 s = trimDecimals(n.FloatString(100))
177 }
178
179 io.WriteString(w, s)
180 _, err = io.WriteString(w, "\n")
181
182 if err != nil {
183 break
184 }
185 }
186
187 return nil
188 }
189
190 // trimDecimals ignores excessive trailing decimal zeros, if any, as well as
191 // the decimal dot itself, if all decimals turn out to be zeros; integers are
192 // returned as given
193 func trimDecimals(s string) string {
194 // with no decimals, keep all/any trailing zeros
195 if strings.IndexByte(s, '.') < 0 {
196 return s
197 }
198
199 // ignore all trailing zero decimals
200 for len(s) > 0 && s[len(s)-1] == '0' {
201 s = s[:len(s)-1]
202 }
203 // ignore trailing decimal
204 if len(s) > 0 && s[len(s)-1] == '.' {
205 s = s[:len(s)-1]
206 }
207 return s
208 }
209
210 func eval(expr ast.Expr) (*big.Rat, error) {
211 switch expr := expr.(type) {
212 case *ast.BasicLit:
213 return evalLit(expr)
214 case *ast.ParenExpr:
215 return eval(expr.X)
216 case *ast.UnaryExpr:
217 return evalUnary(expr)
218 case *ast.SelectorExpr:
219 return evalSelector(expr)
220 case *ast.BinaryExpr:
221 return evalBinary(expr)
222 case *ast.CallExpr:
223 return evalCall(expr)
224 case *ast.Ident:
225 return evalIdent(expr)
226 }
227
228 return nil, errors.New(`unsupported expression type`)
229 }
230
231 func evalLit(expr *ast.BasicLit) (*big.Rat, error) {
232 switch expr.Kind {
233 case token.INT, token.FLOAT:
234 n := big.NewRat(0, 1)
235 n, _ = n.SetString(expr.Value)
236 return n, nil
237 }
238
239 return nil, errors.New(`unsupported literal type`)
240 }
241
242 func evalSelector(expr *ast.SelectorExpr) (*big.Rat, error) {
243 name := expr.Sel.Name
244 call := ast.CallExpr{
245 Fun: ast.NewIdent(name),
246 Args: []ast.Expr{expr.X},
247 }
248 return evalCall1(name, &call)
249 }
250
251 func evalUnary(expr *ast.UnaryExpr) (*big.Rat, error) {
252 switch expr.Op {
253 case token.ADD:
254 return eval(expr.X)
255
256 case token.SUB:
257 n, err := eval(expr.X)
258 if n != nil {
259 n = n.Neg(n)
260 }
261 return n, err
262
263 case token.NOT:
264 return eval(&ast.CallExpr{
265 Fun: ast.NewIdent(`factorial`),
266 Args: []ast.Expr{expr.X},
267 })
268 }
269
270 return nil, errors.New(`unsupported unary operation ` + expr.Op.String())
271 }
272
273 func evalBinary(expr *ast.BinaryExpr) (*big.Rat, error) {
274 x, err := eval(expr.X)
275 if err != nil {
276 return nil, err
277 }
278
279 y, err := eval(expr.Y)
280 if err != nil {
281 return nil, err
282 }
283
284 z := big.NewRat(0, 1)
285
286 switch expr.Op {
287 case token.ADD:
288 z = z.Add(x, y)
289 return z, nil
290
291 case token.SUB:
292 z = z.Sub(x, y)
293 return z, nil
294
295 case token.MUL:
296 z = z.Mul(x, y)
297 return z, nil
298
299 case token.QUO:
300 if y.Sign() == 0 {
301 return nil, errors.New(`can't divide by zero`)
302 }
303 z = z.Quo(x, y)
304 return z, nil
305
306 case token.REM:
307 return remainder(x, y)
308 }
309
310 return nil, errors.New(`unsupported binary operation ` + expr.Op.String())
311 }
312
313 func evalCall(expr *ast.CallExpr) (*big.Rat, error) {
314 ident, ok := expr.Fun.(*ast.Ident)
315 if !ok {
316 return nil, errors.New(`unsupported function type`)
317 }
318 s := ident.Name
319
320 if _, ok := varFuncs[s]; ok {
321 return evalVarCall(s, expr)
322 }
323
324 switch len(expr.Args) {
325 case 1:
326 return evalCall1(s, expr)
327 case 2:
328 return evalCall2(s, expr)
329 case 3:
330 return evalCall3(s, expr)
331 }
332
333 return nil, errors.New(`function '` + s + `' not available`)
334 }
335
336 func evalIdent(expr *ast.Ident) (*big.Rat, error) {
337 s := strings.ToLower(expr.Name)
338 if v, ok := values[s]; ok {
339 if f, ok := big.NewRat(0, 1).SetString(v); ok {
340 return f, nil
341 }
342 return nil, errors.New(`value '` + s + `' isn't a valid number`)
343 }
344 return nil, errors.New(`value '` + s + `' not available`)
345 }
346
347 func copyFrac(x *big.Rat) *big.Rat {
348 y := big.NewRat(0, 1)
349 y = y.Add(y, x)
350 return y
351 }
352
353 var values = map[string]string{
354 `kb`: `1024`,
355 `mb`: `1048576`,
356 `gb`: `1073741824`,
357 `tb`: `1099511627776`,
358 `pb`: `1125899906842624`,
359 `kib`: `1024`,
360 `mib`: `1048576`,
361 `gib`: `1073741824`,
362 `tib`: `1099511627776`,
363 `pib`: `1125899906842624`,
364
365 `hour`: `3600`,
366 `hr`: `3600`,
367 `day`: `86400`,
368 `week`: `604800`,
369 `wk`: `604800`,
370
371 `mol`: `602214076000000000000000`,
372 `mole`: `602214076000000000000000`,
373 }
374
375 var funcs1 = map[string]func(*big.Rat) (*big.Rat, error){
376 `abs`: abs,
377 `bits`: bits,
378 `ceil`: ceiling,
379 `ceiling`: ceiling,
380 `den`: denominator,
381 `denom`: denominator,
382 `denominator`: denominator,
383 `digits`: digits,
384 `f`: factorial,
385 `fac`: factorial,
386 `fact`: factorial,
387 `factorial`: factorial,
388 `floor`: floor,
389 `isprime`: isPrime,
390 `prime`: isPrime,
391 `num`: numerator,
392 `numer`: numerator,
393 `numerator`: numerator,
394 `pow2`: power2,
395 `power2`: power2,
396 `pow10`: power10,
397 `power10`: power10,
398 `sgn`: sign,
399 `sign`: sign,
400 }
401
402 func evalCall1(name string, expr *ast.CallExpr) (*big.Rat, error) {
403 x, err := eval(expr.Args[0])
404 if err != nil {
405 return nil, err
406 }
407
408 fn, ok := funcs1[name]
409 if !ok {
410 return nil, errors.New(`function '` + name + `' not available`)
411 }
412
413 return fn(x)
414 }
415
416 var funcs2 = map[string]func(*big.Rat, *big.Rat) (*big.Rat, error){
417 `c`: combinations,
418 `com`: combinations,
419 `comb`: combinations,
420 `combinations`: combinations,
421 `choose`: combinations,
422 `gcd`: gcd,
423 `gcf`: gcd,
424 `lcm`: lcm,
425 `p`: permutations,
426 `per`: permutations,
427 `perm`: permutations,
428 `permutations`: permutations,
429 `pow`: power,
430 `power`: power,
431 `rem`: remainder,
432 `remainder`: remainder,
433 }
434
435 func evalCall2(name string, expr *ast.CallExpr) (*big.Rat, error) {
436 x, err := eval(expr.Args[0])
437 if err != nil {
438 return nil, err
439 }
440
441 y, err := eval(expr.Args[1])
442 if err != nil {
443 return nil, err
444 }
445
446 fn, ok := funcs2[name]
447 if !ok {
448 return nil, errors.New(`function '` + name + `' not available`)
449 }
450
451 return fn(x, y)
452 }
453
454 var funcs3 = map[string]func(*big.Rat, *big.Rat, *big.Rat) (*big.Rat, error){
455 `db`: dbinom,
456 `dbin`: dbinom,
457 `dbinom`: dbinom,
458 }
459
460 func evalCall3(name string, expr *ast.CallExpr) (*big.Rat, error) {
461 x, err := eval(expr.Args[0])
462 if err != nil {
463 return nil, err
464 }
465
466 y, err := eval(expr.Args[1])
467 if err != nil {
468 return nil, err
469 }
470
471 z, err := eval(expr.Args[2])
472 if err != nil {
473 return nil, err
474 }
475
476 fn, ok := funcs3[name]
477 if !ok {
478 return nil, errors.New(`function '` + name + `' not available`)
479 }
480
481 return fn(x, y, z)
482 }
483
484 var varFuncs = map[string]func(...*big.Rat) (*big.Rat, error){
485 `avg`: avgNum,
486 `horner`: polyval,
487 `max`: maxNum,
488 `mean`: avgNum,
489 `min`: minNum,
490 `polyval`: polyval,
491 `sum`: sumNum,
492 }
493
494 func evalVarCall(name string, expr *ast.CallExpr) (*big.Rat, error) {
495 fn, ok := varFuncs[name]
496 if !ok {
497 return nil, errors.New(`function '` + name + `' not available`)
498 }
499
500 inputs := make([]*big.Rat, 0, len(expr.Args))
501 for _, a := range expr.Args {
502 v, err := eval(a)
503 if err != nil {
504 return nil, err
505 }
506 inputs = append(inputs, v)
507 }
508
509 return fn(inputs...)
510 }
511
512 func abs(n *big.Rat) (*big.Rat, error) {
513 n = n.Abs(n)
514 return n, nil
515 }
516
517 func avgNum(values ...*big.Rat) (*big.Rat, error) {
518 if len(values) == 0 {
519 return nil, errors.New(`mean: no numbers given`)
520 }
521
522 res := big.NewRat(0, 1)
523 for _, v := range values {
524 res = res.Add(res, v)
525 }
526 res = res.Quo(res, big.NewRat(int64(len(values)), 1))
527 return res, nil
528 }
529
530 func bits(n *big.Rat) (*big.Rat, error) {
531 if !n.IsInt() {
532 return nil, errors.New(`function 'bits' only works with integers`)
533 }
534
535 bits := big.NewRat(0, 1)
536 bits.SetInt64(int64(n.Num().BitLen()))
537 return bits, nil
538 }
539
540 func ceiling(n *big.Rat) (*big.Rat, error) {
541 if n.IsInt() {
542 return n, nil
543 }
544
545 v := big.NewInt(0)
546 v = v.Quo(n.Num(), n.Denom())
547 if n.Sign() >= 0 {
548 v = v.Add(v, big.NewInt(1))
549 }
550 n = n.SetInt(v)
551 return n, nil
552 }
553
554 func combinations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
555 if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
556 const msg = `combinations are defined only for non-negative integers`
557 return nil, errors.New(msg)
558 }
559
560 v, err := permutations(n, k)
561 if err != nil {
562 return v, err
563 }
564
565 f, err := factorial(k)
566 if err != nil {
567 return nil, err
568 }
569
570 if f.Sign() <= 0 {
571 return nil, errors.New(`combinations: factorial isn't positive`)
572 }
573 return v.Quo(v, f), nil
574 }
575
576 func dbinom(x *big.Rat, n *big.Rat, p *big.Rat) (*big.Rat, error) {
577 a, err := combinations(copyFrac(n), copyFrac(x))
578 if err != nil {
579 return nil, err
580 }
581
582 b, err := power(copyFrac(p), copyFrac(x))
583 if err != nil {
584 return nil, err
585 }
586
587 // c = (1 - p) ** (n - x)
588 y := big.NewRat(1, 1)
589 y = y.Sub(y, p)
590 z := copyFrac(n)
591 z = z.Sub(z, x)
592 c, err := power(y, z)
593 if err != nil {
594 return nil, err
595 }
596
597 // return combinations(n, x) * (p ** x) * ((1 - p) ** (n - x))
598 d := big.NewRat(0, 1)
599 d = d.Add(d, a)
600 d = d.Mul(d, b)
601 d = d.Mul(d, c)
602 return d, nil
603 }
604
605 func denominator(n *big.Rat) (*big.Rat, error) {
606 return big.NewRat(0, 1).SetFrac(n.Denom(), big.NewInt(1)), nil
607 }
608
609 func digits(n *big.Rat) (*big.Rat, error) {
610 if !n.IsInt() {
611 return nil, errors.New(`function 'digits' only works with integers`)
612 }
613
614 digits := big.NewRat(0, 1)
615 digits.SetInt64(int64(len(n.Num().String())))
616 return digits, nil
617 }
618
619 func factorial(n *big.Rat) (*big.Rat, error) {
620 sign := n.Sign()
621 if sign < 0 {
622 return nil, errors.New(`factorials aren't defined for negatives`)
623 }
624 if sign == 0 {
625 return big.NewRat(1, 1), nil
626 }
627
628 f := big.NewRat(1, 1)
629 for one := big.NewRat(1, 1); n.Sign() > 0; n = n.Sub(n, one) {
630 f = f.Mul(f, n)
631 }
632 return f, nil
633 }
634
635 func floor(n *big.Rat) (*big.Rat, error) {
636 if n.IsInt() {
637 return n, nil
638 }
639
640 v := big.NewInt(0)
641 v = v.Quo(n.Num(), n.Denom())
642 if n.Sign() < 0 {
643 v = v.Sub(v, big.NewInt(1))
644 }
645 n = n.SetInt(v)
646 return n, nil
647 }
648
649 func gcd(x *big.Rat, y *big.Rat) (*big.Rat, error) {
650 if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
651 const msg = `gcd are defined only for positive integers`
652 return nil, errors.New(msg)
653 }
654
655 gcd := big.NewRat(0, 1)
656 gcd = gcd.Add(gcd, x)
657 gcd = gcd.Mul(gcd, y)
658
659 lcm, err := lcm(x, y)
660 if err != nil {
661 return nil, err
662 }
663 if lcm.Sign() <= 0 {
664 return nil, errors.New(`gcd: lcm isn't positive`)
665 }
666
667 gcd = gcd.Quo(gcd, lcm)
668 return gcd, nil
669 }
670
671 func isPrime(n *big.Rat) (*big.Rat, error) {
672 if !n.IsInt() {
673 return nil, errors.New(`function 'isprime' only works with integers`)
674 }
675
676 if n.Sign() <= 0 {
677 return big.NewRat(0, 1), nil
678 }
679
680 v := n.Num()
681 if v.IsInt64() {
682 n := v.Int64()
683 if n == 2 {
684 return big.NewRat(1, 1), nil
685 }
686 if n < 2 || n%2 == 0 {
687 return big.NewRat(0, 1), nil
688 }
689 }
690
691 two := big.NewInt(2)
692 max := big.NewInt(1).Sqrt(v)
693 mod := big.NewInt(0)
694 for i := big.NewInt(3); i.Cmp(max) <= 0; i = i.Add(i, two) {
695 mod = mod.Rem(v, i)
696 if mod.Sign() == 0 {
697 return big.NewRat(0, 1), nil
698 }
699 }
700 return big.NewRat(1, 1), nil
701 }
702
703 func lcm(x *big.Rat, y *big.Rat) (*big.Rat, error) {
704 if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
705 const msg = `lcm is defined only for positive integers`
706 return nil, errors.New(msg)
707 }
708
709 // a = min(x, y)
710 // b = max(x, y)
711 var a, b *big.Int
712 if x.Cmp(y) < 0 {
713 a = x.Num()
714 b = y.Num()
715 } else {
716 a = y.Num()
717 b = x.Num()
718 }
719
720 // c = b
721 c := big.NewInt(0)
722 c = c.Add(c, b)
723
724 // while (c % a > 0) c += b
725 for r := big.NewInt(1); r.Sign() > 0; r = r.Rem(c, a) {
726 c = c.Add(c, b)
727 }
728
729 // return c
730 return big.NewRat(0, 1).SetFrac(c, big.NewInt(1)), nil
731 }
732
733 func maxNum(values ...*big.Rat) (*big.Rat, error) {
734 if len(values) == 0 {
735 return nil, errors.New(`max: no numbers given`)
736 }
737
738 var max *big.Rat
739 for i, v := range values {
740 if i == 0 || max.Cmp(v) < 0 {
741 max = v
742 }
743 }
744 return max, nil
745 }
746
747 func minNum(values ...*big.Rat) (*big.Rat, error) {
748 if len(values) == 0 {
749 return nil, errors.New(`min: no numbers given`)
750 }
751
752 var min *big.Rat
753 for i, v := range values {
754 if i == 0 || min.Cmp(v) < 0 {
755 min = v
756 }
757 }
758 return min, nil
759 }
760
761 func numerator(n *big.Rat) (*big.Rat, error) {
762 return big.NewRat(0, 1).SetFrac(n.Num(), big.NewInt(1)), nil
763 }
764
765 func permutations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
766 if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
767 const msg = `permutations are defined only for non-negative integers`
768 return nil, errors.New(msg)
769 }
770
771 one := big.NewRat(1, 1)
772 perm := big.NewRat(1, 1)
773 // end = n - k + 1
774 end := big.NewRat(1, 1).Set(n)
775 end = end.Sub(end, k)
776 end = end.Add(end, one)
777
778 for v := big.NewRat(1, 1).Set(n); v.Cmp(end) >= 0; v = v.Sub(v, one) {
779 perm = perm.Mul(perm, v)
780 }
781 return perm, nil
782 }
783
784 // polyval evaluates a polynomial using Horner's algorithm: the first number is
785 // the x value to evaulate the polynomial with, followed by all the polynomial
786 // coefficients in textbook order, from the highest power down to the final
787 // constant
788 func polyval(values ...*big.Rat) (*big.Rat, error) {
789 if len(values) == 0 {
790 // return big.NewRat(0, 1), nil
791 return nil, errors.New(`polyval: no numbers given`)
792 }
793
794 x0 := values[0]
795 values = values[1:]
796
797 x := big.NewRat(1, 1)
798 y := big.NewRat(0, 1)
799 prod := big.NewRat(0, 1)
800
801 for i := len(values) - 1; i >= 0; i-- {
802 prod = prod.Mul(values[i], x)
803 y = y.Add(y, prod)
804 x = x.Mul(x, x0)
805 }
806
807 return y, nil
808 }
809
810 func power(x *big.Rat, y *big.Rat) (*big.Rat, error) {
811 // if !y.IsInt() {
812 // return nil, errors.New(`only integer exponents are supported`)
813 // }
814
815 if !y.IsInt() {
816 a, _ := x.Float64()
817 b, _ := y.Float64()
818 c := math.Pow(a, b)
819 if math.IsNaN(c) || math.IsInf(c, 0) {
820 return nil, errors.New(`can't calculate/approximate power given`)
821 }
822 z := big.NewRat(0, 1)
823 z = z.SetFloat64(c)
824 return z, nil
825 }
826
827 if x.Sign() == 0 && y.Sign() == 0 {
828 return nil, errors.New(`zero to the zero power isn't defined`)
829 }
830
831 if x.Sign() == 0 {
832 return big.NewRat(0, 1), nil
833 }
834 if y.Sign() == 0 {
835 return big.NewRat(1, 1), nil
836 }
837
838 return powFractionInPlace(x, y.Num())
839 }
840
841 // powFractionInPlace calculates values in place: since bignums are pointers
842 // to their representations, this means the original values will change
843 func powFractionInPlace(x *big.Rat, y *big.Int) (*big.Rat, error) {
844 xsign := x.Sign()
845 ysign := y.Sign()
846
847 // 0 ** 0 is undefined
848 if xsign == 0 && ysign == 0 {
849 const msg = `0 to the 0 doesn't make sense`
850 return nil, errors.New(msg)
851 }
852
853 // otherwise x ** 0 is 1
854 if ysign == 0 {
855 return big.NewRat(1, 1), nil
856 }
857
858 // x ** (y < 0) is like (1/x) ** -y
859 if ysign < 0 {
860 inv := big.NewRat(1, 1).Inv(x)
861 neg := big.NewInt(1).Neg(y)
862 return powFractionInPlace(inv, neg)
863 }
864
865 // 0 ** (y > 0) is 0
866 if xsign == 0 {
867 return x, nil
868 }
869
870 // x ** 0 is 0
871 if ysign == 0 {
872 return big.NewRat(0, 1), nil
873 }
874
875 // x ** 1 is x
876 if y.IsInt64() && y.Int64() == 1 {
877 return x, nil
878 }
879
880 return _powFractionRec(x, y), nil
881 }
882
883 func _powFractionRec(x *big.Rat, y *big.Int) *big.Rat {
884 switch y.Sign() {
885 case -1:
886 return big.NewRat(0, 1)
887 case 0:
888 return big.NewRat(1, 1)
889 case 1:
890 if y.IsInt64() && y.Int64() == 1 {
891 return x
892 }
893 }
894
895 yhalf := big.NewInt(0)
896 oddrem := big.NewInt(0)
897 yhalf.QuoRem(y, big.NewInt(2), oddrem)
898
899 if oddrem.Sign() == 0 {
900 xsquare := big.NewRat(0, 1)
901 return _powFractionRec(xsquare.Mul(x, x), yhalf)
902 }
903 prevpow := _powFractionRec(x, y.Sub(y, big.NewInt(1)))
904 return prevpow.Mul(prevpow, x)
905 }
906
907 func power2(x *big.Rat) (*big.Rat, error) {
908 return power(big.NewRat(2, 1), x)
909 }
910
911 func power10(x *big.Rat) (*big.Rat, error) {
912 return power(big.NewRat(10, 1), x)
913 }
914
915 func remainder(x *big.Rat, y *big.Rat) (*big.Rat, error) {
916 if !x.IsInt() || !y.IsInt() {
917 return nil, errors.New(`remainder only works with 2 integers`)
918 }
919
920 if y.Sign() == 0 {
921 return nil, errors.New(`can't divide by 0`)
922 }
923
924 a := x.Num()
925 b := y.Num()
926 c := big.NewInt(0)
927 c = c.Rem(a, b)
928 rem := big.NewRat(0, 1)
929 rem = rem.SetInt(c)
930 return rem, nil
931 }
932
933 func sign(n *big.Rat) (*big.Rat, error) {
934 sign := n.Sign()
935 if sign > 0 {
936 n = big.NewRat(1, 1)
937 } else if sign < 0 {
938 n = big.NewRat(-1, 1)
939 } else {
940 n = big.NewRat(0, 1)
941 }
942 return n, nil
943 }
944
945 func sumNum(values ...*big.Rat) (*big.Rat, error) {
946 sum := big.NewRat(0, 1)
947 for _, v := range values {
948 sum = sum.Add(sum, v)
949 }
950 return sum, nil
951 }
File: ./cat/cat.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package cat
26
27 import (
28 "io"
29 "os"
30 )
31
32 const info = `
33 cat [options...] [files...]
34
35 Concatenate files to the standard output.
36
37 Options
38
39 --help show this help message
40 `
41
42 func Main() {
43 args := os.Args[1:]
44 for len(args) > 0 {
45 switch args[0] {
46 case `--help`:
47 os.Stderr.WriteString(info[1:])
48 return
49 }
50
51 break
52 }
53
54 if len(args) > 0 && args[0] == `--` {
55 args = args[1:]
56 }
57
58 for _, path := range args {
59 if err := handleFile(os.Stdout, path); err != nil {
60 if err == io.EOF {
61 break
62 }
63
64 os.Stderr.WriteString(`cat: `)
65 os.Stderr.WriteString(err.Error())
66 os.Stderr.WriteString("\n")
67 os.Exit(1)
68 return
69 }
70 }
71
72 if len(args) == 0 {
73 cat(os.Stdout, os.Stdin)
74 }
75 }
76
77 func handleFile(w io.Writer, path string) error {
78 f, err := os.Open(path)
79 if err != nil {
80 return err
81 }
82 defer f.Close()
83 return cat(w, f)
84 }
85
86 func cat(w io.Writer, r io.Reader) error {
87 var buf [32 * 1024]byte
88
89 for {
90 got, err := r.Read(buf[:])
91 if err == io.EOF {
92 if got > 0 {
93 w.Write(buf[:got])
94 }
95 break
96 }
97
98 if err != nil {
99 return err
100 }
101
102 if _, err := w.Write(buf[:got]); err != nil {
103 return io.EOF
104 }
105 }
106
107 return nil
108 }
File: ./catl/catl.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package catl
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 )
34
35 const info = `
36 catl [options...] [file...]
37
38
39 Unlike "cat", conCATenate Lines ensures lines across inputs are never joined
40 by accident, when an input's last line doesn't end with a line-feed.
41
42 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
43 feeds. Leading BOM (byte-order marks) on first lines are also ignored.
44
45 All (optional) leading options start with either single or double-dash:
46
47 -h, -help show this help message
48 -0, -null turn null-byte-delimited chunks into proper lines
49 `
50
51 type config struct {
52 null bool
53 liveLines bool
54 }
55
56 func Main() {
57 var cfg config
58 cfg.liveLines = true
59 args := os.Args[1:]
60
61 for len(args) > 0 {
62 switch args[0] {
63 case `-0`, `--0`, `-null`, `--null`:
64 cfg.null = true
65 args = args[1:]
66 continue
67
68 case `-b`, `--b`, `-buffered`, `--buffered`:
69 cfg.liveLines = false
70 args = args[1:]
71 continue
72
73 case `-h`, `--h`, `-help`, `--help`:
74 os.Stdout.WriteString(info[1:])
75 return
76 }
77
78 break
79 }
80
81 if len(args) > 0 && args[0] == `--` {
82 args = args[1:]
83 }
84
85 if cfg.liveLines {
86 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
87 cfg.liveLines = false
88 }
89 }
90
91 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
92 os.Stderr.WriteString(`catl: `)
93 os.Stderr.WriteString(err.Error())
94 os.Stderr.WriteString("\n")
95 os.Exit(1)
96 return
97 }
98 }
99
100 func run(w io.Writer, args []string, cfg config) error {
101 bw := bufio.NewWriterSize(w, 32*1024)
102 defer bw.Flush()
103
104 dashes := 0
105 for _, name := range args {
106 if name == `-` {
107 dashes++
108 }
109 if dashes > 1 {
110 break
111 }
112 }
113
114 if len(args) == 0 {
115 return catl(bw, os.Stdin, cfg)
116 }
117
118 var stdin []byte
119 gotStdin := false
120
121 for _, name := range args {
122 if name == `-` {
123 if dashes == 1 {
124 if err := catl(bw, os.Stdin, cfg); err != nil {
125 return err
126 }
127 continue
128 }
129
130 if !gotStdin {
131 data, err := io.ReadAll(os.Stdin)
132 if err != nil {
133 return err
134 }
135 stdin = data
136 gotStdin = true
137 }
138
139 bw.Write(stdin)
140 if len(stdin) > 0 && stdin[len(stdin)-1] != '\n' {
141 bw.WriteByte('\n')
142 }
143
144 if cfg.liveLines && bw.Flush() != nil {
145 return io.EOF
146 }
147
148 continue
149 }
150
151 if err := handleFile(bw, name, cfg); err != nil {
152 return err
153 }
154 }
155 return nil
156 }
157
158 func handleFile(w *bufio.Writer, name string, cfg config) error {
159 if name == `` || name == `-` {
160 return catl(w, os.Stdin, cfg)
161 }
162
163 f, err := os.Open(name)
164 if err != nil {
165 return errors.New(`can't read from file named "` + name + `"`)
166 }
167 defer f.Close()
168
169 return catl(w, f, cfg)
170 }
171
172 func catl(w *bufio.Writer, r io.Reader, cfg config) error {
173 if !cfg.liveLines {
174 return catlFast(w, r, cfg.null)
175 }
176
177 const gb = 1024 * 1024 * 1024
178 sc := bufio.NewScanner(r)
179 sc.Buffer(nil, 8*gb)
180 if cfg.null {
181 sc.Split(splitNull)
182 }
183
184 for i := 0; sc.Scan(); i++ {
185 s := sc.Bytes()
186 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
187 s = s[3:]
188 }
189
190 w.Write(s)
191 if w.WriteByte('\n') != nil {
192 return io.EOF
193 }
194
195 if w.Flush() != nil {
196 return io.EOF
197 }
198 }
199
200 return sc.Err()
201 }
202
203 func catlFast(w *bufio.Writer, r io.Reader, null bool) error {
204 var buf [32 * 1024]byte
205 var last byte = '\n'
206
207 for i := 0; true; i++ {
208 n, err := r.Read(buf[:])
209 if n > 0 && err == io.EOF {
210 err = nil
211 }
212 if err == io.EOF {
213 if last != '\n' {
214 w.WriteByte('\n')
215 }
216 return nil
217 }
218
219 if err != nil {
220 return err
221 }
222
223 chunk := buf[:n]
224 if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
225 chunk = chunk[3:]
226 }
227
228 // change nulls into line-feeds to handle null-terminated lines
229 if null {
230 for i, b := range chunk {
231 if b == 0 {
232 chunk[i] = '\n'
233 }
234 }
235 }
236
237 if len(chunk) >= 1 {
238 if _, err := w.Write(chunk); err != nil {
239 return io.EOF
240 }
241 last = chunk[len(chunk)-1]
242 }
243 }
244
245 return nil
246 }
247
248 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
249 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
250 // handle leading null-terminated line, if found in the current chunk
251 if i := bytes.IndexByte(data, 0); i >= 0 {
252 return i + 1, data[:i], nil
253 }
254
255 // request more data, in case there's a null coming up later
256 if !atEOF {
257 return 0, nil, nil
258 }
259
260 // handle non-empty non-terminated last chunk
261 if len(data) > 0 {
262 return len(data), data, bufio.ErrFinalToken
263 }
264
265 // handle empty non-terminated last chunk
266 return 0, nil, bufio.ErrFinalToken
267 }
File: ./cbl/cbl.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package cbl
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "io/fs"
33 "os"
34 "path/filepath"
35 "runtime"
36 "strconv"
37 "sync"
38 )
39
40 const info = `
41 cbl [options...] [files/folders...]
42
43
44 Count Bytes and Lines finds out these simple byte-related stats: the latter
45 stat (line-count) assumes files are text-based, and thus isn't meaningful
46 for general binary data.
47
48 The output is TSV (tab-separated values) lines, where the first line has
49 all the column names.
50
51 When no filepaths are given, the standard input is used by default. All
52 folder names given expand recursively into all filenames in them. A mix
53 of files/folders is supported for convenience.
54
55 The only option available is to show this help message, using any of
56 "-h", "--h", "-help", or "--help", without the quotes.
57 `
58
59 // event has what the output-reporting task needs to show the results of a
60 // task which has just completed, perhaps unsuccessfully
61 type event struct {
62 // Index points to the task's entry in the results-slice
63 Index int
64
65 // Stats has all the byte-related stats
66 Stats stats
67
68 // Err is the completed task's error, or lack of
69 Err error
70 }
71
72 func Main() {
73 args := os.Args[1:]
74
75 if len(args) > 0 {
76 switch args[0] {
77 case `-h`, `--h`, `-help`, `--help`:
78 os.Stdout.WriteString(info[1:])
79 return
80
81 case `--`:
82 args = args[1:]
83 }
84 }
85
86 // show first/heading line right away, to let users know things are
87 // happening
88 os.Stdout.WriteString("name\tbytes\tlines")
89
90 // assume an error means later stages/apps in a pipe had enough input and
91 // quit successfully, so quit successfully too
92 _, err := os.Stdout.WriteString("\n")
93 if err != nil {
94 return
95 }
96
97 // names has all filepaths given, ignoring repetitions
98 names, ok := findAllFiles(args)
99 if !ok {
100 os.Exit(1)
101 return
102 }
103 if len(names) == 0 {
104 names = []string{`-`}
105 }
106
107 events := make(chan event)
108 go handleInputs(names, events)
109 if !handleOutput(os.Stdout, len(names), events) {
110 os.Exit(1)
111 return
112 }
113 }
114
115 // handleInputs launches all the tasks which do the actual work, limiting how
116 // many inputs are being worked on at the same time
117 func handleInputs(names []string, events chan<- event) {
118 defer close(events) // allow the output-reporter task to end
119
120 var tasks sync.WaitGroup
121 // the number of tasks is always known in advance
122 tasks.Add(len(names))
123
124 // permissions is buffered to limit concurrency to the core-count
125 permissions := make(chan struct{}, runtime.NumCPU())
126 defer close(permissions)
127
128 for i, name := range names {
129 // wait until some concurrency-room is available, before proceeding
130 permissions <- struct{}{}
131
132 go func(i int, name string) {
133 defer tasks.Done()
134
135 res, err := handleInput(name)
136 <-permissions
137 events <- event{Index: i, Stats: res, Err: err}
138 }(i, name)
139 }
140
141 // wait for all inputs, before closing the `events` channel, which in turn
142 // would quit the whole app right away
143 tasks.Wait()
144 }
145
146 // handleInput handles each work-item for func handleInputs
147 func handleInput(path string) (stats, error) {
148 var res stats
149 res.name = path
150
151 if path == `-` {
152 err := res.updateStats(os.Stdin)
153 return res, err
154 }
155
156 f, err := os.Open(path)
157 if err != nil {
158 res.result = resultError
159 // on windows, file-not-found error messages may mention `CreateFile`,
160 // even when trying to open files in read-only mode
161 return res, errors.New(`can't open file named ` + path)
162 }
163 defer f.Close()
164
165 err = res.updateStats(f)
166 return res, err
167 }
168
169 // handleOutput asynchronously updates output as results are known, whether
170 // it's errors or successful results; returns whether it succeeded, which
171 // means no errors happened
172 func handleOutput(w io.Writer, inputs int, events <-chan event) (ok bool) {
173 bw := bufio.NewWriterSize(w, 32*1024)
174 defer bw.Flush()
175
176 ok = true
177 results := make([]stats, inputs)
178
179 // keep track of which tasks are over, so that on each event all leading
180 // results which are ready are shown: all of this ensures prompt output
181 // updates as soon as results come in, while keeping the original order
182 // of the names/filepaths given
183 resultsLeft := results
184
185 for v := range events {
186 results[v.Index] = v.Stats
187 if v.Err != nil {
188 ok = false
189 bw.Flush()
190 showError(v.Err)
191
192 // stay in the current loop, in case this failure was keeping
193 // previous successes from showing up
194 }
195
196 for len(resultsLeft) > 0 {
197 if resultsLeft[0].result == resultPending {
198 break
199 }
200
201 if err := showResult(bw, resultsLeft[0]); err != nil {
202 // assume later stages/apps in a pipe had enough input
203 return ok
204 }
205 resultsLeft = resultsLeft[1:]
206 }
207
208 // show leading results immediately, if any
209 bw.Flush()
210 }
211
212 return ok
213 }
214
215 func showError(err error) {
216 os.Stderr.WriteString(`cbl: `)
217 os.Stderr.WriteString(err.Error())
218 os.Stderr.WriteString("\n")
219 }
220
221 // showResult shows a TSV line for results marked as successful, doing nothing
222 // when given other types of results
223 func showResult(w *bufio.Writer, s stats) error {
224 if s.result != resultSuccess {
225 return nil
226 }
227
228 var buf [24]byte
229 w.WriteString(s.name)
230 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.bytes), 10))
231 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.lines), 10))
232 return w.WriteByte('\n')
233 }
234
235 // findAllFiles can be given a mix of file/folder paths, finding all files
236 // recursively in folders, avoiding duplicates
237 func findAllFiles(paths []string) (files []string, success bool) {
238 walk := filepath.WalkDir
239 got := make(map[string]struct{})
240 success = true
241
242 for _, path := range paths {
243 if _, ok := got[path]; ok {
244 continue
245 }
246 got[path] = struct{}{}
247
248 // a dash means standard input
249 if path == `-` {
250 files = append(files, path)
251 continue
252 }
253
254 info, err := os.Stat(path)
255 if os.IsNotExist(err) {
256 // on windows, file-not-found messages may mention `CreateFile`,
257 // even when trying to open files in read-only mode
258 err = errors.New(`can't find file/folder named ` + path)
259 }
260
261 if err != nil {
262 showError(err)
263 success = false
264 continue
265 }
266
267 if !info.IsDir() {
268 files = append(files, path)
269 continue
270 }
271
272 err = walk(path, func(path string, info fs.DirEntry, err error) error {
273 path, err = filepath.Abs(path)
274 if err != nil {
275 showError(err)
276 success = false
277 return err
278 }
279
280 if _, ok := got[path]; ok {
281 if info.IsDir() {
282 return fs.SkipDir
283 }
284 return nil
285 }
286 got[path] = struct{}{}
287
288 if err != nil {
289 showError(err)
290 success = false
291 return err
292 }
293
294 if info.IsDir() {
295 return nil
296 }
297
298 files = append(files, path)
299 return nil
300 })
301
302 if err != nil {
303 showError(err)
304 success = false
305 }
306 }
307
308 return files, success
309 }
310
311 // counter makes it easy to change the int-size of almost all counters
312 type counter uint64
313
314 // statResult constrains possible result-states/values in type stats
315 type statResult int
316
317 const (
318 // resultPending is the default not-yet-ready result-status
319 resultPending = statResult(0)
320
321 // resultError means result should show as an error, instead of data
322 resultError = statResult(1)
323
324 // resultSuccess means a result's stats are ready to show
325 resultSuccess = statResult(2)
326 )
327
328 // bomType is the type for the byte-order-mark enumeration
329 type bomType int
330
331 const (
332 noBOM = bomType(0)
333 utf8BOM = bomType(1)
334 utf16leBOM = bomType(2)
335 utf16beBOM = bomType(3)
336 utf32leBOM = bomType(4)
337 utf32beBOM = bomType(5)
338 )
339
340 // bomLegend has the string-equivalents of the bomType constants
341 var bomLegend = []string{
342 ``,
343 `UTF-8`,
344 `UTF-16 LE`,
345 `UTF-16 BE`,
346 `UTF-32 LE`,
347 `UTF-32 BE`,
348 }
349
350 // stats has all the size-stats for some input, as well as a way to
351 // skip showing results, in case of an error such as `file not found`
352 type stats struct {
353 // bytes counts all bytes read
354 bytes counter
355
356 // lines counts lines, and is 0 only when the byte-count is also 0
357 lines counter
358
359 // name is the filepath of the file/source these stats are about
360 name string
361
362 // results keeps track of whether results are valid and/or ready
363 result statResult
364 }
365
366 // updateStats does what it says, reading everything from a reader
367 func (res *stats) updateStats(r io.Reader) error {
368 err := res.updateUsing(r)
369 if err == io.EOF {
370 err = nil
371 }
372
373 if err == nil {
374 res.result = resultSuccess
375 } else {
376 res.result = resultError
377 }
378 return err
379 }
380
381 // updateUsing helps func updateStats do its job
382 func (res *stats) updateUsing(r io.Reader) error {
383 var buf [32 * 1024]byte
384
385 var prev byte
386
387 for {
388 n, err := r.Read(buf[:])
389 if n < 1 {
390 if err == io.EOF {
391 if prev != '\n' {
392 res.lines++
393 }
394 }
395 return err
396 }
397
398 prev = buf[n-1]
399 res.bytes += counter(n)
400 res.lines += counter(bytes.Count(buf[:n], []byte{'\n'}))
401 }
402 }
File: ./chopleads/chopleads.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package chopleads
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "os"
32 "strings"
33 )
34
35 const info = `
36 chopleads [options...] [files...]
37
38 Ignore the longest common leading parts of all non-empty lines. A typical
39 use case is when dealing with lines starting with full filenames, where
40 all files come from the same subfolder, as getting rid of the long common
41 starts can vastly reduce the clutter.
42
43 The options are, available both in single and double-dash versions
44
45 -h, -help show this help message
46 `
47
48 type config struct {
49 state
50
51 liveLines bool
52 }
53
54 func (cfg config) maybeFlush(w *bufio.Writer) error {
55 if cfg.liveLines && w.Flush() != nil {
56 return io.EOF
57 }
58 return nil
59 }
60
61 type state struct {
62 // lines remembers all input lines so far, since finding common leads
63 // may require knowning all input lines, in general
64 lines []string
65
66 // lead is the current (leading) common-lead among lines seen so far
67 lead string
68
69 // nothing remembers if there's no common lead for sure
70 nothing bool
71 }
72
73 // dump emits all lines kept so far, then forgets about them
74 func (s *state) dump(w *bufio.Writer) error {
75 for _, s := range s.lines {
76 w.WriteString(s)
77 if w.WriteByte('\n') != nil {
78 return io.EOF
79 }
80 }
81
82 // forgetting the lines may save some memory, with enough lines
83 s.lines = nil
84
85 return nil
86 }
87
88 func Main() {
89 var cfg config
90 cfg.liveLines = true
91 args := os.Args[1:]
92
93 for len(args) > 0 {
94 switch args[0] {
95 case `-b`, `--b`, `-buffered`, `--buffered`:
96 cfg.liveLines = false
97 args = args[1:]
98 continue
99
100 case `-h`, `--h`, `-help`, `--help`:
101 os.Stdout.WriteString(info[1:])
102 return
103 }
104
105 break
106 }
107
108 if len(args) > 0 && args[0] == `--` {
109 args = args[1:]
110 }
111
112 if cfg.liveLines {
113 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
114 cfg.liveLines = false
115 }
116 }
117
118 if err := run(args, &cfg); err != nil && err != io.EOF {
119 os.Stderr.WriteString(`chopleads: `)
120 os.Stderr.WriteString(err.Error())
121 os.Stderr.WriteString("\n")
122 os.Exit(1)
123 return
124 }
125 }
126
127 func run(paths []string, cfg *config) error {
128 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
129 defer bw.Flush()
130
131 for _, p := range paths {
132 if err := handleFile(bw, p, cfg); err != nil {
133 return err
134 }
135 }
136
137 if len(paths) == 0 {
138 if err := handleReader(bw, os.Stdin, cfg); err != nil {
139 return err
140 }
141 }
142
143 if cfg.nothing {
144 return nil
145 }
146
147 skip := len(cfg.lead)
148 // with only 1 line of input, avoid emitting a single empty line for sure
149 if len(cfg.lines) == 1 {
150 skip = 0
151 }
152
153 for _, s := range cfg.lines {
154 if len(s) > 0 {
155 bw.WriteString(s[skip:])
156 }
157 if bw.WriteByte('\n') != nil {
158 return io.EOF
159 }
160 }
161 return nil
162 }
163
164 func handleFile(w *bufio.Writer, path string, cfg *config) error {
165 f, err := os.Open(path)
166 if err != nil {
167 // on windows, file-not-found error messages may mention `CreateFile`,
168 // even when trying to open files in read-only mode
169 return errors.New(`can't open file named ` + path)
170 }
171 defer f.Close()
172 return handleReader(w, f, cfg)
173 }
174
175 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
176 const gb = 1024 * 1024 * 1024
177 sc := bufio.NewScanner(r)
178 sc.Buffer(nil, 8*gb)
179
180 for i := 0; sc.Scan(); i++ {
181 s := sc.Text()
182 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
183 s = s[3:]
184 }
185
186 // when no common lead is possible, show lines as they come and as
187 // they are; also don't bother remembering them, thus saving memory
188 if cfg.nothing {
189 w.WriteString(s)
190 if w.WriteByte('\n') != nil {
191 return io.EOF
192 }
193
194 if err := cfg.maybeFlush(w); err != nil {
195 return err
196 }
197 continue
198 }
199
200 cfg.lines = append(cfg.lines, s)
201
202 if len(s) == 0 {
203 // empty lines don't count for common leads
204 continue
205 }
206
207 // the full first non-empty line automatically becomes the lead
208 if len(cfg.lead) == 0 {
209 cfg.lead = s
210 }
211
212 // the common lead can only be as long as the shortest non-empty line
213 if len(cfg.lead) > len(s) {
214 cfg.lead = cfg.lead[:len(s)]
215 }
216
217 // avoid lead-shrink loop in case leading byte is different, which
218 // by itself implies there's no common lead
219 if len(s) > 0 && len(cfg.lead) > 0 && s[0] != cfg.lead[0] {
220 if err := cfg.dump(w); err != nil {
221 return err
222 }
223 if err := cfg.maybeFlush(w); err != nil {
224 return err
225 }
226 cfg.nothing = true
227 continue
228 }
229
230 // shrink common lead from the end
231 for len(cfg.lead) > 0 && !strings.HasPrefix(s, cfg.lead) {
232 cfg.lead = cfg.lead[:len(cfg.lead)-1]
233 continue
234 }
235
236 // if previous loop ends with an empty string: there's no common lead
237 if len(cfg.lead) == 0 {
238 if err := cfg.dump(w); err != nil {
239 return err
240 }
241 if err := cfg.maybeFlush(w); err != nil {
242 return err
243 }
244 cfg.nothing = true
245 continue
246 }
247 }
248
249 return sc.Err()
250 }
File: ./clock/clock.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package clock
26
27 import (
28 "os"
29 "time"
30 )
31
32 const (
33 spaces = ` `
34
35 // clear has enough spaces in it to cover any date-time output
36 clear = "\r" + spaces + spaces + spaces + "\r"
37
38 dateTime = `Mon Jan 02 15:04`
39 timeDate = `15:04 Mon Jan 02`
40
41 shortFormat = dateTime
42 longFormat = `2006-01-02 ` + shortFormat
43 )
44
45 const info = `
46 clock [options...]
47
48 Keep showing the current time, updating every new minute until force-quit.
49
50 All (optional) leading options start with either single or double-dash:
51
52 -f, -full, -l, -long also show year-month-day
53 -h, -help show this help message
54 `
55
56 func Main() {
57 args := os.Args[1:]
58 clockFormat := shortFormat
59
60 if len(args) > 0 {
61 switch args[0] {
62 case `-h`, `--h`, `-help`, `--help`:
63 os.Stdout.WriteString(info[1:])
64 return
65
66 case `-f`, `--f`, `-full`, `--full`, `-l`, `--l`, `-long`, `--long`:
67 clockFormat = longFormat
68 args = args[1:]
69 }
70 }
71
72 if len(args) > 0 && args[0] == `--` {
73 args = args[1:]
74 }
75
76 if len(args) > 0 {
77 os.Stderr.WriteString(info[1:])
78 os.Exit(1)
79 return
80 }
81
82 var buf [128]byte
83
84 for {
85 now := time.Now()
86
87 s := buf[:0]
88 s = append(s, clear...)
89 s = now.AppendFormat(s, clockFormat)
90 os.Stderr.Write(s)
91
92 time.Sleep(time.Second * time.Duration(60-now.Second()))
93 }
94 }
File: ./coby/coby.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package coby
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "io/fs"
32 "os"
33 "path/filepath"
34 "runtime"
35 "strconv"
36 "sync"
37 )
38
39 const info = `
40 coby [options...] [files/folders...]
41
42
43 COunt BYtes finds out some simple byte-related stats, counting
44
45 - bytes
46 - lines
47 - how many lines have trailing spaces (trails)
48 - how many lines end with a CRLF pair
49 - all-bits-off (null) bytes
50 - all-bits-on (full) bytes
51 - top-bit-on (high) bytes
52 - which unicode byte-order-mark (bom) sequence the data start with
53
54 Some of these stats (lines, CRLFs, BOMs) only make sense for plain-text
55 data, and thus may not be meaningful for general binary data.
56
57 The output is TSV (tab-separated values) lines, where the first line has
58 all the column names.
59
60 When no filepaths are given, the standard input is used by default. All
61 folder names given expand recursively into all filenames in them. A mix
62 of files/folders is supported for convenience.
63
64 The only option available is to show this help message, using any of
65 "-h", "--h", "-help", or "--help", without the quotes.
66 `
67
68 // header has all the values for the first output line
69 var header = []string{
70 `name`,
71 `bytes`,
72 `lines`,
73 `lf`,
74 `crlf`,
75 `spaces`,
76 `tabs`,
77 `trails`,
78 `nulls`,
79 `fulls`,
80 `highs`,
81 `bom`,
82 }
83
84 // event has what the output-reporting task needs to show the results of a
85 // task which has just completed, perhaps unsuccessfully
86 type event struct {
87 // Index points to the task's entry in the results-slice
88 Index int
89
90 // Stats has all the byte-related stats
91 Stats stats
92
93 // Err is the completed task's error, or lack of
94 Err error
95 }
96
97 func Main() {
98 args := os.Args[1:]
99
100 if len(args) > 0 {
101 switch args[0] {
102 case `-h`, `--h`, `-help`, `--help`:
103 os.Stdout.WriteString(info[1:])
104 return
105
106 case `--`:
107 args = args[1:]
108 }
109 }
110
111 // show first/heading line right away, to let users know things are
112 // happening
113 for i, s := range header {
114 if i > 0 {
115 os.Stdout.WriteString("\t")
116 }
117 os.Stdout.WriteString(s)
118 }
119 // assume an error means later stages/apps in a pipe had enough input and
120 // quit successfully, so quit successfully too
121 _, err := os.Stdout.WriteString("\n")
122 if err != nil {
123 return
124 }
125
126 // names has all filepaths given, ignoring repetitions
127 names, ok := findAllFiles(args)
128 if !ok {
129 os.Exit(1)
130 return
131 }
132 if len(names) == 0 {
133 names = []string{`-`}
134 }
135
136 events := make(chan event)
137 go handleInputs(names, events)
138 if !handleOutput(os.Stdout, len(names), events) {
139 os.Exit(1)
140 return
141 }
142 }
143
144 // handleInputs launches all the tasks which do the actual work, limiting how
145 // many inputs are being worked on at the same time
146 func handleInputs(names []string, events chan<- event) {
147 defer close(events) // allow the output-reporter task to end
148
149 var tasks sync.WaitGroup
150 // the number of tasks is always known in advance
151 tasks.Add(len(names))
152
153 // permissions is buffered to limit concurrency to the core-count
154 permissions := make(chan struct{}, runtime.NumCPU())
155 defer close(permissions)
156
157 for i, name := range names {
158 // wait until some concurrency-room is available, before proceeding
159 permissions <- struct{}{}
160
161 go func(i int, name string) {
162 defer tasks.Done()
163
164 res, err := handleInput(name)
165 <-permissions
166 events <- event{Index: i, Stats: res, Err: err}
167 }(i, name)
168 }
169
170 // wait for all inputs, before closing the `events` channel, which in turn
171 // would quit the whole app right away
172 tasks.Wait()
173 }
174
175 // handleInput handles each work-item for func handleInputs
176 func handleInput(path string) (stats, error) {
177 var res stats
178 res.name = path
179
180 if path == `-` {
181 err := res.updateStats(os.Stdin)
182 return res, err
183 }
184
185 f, err := os.Open(path)
186 if err != nil {
187 res.result = resultError
188 // on windows, file-not-found error messages may mention `CreateFile`,
189 // even when trying to open files in read-only mode
190 return res, errors.New(`can't open file named ` + path)
191 }
192 defer f.Close()
193
194 err = res.updateStats(f)
195 return res, err
196 }
197
198 // handleOutput asynchronously updates output as results are known, whether
199 // it's errors or successful results; returns whether it succeeded, which
200 // means no errors happened
201 func handleOutput(w io.Writer, inputs int, events <-chan event) (ok bool) {
202 bw := bufio.NewWriterSize(w, 32*1024)
203 defer bw.Flush()
204
205 ok = true
206 results := make([]stats, inputs)
207
208 // keep track of which tasks are over, so that on each event all leading
209 // results which are ready are shown: all of this ensures prompt output
210 // updates as soon as results come in, while keeping the original order
211 // of the names/filepaths given
212 resultsLeft := results
213
214 for v := range events {
215 results[v.Index] = v.Stats
216 if v.Err != nil {
217 ok = false
218 bw.Flush()
219 showError(v.Err)
220
221 // stay in the current loop, in case this failure was keeping
222 // previous successes from showing up
223 }
224
225 for len(resultsLeft) > 0 {
226 if resultsLeft[0].result == resultPending {
227 break
228 }
229
230 if err := showResult(bw, resultsLeft[0]); err != nil {
231 // assume later stages/apps in a pipe had enough input
232 return ok
233 }
234 resultsLeft = resultsLeft[1:]
235 }
236
237 // show leading results immediately, if any
238 bw.Flush()
239 }
240
241 return ok
242 }
243
244 func showError(err error) {
245 os.Stderr.WriteString(`coby: `)
246 os.Stderr.WriteString(err.Error())
247 os.Stderr.WriteString("\n")
248 }
249
250 // showResult shows a TSV line for results marked as successful, doing nothing
251 // when given other types of results
252 func showResult(w *bufio.Writer, s stats) error {
253 if s.result != resultSuccess {
254 return nil
255 }
256
257 var buf [24]byte
258 w.WriteString(s.name)
259 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.bytes), 10))
260 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.lines), 10))
261 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.lf), 10))
262 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.crlf), 10))
263 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.spaces), 10))
264 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.tabs), 10))
265 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.trailing), 10))
266 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.nulls), 10))
267 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.fulls), 10))
268 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.highs), 10))
269 w.WriteByte('\t')
270 w.WriteString(bomLegend[s.bom])
271 return w.WriteByte('\n')
272 }
273
274 // findAllFiles can be given a mix of file/folder paths, finding all files
275 // recursively in folders, avoiding duplicates
276 func findAllFiles(paths []string) (files []string, success bool) {
277 walk := filepath.WalkDir
278 got := make(map[string]struct{})
279 success = true
280
281 for _, path := range paths {
282 if _, ok := got[path]; ok {
283 continue
284 }
285 got[path] = struct{}{}
286
287 // a dash means standard input
288 if path == `-` {
289 files = append(files, path)
290 continue
291 }
292
293 info, err := os.Stat(path)
294 if os.IsNotExist(err) {
295 // on windows, file-not-found messages may mention `CreateFile`,
296 // even when trying to open files in read-only mode
297 err = errors.New(`can't find file/folder named ` + path)
298 }
299
300 if err != nil {
301 showError(err)
302 success = false
303 continue
304 }
305
306 if !info.IsDir() {
307 files = append(files, path)
308 continue
309 }
310
311 err = walk(path, func(path string, info fs.DirEntry, err error) error {
312 path, err = filepath.Abs(path)
313 if err != nil {
314 showError(err)
315 success = false
316 return err
317 }
318
319 if _, ok := got[path]; ok {
320 if info.IsDir() {
321 return fs.SkipDir
322 }
323 return nil
324 }
325 got[path] = struct{}{}
326
327 if err != nil {
328 showError(err)
329 success = false
330 return err
331 }
332
333 if info.IsDir() {
334 return nil
335 }
336
337 files = append(files, path)
338 return nil
339 })
340
341 if err != nil {
342 showError(err)
343 success = false
344 }
345 }
346
347 return files, success
348 }
349
350 // counter makes it easy to change the int-size of almost all counters
351 type counter uint64
352
353 // statResult constrains possible result-states/values in type stats
354 type statResult int
355
356 const (
357 // resultPending is the default not-yet-ready result-status
358 resultPending = statResult(0)
359
360 // resultError means result should show as an error, instead of data
361 resultError = statResult(1)
362
363 // resultSuccess means a result's stats are ready to show
364 resultSuccess = statResult(2)
365 )
366
367 // bomType is the type for the byte-order-mark enumeration
368 type bomType int
369
370 const (
371 noBOM = bomType(0)
372 utf8BOM = bomType(1)
373 utf16leBOM = bomType(2)
374 utf16beBOM = bomType(3)
375 utf32leBOM = bomType(4)
376 utf32beBOM = bomType(5)
377 )
378
379 // bomLegend has the string-equivalents of the bomType constants
380 var bomLegend = []string{
381 ``,
382 `UTF-8`,
383 `UTF-16 LE`,
384 `UTF-16 BE`,
385 `UTF-32 LE`,
386 `UTF-32 BE`,
387 }
388
389 // stats has all the size-stats for some input, as well as a way to
390 // skip showing results, in case of an error such as `file not found`
391 type stats struct {
392 // bytes counts all bytes read
393 bytes counter
394
395 // lines counts lines, and is 0 only when the byte-count is also 0
396 lines counter
397
398 // maxWidth is maximum byte-width of lines, excluding carriage-returns
399 // and/or line-feeds
400 maxWidth counter
401
402 // nulls counts all-bits-off bytes
403 nulls counter
404
405 // fulls counts all-bits-on bytes
406 fulls counter
407
408 // highs counts bytes with their `top` (highest-order) bit on
409 highs counter
410
411 // spaces counts ASCII spaces
412 spaces counter
413
414 // tabs counts ASCII tabs
415 tabs counter
416
417 // trailing counts lines with trailing spaces in them
418 trailing counter
419
420 // lf counts ASCII line-feeds as their own byte-values: this means its
421 // value will always be at least the same as field `crlf`
422 lf counter
423
424 // crlf counts ASCII CRLF byte-pairs
425 crlf counter
426
427 // the type of byte-order mark detected
428 bom bomType
429
430 // name is the filepath of the file/source these stats are about
431 name string
432
433 // results keeps track of whether results are valid and/or ready
434 result statResult
435 }
436
437 // updateStats does what it says, reading everything from a reader
438 func (res *stats) updateStats(r io.Reader) error {
439 err := res.updateUsing(r)
440 if err == io.EOF {
441 err = nil
442 }
443
444 if err == nil {
445 res.result = resultSuccess
446 } else {
447 res.result = resultError
448 }
449 return err
450 }
451
452 func checkBOM(data []byte) bomType {
453 d := data
454 l := len(data)
455
456 if l >= 3 && data[0] == 0xef && data[1] == 0xbb && data[2] == 0xbf {
457 return utf8BOM
458 }
459 if l >= 4 && d[0] == 0xff && d[1] == 0xfe && d[2] == 0 && d[3] == 0 {
460 return utf32leBOM
461 }
462 if l >= 4 && d[0] == 0 && d[1] == 0 && d[2] == 0xfe && d[3] == 0xff {
463 return utf32beBOM
464 }
465 if l >= 2 && data[0] == 0xff && data[1] == 0xfe {
466 return utf16leBOM
467 }
468 if l >= 2 && data[0] == 0xfe && data[1] == 0xff {
469 return utf16beBOM
470 }
471
472 return noBOM
473 }
474
475 // updateUsing helps func updateStats do its job
476 func (res *stats) updateUsing(r io.Reader) error {
477 var buf [32 * 1024]byte
478 var tallies [256]uint64
479
480 var width counter
481 var prev1, prev2 byte
482
483 for {
484 n, err := r.Read(buf[:])
485 if n < 1 {
486 res.lines = counter(tallies['\n'])
487 res.tabs = counter(tallies['\t'])
488 res.spaces = counter(tallies[' '])
489 res.lf = counter(tallies['\n'])
490 res.nulls = counter(tallies[0])
491 res.fulls = counter(tallies[255])
492 for i := 128; i < len(tallies); i++ {
493 res.highs += counter(tallies[i])
494 }
495
496 if err == io.EOF {
497 return res.handleEnd(width, prev1, prev2)
498 }
499 return err
500 }
501
502 chunk := buf[:n]
503 if res.bytes == 0 {
504 res.bom = checkBOM(chunk)
505 }
506 res.bytes += counter(n)
507
508 for _, b := range chunk {
509 // count values without branching, because it's fun
510 tallies[b]++
511
512 if b != '\n' {
513 prev2 = prev1
514 prev1 = b
515 width++
516 continue
517 }
518
519 // handle line-feeds
520
521 crlf := count(prev1, '\r')
522 res.crlf += crlf
523
524 // count lines with trailing spaces, whether these end with
525 // a CRLF byte-pair or just a line-feed byte
526 if prev1 == ' ' || (prev2 == ' ' && prev1 == '\r') {
527 res.trailing++
528 }
529
530 // exclude any CR from the current line's width-count
531 width -= crlf
532 if res.maxWidth < width {
533 res.maxWidth = width
534 }
535
536 prev2 = prev1
537 prev1 = b
538 width = 0
539 }
540 }
541 }
542
543 // handleEnd fixes/finalizes stats when input data end; this func is only
544 // meant to be used by func updateStats, since it takes some of the latter's
545 // local variables
546 func (res *stats) handleEnd(width counter, prev1, prev2 byte) error {
547 if prev1 == ' ' || (prev2 == ' ' && prev1 == '\r') {
548 res.trailing++
549 }
550
551 if res.maxWidth < width {
552 res.maxWidth = width
553 }
554
555 // avoid reporting 0 lines with a non-0 byte-count: this is unlike the
556 // standard cmd-line tool `wc`
557 if res.bytes > 0 && prev1 != '\n' {
558 res.lines++
559 }
560
561 return nil
562 }
563
564 // count checks if 2 bytes are the same, returning either 0 or 1, which can
565 // be added directly/branchlessly to totals
566 func count(x, y byte) counter {
567 var c counter
568 if x == y {
569 c = 1
570 } else {
571 c = 0
572 }
573 return c
574 }
File: ./colorplus/datatables.go
1 package colorplus
2
3 // I'm using data straight from the original implementation of Viridis
4 // by Nathaniel Smith & Stefan van der Walt:
5 // https://github.com/BIDS/colormap/blob/master/option_d.py
6 var viridisData = [...]float64{
7 0.26700401, 0.00487433, 0.32941519,
8 0.26851048, 0.00960483, 0.33542652,
9 0.26994384, 0.01462494, 0.34137895,
10 0.27130489, 0.01994186, 0.34726862,
11 0.27259384, 0.02556309, 0.35309303,
12 0.27380934, 0.03149748, 0.35885256,
13 0.27495242, 0.03775181, 0.36454323,
14 0.27602238, 0.04416723, 0.37016418,
15 0.2770184, 0.05034437, 0.37571452,
16 0.27794143, 0.05632444, 0.38119074,
17 0.27879067, 0.06214536, 0.38659204,
18 0.2795655, 0.06783587, 0.39191723,
19 0.28026658, 0.07341724, 0.39716349,
20 0.28089358, 0.07890703, 0.40232944,
21 0.28144581, 0.0843197, 0.40741404,
22 0.28192358, 0.08966622, 0.41241521,
23 0.28232739, 0.09495545, 0.41733086,
24 0.28265633, 0.10019576, 0.42216032,
25 0.28291049, 0.10539345, 0.42690202,
26 0.28309095, 0.11055307, 0.43155375,
27 0.28319704, 0.11567966, 0.43611482,
28 0.28322882, 0.12077701, 0.44058404,
29 0.28318684, 0.12584799, 0.44496,
30 0.283072, 0.13089477, 0.44924127,
31 0.28288389, 0.13592005, 0.45342734,
32 0.28262297, 0.14092556, 0.45751726,
33 0.28229037, 0.14591233, 0.46150995,
34 0.28188676, 0.15088147, 0.46540474,
35 0.28141228, 0.15583425, 0.46920128,
36 0.28086773, 0.16077132, 0.47289909,
37 0.28025468, 0.16569272, 0.47649762,
38 0.27957399, 0.17059884, 0.47999675,
39 0.27882618, 0.1754902, 0.48339654,
40 0.27801236, 0.18036684, 0.48669702,
41 0.27713437, 0.18522836, 0.48989831,
42 0.27619376, 0.19007447, 0.49300074,
43 0.27519116, 0.1949054, 0.49600488,
44 0.27412802, 0.19972086, 0.49891131,
45 0.27300596, 0.20452049, 0.50172076,
46 0.27182812, 0.20930306, 0.50443413,
47 0.27059473, 0.21406899, 0.50705243,
48 0.26930756, 0.21881782, 0.50957678,
49 0.26796846, 0.22354911, 0.5120084,
50 0.26657984, 0.2282621, 0.5143487,
51 0.2651445, 0.23295593, 0.5165993,
52 0.2636632, 0.23763078, 0.51876163,
53 0.26213801, 0.24228619, 0.52083736,
54 0.26057103, 0.2469217, 0.52282822,
55 0.25896451, 0.25153685, 0.52473609,
56 0.25732244, 0.2561304, 0.52656332,
57 0.25564519, 0.26070284, 0.52831152,
58 0.25393498, 0.26525384, 0.52998273,
59 0.25219404, 0.26978306, 0.53157905,
60 0.25042462, 0.27429024, 0.53310261,
61 0.24862899, 0.27877509, 0.53455561,
62 0.2468114, 0.28323662, 0.53594093,
63 0.24497208, 0.28767547, 0.53726018,
64 0.24311324, 0.29209154, 0.53851561,
65 0.24123708, 0.29648471, 0.53970946,
66 0.23934575, 0.30085494, 0.54084398,
67 0.23744138, 0.30520222, 0.5419214,
68 0.23552606, 0.30952657, 0.54294396,
69 0.23360277, 0.31382773, 0.54391424,
70 0.2316735, 0.3181058, 0.54483444,
71 0.22973926, 0.32236127, 0.54570633,
72 0.22780192, 0.32659432, 0.546532,
73 0.2258633, 0.33080515, 0.54731353,
74 0.22392515, 0.334994, 0.54805291,
75 0.22198915, 0.33916114, 0.54875211,
76 0.22005691, 0.34330688, 0.54941304,
77 0.21812995, 0.34743154, 0.55003755,
78 0.21620971, 0.35153548, 0.55062743,
79 0.21429757, 0.35561907, 0.5511844,
80 0.21239477, 0.35968273, 0.55171011,
81 0.2105031, 0.36372671, 0.55220646,
82 0.20862342, 0.36775151, 0.55267486,
83 0.20675628, 0.37175775, 0.55311653,
84 0.20490257, 0.37574589, 0.55353282,
85 0.20306309, 0.37971644, 0.55392505,
86 0.20123854, 0.38366989, 0.55429441,
87 0.1994295, 0.38760678, 0.55464205,
88 0.1976365, 0.39152762, 0.55496905,
89 0.19585993, 0.39543297, 0.55527637,
90 0.19410009, 0.39932336, 0.55556494,
91 0.19235719, 0.40319934, 0.55583559,
92 0.19063135, 0.40706148, 0.55608907,
93 0.18892259, 0.41091033, 0.55632606,
94 0.18723083, 0.41474645, 0.55654717,
95 0.18555593, 0.4185704, 0.55675292,
96 0.18389763, 0.42238275, 0.55694377,
97 0.18225561, 0.42618405, 0.5571201,
98 0.18062949, 0.42997486, 0.55728221,
99 0.17901879, 0.43375572, 0.55743035,
100 0.17742298, 0.4375272, 0.55756466,
101 0.17584148, 0.44128981, 0.55768526,
102 0.17427363, 0.4450441, 0.55779216,
103 0.17271876, 0.4487906, 0.55788532,
104 0.17117615, 0.4525298, 0.55796464,
105 0.16964573, 0.45626209, 0.55803034,
106 0.16812641, 0.45998802, 0.55808199,
107 0.1666171, 0.46370813, 0.55811913,
108 0.16511703, 0.4674229, 0.55814141,
109 0.16362543, 0.47113278, 0.55814842,
110 0.16214155, 0.47483821, 0.55813967,
111 0.16066467, 0.47853961, 0.55811466,
112 0.15919413, 0.4822374, 0.5580728,
113 0.15772933, 0.48593197, 0.55801347,
114 0.15626973, 0.4896237, 0.557936,
115 0.15481488, 0.49331293, 0.55783967,
116 0.15336445, 0.49700003, 0.55772371,
117 0.1519182, 0.50068529, 0.55758733,
118 0.15047605, 0.50436904, 0.55742968,
119 0.14903918, 0.50805136, 0.5572505,
120 0.14760731, 0.51173263, 0.55704861,
121 0.14618026, 0.51541316, 0.55682271,
122 0.14475863, 0.51909319, 0.55657181,
123 0.14334327, 0.52277292, 0.55629491,
124 0.14193527, 0.52645254, 0.55599097,
125 0.14053599, 0.53013219, 0.55565893,
126 0.13914708, 0.53381201, 0.55529773,
127 0.13777048, 0.53749213, 0.55490625,
128 0.1364085, 0.54117264, 0.55448339,
129 0.13506561, 0.54485335, 0.55402906,
130 0.13374299, 0.54853458, 0.55354108,
131 0.13244401, 0.55221637, 0.55301828,
132 0.13117249, 0.55589872, 0.55245948,
133 0.1299327, 0.55958162, 0.55186354,
134 0.12872938, 0.56326503, 0.55122927,
135 0.12756771, 0.56694891, 0.55055551,
136 0.12645338, 0.57063316, 0.5498411,
137 0.12539383, 0.57431754, 0.54908564,
138 0.12439474, 0.57800205, 0.5482874,
139 0.12346281, 0.58168661, 0.54744498,
140 0.12260562, 0.58537105, 0.54655722,
141 0.12183122, 0.58905521, 0.54562298,
142 0.12114807, 0.59273889, 0.54464114,
143 0.12056501, 0.59642187, 0.54361058,
144 0.12009154, 0.60010387, 0.54253043,
145 0.11973756, 0.60378459, 0.54139999,
146 0.11951163, 0.60746388, 0.54021751,
147 0.11942341, 0.61114146, 0.53898192,
148 0.11948255, 0.61481702, 0.53769219,
149 0.11969858, 0.61849025, 0.53634733,
150 0.12008079, 0.62216081, 0.53494633,
151 0.12063824, 0.62582833, 0.53348834,
152 0.12137972, 0.62949242, 0.53197275,
153 0.12231244, 0.63315277, 0.53039808,
154 0.12344358, 0.63680899, 0.52876343,
155 0.12477953, 0.64046069, 0.52706792,
156 0.12632581, 0.64410744, 0.52531069,
157 0.12808703, 0.64774881, 0.52349092,
158 0.13006688, 0.65138436, 0.52160791,
159 0.13226797, 0.65501363, 0.51966086,
160 0.13469183, 0.65863619, 0.5176488,
161 0.13733921, 0.66225157, 0.51557101,
162 0.14020991, 0.66585927, 0.5134268,
163 0.14330291, 0.66945881, 0.51121549,
164 0.1466164, 0.67304968, 0.50893644,
165 0.15014782, 0.67663139, 0.5065889,
166 0.15389405, 0.68020343, 0.50417217,
167 0.15785146, 0.68376525, 0.50168574,
168 0.16201598, 0.68731632, 0.49912906,
169 0.1663832, 0.69085611, 0.49650163,
170 0.1709484, 0.69438405, 0.49380294,
171 0.17570671, 0.6978996, 0.49103252,
172 0.18065314, 0.70140222, 0.48818938,
173 0.18578266, 0.70489133, 0.48527326,
174 0.19109018, 0.70836635, 0.48228395,
175 0.19657063, 0.71182668, 0.47922108,
176 0.20221902, 0.71527175, 0.47608431,
177 0.20803045, 0.71870095, 0.4728733,
178 0.21400015, 0.72211371, 0.46958774,
179 0.22012381, 0.72550945, 0.46622638,
180 0.2263969, 0.72888753, 0.46278934,
181 0.23281498, 0.73224735, 0.45927675,
182 0.2393739, 0.73558828, 0.45568838,
183 0.24606968, 0.73890972, 0.45202405,
184 0.25289851, 0.74221104, 0.44828355,
185 0.25985676, 0.74549162, 0.44446673,
186 0.26694127, 0.74875084, 0.44057284,
187 0.27414922, 0.75198807, 0.4366009,
188 0.28147681, 0.75520266, 0.43255207,
189 0.28892102, 0.75839399, 0.42842626,
190 0.29647899, 0.76156142, 0.42422341,
191 0.30414796, 0.76470433, 0.41994346,
192 0.31192534, 0.76782207, 0.41558638,
193 0.3198086, 0.77091403, 0.41115215,
194 0.3277958, 0.77397953, 0.40664011,
195 0.33588539, 0.7770179, 0.40204917,
196 0.34407411, 0.78002855, 0.39738103,
197 0.35235985, 0.78301086, 0.39263579,
198 0.36074053, 0.78596419, 0.38781353,
199 0.3692142, 0.78888793, 0.38291438,
200 0.37777892, 0.79178146, 0.3779385,
201 0.38643282, 0.79464415, 0.37288606,
202 0.39517408, 0.79747541, 0.36775726,
203 0.40400101, 0.80027461, 0.36255223,
204 0.4129135, 0.80304099, 0.35726893,
205 0.42190813, 0.80577412, 0.35191009,
206 0.43098317, 0.80847343, 0.34647607,
207 0.44013691, 0.81113836, 0.3409673,
208 0.44936763, 0.81376835, 0.33538426,
209 0.45867362, 0.81636288, 0.32972749,
210 0.46805314, 0.81892143, 0.32399761,
211 0.47750446, 0.82144351, 0.31819529,
212 0.4870258, 0.82392862, 0.31232133,
213 0.49661536, 0.82637633, 0.30637661,
214 0.5062713, 0.82878621, 0.30036211,
215 0.51599182, 0.83115784, 0.29427888,
216 0.52577622, 0.83349064, 0.2881265,
217 0.5356211, 0.83578452, 0.28190832,
218 0.5455244, 0.83803918, 0.27562602,
219 0.55548397, 0.84025437, 0.26928147,
220 0.5654976, 0.8424299, 0.26287683,
221 0.57556297, 0.84456561, 0.25641457,
222 0.58567772, 0.84666139, 0.24989748,
223 0.59583934, 0.84871722, 0.24332878,
224 0.60604528, 0.8507331, 0.23671214,
225 0.61629283, 0.85270912, 0.23005179,
226 0.62657923, 0.85464543, 0.22335258,
227 0.63690157, 0.85654226, 0.21662012,
228 0.64725685, 0.85839991, 0.20986086,
229 0.65764197, 0.86021878, 0.20308229,
230 0.66805369, 0.86199932, 0.19629307,
231 0.67848868, 0.86374211, 0.18950326,
232 0.68894351, 0.86544779, 0.18272455,
233 0.69941463, 0.86711711, 0.17597055,
234 0.70989842, 0.86875092, 0.16925712,
235 0.72039115, 0.87035015, 0.16260273,
236 0.73088902, 0.87191584, 0.15602894,
237 0.74138803, 0.87344918, 0.14956101,
238 0.75188414, 0.87495143, 0.14322828,
239 0.76237342, 0.87642392, 0.13706449,
240 0.77285183, 0.87786808, 0.13110864,
241 0.78331535, 0.87928545, 0.12540538,
242 0.79375994, 0.88067763, 0.12000532,
243 0.80418159, 0.88204632, 0.11496505,
244 0.81457634, 0.88339329, 0.11034678,
245 0.82494028, 0.88472036, 0.10621724,
246 0.83526959, 0.88602943, 0.1026459,
247 0.84556056, 0.88732243, 0.09970219,
248 0.8558096, 0.88860134, 0.09745186,
249 0.86601325, 0.88986815, 0.09595277,
250 0.87616824, 0.89112487, 0.09525046,
251 0.88627146, 0.89237353, 0.09537439,
252 0.89632002, 0.89361614, 0.09633538,
253 0.90631121, 0.89485467, 0.09812496,
254 0.91624212, 0.89609127, 0.1007168,
255 0.92610579, 0.89732977, 0.10407067,
256 0.93590444, 0.8985704, 0.10813094,
257 0.94563626, 0.899815, 0.11283773,
258 0.95529972, 0.90106534, 0.11812832,
259 0.96489353, 0.90232311, 0.12394051,
260 0.97441665, 0.90358991, 0.13021494,
261 0.98386829, 0.90486726, 0.13689671,
262 0.99324789, 0.90615657, 0.1439362,
263 }
264
265 // I'm using data straight from the original implementation of Magma
266 // by Nathaniel Smith & Stefan van der Walt:
267 // https://github.com/BIDS/colormap/blob/master/option_a.py
268 var magmaData = [...]float64{
269 1.46159096e-03, 4.66127766e-04, 1.38655200e-02,
270 2.25764007e-03, 1.29495431e-03, 1.83311461e-02,
271 3.27943222e-03, 2.30452991e-03, 2.37083291e-02,
272 4.51230222e-03, 3.49037666e-03, 2.99647059e-02,
273 5.94976987e-03, 4.84285000e-03, 3.71296695e-02,
274 7.58798550e-03, 6.35613622e-03, 4.49730774e-02,
275 9.42604390e-03, 8.02185006e-03, 5.28443561e-02,
276 1.14654337e-02, 9.82831486e-03, 6.07496380e-02,
277 1.37075706e-02, 1.17705913e-02, 6.86665843e-02,
278 1.61557566e-02, 1.38404966e-02, 7.66026660e-02,
279 1.88153670e-02, 1.60262753e-02, 8.45844897e-02,
280 2.16919340e-02, 1.83201254e-02, 9.26101050e-02,
281 2.47917814e-02, 2.07147875e-02, 1.00675555e-01,
282 2.81228154e-02, 2.32009284e-02, 1.08786954e-01,
283 3.16955304e-02, 2.57651161e-02, 1.16964722e-01,
284 3.55204468e-02, 2.83974570e-02, 1.25209396e-01,
285 3.96084872e-02, 3.10895652e-02, 1.33515085e-01,
286 4.38295350e-02, 3.38299885e-02, 1.41886249e-01,
287 4.80616391e-02, 3.66066101e-02, 1.50326989e-01,
288 5.23204388e-02, 3.94066020e-02, 1.58841025e-01,
289 5.66148978e-02, 4.21598925e-02, 1.67445592e-01,
290 6.09493930e-02, 4.47944924e-02, 1.76128834e-01,
291 6.53301801e-02, 4.73177796e-02, 1.84891506e-01,
292 6.97637296e-02, 4.97264666e-02, 1.93735088e-01,
293 7.42565152e-02, 5.20167766e-02, 2.02660374e-01,
294 7.88150034e-02, 5.41844801e-02, 2.11667355e-01,
295 8.34456313e-02, 5.62249365e-02, 2.20755099e-01,
296 8.81547730e-02, 5.81331465e-02, 2.29921611e-01,
297 9.29486914e-02, 5.99038167e-02, 2.39163669e-01,
298 9.78334770e-02, 6.15314414e-02, 2.48476662e-01,
299 1.02814972e-01, 6.30104053e-02, 2.57854400e-01,
300 1.07898679e-01, 6.43351102e-02, 2.67288933e-01,
301 1.13094451e-01, 6.54920358e-02, 2.76783978e-01,
302 1.18405035e-01, 6.64791593e-02, 2.86320656e-01,
303 1.23832651e-01, 6.72946449e-02, 2.95879431e-01,
304 1.29380192e-01, 6.79349264e-02, 3.05442931e-01,
305 1.35053322e-01, 6.83912798e-02, 3.14999890e-01,
306 1.40857952e-01, 6.86540710e-02, 3.24537640e-01,
307 1.46785234e-01, 6.87382323e-02, 3.34011109e-01,
308 1.52839217e-01, 6.86368599e-02, 3.43404450e-01,
309 1.59017511e-01, 6.83540225e-02, 3.52688028e-01,
310 1.65308131e-01, 6.79108689e-02, 3.61816426e-01,
311 1.71713033e-01, 6.73053260e-02, 3.70770827e-01,
312 1.78211730e-01, 6.65758073e-02, 3.79497161e-01,
313 1.84800877e-01, 6.57324381e-02, 3.87972507e-01,
314 1.91459745e-01, 6.48183312e-02, 3.96151969e-01,
315 1.98176877e-01, 6.38624166e-02, 4.04008953e-01,
316 2.04934882e-01, 6.29066192e-02, 4.11514273e-01,
317 2.11718061e-01, 6.19917876e-02, 4.18646741e-01,
318 2.18511590e-01, 6.11584918e-02, 4.25391816e-01,
319 2.25302032e-01, 6.04451843e-02, 4.31741767e-01,
320 2.32076515e-01, 5.98886855e-02, 4.37694665e-01,
321 2.38825991e-01, 5.95170384e-02, 4.43255999e-01,
322 2.45543175e-01, 5.93524384e-02, 4.48435938e-01,
323 2.52220252e-01, 5.94147119e-02, 4.53247729e-01,
324 2.58857304e-01, 5.97055998e-02, 4.57709924e-01,
325 2.65446744e-01, 6.02368754e-02, 4.61840297e-01,
326 2.71994089e-01, 6.09935552e-02, 4.65660375e-01,
327 2.78493300e-01, 6.19778136e-02, 4.69190328e-01,
328 2.84951097e-01, 6.31676261e-02, 4.72450879e-01,
329 2.91365817e-01, 6.45534486e-02, 4.75462193e-01,
330 2.97740413e-01, 6.61170432e-02, 4.78243482e-01,
331 3.04080941e-01, 6.78353452e-02, 4.80811572e-01,
332 3.10382027e-01, 6.97024767e-02, 4.83186340e-01,
333 3.16654235e-01, 7.16895272e-02, 4.85380429e-01,
334 3.22899126e-01, 7.37819504e-02, 4.87408399e-01,
335 3.29114038e-01, 7.59715081e-02, 4.89286796e-01,
336 3.35307503e-01, 7.82361045e-02, 4.91024144e-01,
337 3.41481725e-01, 8.05635079e-02, 4.92631321e-01,
338 3.47635742e-01, 8.29463512e-02, 4.94120923e-01,
339 3.53773161e-01, 8.53726329e-02, 4.95501096e-01,
340 3.59897941e-01, 8.78311772e-02, 4.96778331e-01,
341 3.66011928e-01, 9.03143031e-02, 4.97959963e-01,
342 3.72116205e-01, 9.28159917e-02, 4.99053326e-01,
343 3.78210547e-01, 9.53322947e-02, 5.00066568e-01,
344 3.84299445e-01, 9.78549106e-02, 5.01001964e-01,
345 3.90384361e-01, 1.00379466e-01, 5.01864236e-01,
346 3.96466670e-01, 1.02902194e-01, 5.02657590e-01,
347 4.02547663e-01, 1.05419865e-01, 5.03385761e-01,
348 4.08628505e-01, 1.07929771e-01, 5.04052118e-01,
349 4.14708664e-01, 1.10431177e-01, 5.04661843e-01,
350 4.20791157e-01, 1.12920210e-01, 5.05214935e-01,
351 4.26876965e-01, 1.15395258e-01, 5.05713602e-01,
352 4.32967001e-01, 1.17854987e-01, 5.06159754e-01,
353 4.39062114e-01, 1.20298314e-01, 5.06555026e-01,
354 4.45163096e-01, 1.22724371e-01, 5.06900806e-01,
355 4.51270678e-01, 1.25132484e-01, 5.07198258e-01,
356 4.57385535e-01, 1.27522145e-01, 5.07448336e-01,
357 4.63508291e-01, 1.29892998e-01, 5.07651812e-01,
358 4.69639514e-01, 1.32244819e-01, 5.07809282e-01,
359 4.75779723e-01, 1.34577500e-01, 5.07921193e-01,
360 4.81928997e-01, 1.36891390e-01, 5.07988509e-01,
361 4.88088169e-01, 1.39186217e-01, 5.08010737e-01,
362 4.94257673e-01, 1.41462106e-01, 5.07987836e-01,
363 5.00437834e-01, 1.43719323e-01, 5.07919772e-01,
364 5.06628929e-01, 1.45958202e-01, 5.07806420e-01,
365 5.12831195e-01, 1.48179144e-01, 5.07647570e-01,
366 5.19044825e-01, 1.50382611e-01, 5.07442938e-01,
367 5.25269968e-01, 1.52569121e-01, 5.07192172e-01,
368 5.31506735e-01, 1.54739247e-01, 5.06894860e-01,
369 5.37755194e-01, 1.56893613e-01, 5.06550538e-01,
370 5.44015371e-01, 1.59032895e-01, 5.06158696e-01,
371 5.50287252e-01, 1.61157816e-01, 5.05718782e-01,
372 5.56570783e-01, 1.63269149e-01, 5.05230210e-01,
373 5.62865867e-01, 1.65367714e-01, 5.04692365e-01,
374 5.69172368e-01, 1.67454379e-01, 5.04104606e-01,
375 5.75490107e-01, 1.69530062e-01, 5.03466273e-01,
376 5.81818864e-01, 1.71595728e-01, 5.02776690e-01,
377 5.88158375e-01, 1.73652392e-01, 5.02035167e-01,
378 5.94508337e-01, 1.75701122e-01, 5.01241011e-01,
379 6.00868399e-01, 1.77743036e-01, 5.00393522e-01,
380 6.07238169e-01, 1.79779309e-01, 4.99491999e-01,
381 6.13617209e-01, 1.81811170e-01, 4.98535746e-01,
382 6.20005032e-01, 1.83839907e-01, 4.97524075e-01,
383 6.26401108e-01, 1.85866869e-01, 4.96456304e-01,
384 6.32804854e-01, 1.87893468e-01, 4.95331769e-01,
385 6.39215638e-01, 1.89921182e-01, 4.94149821e-01,
386 6.45632778e-01, 1.91951556e-01, 4.92909832e-01,
387 6.52055535e-01, 1.93986210e-01, 4.91611196e-01,
388 6.58483116e-01, 1.96026835e-01, 4.90253338e-01,
389 6.64914668e-01, 1.98075202e-01, 4.88835712e-01,
390 6.71349279e-01, 2.00133166e-01, 4.87357807e-01,
391 6.77785975e-01, 2.02202663e-01, 4.85819154e-01,
392 6.84223712e-01, 2.04285721e-01, 4.84219325e-01,
393 6.90661380e-01, 2.06384461e-01, 4.82557941e-01,
394 6.97097796e-01, 2.08501100e-01, 4.80834678e-01,
395 7.03531700e-01, 2.10637956e-01, 4.79049270e-01,
396 7.09961888e-01, 2.12797337e-01, 4.77201121e-01,
397 7.16387038e-01, 2.14981693e-01, 4.75289780e-01,
398 7.22805451e-01, 2.17193831e-01, 4.73315708e-01,
399 7.29215521e-01, 2.19436516e-01, 4.71278924e-01,
400 7.35615545e-01, 2.21712634e-01, 4.69179541e-01,
401 7.42003713e-01, 2.24025196e-01, 4.67017774e-01,
402 7.48378107e-01, 2.26377345e-01, 4.64793954e-01,
403 7.54736692e-01, 2.28772352e-01, 4.62508534e-01,
404 7.61077312e-01, 2.31213625e-01, 4.60162106e-01,
405 7.67397681e-01, 2.33704708e-01, 4.57755411e-01,
406 7.73695380e-01, 2.36249283e-01, 4.55289354e-01,
407 7.79967847e-01, 2.38851170e-01, 4.52765022e-01,
408 7.86212372e-01, 2.41514325e-01, 4.50183695e-01,
409 7.92426972e-01, 2.44242250e-01, 4.47543155e-01,
410 7.98607760e-01, 2.47039798e-01, 4.44848441e-01,
411 8.04751511e-01, 2.49911350e-01, 4.42101615e-01,
412 8.10854841e-01, 2.52861399e-01, 4.39304963e-01,
413 8.16914186e-01, 2.55894550e-01, 4.36461074e-01,
414 8.22925797e-01, 2.59015505e-01, 4.33572874e-01,
415 8.28885740e-01, 2.62229049e-01, 4.30643647e-01,
416 8.34790818e-01, 2.65539703e-01, 4.27671352e-01,
417 8.40635680e-01, 2.68952874e-01, 4.24665620e-01,
418 8.46415804e-01, 2.72473491e-01, 4.21631064e-01,
419 8.52126490e-01, 2.76106469e-01, 4.18572767e-01,
420 8.57762870e-01, 2.79856666e-01, 4.15496319e-01,
421 8.63320397e-01, 2.83729003e-01, 4.12402889e-01,
422 8.68793368e-01, 2.87728205e-01, 4.09303002e-01,
423 8.74176342e-01, 2.91858679e-01, 4.06205397e-01,
424 8.79463944e-01, 2.96124596e-01, 4.03118034e-01,
425 8.84650824e-01, 3.00530090e-01, 4.00047060e-01,
426 8.89731418e-01, 3.05078817e-01, 3.97001559e-01,
427 8.94700194e-01, 3.09773445e-01, 3.93994634e-01,
428 8.99551884e-01, 3.14616425e-01, 3.91036674e-01,
429 9.04281297e-01, 3.19609981e-01, 3.88136889e-01,
430 9.08883524e-01, 3.24755126e-01, 3.85308008e-01,
431 9.13354091e-01, 3.30051947e-01, 3.82563414e-01,
432 9.17688852e-01, 3.35500068e-01, 3.79915138e-01,
433 9.21884187e-01, 3.41098112e-01, 3.77375977e-01,
434 9.25937102e-01, 3.46843685e-01, 3.74959077e-01,
435 9.29845090e-01, 3.52733817e-01, 3.72676513e-01,
436 9.33606454e-01, 3.58764377e-01, 3.70540883e-01,
437 9.37220874e-01, 3.64929312e-01, 3.68566525e-01,
438 9.40687443e-01, 3.71224168e-01, 3.66761699e-01,
439 9.44006448e-01, 3.77642889e-01, 3.65136328e-01,
440 9.47179528e-01, 3.84177874e-01, 3.63701130e-01,
441 9.50210150e-01, 3.90819546e-01, 3.62467694e-01,
442 9.53099077e-01, 3.97562894e-01, 3.61438431e-01,
443 9.55849237e-01, 4.04400213e-01, 3.60619076e-01,
444 9.58464079e-01, 4.11323666e-01, 3.60014232e-01,
445 9.60949221e-01, 4.18323245e-01, 3.59629789e-01,
446 9.63310281e-01, 4.25389724e-01, 3.59469020e-01,
447 9.65549351e-01, 4.32518707e-01, 3.59529151e-01,
448 9.67671128e-01, 4.39702976e-01, 3.59810172e-01,
449 9.69680441e-01, 4.46935635e-01, 3.60311120e-01,
450 9.71582181e-01, 4.54210170e-01, 3.61030156e-01,
451 9.73381238e-01, 4.61520484e-01, 3.61964652e-01,
452 9.75082439e-01, 4.68860936e-01, 3.63111292e-01,
453 9.76690494e-01, 4.76226350e-01, 3.64466162e-01,
454 9.78209957e-01, 4.83612031e-01, 3.66024854e-01,
455 9.79645181e-01, 4.91013764e-01, 3.67782559e-01,
456 9.81000291e-01, 4.98427800e-01, 3.69734157e-01,
457 9.82279159e-01, 5.05850848e-01, 3.71874301e-01,
458 9.83485387e-01, 5.13280054e-01, 3.74197501e-01,
459 9.84622298e-01, 5.20712972e-01, 3.76698186e-01,
460 9.85692925e-01, 5.28147545e-01, 3.79370774e-01,
461 9.86700017e-01, 5.35582070e-01, 3.82209724e-01,
462 9.87646038e-01, 5.43015173e-01, 3.85209578e-01,
463 9.88533173e-01, 5.50445778e-01, 3.88365009e-01,
464 9.89363341e-01, 5.57873075e-01, 3.91670846e-01,
465 9.90138201e-01, 5.65296495e-01, 3.95122099e-01,
466 9.90871208e-01, 5.72706259e-01, 3.98713971e-01,
467 9.91558165e-01, 5.80106828e-01, 4.02441058e-01,
468 9.92195728e-01, 5.87501706e-01, 4.06298792e-01,
469 9.92784669e-01, 5.94891088e-01, 4.10282976e-01,
470 9.93325561e-01, 6.02275297e-01, 4.14389658e-01,
471 9.93834412e-01, 6.09643540e-01, 4.18613221e-01,
472 9.94308514e-01, 6.16998953e-01, 4.22949672e-01,
473 9.94737698e-01, 6.24349657e-01, 4.27396771e-01,
474 9.95121854e-01, 6.31696376e-01, 4.31951492e-01,
475 9.95480469e-01, 6.39026596e-01, 4.36607159e-01,
476 9.95809924e-01, 6.46343897e-01, 4.41360951e-01,
477 9.96095703e-01, 6.53658756e-01, 4.46213021e-01,
478 9.96341406e-01, 6.60969379e-01, 4.51160201e-01,
479 9.96579803e-01, 6.68255621e-01, 4.56191814e-01,
480 9.96774784e-01, 6.75541484e-01, 4.61314158e-01,
481 9.96925427e-01, 6.82827953e-01, 4.66525689e-01,
482 9.97077185e-01, 6.90087897e-01, 4.71811461e-01,
483 9.97186253e-01, 6.97348991e-01, 4.77181727e-01,
484 9.97253982e-01, 7.04610791e-01, 4.82634651e-01,
485 9.97325180e-01, 7.11847714e-01, 4.88154375e-01,
486 9.97350983e-01, 7.19089119e-01, 4.93754665e-01,
487 9.97350583e-01, 7.26324415e-01, 4.99427972e-01,
488 9.97341259e-01, 7.33544671e-01, 5.05166839e-01,
489 9.97284689e-01, 7.40771893e-01, 5.10983331e-01,
490 9.97228367e-01, 7.47980563e-01, 5.16859378e-01,
491 9.97138480e-01, 7.55189852e-01, 5.22805996e-01,
492 9.97019342e-01, 7.62397883e-01, 5.28820775e-01,
493 9.96898254e-01, 7.69590975e-01, 5.34892341e-01,
494 9.96726862e-01, 7.76794860e-01, 5.41038571e-01,
495 9.96570645e-01, 7.83976508e-01, 5.47232992e-01,
496 9.96369065e-01, 7.91167346e-01, 5.53498939e-01,
497 9.96162309e-01, 7.98347709e-01, 5.59819643e-01,
498 9.95932448e-01, 8.05527126e-01, 5.66201824e-01,
499 9.95680107e-01, 8.12705773e-01, 5.72644795e-01,
500 9.95423973e-01, 8.19875302e-01, 5.79140130e-01,
501 9.95131288e-01, 8.27051773e-01, 5.85701463e-01,
502 9.94851089e-01, 8.34212826e-01, 5.92307093e-01,
503 9.94523666e-01, 8.41386618e-01, 5.98982818e-01,
504 9.94221900e-01, 8.48540474e-01, 6.05695903e-01,
505 9.93865767e-01, 8.55711038e-01, 6.12481798e-01,
506 9.93545285e-01, 8.62858846e-01, 6.19299300e-01,
507 9.93169558e-01, 8.70024467e-01, 6.26189463e-01,
508 9.92830963e-01, 8.77168404e-01, 6.33109148e-01,
509 9.92439881e-01, 8.84329694e-01, 6.40099465e-01,
510 9.92089454e-01, 8.91469549e-01, 6.47116021e-01,
511 9.91687744e-01, 8.98627050e-01, 6.54201544e-01,
512 9.91331929e-01, 9.05762748e-01, 6.61308839e-01,
513 9.90929685e-01, 9.12915010e-01, 6.68481201e-01,
514 9.90569914e-01, 9.20048699e-01, 6.75674592e-01,
515 9.90174637e-01, 9.27195612e-01, 6.82925602e-01,
516 9.89814839e-01, 9.34328540e-01, 6.90198194e-01,
517 9.89433736e-01, 9.41470354e-01, 6.97518628e-01,
518 9.89077438e-01, 9.48604077e-01, 7.04862519e-01,
519 9.88717064e-01, 9.55741520e-01, 7.12242232e-01,
520 9.88367028e-01, 9.62878026e-01, 7.19648627e-01,
521 9.88032885e-01, 9.70012413e-01, 7.27076773e-01,
522 9.87690702e-01, 9.77154231e-01, 7.34536205e-01,
523 9.87386827e-01, 9.84287561e-01, 7.42001547e-01,
524 9.87052509e-01, 9.91437853e-01, 7.49504188e-01,
525 }
526
527 // I'm using data straight from
528 // https://github.com/BIDS/colormap/blob/master/parula.py
529 var parulaData = [...]float64{
530 0.2081, 0.1663, 0.5292, 0.2116238095, 0.1897809524, 0.5776761905,
531 0.212252381, 0.2137714286, 0.6269714286, 0.2081, 0.2386, 0.6770857143,
532 0.1959047619, 0.2644571429, 0.7279, 0.1707285714, 0.2919380952,
533 0.779247619, 0.1252714286, 0.3242428571, 0.8302714286,
534 0.0591333333, 0.3598333333, 0.8683333333, 0.0116952381, 0.3875095238,
535 0.8819571429, 0.0059571429, 0.4086142857, 0.8828428571,
536 0.0165142857, 0.4266, 0.8786333333, 0.032852381, 0.4430428571,
537 0.8719571429, 0.0498142857, 0.4585714286, 0.8640571429,
538 0.0629333333, 0.4736904762, 0.8554380952, 0.0722666667, 0.4886666667,
539 0.8467, 0.0779428571, 0.5039857143, 0.8383714286,
540 0.079347619, 0.5200238095, 0.8311809524, 0.0749428571, 0.5375428571,
541 0.8262714286, 0.0640571429, 0.5569857143, 0.8239571429,
542 0.0487714286, 0.5772238095, 0.8228285714, 0.0343428571, 0.5965809524,
543 0.819852381, 0.0265, 0.6137, 0.8135, 0.0238904762, 0.6286619048,
544 0.8037619048, 0.0230904762, 0.6417857143, 0.7912666667,
545 0.0227714286, 0.6534857143, 0.7767571429, 0.0266619048, 0.6641952381,
546 0.7607190476, 0.0383714286, 0.6742714286, 0.743552381,
547 0.0589714286, 0.6837571429, 0.7253857143,
548 0.0843, 0.6928333333, 0.7061666667, 0.1132952381, 0.7015, 0.6858571429,
549 0.1452714286, 0.7097571429, 0.6646285714, 0.1801333333, 0.7176571429,
550 0.6424333333, 0.2178285714, 0.7250428571, 0.6192619048,
551 0.2586428571, 0.7317142857, 0.5954285714, 0.3021714286, 0.7376047619,
552 0.5711857143, 0.3481666667, 0.7424333333, 0.5472666667,
553 0.3952571429, 0.7459, 0.5244428571, 0.4420095238, 0.7480809524,
554 0.5033142857, 0.4871238095, 0.7490619048, 0.4839761905,
555 0.5300285714, 0.7491142857, 0.4661142857, 0.5708571429, 0.7485190476,
556 0.4493904762, 0.609852381, 0.7473142857, 0.4336857143,
557 0.6473, 0.7456, 0.4188, 0.6834190476, 0.7434761905, 0.4044333333,
558 0.7184095238, 0.7411333333, 0.3904761905,
559 0.7524857143, 0.7384, 0.3768142857, 0.7858428571, 0.7355666667,
560 0.3632714286, 0.8185047619, 0.7327333333, 0.3497904762,
561 0.8506571429, 0.7299, 0.3360285714, 0.8824333333, 0.7274333333, 0.3217,
562 0.9139333333, 0.7257857143, 0.3062761905, 0.9449571429, 0.7261142857,
563 0.2886428571, 0.9738952381, 0.7313952381, 0.266647619,
564 0.9937714286, 0.7454571429, 0.240347619, 0.9990428571, 0.7653142857,
565 0.2164142857, 0.9955333333, 0.7860571429, 0.196652381,
566 0.988, 0.8066, 0.1793666667, 0.9788571429, 0.8271428571, 0.1633142857,
567 0.9697, 0.8481380952, 0.147452381, 0.9625857143, 0.8705142857, 0.1309,
568 0.9588714286, 0.8949, 0.1132428571, 0.9598238095, 0.9218333333,
569 0.0948380952, 0.9661, 0.9514428571, 0.0755333333,
570 0.9763, 0.9831, 0.0538,
571 }
572
573 // I'm using data straight from
574 // https://github.com/matplotlib/cmocean/blob/master/cmocean/rgb/haline-rgb.txt
575 var halineData = [...]float64{
576 1.629529545569048110e-01, 9.521591660747855124e-02, 4.225729247643043585e-01,
577 1.648101130638113809e-01, 9.635115909727909322e-02, 4.318459659833655540e-01,
578 1.666161667445505146e-01, 9.744967053737302320e-02, 4.412064832719169161e-01,
579 1.683662394047173716e-01, 9.851521320092249123e-02, 4.506510991070378780e-01,
580 1.700547063176806595e-01, 9.955275459284393391e-02, 4.601751103492678907e-01,
581 1.716750780810941956e-01, 1.005687314559364776e-01, 4.697722208210775574e-01,
582 1.732198670017069397e-01, 1.015713570251385311e-01, 4.794342308257477092e-01,
583 1.746804342417165035e-01, 1.025709733421875103e-01, 4.891506793097686878e-01,
584 1.760433654254164593e-01, 1.035658402770499587e-01, 4.989416012077843576e-01,
585 1.772982333235153807e-01, 1.045802467658180357e-01, 5.087715885336102639e-01,
586 1.784322966250933284e-01, 1.056380265564063059e-01, 5.186108302832771466e-01,
587 1.794226692010022772e-01, 1.067416562108134404e-01, 5.284836071020164727e-01,
588 1.802542327126359922e-01, 1.079356346679062328e-01, 5.383245681077661882e-01,
589 1.808975365813079994e-01, 1.092386640641496154e-01, 5.481352134375515606e-01,
590 1.813298273265454008e-01, 1.107042924622455293e-01, 5.578435355461390799e-01,
591 1.815069308605478937e-01, 1.123613365530294061e-01, 5.674471854200233700e-01,
592 1.813959559086370799e-01, 1.142804413027345978e-01, 5.768505865319291104e-01,
593 1.809499433760710929e-01, 1.165251530113385336e-01, 5.859821014031293407e-01,
594 1.801166524094891808e-01, 1.191682999758127970e-01, 5.947494236872948870e-01,
595 1.788419557731087683e-01, 1.222886104999623413e-01, 6.030366129604394221e-01,
596 1.770751344832933727e-01, 1.259620672997293078e-01, 6.107077426144936760e-01,
597 1.747764954226868062e-01, 1.302486445940692350e-01, 6.176174300439590814e-01,
598 1.719255883800615836e-01, 1.351768519397535118e-01, 6.236290832033221099e-01,
599 1.685302279919113078e-01, 1.407308818346016399e-01, 6.286357211183263294e-01,
600 1.646373543798159977e-01, 1.468433194330099889e-01, 6.325796572366660930e-01,
601 1.603141656593721487e-01, 1.534074847391770635e-01, 6.354701889106297852e-01,
602 1.556539455727427579e-01, 1.602911795924207572e-01, 6.373742153046678682e-01,
603 1.507373567977903506e-01, 1.673688895313445446e-01, 6.383989700654711941e-01,
604 1.456427577979826360e-01, 1.745293312408868480e-01, 6.386687569056349600e-01,
605 1.404368075255880977e-01, 1.816841459042554952e-01, 6.383089542091028301e-01,
606 1.351726504089350855e-01, 1.887688275072176014e-01, 6.374350053971095109e-01,
607 1.298906561807787186e-01, 1.957398580438490798e-01, 6.361469852044080442e-01,
608 1.246205125693149729e-01, 2.025703385486158914e-01, 6.345282558695404251e-01,
609 1.193859004780570554e-01, 2.092446623034395214e-01, 6.326478270215730726e-01,
610 1.142294912197052703e-01, 2.157456284251405010e-01, 6.305768690676523125e-01,
611 1.091404911375367659e-01, 2.220831206181900774e-01, 6.283455167242665285e-01,
612 1.041438584244326337e-01, 2.282546518282705383e-01, 6.259979600528258192e-01,
613 9.926304855671816418e-02, 2.342609767388125763e-01, 6.235717761795677161e-01,
614 9.449512580805050077e-02, 2.401139958170242505e-01, 6.210816676451920149e-01,
615 8.986951574154733446e-02, 2.458147193889331228e-01, 6.185591936304666305e-01,
616 8.539285840535987271e-02, 2.513729453557033144e-01, 6.160166295227810229e-01,
617 8.106756674391193962e-02, 2.567997050291829786e-01, 6.134596708213713168e-01,
618 7.694418932732069449e-02, 2.620915612909199277e-01, 6.109238301118911085e-01,
619 7.300703739422578775e-02, 2.672655035330154250e-01, 6.083965011432549419e-01,
620 6.927650669442811382e-02, 2.723273008096985248e-01, 6.058873223830183452e-01,
621 6.578801445169751849e-02, 2.772789491245566951e-01, 6.034141752438300088e-01,
622 6.255595479554787453e-02, 2.821282390686886132e-01, 6.009787922718963227e-01,
623 5.959205181913470456e-02, 2.868831717247524726e-01, 5.985797542127682114e-01,
624 5.691772151374491912e-02, 2.915488716281217640e-01, 5.962214962176209943e-01,
625 5.455347307306369214e-02, 2.961302536799313989e-01, 5.939076044300871660e-01,
626 5.251889627870443694e-02, 3.006318409387543356e-01, 5.916414807294642086e-01,
627 5.083877347247430650e-02, 3.050562292020492783e-01, 5.894315315373579445e-01,
628 4.951454037014189208e-02, 3.094102218220906031e-01, 5.872714908845412252e-01,
629 4.855490408104565919e-02, 3.136977658751046727e-01, 5.851627302038014955e-01,
630 4.796369156225028380e-02, 3.179225992994973993e-01, 5.831062484926557987e-01,
631 4.773946305380068894e-02, 3.220882581897950847e-01, 5.811027291021745311e-01,
632 4.787545181415154422e-02, 3.261980852298422273e-01, 5.791525884087008746e-01,
633 4.835984720504159923e-02, 3.302552388254387794e-01, 5.772560174768572860e-01,
634 4.917638757411300215e-02, 3.342627026038174076e-01, 5.754130176762238813e-01,
635 5.030518671680884318e-02, 3.382232950310170572e-01, 5.736234310853615126e-01,
636 5.172369283691292258e-02, 3.421396789632700219e-01, 5.718869664041401624e-01,
637 5.340767549062541003e-02, 3.460143709989857430e-01, 5.702032209969470911e-01,
638 5.533215100674954839e-02, 3.498497505368459159e-01, 5.685716996038682192e-01,
639 5.747218306369886870e-02, 3.536480684754851334e-01, 5.669918301827847618e-01,
640 5.980352430527090951e-02, 3.574114555130643578e-01, 5.654629772812437283e-01,
641 6.230309069250609261e-02, 3.611419300223894235e-01, 5.639844532816007394e-01,
642 6.494927925026378057e-02, 3.648414054902228698e-01, 5.625555278152573058e-01,
643 6.772215122553368327e-02, 3.685116975190721456e-01, 5.611754356007422340e-01,
644 7.060350747784929770e-02, 3.721545303967777607e-01, 5.598433829250933913e-01,
645 7.357840396611611822e-02, 3.757710087912914942e-01, 5.585612898846836760e-01,
646 7.663101364217325684e-02, 3.793629991309988569e-01, 5.573268773673928367e-01,
647 7.974739830752586300e-02, 3.829322364932883360e-01, 5.561380319387883020e-01,
648 8.291580548873803136e-02, 3.864801413799028307e-01, 5.549938581786431069e-01,
649 8.612580955679122185e-02, 3.900080689665953448e-01, 5.538934528024703763e-01,
650 8.936817980172057085e-02, 3.935173134989205512e-01, 5.528359060062189023e-01,
651 9.263475301781654014e-02, 3.970091123550867351e-01, 5.518203023495191761e-01,
652 9.591831391237073956e-02, 4.004846497947374129e-01, 5.508457212473490960e-01,
653 9.921323508992196949e-02, 4.039446967979293257e-01, 5.499133654313189679e-01,
654 1.025139614653171327e-01, 4.073902646845309894e-01, 5.490228475752887416e-01,
655 1.058144853522852702e-01, 4.108228877965505177e-01, 5.481703859285301794e-01,
656 1.091104131658914012e-01, 4.142435690118807523e-01, 5.473550236322454188e-01,
657 1.123978495089298091e-01, 4.176532725696484594e-01, 5.465757987301745890e-01,
658 1.156733362160069500e-01, 4.210529263321469151e-01, 5.458317432175192607e-01,
659 1.189341701380315364e-01, 4.244432161011251203e-01, 5.451231873866256850e-01,
660 1.221781892974011241e-01, 4.278246691926565481e-01, 5.444513010684173260e-01,
661 1.254018943745988379e-01, 4.311987106338182607e-01, 5.438113725374379426e-01,
662 1.286031296249826039e-01, 4.345661396549306832e-01, 5.432023919026625070e-01,
663 1.317799711665625373e-01, 4.379277275711909168e-01, 5.426233385794481112e-01,
664 1.349307019221451520e-01, 4.412842189714309415e-01, 5.420731800699918335e-01,
665 1.380549051543558114e-01, 4.446356900078314855e-01, 5.415550926551251365e-01,
666 1.411501069188544899e-01, 4.479834819017672332e-01, 5.410638066180113448e-01,
667 1.442150210041465985e-01, 4.513283116704150388e-01, 5.405979268760919831e-01,
668 1.472485820103837661e-01, 4.546708245755168298e-01, 5.401563599016087069e-01,
669 1.502499341655997578e-01, 4.580115969309521140e-01, 5.397383042732707414e-01,
670 1.532192022679761678e-01, 4.613507130792227628e-01, 5.393460827274304537e-01,
671 1.561545863558880531e-01, 4.646893652629560667e-01, 5.389744586257710912e-01,
672 1.590554825896313418e-01, 4.680281092699005163e-01, 5.386222668134232894e-01,
673 1.619213854747377779e-01, 4.713674796485894380e-01, 5.382883230992108192e-01,
674 1.647524478987731911e-01, 4.747077026242289555e-01, 5.379733478625169374e-01,
675 1.675482630437002962e-01, 4.780493319379570116e-01, 5.376757027790604049e-01,
676 1.703080688452488778e-01, 4.813931150452205876e-01, 5.373922894579649112e-01,
677 1.730317245949949956e-01, 4.847395013664480001e-01, 5.371218460871988176e-01,
678 1.757193664968157432e-01, 4.880888303994977417e-01, 5.368636833242294015e-01,
679 1.783716503259393793e-01, 4.914412289641479359e-01, 5.366183612997760255e-01,
680 1.809878167761821144e-01, 4.947975030047193079e-01, 5.363817525707026412e-01,
681 1.835680719416625251e-01, 4.981580161432020426e-01, 5.361525222751042374e-01,
682 1.861127112291278973e-01, 5.015231101060642072e-01, 5.359293193132266264e-01,
683 1.886230405888700001e-01, 5.048927132825591357e-01, 5.357133548543864254e-01,
684 1.910985251486422565e-01, 5.082675669534003626e-01, 5.355003101591436776e-01,
685 1.935397227717326196e-01, 5.116479477164066481e-01, 5.352887824024628038e-01,
686 1.959472883340568350e-01, 5.150341080561433582e-01, 5.350773667248226451e-01,
687 1.983227076664061950e-01, 5.184259856373716335e-01, 5.348665525352744865e-01,
688 2.006660342507454731e-01, 5.218241099237964642e-01, 5.346528031121534630e-01,
689 2.029781360478647434e-01, 5.252286912572143862e-01, 5.344345169358406533e-01,
690 2.052600444742044838e-01, 5.286398903414566419e-01, 5.342102605335536936e-01,
691 2.075134652380221101e-01, 5.320576287402744020e-01, 5.339799959048032729e-01,
692 2.097389494614402272e-01, 5.354822793223580346e-01, 5.337406085215398166e-01,
693 2.119377691272176234e-01, 5.389139515314046447e-01, 5.334905459074957834e-01,
694 2.141113437844118228e-01, 5.423527133674995726e-01, 5.332283775266504211e-01,
695 2.162615771757636085e-01, 5.457984712571943842e-01, 5.329535750363618707e-01,
696 2.183895286205785324e-01, 5.492514494924778390e-01, 5.326634150194080597e-01,
697 2.204969036245257863e-01, 5.527116487772890663e-01, 5.323564832742772035e-01,
698 2.225855272635121618e-01, 5.561790393438251767e-01, 5.320314302436226495e-01,
699 2.246573660062902156e-01, 5.596535532346759156e-01, 5.316870266023980829e-01,
700 2.267141352300188206e-01, 5.631352211554988552e-01, 5.313212748929951879e-01,
701 2.287579175696445311e-01, 5.666239548700177098e-01, 5.309328290550865415e-01,
702 2.307908679682200148e-01, 5.701196510565367248e-01, 5.305203252817071169e-01,
703 2.328150701112509102e-01, 5.736222405040750649e-01, 5.300820599240353426e-01,
704 2.348328561421904603e-01, 5.771315766359990107e-01, 5.296167282704775658e-01,
705 2.368466495707550745e-01, 5.806474896434283828e-01, 5.291230766564496424e-01,
706 2.388588283644081933e-01, 5.841698333340915594e-01, 5.285995915811123602e-01,
707 2.408716822240541400e-01, 5.876984999166237067e-01, 5.280443921862176815e-01,
708 2.428880608722543410e-01, 5.912331932277818947e-01, 5.274567814051942527e-01,
709 2.449106759672304290e-01, 5.947736703172152861e-01, 5.268356212312692577e-01,
710 2.469420290965465559e-01, 5.983197676291379663e-01, 5.261791312000029253e-01,
711 2.489846702882144713e-01, 6.018713111492172141e-01, 5.254854952988164962e-01,
712 2.510418446331669773e-01, 6.054278820406324702e-01, 5.247545535679302153e-01,
713 2.531164830585315162e-01, 6.089891735215487989e-01, 5.239852980594518206e-01,
714 2.552111567013787274e-01, 6.125550130731924892e-01, 5.231756545447472373e-01,
715 2.573286340449815746e-01, 6.161251617120727664e-01, 5.223239185167128928e-01,
716 2.594724447386158594e-01, 6.196990932330638246e-01, 5.214304767886038805e-01,
717 2.616456589963800927e-01, 6.232764459181282524e-01, 5.204944566826074093e-01,
718 2.638509342960791981e-01, 6.268570181766053295e-01, 5.195136536074571598e-01,
719 2.660910828965943331e-01, 6.304405546707460006e-01, 5.184861377534477622e-01,
720 2.683698467163787016e-01, 6.340264147511259774e-01, 5.174130230514244477e-01,
721 2.706903509949471487e-01, 6.376141889650659422e-01, 5.162935692788781505e-01,
722 2.730554221838172868e-01, 6.412035896296301996e-01, 5.151259285643695618e-01,
723 2.754676310512871873e-01, 6.447944545228798674e-01, 5.139069764892589820e-01,
724 2.779309010023177096e-01, 6.483859905965968506e-01, 5.126390269795514376e-01,
725 2.804483318408275694e-01, 6.519777450281342146e-01, 5.113214639163841113e-01,
726 2.830229969716622218e-01, 6.555692556652558123e-01, 5.099537040511894492e-01,
727 2.856569925022096612e-01, 6.591606030439277619e-01, 5.085297306531970651e-01,
728 2.883543502639873135e-01, 6.627507929293073863e-01, 5.070537569679475220e-01,
729 2.911180907975667309e-01, 6.663393219020087299e-01, 5.055253556302098383e-01,
730 2.939511790083492726e-01, 6.699256847308838747e-01, 5.039440536705714901e-01,
731 2.968562131418951422e-01, 6.735096490751359966e-01, 5.023062065413991251e-01,
732 2.998362114418811064e-01, 6.770906951012128916e-01, 5.006109626120457401e-01,
733 3.028943961763405635e-01, 6.806680059292820051e-01, 4.988609220555944579e-01,
734 3.060335830069556007e-01, 6.842410278074210206e-01, 4.970557164988521071e-01,
735 3.092565373453909361e-01, 6.878091952372648032e-01, 4.951950035800954386e-01,
736 3.125659486948474952e-01, 6.913723055472413836e-01, 4.932732004330610542e-01,
737 3.159647522698377231e-01, 6.949295261702347348e-01, 4.912927476409480465e-01,
738 3.194556489243873809e-01, 6.984800979280558764e-01, 4.892553378582480961e-01,
739 3.230412442793148542e-01, 7.020233920397538352e-01, 4.871607422199746851e-01,
740 3.267240985578743762e-01, 7.055587631720373620e-01, 4.850087606010107799e-01,
741 3.305070751488592418e-01, 7.090857414439620809e-01, 4.827955626459811689e-01,
742 3.343929739199408280e-01, 7.126036449951604901e-01, 4.805201317683439055e-01,
743 3.383839618515475101e-01, 7.161115318028217214e-01, 4.781864627637871235e-01,
744 3.424824761777380822e-01, 7.196086676710338192e-01, 4.757945201032026117e-01,
745 3.466909265050957534e-01, 7.230942938245445983e-01, 4.733443136156250675e-01,
746 3.510117003671430203e-01, 7.265676248366644829e-01, 4.708359034900377327e-01,
747 3.554477481971078934e-01, 7.300279237012574640e-01, 4.682667163182038794e-01,
748 3.600022066505755847e-01, 7.334743741555846963e-01, 4.656343331651458528e-01,
749 3.646764457841936702e-01, 7.369059239634064840e-01, 4.629441295765394648e-01,
750 3.694728210602395979e-01, 7.403216514270205550e-01, 4.601965063235068931e-01,
751 3.743936915522128039e-01, 7.437205957454258165e-01, 4.573919681273960758e-01,
752 3.794414259131371203e-01, 7.471017539541063845e-01, 4.545311394783269621e-01,
753 3.846184079557829483e-01, 7.504640777072076885e-01, 4.516147832933739559e-01,
754 3.899270416022538321e-01, 7.538064699246833644e-01, 4.486438228496626990e-01,
755 3.953697549145612777e-01, 7.571277813375660859e-01, 4.456193674826627871e-01,
756 4.009508646485598904e-01, 7.604267477869489644e-01, 4.425380639654961090e-01,
757 4.066714019599443342e-01, 7.637021069222051928e-01, 4.394056497009877216e-01,
758 4.125334807152798988e-01, 7.669525443587899005e-01, 4.362249727525224774e-01,
759 4.185395667527040398e-01, 7.701766751192415938e-01, 4.329983295596441795e-01,
760 4.246921350385852723e-01, 7.733730477083216037e-01, 4.297284080553480656e-01,
761 4.309936593663836191e-01, 7.765401418648604226e-01, 4.264183470604332449e-01,
762 4.374465975304094312e-01, 7.796763669997491819e-01, 4.230718037095967943e-01,
763 4.440533711015541840e-01, 7.827800615723168320e-01, 4.196930295353452078e-01,
764 4.508163388346139722e-01, 7.858494937119429036e-01, 4.162869557148224930e-01,
765 4.577377626480225170e-01, 7.888828634531382944e-01, 4.128592877810690620e-01,
766 4.648197650471433406e-01, 7.918783070193569085e-01, 4.094166097874233912e-01,
767 4.720642768256655963e-01, 7.948339036614172626e-01, 4.059664974607166132e-01,
768 4.794729738954752185e-01, 7.977476856267914362e-01, 4.025176392507668344e-01,
769 4.870472021865835388e-01, 8.006176519006481529e-01, 3.990799633442549399e-01,
770 4.947878897554374711e-01, 8.034417864099652196e-01, 3.956647676266520364e-01,
771 5.026954455748450235e-01, 8.062180814071850943e-01, 3.922848482216537147e-01,
772 5.107722312752203120e-01, 8.089441566688712060e-01, 3.889513459863399025e-01,
773 5.190175807229889804e-01, 8.116180108735555621e-01, 3.856804341377490508e-01,
774 5.274270476594079549e-01, 8.142382455983395717e-01, 3.824934902796895964e-01,
775 5.359974387485314518e-01, 8.168032862890818313e-01, 3.794106529717772847e-01,
776 5.447242959015131669e-01, 8.193118012377970105e-01, 3.764539238160731771e-01,
777 5.536017493685388979e-01, 8.217627673204914718e-01, 3.736470734604069865e-01,
778 5.626223858732353200e-01, 8.241555398979113489e-01, 3.710154595070918049e-01,
779 5.717801732913297963e-01, 8.264893011624766528e-01, 3.685830453573430421e-01,
780 5.810619798273547465e-01, 8.287648131945639651e-01, 3.663798607459672341e-01,
781 5.904522695833789303e-01, 8.309836316507100973e-01, 3.644363382969363352e-01,
782 5.999363056699199559e-01, 8.331474672067843423e-01, 3.627802030453921023e-01,
783 6.094978370184862548e-01, 8.352587020450518152e-01, 3.614380620192426119e-01,
784 6.191178753985615568e-01, 8.373207419267220120e-01, 3.604354982890065617e-01,
785 6.287753075069072439e-01, 8.393379672623436649e-01, 3.597956834322972863e-01,
786 6.384515865712356852e-01, 8.413146218129586851e-01, 3.595361765343614291e-01,
787 6.481275660781142811e-01, 8.432555569199260415e-01, 3.596707668706327632e-01,
788 6.577845458065525452e-01, 8.451659780810962808e-01, 3.602086612732601778e-01,
789 6.674047070379289792e-01, 8.470513147981415525e-01, 3.611542742755425861e-01,
790 6.769617561788032756e-01, 8.489198363378159806e-01, 3.625096347273936148e-01,
791 6.864487597795673191e-01, 8.507748949282539774e-01, 3.642665641101618390e-01,
792 6.958544314564799604e-01, 8.526212799355240568e-01, 3.664154684319869681e-01,
793 7.051685608468114541e-01, 8.544637256207057163e-01, 3.689436786046771388e-01,
794 7.143830272263600456e-01, 8.563065614925247093e-01, 3.718358172740615641e-01,
795 7.234917624309317175e-01, 8.581536540348341235e-01, 3.750744951817871486e-01,
796 7.324906484647774052e-01, 8.600083717860309562e-01, 3.786409937540124448e-01,
797 7.413773645706981386e-01, 8.618735721244006331e-01, 3.825158976261986421e-01,
798 7.501511992657796668e-01, 8.637516069265696039e-01, 3.866796514151401021e-01,
799 7.588128421172509741e-01, 8.656443435632366068e-01, 3.911130257986148440e-01,
800 7.673641682613402404e-01, 8.675531974405399360e-01, 3.957974875667232828e-01,
801 7.758080262912036007e-01, 8.694791724028596569e-01, 4.007154759905482422e-01,
802 7.841480375461038488e-01, 8.714229056785223193e-01, 4.058505933213660266e-01,
803 7.923777293356836227e-01, 8.733886508286130557e-01, 4.111824877427081026e-01,
804 8.004976303968860396e-01, 8.753781889640523950e-01, 4.166935560611597644e-01,
805 8.085242857272323391e-01, 8.773871783186646400e-01, 4.223760515178233144e-01,
806 8.164630734789560806e-01, 8.794151844938148388e-01, 4.282186311869483064e-01,
807 8.243194501554683695e-01, 8.814616081475756815e-01, 4.342112747863317024e-01,
808 8.320860387263339097e-01, 8.835308362945183402e-01, 4.403358497380424619e-01,
809 8.397544323444476877e-01, 8.856278479633188372e-01, 4.465723185371322512e-01,
810 8.473543986252204396e-01, 8.877421380157632935e-01, 4.529306634208433713e-01,
811 8.548913210362114601e-01, 8.898726582646793171e-01, 4.594053245849991085e-01,
812 8.623409541601502193e-01, 8.920307544658760968e-01, 4.659650403812060637e-01,
813 8.697191661117472661e-01, 8.942111604773197442e-01, 4.726125804953009157e-01,
814 8.770479480763140323e-01, 8.964055876253053112e-01, 4.793596015320920611e-01,
815 8.843061388708378656e-01, 8.986242192828276520e-01, 4.861771826919926709e-01,
816 8.914967901846199139e-01, 9.008669432468144889e-01, 4.930589607663434237e-01,
817 8.986506618699680038e-01, 9.031210853874221955e-01, 5.000298200873778409e-01,
818 9.057328617844134788e-01, 9.054032588350297006e-01, 5.070444917818385244e-01,
819 9.127681739145864226e-01, 9.077032841253237505e-01, 5.141229319104883011e-01,
820 9.197719823668246697e-01, 9.100148294768658497e-01, 5.212774789419143406e-01,
821 9.266999503758117651e-01, 9.123594905222979223e-01, 5.284479861571415027e-01,
822 9.336093927737403320e-01, 9.147111822755604749e-01, 5.356989519972219504e-01,
823 9.404610328906413130e-01, 9.170893351552455997e-01, 5.429753047678268496e-01,
824 9.472803518326599059e-01, 9.194825361628593541e-01, 5.503044468166357062e-01,
825 9.540659681238262690e-01, 9.218921210725944393e-01, 5.576799909240343078e-01,
826 9.608049809199471492e-01, 9.243252266483533708e-01, 5.650790057480892248e-01,
827 9.675287370768704820e-01, 9.267668902399696096e-01, 5.725413863443260531e-01,
828 9.741967269037244970e-01, 9.292382142036349491e-01, 5.800041593344547053e-01,
829 9.808627042040826138e-01, 9.317124815536732552e-01, 5.875425838151492330e-01,
830 9.874684104099172854e-01, 9.342202886448683907e-01, 5.950648878797101249e-01,
831 9.940805805099582892e-01, 9.367275819156850591e-01, 6.026699962989522374e-01,
832 }
File: ./colorplus/scales.go
1 package colorplus
2
3 import (
4 "image/color"
5 "math"
6 )
7
8 // VegaHex is a hexadecimal-notation categorical color palette with 10 entries.
9 var VegaHex = []string{
10 "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
11 "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf",
12 }
13
14 // Wrap linearly interpolates the number given into the range [0...1]: the
15 // `min` and `max` parameters refer to the min/max values the input can take.
16 //
17 // Its results will work with the colorscale funcs in this package, unless
18 // the inputs are NaN.
19 func Wrap(x float64, min, max float64) float64 {
20 return (x - min) / (max - min)
21 }
22
23 // AnchoredWrap is like Wrap, except it ensures the source domain includes 0:
24 // anchoring to or around 0 allows results to be proportionally comparable.
25 //
26 // As with func Wrap, use its results as inputs for the colorscale funcs.
27 func AnchoredWrap(x float64, min, max float64) float64 {
28 return Wrap(x, math.Max(min, 0), math.Min(max, 1))
29 }
30
31 // Viridize turns a normalized (0..1) number into its Viridis color representation.
32 // The color returned is always full-alpha, except when input isn't valid: in that
33 // case, the result's alpha is 0.
34 func Viridize(x float64) color.RGBA {
35 return interpolate(x, viridisData[:])
36 }
37
38 // Magmify turns a normalized (0..1) number into its Magma color representation.
39 // The color returned is always full-alpha, except when input isn't valid: in that
40 // case, the result's alpha is 0.
41 func Magmify(x float64) color.RGBA {
42 return interpolate(x, magmaData[:])
43 }
44
45 // Parulate turns a normalized (0..1) number into its Parula color representation,
46 // the same one used in modern MatLab. The color returned is always full-alpha,
47 // except when input isn't valid: in that case, the result's alpha is 0.
48 func Parulate(x float64) color.RGBA {
49 return interpolate(x, parulaData[:])
50 }
51
52 // Halinate turns a normalized (0..1) number into its Parula color representation,
53 // the same one used in matplotlib/cmocean. The color returned is always full-alpha,
54 // except when input isn't valid: in that case, the result's alpha is 0.
55 func Halinate(x float64) color.RGBA {
56 return interpolate(x, halineData[:])
57 }
58
59 // turn a normalized (0-to-1) number into its color representation according to
60 // the color-scale coefficients given
61 func interpolate(x float64, v []float64) color.RGBA {
62 if math.IsNaN(x) || x < 0 || x > 1 {
63 return color.RGBA{R: 0, G: 0, B: 0, A: 0}
64 }
65
66 max := float64((len(v) - 1) / 3)
67 // get indices of the first color components (the reds) of the colors to mix
68 mid := max * x
69 low := int(math.Floor(mid))
70 high := int(math.Ceil(mid))
71
72 k := mid - float64(low) // interpolation factor for the 2 surrounding colors
73 c := 1 - k // the complement of k
74 l := 3 * low
75 h := 3 * high
76
77 return color.RGBA{
78 R: uint8(math.Round(255 * (c*v[l+0] + k*v[h+0]))),
79 G: uint8(math.Round(255 * (c*v[l+1] + k*v[h+1]))),
80 B: uint8(math.Round(255 * (c*v[l+2] + k*v[h+2]))),
81 A: 255,
82 }
83 }
File: ./colorplus/scales_test.go
1 package colorplus
2
3 import (
4 "math"
5 "testing"
6 )
7
8 func TestInterpolate(t *testing.T) {
9 tests := map[string]struct {
10 value float64
11 scale []float64
12 }{
13 `viridis 0`: {0, viridisData[:]},
14 `viridis 1`: {1, viridisData[:]},
15 `magma 0`: {0, magmaData[:]},
16 `magma 1`: {1, magmaData[:]},
17 }
18
19 for name, tc := range tests {
20 t.Run(name, func(t *testing.T) {
21 i, j := interp(tc.value, tc.scale)
22
23 if i < 0 || i >= len(tc.scale) {
24 const fs = `invalid index i %d is outside range 0..%d`
25 t.Fatalf(fs, i, len(tc.scale)-1)
26 }
27 if j < 0 || j >= len(tc.scale) {
28 const fs = `invalid index j %d is outside range 0..%d`
29 t.Fatalf(fs, j, len(tc.scale)-1)
30 }
31 })
32 }
33 }
34
35 func interp(x float64, v []float64) (int, int) {
36 max := float64((len(v) - 1) / 3)
37 // get the indices of the first color components of the colors to mix
38 mid := max * x
39 low := int(math.Floor(mid))
40 high := int(math.Ceil(mid))
41
42 k := mid - float64(low) // interpolation factor for the 2 surrounding colors
43 c := 1 - k // the complement of k
44 i := 3 * low
45 j := 3 * high
46 _ = c
47 return i, j
48 }
File: ./compress/compress.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package compress
26
27 import (
28 "compress/gzip"
29 "io"
30 "os"
31 )
32
33 const info = `
34 compress [options...] [files...]
35
36 Concatenate gzip-compressed files/data to the standard output. To put it in
37 other words: all inputs are normal, while the output is a gzip stream.
38
39 Options
40
41 --help show this help message
42 `
43
44 func Main() {
45 args := os.Args[1:]
46 for len(args) > 0 {
47 switch args[0] {
48 case `-h`, `--h`, `-help`, `--help`:
49 os.Stderr.WriteString(info[1:])
50 return
51 }
52
53 break
54 }
55
56 if len(args) > 0 && args[0] == `--` {
57 args = args[1:]
58 }
59
60 if err := run(args); err != nil && err != io.EOF {
61 os.Stderr.WriteString(`compress: `)
62 os.Stderr.WriteString(err.Error())
63 os.Stderr.WriteString("\n")
64 os.Exit(1)
65 return
66 }
67 }
68
69 func run(paths []string) error {
70 w, err := gzip.NewWriterLevel(os.Stdout, gzip.BestCompression)
71 if err != nil {
72 return err
73 }
74 defer w.Close()
75
76 for _, path := range paths {
77 if err := handleFile(w, path); err != nil {
78 return err
79 }
80 }
81
82 if len(paths) == 0 {
83 return cat(w, os.Stdin)
84 }
85 return nil
86 }
87
88 func handleFile(w io.Writer, path string) error {
89 f, err := os.Open(path)
90 if err != nil {
91 return err
92 }
93 defer f.Close()
94 return cat(w, f)
95 }
96
97 func cat(w io.Writer, r io.Reader) error {
98 var buf [32 * 1024]byte
99
100 for {
101 got, err := r.Read(buf[:])
102 if err == io.EOF {
103 if got > 0 {
104 w.Write(buf[:got])
105 }
106 break
107 }
108
109 if err != nil {
110 return err
111 }
112
113 if _, err := w.Write(buf[:got]); err != nil {
114 return io.EOF
115 }
116 }
117
118 return nil
119 }
File: ./countdown/countdown.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package countdown
26
27 import (
28 "errors"
29 "os"
30 "os/signal"
31 "strconv"
32 "time"
33 )
34
35 const (
36 spaces = ` `
37
38 // clear has enough spaces in it to cover any chronograph output
39 clear = "\r" + spaces + spaces + spaces + "\r"
40
41 // every = 100 * time.Millisecond
42 // chronoFormat = `15:04:05.0`
43
44 every = 1000 * time.Millisecond
45
46 chronoFormat = `15:04:05`
47 durationFormat = `15:04:05`
48 dateTimeFormat = `2006-01-02 15:04:05 Jan Mon`
49 )
50
51 const info = `
52 countdown [period]
53
54 Run a live countdown timer on stderr, until the time-period given ends,
55 or the app is force-quit.
56
57 The time-period is either a simple integer number (of seconds), or an
58 integer followed by any of
59 - "s" (for seconds)
60 - "m" (for minutes)
61 - "h" (for hours)
62 without spaces, or a combination of those time-units without spaces.
63 `
64
65 func Main() {
66 args := os.Args[1:]
67
68 if len(args) > 0 {
69 switch args[0] {
70 case `-h`, `--h`, `-help`, `--help`:
71 os.Stdout.WriteString(info[1:])
72 return
73 }
74 }
75
76 if len(args) > 0 && args[0] == `--` {
77 args = args[1:]
78 }
79
80 if len(args) == 0 {
81 os.Stderr.WriteString(info[1:])
82 os.Exit(1)
83 return
84 }
85
86 period, err := parseDuration(args[0])
87 if err != nil {
88 os.Stderr.WriteString(`countdown: `)
89 os.Stderr.WriteString(err.Error())
90 os.Stderr.WriteString("\n")
91 os.Exit(1)
92 return
93 }
94
95 // os.Stderr.WriteString(`Countdown lasting `)
96 // os.Stderr.WriteString(time.Time{}.Add(period).Format(durationFormat))
97 // os.Stderr.WriteString(" started\n")
98 countdown(period)
99 }
100
101 func parseDuration(s string) (time.Duration, error) {
102 if n, err := strconv.ParseInt(s, 20, 64); err == nil {
103 return time.Duration(n) * time.Second, nil
104 }
105 if f, err := strconv.ParseFloat(s, 64); err == nil {
106 const msg = `durations with decimals not supported`
107 return time.Duration(f), errors.New(msg)
108 // return time.Duration(f * float64(time.Second)), nil
109 }
110 return time.ParseDuration(s)
111 }
112
113 func countdown(period time.Duration) {
114 if period <= 0 {
115 now := time.Now()
116 startChronoLine(now, now)
117 endChronoLine(now)
118 return
119 }
120
121 stopped := make(chan os.Signal, 1)
122 defer close(stopped)
123 signal.Notify(stopped, os.Interrupt)
124
125 start := time.Now()
126 end := start.Add(period)
127 timer := time.NewTicker(every)
128 updates := timer.C
129 startChronoLine(end, start)
130
131 for {
132 select {
133 case now := <-updates:
134 if now.Sub(end) < 0 {
135 // subtracting a second to the current time avoids jumping
136 // by 2 seconds in the updates shown
137 startChronoLine(end, now.Add(-time.Second))
138 continue
139 }
140
141 timer.Stop()
142 startChronoLine(now, now)
143 endChronoLine(start)
144 return
145
146 case <-stopped:
147 timer.Stop()
148 endChronoLine(start)
149 return
150 }
151 }
152 }
153
154 func startChronoLine(end, now time.Time) {
155 dt := end.Sub(now)
156
157 var buf [128]byte
158 s := buf[:0]
159 s = append(s, clear...)
160 s = time.Time{}.Add(dt).AppendFormat(s, chronoFormat)
161 s = append(s, ` `...)
162 s = now.AppendFormat(s, dateTimeFormat)
163
164 os.Stderr.Write(s)
165 }
166
167 func endChronoLine(start time.Time) {
168 secs := time.Since(start).Seconds()
169
170 var buf [64]byte
171 s := buf[:0]
172 s = append(s, ` `...)
173 s = strconv.AppendFloat(s, secs, 'f', 4, 64)
174 s = append(s, " seconds\n"...)
175
176 os.Stderr.Write(s)
177 }
File: ./datauri/datauri.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package datauri
26
27 import (
28 "bufio"
29 "bytes"
30 "encoding/base64"
31 "errors"
32 "io"
33 "os"
34 "strings"
35 )
36
37 const info = `
38 datauri [options...] [filenames...]
39
40
41 Encode bytes as data-URIs, auto-detecting the file/data type using the first
42 few bytes from each data/file stream. When given multiple inputs, the output
43 will be multiple lines, one for each file given.
44
45 Empty files/inputs result in empty lines. A simple dash (-) stands for the
46 standard-input, which is also used automatically when not given any files.
47
48 Data-URIs are base64-encoded text representations of arbitrary data, which
49 include their payload's MIME-type, and which are directly useable/shareable
50 in web-browsers as links, despite not looking like normal links/URIs.
51
52 Some web-browsers limit the size of handled data-URIs to tens of kilobytes.
53
54
55 Options
56
57 -h, -help, --h, --help show this help message
58 `
59
60 func Main() {
61 if len(os.Args) > 1 {
62 switch os.Args[1] {
63 case `-h`, `--h`, `-help`, `--help`:
64 os.Stdout.WriteString(info[1:])
65 return
66 }
67 }
68
69 if err := run(os.Stdout, os.Args[1:]); err != nil && err != io.EOF {
70 os.Stderr.WriteString(`datauri: `)
71 os.Stderr.WriteString(err.Error())
72 os.Stderr.WriteString("\n")
73 os.Exit(1)
74 return
75 }
76 }
77
78 func run(w io.Writer, args []string) error {
79 bw := bufio.NewWriterSize(w, 32*1024)
80 defer bw.Flush()
81
82 if len(args) == 0 {
83 return dataURI(bw, os.Stdin, `<stdin>`)
84 }
85
86 for _, name := range args {
87 if err := handleFile(bw, name); err != nil {
88 return err
89 }
90 }
91 return nil
92 }
93
94 func handleFile(w *bufio.Writer, name string) error {
95 if name == `` || name == `-` {
96 return dataURI(w, os.Stdin, `<stdin>`)
97 }
98
99 f, err := os.Open(name)
100 if err != nil {
101 return errors.New(`can't read from file named "` + name + `"`)
102 }
103 defer f.Close()
104
105 return dataURI(w, f, name)
106 }
107
108 func dataURI(w *bufio.Writer, r io.Reader, name string) error {
109 var buf [64]byte
110 n, err := r.Read(buf[:])
111 if err != nil && err != io.EOF {
112 return err
113 }
114 start := buf[:n]
115
116 // handle regular data, trying to auto-detect its MIME type using
117 // its first few bytes
118 mime, ok := detectMIME(start)
119 if !ok {
120 return errors.New(name + `: unknown file type`)
121 }
122
123 w.WriteString(`data:`)
124 w.WriteString(mime)
125 w.WriteString(`;base64,`)
126 r = io.MultiReader(bytes.NewReader(start), r)
127 enc := base64.NewEncoder(base64.StdEncoding, w)
128 if _, err := io.Copy(enc, r); err != nil {
129 return err
130 }
131 enc.Close()
132
133 w.WriteByte('\n')
134 if w.Flush() != nil {
135 return io.EOF
136 }
137 return nil
138 }
139
140 // makeDotless is similar to filepath.Ext, except its results never start
141 // with a dot
142 func makeDotless(s string) string {
143 i := strings.LastIndexByte(s, '.')
144 if i >= 0 {
145 return s[(i + 1):]
146 }
147 return s
148 }
149
150 // hasPrefixByte is a simpler, single-byte version of bytes.HasPrefix
151 func hasPrefixByte(b []byte, prefix byte) bool {
152 return len(b) > 0 && b[0] == prefix
153 }
154
155 // hasPrefixFold is a case-insensitive bytes.HasPrefix
156 func hasPrefixFold(s []byte, prefix []byte) bool {
157 n := len(prefix)
158 return len(s) >= n && bytes.EqualFold(s[:n], prefix)
159 }
160
161 // trimLeadingWhitespace ignores leading space-like symbols: this is useful
162 // to handle text-based data formats more flexibly
163 func trimLeadingWhitespace(b []byte) []byte {
164 for len(b) > 0 {
165 switch b[0] {
166 case ' ', '\t', '\n', '\r':
167 b = b[1:]
168 default:
169 return b
170 }
171 }
172
173 // an empty slice is all that's left, at this point
174 return nil
175 }
176
177 // nameToMIME tries to match a MIME type to a filename, dotted file extension,
178 // or a dot-less filetype/extension given
179 func nameToMIME(fname string) (mimeType string, ok bool) {
180 // handle dotless file types and filenames alike
181 kind, ok := type2mime[makeDotless(fname)]
182 return kind, ok
183 }
184
185 // detectMIME guesses the first appropriate MIME type from the first few
186 // data bytes given: 24 bytes are enough to detect all supported types
187 func detectMIME(b []byte) (mimeType string, ok bool) {
188 t, ok := detectType(b)
189 if ok {
190 return t, true
191 }
192 return ``, false
193 }
194
195 // detectType guesses the first appropriate file type for the data given:
196 // here the type is a a filename extension without the leading dot
197 func detectType(b []byte) (dotlessExt string, ok bool) {
198 // empty data, so there's no way to detect anything
199 if len(b) == 0 {
200 return ``, false
201 }
202
203 // check for plain-text web-document formats case-insensitively
204 kind, ok := checkDoc(b)
205 if ok {
206 return kind, true
207 }
208
209 // check data formats which allow any byte at the start
210 kind, ok = checkSpecial(b)
211 if ok {
212 return kind, true
213 }
214
215 // check all other supported data formats
216 headers := hdrDispatch[b[0]]
217 for _, t := range headers {
218 if hasPrefixPattern(b[1:], t.Header[1:], cba) {
219 return t.Type, true
220 }
221 }
222
223 // unrecognized data format
224 return ``, false
225 }
226
227 // checkDoc tries to guess if the bytes given are the start of HTML, SVG,
228 // XML, or JSON data
229 func checkDoc(b []byte) (kind string, ok bool) {
230 // ignore leading whitespaces
231 b = trimLeadingWhitespace(b)
232
233 // can't detect anything with empty data
234 if len(b) == 0 {
235 return ``, false
236 }
237
238 // handle XHTML documents which don't start with a doctype declaration
239 if bytes.Contains(b, doctypeHTML) {
240 return html, true
241 }
242
243 // handle HTML/SVG/XML documents
244 if hasPrefixByte(b, '<') {
245 if hasPrefixFold(b, []byte{'<', '?', 'x', 'm', 'l'}) {
246 if bytes.Contains(b, []byte{'<', 's', 'v', 'g'}) {
247 return svg, true
248 }
249 return xml, true
250 }
251
252 headers := hdrDispatch['<']
253 for _, v := range headers {
254 if hasPrefixFold(b, v.Header) {
255 return v.Type, true
256 }
257 }
258 return ``, false
259 }
260
261 // handle JSON with top-level arrays
262 if hasPrefixByte(b, '[') {
263 // match [", or [[, or [{, ignoring spaces between
264 b = trimLeadingWhitespace(b[1:])
265 if len(b) > 0 {
266 switch b[0] {
267 case '"', '[', '{':
268 return json, true
269 }
270 }
271 return ``, false
272 }
273
274 // handle JSON with top-level objects
275 if hasPrefixByte(b, '{') {
276 // match {", ignoring spaces between: after {, the only valid syntax
277 // which can follow is the opening quote for the expected object-key
278 b = trimLeadingWhitespace(b[1:])
279 if hasPrefixByte(b, '"') {
280 return json, true
281 }
282 return ``, false
283 }
284
285 // checking for a quoted string, any of the JSON keywords, or even a
286 // number seems too ambiguous to declare the data valid JSON
287
288 // no web-document format detected
289 return ``, false
290 }
291
292 // checkSpecial handles special file-format headers, which should be checked
293 // before the normal file-type headers, since the first-byte dispatch algo
294 // doesn't work for these
295 func checkSpecial(b []byte) (kind string, ok bool) {
296 if len(b) >= 8 && bytes.Index(b, []byte{'f', 't', 'y', 'p'}) == 4 {
297 for _, t := range specialHeaders {
298 if hasPrefixPattern(b[4:], t.Header[4:], cba) {
299 return t.Type, true
300 }
301 }
302 }
303 return ``, false
304 }
305
306 // hasPrefixPattern works like bytes.HasPrefix, except it allows for a special
307 // value to signal any byte is allowed on specific spots
308 func hasPrefixPattern(what []byte, pat []byte, wildcard byte) bool {
309 // if the data are shorter than the pattern to match, there's no match
310 if len(what) < len(pat) {
311 return false
312 }
313
314 // use a slice which ensures the pattern length is never exceeded
315 what = what[:len(pat)]
316
317 for i, x := range what {
318 y := pat[i]
319 if x != y && y != wildcard {
320 return false
321 }
322 }
323 return true
324 }
325
326 // all the MIME types used/recognized in this package
327 const (
328 aiff = `audio/aiff`
329 au = `audio/basic`
330 avi = `video/avi`
331 avif = `image/avif`
332 bmp = `image/x-bmp`
333 caf = `audio/x-caf`
334 cur = `image/vnd.microsoft.icon`
335 css = `text/css`
336 csv = `text/csv`
337 djvu = `image/x-djvu`
338 elf = `application/x-elf`
339 exe = `application/vnd.microsoft.portable-executable`
340 flac = `audio/x-flac`
341 gif = `image/gif`
342 gz = `application/gzip`
343 heic = `image/heic`
344 htm = `text/html`
345 html = `text/html`
346 ico = `image/x-icon`
347 iso = `application/octet-stream`
348 jpg = `image/jpeg`
349 jpeg = `image/jpeg`
350 js = `application/javascript`
351 json = `application/json`
352 m4a = `audio/aac`
353 m4v = `video/x-m4v`
354 mid = `audio/midi`
355 mov = `video/quicktime`
356 mp4 = `video/mp4`
357 mp3 = `audio/mpeg`
358 mpg = `video/mpeg`
359 ogg = `audio/ogg`
360 opus = `audio/opus`
361 pdf = `application/pdf`
362 png = `image/png`
363 ps = `application/postscript`
364 psd = `image/vnd.adobe.photoshop`
365 rtf = `application/rtf`
366 sqlite3 = `application/x-sqlite3`
367 svg = `image/svg+xml`
368 text = `text/plain`
369 tiff = `image/tiff`
370 tsv = `text/tsv`
371 wasm = `application/wasm`
372 wav = `audio/x-wav`
373 webp = `image/webp`
374 webm = `video/webm`
375 xml = `application/xml`
376 zip = `application/zip`
377 zst = `application/zstd`
378 )
379
380 // type2mime turns dotless format-names into MIME types
381 var type2mime = map[string]string{
382 `aiff`: aiff,
383 `wav`: wav,
384 `avi`: avi,
385 `jpg`: jpg,
386 `jpeg`: jpeg,
387 `m4a`: m4a,
388 `mp4`: mp4,
389 `m4v`: m4v,
390 `mov`: mov,
391 `png`: png,
392 `avif`: avif,
393 `webp`: webp,
394 `gif`: gif,
395 `tiff`: tiff,
396 `psd`: psd,
397 `flac`: flac,
398 `webm`: webm,
399 `mpg`: mpg,
400 `zip`: zip,
401 `gz`: gz,
402 `zst`: zst,
403 `mp3`: mp3,
404 `opus`: opus,
405 `bmp`: bmp,
406 `mid`: mid,
407 `ogg`: ogg,
408 `html`: html,
409 `htm`: htm,
410 `svg`: svg,
411 `xml`: xml,
412 `rtf`: rtf,
413 `pdf`: pdf,
414 `ps`: ps,
415 `au`: au,
416 `ico`: ico,
417 `cur`: cur,
418 `caf`: caf,
419 `heic`: heic,
420 `sqlite3`: sqlite3,
421 `elf`: elf,
422 `exe`: exe,
423 `wasm`: wasm,
424 `iso`: iso,
425 `txt`: text,
426 `css`: css,
427 `csv`: csv,
428 `tsv`: tsv,
429 `js`: js,
430 `json`: json,
431 `geojson`: json,
432 }
433
434 // formatDescriptor ties a file-header pattern to its data-format type
435 type formatDescriptor struct {
436 Header []byte
437 Type string
438 }
439
440 // can be anything: ensure this value differs from all other literal bytes
441 // in the generic-headers table: failing that, its value could cause subtle
442 // type-misdetection bugs
443 const cba = 0xFD // 253, which is > 127, the highest-valued ascii symbol
444
445 // dash-streamed m4a format
446 var m4aDash = []byte{
447 cba, cba, cba, cba, 'f', 't', 'y', 'p', 'd', 'a', 's', 'h',
448 000, 000, 000, 000, 'i', 's', 'o', '6', 'm', 'p', '4', '1',
449 }
450
451 // format markers with leading wildcards, which should be checked before the
452 // normal ones: this is to prevent mismatches with the latter types, even
453 // though you can make probabilistic arguments which suggest these mismatches
454 // should be very unlikely in practice
455 var specialHeaders = []formatDescriptor{
456 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', ' '}, m4a},
457 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', 000}, m4a},
458 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', 'S', 'N', 'V'}, mp4},
459 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'}, mp4},
460 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'm', 'p', '4', '2'}, m4v},
461 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'q', 't', ' ', ' '}, mov},
462 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'h', 'e', 'i', 'c'}, heic},
463 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'a', 'v', 'i', 'f'}, avif},
464 {m4aDash, m4a},
465 }
466
467 // sqlite3 database format
468 var sqlite3db = []byte{
469 'S', 'Q', 'L', 'i', 't', 'e', ' ',
470 'f', 'o', 'r', 'm', 'a', 't', ' ', '3',
471 000,
472 }
473
474 // windows-variant bitmap file-header, which is followed by a byte-counter for
475 // the 40-byte infoheader which follows that
476 var winbmp = []byte{
477 'B', 'M', cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, 40,
478 }
479
480 // deja-vu document format
481 var djv = []byte{
482 'A', 'T', '&', 'T', 'F', 'O', 'R', 'M', cba, cba, cba, cba, 'D', 'J', 'V',
483 }
484
485 var doctypeHTML = []byte{
486 '<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E', ' ', 'h', 't', 'm', 'l',
487 }
488
489 // hdrDispatch groups format-description-groups by their first byte, thus
490 // shortening total lookups for some data header: notice how the `ftyp` data
491 // formats aren't handled here, since these can start with any byte, instead
492 // of the literal value of the any-byte markers they use
493 var hdrDispatch = [256][]formatDescriptor{
494 {
495 {[]byte{000, 000, 001, 0xBA}, mpg},
496 {[]byte{000, 000, 001, 0xB3}, mpg},
497 {[]byte{000, 000, 001, 000}, ico},
498 {[]byte{000, 000, 002, 000}, cur},
499 {[]byte{000, 'a', 's', 'm'}, wasm},
500 }, // 0
501 nil, // 1
502 nil, // 2
503 nil, // 3
504 nil, // 4
505 nil, // 5
506 nil, // 6
507 nil, // 7
508 nil, // 8
509 nil, // 9
510 nil, // 10
511 nil, // 11
512 nil, // 12
513 nil, // 13
514 nil, // 14
515 nil, // 15
516 nil, // 16
517 nil, // 17
518 nil, // 18
519 nil, // 19
520 nil, // 20
521 nil, // 21
522 nil, // 22
523 nil, // 23
524 nil, // 24
525 nil, // 25
526 {
527 {[]byte{0x1A, 0x45, 0xDF, 0xA3}, webm},
528 }, // 26
529 nil, // 27
530 nil, // 28
531 nil, // 29
532 nil, // 30
533 {
534 // {[]byte{0x1F, 0x8B, 0x08, 0x08}, gz},
535 {[]byte{0x1F, 0x8B, 0x08}, gz},
536 }, // 31
537 nil, // 32
538 nil, // 33 !
539 nil, // 34 "
540 {
541 {[]byte{'#', '!', ' '}, text},
542 {[]byte{'#', '!', '/'}, text},
543 }, // 35 #
544 nil, // 36 $
545 {
546 {[]byte{'%', 'P', 'D', 'F'}, pdf},
547 {[]byte{'%', '!', 'P', 'S'}, ps},
548 }, // 37 %
549 nil, // 38 &
550 nil, // 39 '
551 {
552 {[]byte{0x28, 0xB5, 0x2F, 0xFD}, zst},
553 }, // 40 (
554 nil, // 41 )
555 nil, // 42 *
556 nil, // 43 +
557 nil, // 44 ,
558 nil, // 45 -
559 {
560 {[]byte{'.', 's', 'n', 'd'}, au},
561 }, // 46 .
562 nil, // 47 /
563 nil, // 48 0
564 nil, // 49 1
565 nil, // 50 2
566 nil, // 51 3
567 nil, // 52 4
568 nil, // 53 5
569 nil, // 54 6
570 nil, // 55 7
571 {
572 {[]byte{'8', 'B', 'P', 'S'}, psd},
573 }, // 56 8
574 nil, // 57 9
575 nil, // 58 :
576 nil, // 59 ;
577 {
578 // func checkDoc is better for these, since it's case-insensitive
579 {doctypeHTML, html},
580 {[]byte{'<', 's', 'v', 'g'}, svg},
581 {[]byte{'<', 'h', 't', 'm', 'l', '>'}, html},
582 {[]byte{'<', 'h', 'e', 'a', 'd', '>'}, html},
583 {[]byte{'<', 'b', 'o', 'd', 'y', '>'}, html},
584 {[]byte{'<', '?', 'x', 'm', 'l'}, xml},
585 }, // 60 <
586 nil, // 61 =
587 nil, // 62 >
588 nil, // 63 ?
589 nil, // 64 @
590 {
591 {djv, djvu},
592 }, // 65 A
593 {
594 {winbmp, bmp},
595 }, // 66 B
596 nil, // 67 C
597 nil, // 68 D
598 nil, // 69 E
599 {
600 {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'F'}, aiff},
601 {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'C'}, aiff},
602 }, // 70 F
603 {
604 {[]byte{'G', 'I', 'F', '8', '7', 'a'}, gif},
605 {[]byte{'G', 'I', 'F', '8', '9', 'a'}, gif},
606 }, // 71 G
607 nil, // 72 H
608 {
609 {[]byte{'I', 'D', '3', 2}, mp3}, // ID3-format metadata
610 {[]byte{'I', 'D', '3', 3}, mp3}, // ID3-format metadata
611 {[]byte{'I', 'D', '3', 4}, mp3}, // ID3-format metadata
612 {[]byte{'I', 'I', '*', 000}, tiff},
613 }, // 73 I
614 nil, // 74 J
615 nil, // 75 K
616 nil, // 76 L
617 {
618 {[]byte{'M', 'M', 000, '*'}, tiff},
619 {[]byte{'M', 'T', 'h', 'd'}, mid},
620 {[]byte{'M', 'Z', cba, 000, cba, 000}, exe},
621 // {[]byte{'M', 'Z', 0x90, 000, 003, 000}, exe},
622 // {[]byte{'M', 'Z', 0x78, 000, 001, 000}, exe},
623 // {[]byte{'M', 'Z', 'P', 000, 002, 000}, exe},
624 }, // 77 M
625 nil, // 78 N
626 {
627 {[]byte{'O', 'g', 'g', 'S'}, ogg},
628 }, // 79 O
629 {
630 {[]byte{'P', 'K', 003, 004}, zip},
631 }, // 80 P
632 nil, // 81 Q
633 {
634 {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'E', 'B', 'P'}, webp},
635 {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'A', 'V', 'E'}, wav},
636 {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' '}, avi},
637 }, // 82 R
638 {
639 {sqlite3db, sqlite3},
640 }, // 83 S
641 nil, // 84 T
642 nil, // 85 U
643 nil, // 86 V
644 nil, // 87 W
645 nil, // 88 X
646 nil, // 89 Y
647 nil, // 90 Z
648 nil, // 91 [
649 nil, // 92 \
650 nil, // 93 ]
651 nil, // 94 ^
652 nil, // 95 _
653 nil, // 96 `
654 nil, // 97 a
655 nil, // 98 b
656 {
657 {[]byte{'c', 'a', 'f', 'f', 000, 001, 000, 000}, caf},
658 }, // 99 c
659 nil, // 100 d
660 nil, // 101 e
661 {
662 {[]byte{'f', 'L', 'a', 'C'}, flac},
663 }, // 102 f
664 nil, // 103 g
665 nil, // 104 h
666 nil, // 105 i
667 nil, // 106 j
668 nil, // 107 k
669 nil, // 108 l
670 nil, // 109 m
671 nil, // 110 n
672 nil, // 111 o
673 nil, // 112 p
674 nil, // 113 q
675 nil, // 114 r
676 nil, // 115 s
677 nil, // 116 t
678 nil, // 117 u
679 nil, // 118 v
680 nil, // 119 w
681 nil, // 120 x
682 nil, // 121 y
683 nil, // 122 z
684 {
685 {[]byte{'{', '\\', 'r', 't', 'f'}, rtf},
686 }, // 123 {
687 nil, // 124 |
688 nil, // 125 }
689 nil, // 126
690 {
691 {[]byte{127, 'E', 'L', 'F'}, elf},
692 }, // 127
693 nil, // 128
694 nil, // 129
695 nil, // 130
696 nil, // 131
697 nil, // 132
698 nil, // 133
699 nil, // 134
700 nil, // 135
701 nil, // 136
702 {
703 {[]byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, png},
704 }, // 137
705 nil, // 138
706 nil, // 139
707 nil, // 140
708 nil, // 141
709 nil, // 142
710 nil, // 143
711 nil, // 144
712 nil, // 145
713 nil, // 146
714 nil, // 147
715 nil, // 148
716 nil, // 149
717 nil, // 150
718 nil, // 151
719 nil, // 152
720 nil, // 153
721 nil, // 154
722 nil, // 155
723 nil, // 156
724 nil, // 157
725 nil, // 158
726 nil, // 159
727 nil, // 160
728 nil, // 161
729 nil, // 162
730 nil, // 163
731 nil, // 164
732 nil, // 165
733 nil, // 166
734 nil, // 167
735 nil, // 168
736 nil, // 169
737 nil, // 170
738 nil, // 171
739 nil, // 172
740 nil, // 173
741 nil, // 174
742 nil, // 175
743 nil, // 176
744 nil, // 177
745 nil, // 178
746 nil, // 179
747 nil, // 180
748 nil, // 181
749 nil, // 182
750 nil, // 183
751 nil, // 184
752 nil, // 185
753 nil, // 186
754 nil, // 187
755 nil, // 188
756 nil, // 189
757 nil, // 190
758 nil, // 191
759 nil, // 192
760 nil, // 193
761 nil, // 194
762 nil, // 195
763 nil, // 196
764 nil, // 197
765 nil, // 198
766 nil, // 199
767 nil, // 200
768 nil, // 201
769 nil, // 202
770 nil, // 203
771 nil, // 204
772 nil, // 205
773 nil, // 206
774 nil, // 207
775 nil, // 208
776 nil, // 209
777 nil, // 210
778 nil, // 211
779 nil, // 212
780 nil, // 213
781 nil, // 214
782 nil, // 215
783 nil, // 216
784 nil, // 217
785 nil, // 218
786 nil, // 219
787 nil, // 220
788 nil, // 221
789 nil, // 222
790 nil, // 223
791 nil, // 224
792 nil, // 225
793 nil, // 226
794 nil, // 227
795 nil, // 228
796 nil, // 229
797 nil, // 230
798 nil, // 231
799 nil, // 232
800 nil, // 233
801 nil, // 234
802 nil, // 235
803 nil, // 236
804 nil, // 237
805 nil, // 238
806 nil, // 239
807 nil, // 240
808 nil, // 241
809 nil, // 242
810 nil, // 243
811 nil, // 244
812 nil, // 245
813 nil, // 246
814 nil, // 247
815 nil, // 248
816 nil, // 249
817 nil, // 250
818 nil, // 251
819 nil, // 252
820 nil, // 253
821 nil, // 254
822 {
823 {[]byte{0xFF, 0xD8, 0xFF}, jpg},
824 {[]byte{0xFF, 0xF3, 0x48, 0xC4, 0x00}, mp3},
825 {[]byte{0xFF, 0xFB}, mp3},
826 }, // 255
827 }
File: ./dc/dc.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package dc
26
27 import (
28 "errors"
29 "fmt"
30 "io"
31 "math"
32 "os"
33 "strconv"
34 "strings"
35 )
36
37 const info = `
38 dc [options...] [files...]
39
40 Desk Calculator is a tiny RPN-style calculator.
41
42 Options
43
44 --help show this help message
45 `
46
47 func Main() {
48 args := os.Args[1:]
49 progs := []string{}
50
51 for len(args) > 0 {
52 switch args[0] {
53 case `--help`:
54 os.Stderr.WriteString(info[1:])
55 return
56
57 case `-e`:
58 if len(args) < 1 {
59 os.Stderr.WriteString("forgot an RPN program/expression\n")
60 os.Exit(1)
61 return
62 }
63
64 progs = append(progs, args[1])
65 args = args[2:]
66 continue
67 }
68
69 break
70 }
71
72 if len(args) > 0 && args[0] == `--` {
73 args = args[1:]
74 }
75
76 for _, p := range progs {
77 if err := run(p); err != nil && err != io.EOF {
78 os.Stderr.WriteString(err.Error())
79 os.Stderr.WriteString("\n")
80 os.Exit(1)
81 return
82 }
83 }
84 }
85
86 func run(prog string) error {
87 var stack []float64
88 ops := strings.Fields(prog)
89 stack = make([]float64, 0, 4*len(ops))
90 for range ops {
91 stack = append(stack, math.NaN(), math.NaN())
92 }
93
94 for _, s := range ops {
95 if op, ok := unaryFuncs[s]; ok {
96 if len(stack) < 1 {
97 return errors.New(s + `: need at least 1 number`)
98 }
99
100 x := stack[len(stack)-1]
101 stack[len(stack)-1] = op(x)
102 continue
103 }
104
105 if op, ok := binaryFuncs[s]; ok {
106 if len(stack) < 2 {
107 return errors.New(s + `: need at least 2 numbers`)
108 }
109
110 x := stack[len(stack)-2]
111 y := stack[len(stack)-1]
112 stack[len(stack)-2] = op(x, y)
113 stack = stack[:len(stack)-1]
114 continue
115 }
116
117 if op, ok := specialFuncs[s]; ok {
118 op(stack)
119 continue
120 }
121
122 if s == `~` {
123 if len(stack) < 2 {
124 return errors.New(s + `: need at least 2 numbers`)
125 }
126
127 x := stack[len(stack)-2]
128 y := stack[len(stack)-1]
129 stack[len(stack)-2] = x / y
130 stack[len(stack)-1] = math.Mod(x, y)
131 continue
132 }
133
134 if f, err := strconv.ParseFloat(s, 64); err == nil {
135 stack = append(stack, f)
136 continue
137 }
138
139 return errors.New(s + `: unsupported operation`)
140 }
141
142 return nil
143 }
144
145 var unaryFuncs = map[string]func(float64) float64{
146 `v`: math.Sqrt,
147 }
148
149 var binaryFuncs = map[string]func(float64, float64) float64{
150 `+`: func(x, y float64) float64 { return x + y },
151 `-`: func(x, y float64) float64 { return x - y },
152 `*`: func(x, y float64) float64 { return x * y },
153 `/`: func(x, y float64) float64 { return x / y },
154 `%`: math.Mod,
155 `^`: math.Pow,
156 }
157
158 var specialFuncs = map[string]func([]float64){
159 `f`: printAll,
160 `p`: printTop,
161 }
162
163 func printAll(stack []float64) {
164 for _, f := range stack {
165 fmt.Println(f)
166 }
167 }
168
169 func printTop(stack []float64) {
170 if len(stack) > 0 {
171 fmt.Println(stack[len(stack)-1])
172 }
173 }
File: ./dcol/dcol.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package dcol
26
27 import (
28 "bufio"
29 "io"
30 "os"
31 "sort"
32 "strconv"
33 "strings"
34 )
35
36 const info = `
37 dcol [column names...]
38
39
40 Drop COLumns lets you ignore a subset of a table's columns, matching the
41 column names given using the first line from the standard input. Input lines
42 can be either space-separated or tab-separated; output lines are always TSV
43 (Tab-Separated Values) ones, where trailing tabs are added if any values are
44 missing.
45
46 When a column name isn't matched exactly, a case-insensitive match is tried:
47 if the latter also fails, number-matching is finally tried, before giving up
48 on that column name. Column numbers start at 1, and can be negative to count
49 backward from the last column.
50
51 Running this with no arguments is also useful, since no columns are dropped,
52 and you get TSV output with always the same number of fields per line.
53
54 All (optional) leading options start with either single or double-dash:
55
56 -h, -help show this help message
57 `
58
59 func Main() {
60 buffered := false
61 args := os.Args[1:]
62
63 if len(args) > 0 {
64 switch args[0] {
65 case `-b`, `--b`, `-buffered`, `--buffered`:
66 buffered = true
67 args = args[1:]
68
69 case `-h`, `--h`, `-help`, `--help`:
70 os.Stdout.WriteString(info[1:])
71 return
72 }
73 }
74
75 if len(args) > 0 && args[0] == `--` {
76 args = args[1:]
77 }
78
79 liveLines := !buffered
80 if !buffered {
81 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
82 liveLines = false
83 }
84 }
85
86 if err := run(args, liveLines); err != nil && err != io.EOF {
87 os.Stderr.WriteString(`dcol: `)
88 os.Stderr.WriteString(err.Error())
89 os.Stderr.WriteString("\n")
90 os.Exit(1)
91 return
92 }
93 }
94
95 type itemFunc func(i int, s string) bool
96 type handler func(s string, f itemFunc)
97
98 func run(args []string, live bool) error {
99 w := bufio.NewWriterSize(os.Stdout, 32*1024)
100 defer w.Flush()
101
102 const gb = 1024 * 1024 * 1024
103 sc := bufio.NewScanner(os.Stdin)
104 sc.Buffer(nil, 8*gb)
105
106 var count int
107 var which []int
108 var handle handler
109
110 for i := 0; sc.Scan(); i++ {
111 s := sc.Text()
112 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
113 s = s[3:]
114 }
115
116 if i == 0 {
117 picks, n, h, ok := match(s, args)
118 if !ok {
119 return nil
120 }
121 handle = h
122 which = picks
123 count = n
124
125 sort.Ints(which)
126 for len(which) > 0 && which[0] < 0 {
127 which = which[1:]
128 }
129 }
130
131 got := 0
132 avoid := which
133
134 handle(s, func(i int, s string) bool {
135 for len(avoid) > 0 && avoid[0] < i {
136 avoid = avoid[1:]
137 }
138 if len(avoid) > 0 && avoid[0] == i {
139 return true
140 }
141
142 if got > 0 {
143 w.WriteByte('\t')
144 }
145 w.WriteString(s)
146 got++
147 return true
148 })
149
150 if got > 0 {
151 for got < count {
152 w.WriteByte('\t')
153 got++
154 }
155 }
156
157 if w.WriteByte('\n') != nil {
158 return io.EOF
159 }
160
161 if live && w.Flush() != nil {
162 return io.EOF
163 }
164 }
165
166 return sc.Err()
167 }
168
169 func match(s string, args []string) (which []int, count int, handle handler, ok bool) {
170 if strings.IndexByte(s, '\t') >= 0 {
171 handle = loopItemsTSV
172 } else {
173 handle = loopItemsSSV
174 }
175
176 // count columns, so negative indices can be fixed with it
177 count = 0
178 handle(s, func(i int, s string) bool {
179 count++
180 return true
181 })
182
183 for _, arg := range args {
184 ok := false
185
186 // try exact matches
187 handle(s, func(i int, s string) bool {
188 if s == arg {
189 ok = true
190 which = append(which, i)
191 return false
192 }
193 return true
194 })
195
196 if ok {
197 continue
198 }
199
200 // try case-insensitive matches
201 handle(s, func(i int, s string) bool {
202 if strings.EqualFold(s, arg) {
203 ok = true
204 which = append(which, i)
205 return false
206 }
207 return true
208 })
209
210 if ok {
211 continue
212 }
213
214 // try 1-based indices, even negative ones
215 if n, err := strconv.Atoi(arg); err == nil {
216 if n < 0 {
217 n += count
218 } else if n > 0 {
219 n--
220 }
221
222 if 0 <= n && n < count {
223 which = append(which, n)
224 }
225 }
226 }
227
228 return which, count, handle, true
229 }
230
231 // loopItemsSSV loops over a line's items, allocation-free style; when given
232 // empty strings, the callback func is never called
233 func loopItemsSSV(s string, f itemFunc) {
234 s = trimTrailingSpaces(s)
235
236 for i := 0; true; i++ {
237 s = trimLeadingSpaces(s)
238 if len(s) == 0 {
239 return
240 }
241
242 j := strings.IndexByte(s, ' ')
243 if j < 0 {
244 if !f(i, s) {
245 return
246 }
247 return
248 }
249
250 if !f(i, s[:j]) {
251 return
252 }
253 s = s[j+1:]
254 }
255 }
256
257 func trimLeadingSpaces(s string) string {
258 for len(s) > 0 && s[0] == ' ' {
259 s = s[1:]
260 }
261 return s
262 }
263
264 func trimTrailingSpaces(s string) string {
265 for len(s) > 0 && s[len(s)-1] == ' ' {
266 s = s[:len(s)-1]
267 }
268 return s
269 }
270
271 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
272 // when given empty strings, the callback func is never called
273 func loopItemsTSV(s string, f itemFunc) {
274 if len(s) == 0 {
275 return
276 }
277
278 for i := 0; true; i++ {
279 j := strings.IndexByte(s, '\t')
280 if j < 0 {
281 if !f(i, s) {
282 return
283 }
284 return
285 }
286
287 if !f(i, s[:j]) {
288 return
289 }
290 s = s[j+1:]
291 }
292 }
File: ./debase64/debase64.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package debase64
26
27 import (
28 "bufio"
29 "bytes"
30 "encoding/base64"
31 "errors"
32 "io"
33 "os"
34 "strings"
35 )
36
37 const info = `
38 debase64 [file/data-URI...]
39
40 Decode base64-encoded files and/or data-URIs.
41 `
42
43 func Main() {
44 args := os.Args[1:]
45
46 if len(args) > 0 {
47 switch args[0] {
48 case `-h`, `--h`, `-help`, `--help`:
49 os.Stdout.WriteString(info[1:])
50 return
51
52 case `--`:
53 args = args[1:]
54 }
55 }
56
57 if len(args) > 1 {
58 os.Stderr.WriteString(info[1:])
59 os.Exit(1)
60 return
61 }
62
63 name := `-`
64 if len(args) == 1 {
65 name = args[0]
66 }
67
68 if err := run(name); err != nil {
69 os.Stderr.WriteString(`debase64: `)
70 os.Stderr.WriteString(err.Error())
71 os.Stderr.WriteString("\n")
72 os.Exit(1)
73 return
74 }
75 }
76
77 func run(s string) error {
78 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
79 defer bw.Flush()
80 w := bw
81
82 if s == `-` {
83 return debase64(w, os.Stdin)
84 }
85
86 if seemsDataURI(s) {
87 return debase64(w, strings.NewReader(s))
88 }
89
90 f, err := os.Open(s)
91 if err != nil {
92 return err
93 }
94 defer f.Close()
95
96 return debase64(w, f)
97 }
98
99 // debase64 decodes base64 chunks explicitly, so decoding errors can be told
100 // apart from output-writing ones
101 func debase64(w io.Writer, r io.Reader) error {
102 br := bufio.NewReaderSize(r, 32*1024)
103 start, err := br.Peek(64)
104 if err != nil && err != io.EOF {
105 return err
106 }
107
108 skip, err := skipIntroDataURI(start)
109 if err != nil {
110 return err
111 }
112
113 if skip > 0 {
114 br.Discard(skip)
115 }
116
117 dec := base64.NewDecoder(base64.StdEncoding, br)
118 _, err = io.Copy(w, dec)
119 return err
120 }
121
122 func skipIntroDataURI(chunk []byte) (skip int, err error) {
123 if bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
124 chunk = chunk[3:]
125 skip += 3
126 }
127
128 if !bytes.HasPrefix(chunk, []byte(`data:`)) {
129 return skip, nil
130 }
131
132 const l = len(`data:,`)
133 if len(chunk) == l && chunk[l-1] == ',' {
134 return l, nil
135 }
136
137 start := chunk
138 if len(start) > 64 {
139 start = start[:64]
140 }
141
142 i := bytes.Index(start, []byte(`;base64,`))
143 if i < 0 {
144 return skip, errors.New(`invalid data URI`)
145 }
146
147 skip += i + len(`;base64,`)
148 return skip, nil
149 }
150
151 func seemsDataURI(s string) bool {
152 start := s
153 if len(s) > 64 {
154 start = s[:64]
155 }
156 return strings.HasPrefix(s, `data:`) && strings.Contains(start, `;base64,`)
157 }
File: ./decap/decap.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package decap
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "os/exec"
33 "strconv"
34 "strings"
35 )
36
37 const info = `
38 decap [options...] [max lines...] [command...] [command args...]
39
40 DECAPitate emits the first n lines from the standard input, handing the rest
41 (if any) to the command given. If a leading number isn't given, the default
42 is to 'decapitate' just the first line. If no command is given, this tool
43 just emits up to the first n lines, or just the first line by default.
44
45 All (optional) leading options start with either single or double-dash:
46
47 -h, -help show this help message
48 `
49
50 func Main() {
51 buffered := false
52 args := os.Args[1:]
53 headerLines := 1
54
55 for len(args) > 0 {
56 switch args[0] {
57 case `-b`, `--b`, `-buffered`, `--buffered`:
58 buffered = true
59 args = args[1:]
60 continue
61
62 case `-h`, `--h`, `-help`, `--help`:
63 os.Stdout.WriteString(info[1:])
64 return
65
66 case `-n`, `--n`:
67 if len(args) < 2 {
68 os.Stderr.WriteString("forgot the number\n")
69 os.Exit(1)
70 return
71 }
72 s := strings.Replace(args[1], `_`, ``, -1)
73 if n, err := strconv.ParseInt(s, 10, 64); err == nil {
74 headerLines = int(n)
75 args = args[2:]
76 continue
77 }
78 os.Stderr.WriteString(args[1] + " isn't a valid number\n")
79 os.Exit(1)
80 return
81 }
82
83 break
84 }
85
86 if len(args) > 0 {
87 s := strings.Replace(args[0], `_`, ``, -1)
88 n, err := strconv.ParseInt(s, 10, 64)
89 if err == nil {
90 headerLines = int(n)
91 args = args[1:]
92 }
93 }
94
95 if len(args) > 0 && args[0] == `--` {
96 args = args[1:]
97 }
98
99 if headerLines < 1 && len(args) == 0 {
100 return
101 }
102
103 liveLines := !buffered
104 if !buffered {
105 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
106 liveLines = false
107 }
108 }
109
110 err := run(os.Stdout, args, headerLines, liveLines)
111 if err != nil && err != io.EOF {
112 os.Stderr.WriteString(`decap: `)
113 os.Stderr.WriteString(err.Error())
114 os.Stderr.WriteString("\n")
115 os.Exit(1)
116 return
117 }
118 }
119
120 func run(w io.Writer, args []string, headerLines int, liveLines bool) error {
121 bw := bufio.NewWriterSize(w, 32*1024)
122 defer bw.Flush()
123
124 br := bufio.NewReaderSize(os.Stdin, 32*1024)
125
126 if headerLines > 0 {
127 if err := first(bw, br, &headerLines, liveLines); err != nil {
128 return err
129 }
130 }
131
132 if len(args) == 0 {
133 return nil
134 }
135
136 cmd := exec.Command(args[0], args[1:]...)
137 cmd.Stdin = br
138 if liveLines {
139 cmd.Stdout = lineFlusher{bw}
140 } else {
141 cmd.Stdout = bw
142 }
143 cmd.Stderr = os.Stderr
144
145 bw.Flush()
146
147 if err := cmd.Start(); err != nil {
148 os.Stderr.WriteString(`decap: `)
149 os.Stderr.WriteString(err.Error())
150 os.Stderr.WriteString("\n")
151 os.Exit(1)
152 return nil
153 }
154
155 err := cmd.Wait()
156 if _, ok := err.(*exec.ExitError); ok {
157 err = nil
158 }
159
160 if err != nil {
161 os.Stderr.WriteString(`decap: `)
162 os.Stderr.WriteString(err.Error())
163 os.Stderr.WriteString("\n")
164 }
165
166 bw.Flush()
167
168 os.Exit(cmd.ProcessState.ExitCode())
169 return nil
170 }
171
172 func first(w *bufio.Writer, r *bufio.Reader, left *int, live bool) error {
173 if *left < 1 {
174 return io.EOF
175 }
176
177 due := false
178
179 for i := 0; true; i++ {
180 s, incomplete, err := r.ReadLine()
181 if err != nil {
182 return err
183 }
184
185 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
186 s = s[3:]
187 }
188
189 w.Write(s)
190 if incomplete {
191 due = true
192 continue
193 }
194 due = false
195
196 if w.WriteByte('\n') != nil {
197 return io.EOF
198 }
199
200 if live && w.Flush() != nil {
201 return io.EOF
202 }
203
204 *left--
205 if *left < 1 {
206 break
207 }
208 }
209
210 if due {
211 if w.WriteByte('\n') != nil {
212 return io.EOF
213 }
214
215 if live && w.Flush() != nil {
216 return io.EOF
217 }
218
219 *left--
220 }
221
222 return nil
223 }
224
225 type lineFlusher struct {
226 bw *bufio.Writer
227 }
228
229 func (lf lineFlusher) Write(p []byte) (n int, err error) {
230 n, err = lf.bw.Write(p)
231 if err != nil {
232 return n, err
233 }
234
235 if bytes.IndexByte(p, '\n') >= 0 {
236 err = lf.bw.Flush()
237 }
238
239 return n, err
240 }
File: ./decsv/decsv.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package decsv
26
27 import (
28 "bufio"
29 "encoding/csv"
30 "encoding/json"
31 "errors"
32 "io"
33 "os"
34 "strings"
35 "unicode"
36 )
37
38 const info = `
39 decsv [options...] [filepath...]
40
41
42 This cmd-line app turns CSV (comma-separated values) data into either TSV
43 (tab-separated values), JSONS (JSON Strings), or general JSON (JavaScript
44 Object Notation).
45
46 When not given a filepath, the input is read from the standard input.
47
48 Options, when given, can either start with a single or a double-dash:
49
50 -h, -help show this help message
51 -json emit JSON, where numbers are auto-detected
52 -jsonl emit JSON Lines, where numbers are auto-detected
53 -jsons emit JSON Strings, where object values are strings or null
54 -tsv emit TSV (tab-separated values) lines
55 `
56
57 // handler is the type all CSV-converter funcs adhere to
58 type handler func(*bufio.Writer, *csv.Reader) error
59
60 var handlers = map[string]handler{
61 `-json`: emitJSON,
62 `--json`: emitJSON,
63 `-jsonl`: emitJSONL,
64 `--jsonl`: emitJSONL,
65 `-jsons`: emitJSONS,
66 `--jsons`: emitJSONS,
67 `-tsv`: emitTSV,
68 `--tsv`: emitTSV,
69 }
70
71 func Main() {
72 emit := emitTSV
73 buffered := false
74 args := os.Args[1:]
75
76 for len(args) > 0 {
77 switch args[0] {
78 case `-b`, `--b`, `-buffered`, `--buffered`:
79 buffered = true
80 args = args[1:]
81 continue
82
83 case `-h`, `--h`, `-help`, `--help`:
84 os.Stdout.WriteString(info[1:])
85 return
86 }
87
88 if v, ok := handlers[args[0]]; ok {
89 emit = v
90 args = args[1:]
91 continue
92 }
93
94 break
95 }
96
97 if len(args) > 0 && args[0] == `--` {
98 args = args[1:]
99 }
100
101 if len(args) > 1 {
102 os.Stdout.WriteString(info[1:])
103 os.Exit(1)
104 return
105 }
106
107 liveLines := !buffered
108 if !buffered {
109 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
110 liveLines = false
111 }
112 }
113
114 path := `-`
115 if len(args) > 0 {
116 path = args[0]
117 }
118
119 err := run(os.Stdout, path, emit, liveLines)
120 if err != nil && err != io.EOF {
121 os.Stderr.WriteString(`decsv: `)
122 os.Stderr.WriteString(err.Error())
123 os.Stderr.WriteString("\n")
124 os.Exit(1)
125 return
126 }
127 }
128
129 func run(w io.Writer, path string, handle handler, live bool) error {
130 bw := bufio.NewWriterSize(w, 32*1024)
131 defer bw.Flush()
132
133 if path == `-` {
134 return handle(bw, makeRowReader(os.Stdin))
135 }
136
137 f, err := os.Open(path)
138 if err != nil {
139 // on windows, file-not-found error messages may mention `CreateFile`,
140 // even when trying to open files in read-only mode
141 return errors.New(`can't open file named ` + path)
142 }
143 defer f.Close()
144
145 return handle(bw, makeRowReader(f))
146 }
147
148 func emitJSON(w *bufio.Writer, rr *csv.Reader) error {
149 got := 0
150 var keys []string
151
152 err := loopCSV(rr, func(i int, row []string) error {
153 got++
154
155 if i == 0 {
156 keys = make([]string, 0, len(row))
157 for _, s := range row {
158 keys = append(keys, strings.Clone(s))
159 }
160 return nil
161 }
162
163 if i == 1 {
164 w.WriteByte('[')
165 } else {
166 err := w.WriteByte(',')
167 if err != nil {
168 return io.EOF
169 }
170 }
171
172 w.WriteByte('{')
173 for i, s := range row {
174 if i > 0 {
175 w.WriteByte(',')
176 }
177
178 if numberLike(s) {
179 w.WriteByte('"')
180 writeInnerStringJSON(w, keys[i])
181 w.WriteString(`":`)
182 w.WriteString(s)
183 continue
184 }
185
186 writeKV(w, keys[i], s)
187 }
188
189 for i := len(row); i < len(keys); i++ {
190 if i > 0 {
191 w.WriteByte(',')
192 }
193 w.WriteByte('"')
194 writeInnerStringJSON(w, keys[i])
195 w.WriteString(`":null`)
196 }
197 w.WriteByte('}')
198
199 return nil
200 })
201
202 if err != nil {
203 return err
204 }
205
206 if got > 1 {
207 w.WriteString("]\n")
208 }
209 return nil
210 }
211
212 func emitJSONL(w *bufio.Writer, rr *csv.Reader) error {
213 var keys []string
214
215 return loopCSV(rr, func(i int, row []string) error {
216 if i == 0 {
217 keys = make([]string, 0, len(row))
218 for _, s := range row {
219 c := string(append([]byte{}, s...))
220 keys = append(keys, c)
221 }
222 return nil
223 }
224
225 w.WriteByte('{')
226 for i, s := range row {
227 if i > 0 {
228 w.WriteByte(',')
229 w.WriteByte(' ')
230 }
231
232 if numberLike(s) {
233 w.WriteByte('"')
234 writeInnerStringJSON(w, keys[i])
235 w.WriteString(`": `)
236 w.WriteString(s)
237 continue
238 }
239
240 writeKV(w, keys[i], s)
241 }
242
243 for i := len(row); i < len(keys); i++ {
244 if i > 0 {
245 w.WriteByte(',')
246 w.WriteByte(' ')
247 }
248 w.WriteByte('"')
249 writeInnerStringJSON(w, keys[i])
250 w.WriteString(`": null`)
251 }
252 w.WriteByte('}')
253
254 w.WriteByte('\n')
255 if w.Flush() != nil {
256 return io.EOF
257 }
258 return nil
259 })
260 }
261
262 func emitJSONS(w *bufio.Writer, rr *csv.Reader) error {
263 got := 0
264 var keys []string
265
266 err := loopCSV(rr, func(i int, row []string) error {
267 got++
268
269 if i == 0 {
270 keys = make([]string, 0, len(row))
271 for _, s := range row {
272 c := string(append([]byte{}, s...))
273 keys = append(keys, c)
274 }
275 return nil
276 }
277
278 if i == 1 {
279 w.WriteByte('[')
280 } else {
281 err := w.WriteByte(',')
282 if err != nil {
283 return io.EOF
284 }
285 }
286
287 w.WriteByte('{')
288 for i, s := range row {
289 if i > 0 {
290 w.WriteByte(',')
291 }
292 writeKV(w, keys[i], s)
293 }
294
295 for i := len(row); i < len(keys); i++ {
296 if i > 0 {
297 w.WriteByte(',')
298 }
299 w.WriteByte('"')
300 writeInnerStringJSON(w, keys[i])
301 w.WriteString(`":null`)
302 }
303 w.WriteByte('}')
304
305 return nil
306 })
307
308 if err != nil {
309 return err
310 }
311
312 if got > 1 {
313 w.WriteString("]\n")
314 }
315 return nil
316 }
317
318 func emitTSV(w *bufio.Writer, rr *csv.Reader) error {
319 width := -1
320
321 return loopCSV(rr, func(i int, row []string) error {
322 if width < 0 {
323 width = len(row)
324 }
325
326 for i, s := range row {
327 if strings.IndexByte(s, '\t') >= 0 {
328 const msg = `can't convert CSV whose items have tabs to TSV`
329 return errors.New(msg)
330 }
331 if i > 0 {
332 w.WriteByte('\t')
333 }
334 w.WriteString(s)
335 }
336
337 for i := len(row); i < width; i++ {
338 w.WriteByte('\t')
339 }
340
341 w.WriteByte('\n')
342 if w.Flush() != nil {
343 // a write error may be the consequence of stdout being closed,
344 // perhaps by another app along a pipe
345 return io.EOF
346 }
347 return nil
348 })
349 }
350
351 // writeInnerStringJSON helps JSON-encode strings more quickly
352 func writeInnerStringJSON(w *bufio.Writer, s string) {
353 needsEscaping := false
354 for _, r := range s {
355 if '#' <= r && r <= '~' && r != '\\' {
356 continue
357 }
358 if r == ' ' || r == '!' || unicode.IsLetter(r) {
359 continue
360 }
361
362 needsEscaping = true
363 break
364 }
365
366 if !needsEscaping {
367 w.WriteString(s)
368 return
369 }
370
371 outer, err := json.Marshal(s)
372 if err != nil {
373 return
374 }
375 inner := outer[1 : len(outer)-1]
376 w.Write(inner)
377 }
378
379 func writeKV(w *bufio.Writer, k string, s string) {
380 w.WriteByte('"')
381 writeInnerStringJSON(w, k)
382 w.WriteString(`": "`)
383 writeInnerStringJSON(w, s)
384 w.WriteByte('"')
385 }
386
387 func numberLike(s string) bool {
388 if len(s) == 0 {
389 return false
390 }
391
392 if s[0] == '-' {
393 s = s[1:]
394 }
395
396 if len(s) == 0 || s[0] < '0' || s[0] > '9' {
397 return false
398 }
399
400 for len(s) > 0 {
401 lead := s[0]
402 s = s[1:]
403
404 if lead == '.' {
405 return allDigits(s)
406 }
407 if lead < '0' || lead > '9' {
408 return false
409 }
410 }
411
412 return true
413 }
414
415 func allDigits(s string) bool {
416 if len(s) == 0 {
417 return false
418 }
419
420 for _, r := range s {
421 if r < '0' || r > '9' {
422 return false
423 }
424 }
425 return true
426 }
427
428 func makeRowReader(r io.Reader) *csv.Reader {
429 rr := csv.NewReader(r)
430 rr.LazyQuotes = true
431 rr.ReuseRecord = true
432 rr.FieldsPerRecord = -1
433 return rr
434 }
435
436 func loopCSV(rr *csv.Reader, handle func(i int, row []string) error) error {
437 width := 0
438
439 for i := 0; true; i++ {
440 row, err := rr.Read()
441 if err == io.EOF {
442 return nil
443 }
444
445 if err != nil {
446 return err
447 }
448
449 if i == 0 {
450 width = len(row)
451 }
452
453 if len(row) > width {
454 return errors.New(`data-row has more items than the header`)
455 }
456
457 if err := handle(i, row); err != nil {
458 return err
459 }
460 }
461
462 return nil
463 }
File: ./dedent/dedent.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package dedent
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 )
34
35 const info = `
36 dedent [options...] [files...]
37
38 Ignore the common leading-space indentation from the input(s).
39
40 All (optional) leading options start with either single or double-dash:
41
42 -h, -help show this help message
43 `
44
45 func Main() {
46 buffered := false
47 args := os.Args[1:]
48
49 if len(args) > 0 {
50 switch args[0] {
51 case `-b`, `--b`, `-buffered`, `--buffered`:
52 buffered = true
53 args = args[1:]
54
55 case `-h`, `--h`, `-help`, `--help`:
56 os.Stdout.WriteString(info[1:])
57 return
58 }
59 }
60
61 if len(args) > 0 && args[0] == `--` {
62 args = args[1:]
63 }
64
65 liveLines := !buffered
66 if !buffered {
67 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
68 liveLines = false
69 }
70 }
71
72 err := run(os.Stdout, args, liveLines)
73 if err != nil && err != io.EOF {
74 os.Stderr.WriteString(`dedent: `)
75 os.Stderr.WriteString(err.Error())
76 os.Stderr.WriteString("\n")
77 os.Exit(1)
78 return
79 }
80 }
81
82 type config struct {
83 indent int
84 lines [][]byte
85 live bool
86 }
87
88 func run(w io.Writer, args []string, live bool) error {
89 bw := bufio.NewWriterSize(w, 32*1024)
90 defer bw.Flush()
91
92 var cfg config
93 cfg.indent = -1
94 cfg.live = live
95
96 if len(args) == 0 {
97 if err := handleReader(bw, os.Stdin, &cfg); err != nil {
98 return err
99 }
100 }
101
102 for _, name := range args {
103 if err := handleFile(bw, name, &cfg); err != nil {
104 return err
105 }
106 }
107
108 if dump(bw, cfg.lines, cfg.indent) != nil {
109 return io.EOF
110 }
111 cfg.lines = nil
112 return nil
113 }
114
115 func handleFile(w *bufio.Writer, name string, cfg *config) error {
116 if name == `` || name == `-` {
117 return handleReader(w, os.Stdin, cfg)
118 }
119
120 f, err := os.Open(name)
121 if err != nil {
122 return errors.New(`can't read from file named "` + name + `"`)
123 }
124 defer f.Close()
125
126 return handleReader(w, f, cfg)
127 }
128
129 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
130 const gb = 1024 * 1024 * 1024
131 sc := bufio.NewScanner(r)
132 sc.Buffer(nil, 8*gb)
133
134 for i := 0; sc.Scan(); i++ {
135 s := sc.Bytes()
136 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
137 s = s[3:]
138 }
139
140 if cfg.indent != 0 {
141 n := countIndent(s)
142 if cfg.indent > n || cfg.indent < 0 {
143 cfg.indent = n
144 }
145 }
146
147 if cfg.indent > 0 {
148 cfg.lines = append(cfg.lines, s)
149 continue
150 }
151
152 if dump(w, cfg.lines, cfg.indent) != nil {
153 return io.EOF
154 }
155 cfg.lines = nil
156 w.Write(dedent(s, cfg.indent))
157
158 if w.WriteByte('\n') != nil {
159 return io.EOF
160 }
161
162 if cfg.live && w.Flush() != nil {
163 return io.EOF
164 }
165 }
166
167 return sc.Err()
168 }
169
170 func countIndent(s []byte) int {
171 indent := 0
172 for len(s) > 0 && s[0] == ' ' {
173 indent++
174 s = s[1:]
175 }
176 return indent
177 }
178
179 func dedent(s []byte, max int) []byte {
180 for i := 0; i < max && len(s) > 0 && s[0] == ' '; i++ {
181 s = s[1:]
182 }
183 return s
184 }
185
186 func dump(w *bufio.Writer, lines [][]byte, indent int) error {
187 for _, l := range lines {
188 w.Write(dedent(l, indent))
189 if w.WriteByte('\n') != nil {
190 return io.EOF
191 }
192 }
193
194 if w.Flush() != nil {
195 return io.EOF
196 }
197 return nil
198 }
File: ./dedup/dedup.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package dedup
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "os"
32 "strings"
33 )
34
35 const info = `
36 dedup [options...] [file...]
37
38
39 DEDUPlicate lines prevents the same line from appearing again in the output,
40 after the first time. Unique lines are remembered across inputs.
41
42 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
43 feeds by default.
44
45 All (optional) leading options start with either single or double-dash:
46
47 -h, -help show this help message
48 -i, -ins match lines case-insensitively
49 `
50
51 type stringSet map[string]struct{}
52
53 type config struct {
54 live bool
55 sensitive bool
56 }
57
58 func Main() {
59 buffered := false
60 sensitive := true
61 args := os.Args[1:]
62
63 if len(args) > 0 {
64 switch args[0] {
65 case `-b`, `--b`, `-buffered`, `--buffered`:
66 buffered = true
67 args = args[1:]
68
69 case `-h`, `--h`, `-help`, `--help`:
70 os.Stdout.WriteString(info[1:])
71 return
72
73 case `-i`, `--i`, `-ins`, `--ins`, `-insensitive`, `--insensitive`:
74 sensitive = false
75 args = args[1:]
76 }
77 }
78
79 if len(args) > 0 && args[0] == `--` {
80 args = args[1:]
81 }
82
83 liveLines := !buffered
84 if !buffered {
85 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
86 liveLines = false
87 }
88 }
89
90 var cfg config
91 cfg.live = liveLines
92 cfg.sensitive = sensitive
93
94 err := run(os.Stdout, args, cfg)
95 if err != nil && err != io.EOF {
96 os.Stderr.WriteString(`dedup: `)
97 os.Stderr.WriteString(err.Error())
98 os.Stderr.WriteString("\n")
99 os.Exit(1)
100 return
101 }
102 }
103
104 func run(w io.Writer, args []string, cfg config) error {
105 files := make(stringSet)
106 lines := make(stringSet)
107 bw := bufio.NewWriterSize(w, 32*1024)
108 defer bw.Flush()
109
110 for _, name := range args {
111 if _, ok := files[name]; ok {
112 continue
113 }
114 files[name] = struct{}{}
115
116 if err := handleFile(bw, name, lines, cfg); err != nil {
117 return err
118 }
119 }
120
121 if len(args) == 0 {
122 return dedup(bw, os.Stdin, lines, cfg)
123 }
124 return nil
125 }
126
127 func handleFile(w *bufio.Writer, name string, got stringSet, cfg config) error {
128 if name == `` || name == `-` {
129 return dedup(w, os.Stdin, got, cfg)
130 }
131
132 f, err := os.Open(name)
133 if err != nil {
134 return errors.New(`can't read from file named "` + name + `"`)
135 }
136 defer f.Close()
137
138 return dedup(w, f, got, cfg)
139 }
140
141 func dedup(w *bufio.Writer, r io.Reader, got stringSet, cfg config) error {
142 const gb = 1024 * 1024 * 1024
143 sc := bufio.NewScanner(r)
144 sc.Buffer(nil, 8*gb)
145
146 for sc.Scan() {
147 line := sc.Text()
148 key := line
149 if !cfg.sensitive {
150 key = strings.ToLower(key)
151 }
152
153 if _, ok := got[key]; ok {
154 continue
155 }
156 got[key] = struct{}{}
157
158 w.Write(sc.Bytes())
159 if w.WriteByte('\n') != nil {
160 return io.EOF
161 }
162
163 if cfg.live && w.Flush() != nil {
164 return io.EOF
165 }
166 }
167
168 return sc.Err()
169 }
File: ./dejsonl/dejsonl.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package dejsonl
26
27 import (
28 "bufio"
29 "encoding/json"
30 "errors"
31 "io"
32 "os"
33 "strings"
34 )
35
36 const info = `
37 dejsonl [filepath...]
38
39 Turn JSON Lines (JSONL) into proper-JSON arrays. The JSON Lines format is
40 simply plain-text lines, where each line is valid JSON on its own.
41 `
42
43 const indent = ` `
44
45 func Main() {
46 buffered := false
47 args := os.Args[1:]
48
49 if len(args) > 0 {
50 switch args[0] {
51 case `-b`, `--b`, `-buffered`, `--buffered`:
52 buffered = true
53 args = args[1:]
54
55 case `-h`, `--h`, `-help`, `--help`:
56 os.Stdout.WriteString(info[1:])
57 return
58 }
59 }
60
61 if len(args) > 0 && args[0] == `--` {
62 args = args[1:]
63 }
64
65 liveLines := !buffered
66 if !buffered {
67 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
68 liveLines = false
69 }
70 }
71
72 err := run(os.Stdout, os.Args[1:], liveLines)
73 if err != nil && err != io.EOF {
74 os.Stderr.WriteString(`dejsonl: `)
75 os.Stderr.WriteString(err.Error())
76 os.Stderr.WriteString("\n")
77 os.Exit(1)
78 return
79 }
80 }
81
82 func run(w io.Writer, args []string, live bool) error {
83 dashes := 0
84 for _, path := range args {
85 if path == `-` {
86 dashes++
87 }
88 if dashes > 1 {
89 return errors.New(`can't read stdin (dash) more than once`)
90 }
91 }
92
93 bw := bufio.NewWriterSize(w, 32*1024)
94 defer bw.Flush()
95
96 if len(args) == 0 {
97 return dejsonl(bw, os.Stdin, live)
98 }
99
100 for _, path := range args {
101 if err := handleInput(bw, path, live); err != nil {
102 return err
103 }
104 }
105
106 return nil
107 }
108
109 // handleInput simplifies control-flow for func main
110 func handleInput(w *bufio.Writer, path string, live bool) error {
111 if path == `-` {
112 return dejsonl(w, os.Stdin, live)
113 }
114
115 // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
116 // resp, err := http.Get(path)
117 // if err != nil {
118 // return err
119 // }
120 // defer resp.Body.Close()
121 // return dejsonl(w, resp.Body, live)
122 // }
123
124 f, err := os.Open(path)
125 if err != nil {
126 // on windows, file-not-found error messages may mention `CreateFile`,
127 // even when trying to open files in read-only mode
128 return errors.New(`can't open file named ` + path)
129 }
130 defer f.Close()
131 return dejsonl(w, f, live)
132 }
133
134 // dejsonl simplifies control-flow for func handleInput
135 func dejsonl(w *bufio.Writer, r io.Reader, live bool) error {
136 const gb = 1024 * 1024 * 1024
137 sc := bufio.NewScanner(r)
138 sc.Buffer(nil, 8*gb)
139 got := 0
140
141 for i := 0; sc.Scan(); i++ {
142 s := sc.Text()
143 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
144 s = s[3:]
145 }
146
147 // trim spaces at both ends of the current line
148 for len(s) > 0 && s[0] == ' ' {
149 s = s[1:]
150 }
151 for len(s) > 0 && s[len(s)-1] == ' ' {
152 s = s[:len(s)-1]
153 }
154
155 // ignore empty(ish) lines
156 if len(s) == 0 {
157 continue
158 }
159
160 // ignore lines starting with unix-style comments
161 if len(s) > 0 && s[0] == '#' {
162 continue
163 }
164
165 if err := checkJSONL(strings.NewReader(s)); err != nil {
166 return err
167 }
168
169 if got == 0 {
170 w.WriteByte('[')
171 } else {
172 w.WriteByte(',')
173 }
174 if w.WriteByte('\n') != nil {
175 return io.EOF
176 }
177 w.WriteString(indent)
178 w.WriteString(s)
179 got++
180
181 if live && w.Flush() != nil {
182 return io.EOF
183 }
184 }
185
186 if got == 0 {
187 w.WriteString("[\n]\n")
188 } else {
189 w.WriteString("\n]\n")
190 }
191 return sc.Err()
192 }
193
194 func checkJSONL(r io.Reader) error {
195 dec := json.NewDecoder(r)
196 // avoid parsing numbers, so unusually-long numbers are kept verbatim,
197 // even if JSON parsers aren't required to guarantee such input-fidelity
198 // for numbers
199 dec.UseNumber()
200
201 t, err := dec.Token()
202 if err == io.EOF {
203 return errors.New(`input has no JSON values`)
204 }
205
206 if err := checkToken(dec, t); err != nil {
207 return err
208 }
209
210 _, err = dec.Token()
211 if err == io.EOF {
212 // input is over, so it's a success
213 return nil
214 }
215
216 if err == nil {
217 // a successful `read` is a failure, as it means there are
218 // trailing JSON tokens
219 return errors.New(`unexpected trailing data`)
220 }
221
222 // any other error, perhaps some invalid-JSON-syntax-type error
223 return err
224 }
225
226 // checkToken handles recursion for func checkJSONL
227 func checkToken(dec *json.Decoder, t json.Token) error {
228 switch t := t.(type) {
229 case json.Delim:
230 switch t {
231 case json.Delim('['):
232 return checkArray(dec)
233 case json.Delim('{'):
234 return checkObject(dec)
235 default:
236 return errors.New(`unsupported JSON syntax ` + string(t))
237 }
238
239 case nil, bool, float64, json.Number, string:
240 return nil
241
242 default:
243 // return fmt.Errorf(`unsupported token type %T`, t)
244 return errors.New(`invalid JSON token`)
245 }
246 }
247
248 // handleArray handles arrays for func checkToken
249 func checkArray(dec *json.Decoder) error {
250 for {
251 t, err := dec.Token()
252 if err != nil {
253 return err
254 }
255
256 if t == json.Delim(']') {
257 return nil
258 }
259
260 if err := checkToken(dec, t); err != nil {
261 return err
262 }
263 }
264 }
265
266 // handleObject handles objects for func checkToken
267 func checkObject(dec *json.Decoder) error {
268 for {
269 t, err := dec.Token()
270 if err != nil {
271 return err
272 }
273
274 if t == json.Delim('}') {
275 return nil
276 }
277
278 if _, ok := t.(string); !ok {
279 return errors.New(`expected a string for a key-value pair`)
280 }
281
282 t, err = dec.Token()
283 if err == io.EOF || t == json.Delim('}') {
284 return errors.New(`expected a value for a key-value pair`)
285 }
286
287 if err := checkToken(dec, t); err != nil {
288 return err
289 }
290 }
291 }
File: ./delay/delay.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package delay
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "math"
33 "os"
34 "strconv"
35 "time"
36 )
37
38 const info = `
39 delay [options...] [seconds delay...] [files...]
40
41 Wait the number of seconds given before emitting each input line, or wait 1
42 second before emitting each line by default.
43
44 All (optional) leading options start with either single or double-dash:
45
46 -h, -help show this help message
47 `
48
49 type config struct {
50 delay time.Duration
51 liveLines bool
52 }
53
54 func Main() {
55 var cfg config
56 cfg.delay = time.Second
57 cfg.liveLines = true
58 args := os.Args[1:]
59
60 for len(args) > 0 {
61 switch args[0] {
62 case `-b`, `--b`, `-buffered`, `--buffered`:
63 cfg.liveLines = false
64 args = args[1:]
65 continue
66
67 case `-h`, `--h`, `-help`, `--help`:
68 os.Stdout.WriteString(info[1:])
69 return
70 }
71
72 break
73 }
74
75 if len(args) > 0 {
76 if d, ok := parseDelay(args[0]); ok {
77 total := d
78 args = args[1:]
79
80 for len(args) > 0 {
81 if d, ok := parseDelay(args[0]); ok {
82 total += d
83 args = args[1:]
84 continue
85 }
86 break
87 }
88
89 cfg.delay = total
90 }
91 }
92
93 if len(args) > 0 && args[0] == `--` {
94 args = args[1:]
95 }
96
97 if cfg.liveLines {
98 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
99 cfg.liveLines = false
100 }
101 }
102
103 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
104 os.Stderr.WriteString(`delay: `)
105 os.Stderr.WriteString(err.Error())
106 os.Stderr.WriteString("\n")
107 os.Exit(1)
108 return
109 }
110 }
111
112 func parseDelay(s string) (delay time.Duration, ok bool) {
113 f, err := strconv.ParseFloat(s, 64)
114 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
115 if f < 0 {
116 f = 0
117 }
118 return time.Duration(f * float64(time.Second)), true
119 }
120
121 if d, err := time.ParseDuration(s); err == nil {
122 return d, true
123 }
124
125 return 0, false
126 }
127
128 func run(w io.Writer, args []string, cfg config) error {
129 bw := bufio.NewWriterSize(w, 32*1024)
130 defer bw.Flush()
131
132 dashes := 0
133 for _, name := range args {
134 if name == `-` {
135 dashes++
136 }
137 if dashes > 1 {
138 return errors.New(`can't read stdin (dash) more than once`)
139 }
140 }
141
142 if len(args) == 0 {
143 return delay(bw, os.Stdin, cfg)
144 }
145
146 for _, name := range args {
147 if name == `-` {
148 if err := delay(bw, os.Stdin, cfg); err != nil {
149 return err
150 }
151 continue
152 }
153
154 if err := handleFile(bw, name, cfg); err != nil {
155 return err
156 }
157 }
158 return nil
159 }
160
161 func handleFile(w *bufio.Writer, name string, cfg config) error {
162 if name == `` || name == `-` {
163 return delay(w, os.Stdin, cfg)
164 }
165
166 f, err := os.Open(name)
167 if err != nil {
168 return errors.New(`can't read from file named "` + name + `"`)
169 }
170 defer f.Close()
171
172 return delay(w, f, cfg)
173 }
174
175 func delay(w *bufio.Writer, r io.Reader, cfg config) error {
176 const gb = 1024 * 1024 * 1024
177 sc := bufio.NewScanner(r)
178 sc.Buffer(nil, 8*gb)
179
180 for i := 0; sc.Scan(); i++ {
181 s := sc.Bytes()
182 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
183 s = s[3:]
184 }
185
186 time.Sleep(cfg.delay)
187
188 w.Write(s)
189 if w.WriteByte('\n') != nil {
190 return io.EOF
191 }
192
193 if cfg.liveLines && w.Flush() != nil {
194 return io.EOF
195 }
196 }
197
198 return sc.Err()
199 }
File: ./design.txt
1 Design of `easybox`
2
3 Easybox is a multi-tool command-line app along the lines of `busybox`, where
4 either the leading command-line argument or the alias-name being used is the
5 name of the specific tool to run. Having one app act as several others can
6 save quite some file-space, and can be very filesystem-cache friendly.
7
8 All tools are non-interactive command-line-oriented and platform-agnostic.
9
10 Most tools try to auto-detect whether the standard output is being piped, and
11 by default use line-oriented buffering, flushing each output line.
12
13 Flushing each output line can massively slow down when emitting much output,
14 but it avoids the subtle problem of information possibly being stuck in a pipe
15 for a long time, at the expense of efficiency.
16
17 Most tools defaulting to this slow-but-safe behavior also have a hidden option
18 to fully-buffer without constant line-flushing, via hidden options `-b`, `--b`,
19 `-buffer`, or even `--buffer`, which may massively speed things up.
20
21 The main (main.go) program is a simple dispatch to call the `Main` functions
22 exported by the various sub-packages. Again the app's own filename, in case
23 it's being called from a file-alias deliberately named, or its leading argument
24 is used to lookup the tool. Each `Main` function handles running its respective
25 tool until quitting: some such `Main` funcs even call `os.Exit` directly.
26
27 This deliberate structure makes each sub-package/sub-folder easy to extract
28 and adapt, by simply renaming its package name into `main` and its exported
29 `Main` function into `main`: once done, the copy should be its own compilable
30 go/tinygo standalone tool.
31
32 The subset of `go` being used is compatible both with the official `go`
33 compiler, as well as with the alternative `tinygo` compiler; test-related code
34 isn't following such restrictions, only being meant for the `go test` command.
35
36 Compiling with `tinygo` is arguably preferable, with a few relatively-minor
37 trade-offs, a notable gain in much lower memory use, either no change in speed
38 or a slight gain in speed, and a much smaller final app size.
39
40 One trade-off of compiling with `tinygo` is losing the seamless multi-core
41 speed-up in a few tools like `coby`: while its non-trivial use of channels to
42 manage concurrent behavior is wasted when compiled with `tinygo`, its behavior
43 is needed when compiled with `go`, allowing full-core use, emitting results as
44 soon as they're available, while also keeping the original order of the files
45 given to it.
File: ./dessv/dessv.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package dessv
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 )
34
35 const info = `
36 dessv [filenames...]
37
38 Turn Space(s)-Separated Values (SSV) into Tab-Separated Values (TSV), where
39 both leading and trailing spaces from input lines are ignored.
40 `
41
42 func Main() {
43 buffered := false
44 args := os.Args[1:]
45
46 if len(args) > 0 {
47 switch args[0] {
48 case `-b`, `--b`, `-buffered`, `--buffered`:
49 buffered = true
50 args = args[1:]
51
52 case `-h`, `--h`, `-help`, `--help`:
53 os.Stdout.WriteString(info[1:])
54 return
55 }
56 }
57
58 if len(args) > 0 && args[0] == `--` {
59 args = args[1:]
60 }
61
62 liveLines := !buffered
63 if !buffered {
64 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
65 liveLines = false
66 }
67 }
68
69 if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
70 os.Stderr.WriteString(`dessv: `)
71 os.Stderr.WriteString(err.Error())
72 os.Stderr.WriteString("\n")
73 os.Exit(1)
74 return
75 }
76 }
77
78 func run(w io.Writer, args []string, live bool) error {
79 bw := bufio.NewWriterSize(w, 32*1024)
80 defer bw.Flush()
81
82 if len(args) == 0 {
83 return dessv(bw, os.Stdin, live)
84 }
85
86 for _, name := range args {
87 if err := handleFile(bw, name, live); err != nil {
88 return err
89 }
90 }
91 return nil
92 }
93
94 func handleFile(w *bufio.Writer, name string, live bool) error {
95 if name == `` || name == `-` {
96 return dessv(w, os.Stdin, live)
97 }
98
99 f, err := os.Open(name)
100 if err != nil {
101 return errors.New(`can't read from file named "` + name + `"`)
102 }
103 defer f.Close()
104
105 return dessv(w, f, live)
106 }
107
108 func dessv(w *bufio.Writer, r io.Reader, live bool) error {
109 const gb = 1024 * 1024 * 1024
110 sc := bufio.NewScanner(r)
111 sc.Buffer(nil, 8*gb)
112 handleRow := handleRowSSV
113 numTabs := ^0
114
115 for i := 0; sc.Scan(); i++ {
116 s := sc.Bytes()
117 if i == 0 {
118 if bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
119 s = s[3:]
120 }
121
122 for _, b := range s {
123 if b == '\t' {
124 handleRow = handleRowTSV
125 break
126 }
127 }
128 numTabs = handleRow(w, s, numTabs)
129 } else {
130 handleRow(w, s, numTabs)
131 }
132
133 if w.WriteByte('\n') != nil {
134 return io.EOF
135 }
136
137 if live && w.Flush() != nil {
138 return io.EOF
139 }
140 }
141
142 return sc.Err()
143 }
144
145 func handleRowSSV(w *bufio.Writer, s []byte, n int) int {
146 for len(s) > 0 && s[0] == ' ' {
147 s = s[1:]
148 }
149 for len(s) > 0 && s[len(s)-1] == ' ' {
150 s = s[:len(s)-1]
151 }
152
153 got := 0
154
155 for got = 0; len(s) > 0; got++ {
156 if got > 0 {
157 w.WriteByte('\t')
158 }
159
160 i := bytes.IndexByte(s, ' ')
161 if i < 0 {
162 w.Write(s)
163 s = nil
164 n--
165 break
166 }
167
168 w.Write(s[:i])
169 s = s[i+1:]
170 for len(s) > 0 && s[0] == ' ' {
171 s = s[1:]
172 }
173 n--
174 }
175
176 w.Write(s)
177 writeTabs(w, n)
178 return got
179 }
180
181 func handleRowTSV(w *bufio.Writer, s []byte, n int) int {
182 got := 0
183 for _, b := range s {
184 if b == '\t' {
185 got++
186 }
187 }
188
189 w.Write(s)
190 writeTabs(w, n-got)
191 return got
192 }
193
194 func writeTabs(w *bufio.Writer, n int) {
195 for n > 0 {
196 w.WriteByte('\t')
197 n--
198 }
199 }
File: ./detab/detab.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package detab
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 "strconv"
34 "strings"
35 "unicode/utf8"
36 )
37
38 const info = `
39 detab [options...] [tab-stop...] [files...]
40
41
42 Expand each tab into up to the number of spaces given, or up to 4 spaces by
43 default. Alternatively, you can join TSVs (tab-separated values) using the
44 separator string given, instead of expanding each tab into a run of spaces.
45
46 Also, you can ignore all tabs either by using 0 as the tab-stop, or using
47 an empty separator string via the join option.
48
49 All (optional) leading options start with either single or double-dash:
50
51 -h, -help show this help message
52 -i, -initial don't convert tabs after non-blanks
53 -n, -t change tab-stop number using the next argument
54 -j=SEP, -join=SEP replace tabs with the string given instead
55 `
56
57 type config struct {
58 tabStop uint
59 join string
60 initial bool
61 liveLines bool
62 }
63
64 func Main() {
65 var cfg config
66 cfg.tabStop = 4
67 cfg.liveLines = true
68 args := os.Args[1:]
69
70 for len(args) > 0 {
71 switch args[0] {
72 case `-b`, `--b`, `-buffered`, `--buffered`:
73 cfg.liveLines = false
74 args = args[1:]
75 continue
76
77 case `-h`, `--h`, `-help`, `--help`:
78 os.Stdout.WriteString(info[1:])
79 return
80
81 case `-i`, `--i`, `-initial`, `--initial`:
82 cfg.initial = true
83 args = args[1:]
84 continue
85
86 case `-j`, `--j`, `-join`, `--join`:
87 if len(args) < 2 {
88 os.Stderr.WriteString("forgot the join/separator string\n")
89 os.Exit(1)
90 return
91 }
92 cfg.join = args[1]
93 cfg.tabStop = 0
94 args = args[2:]
95 continue
96
97 case `-n`, `--n`, `-t`, `--t`:
98 if len(args) < 2 {
99 os.Stderr.WriteString("forgot the tab-stop number\n")
100 os.Exit(1)
101 return
102 }
103 if n, err := strconv.ParseUint(args[1], 10, 64); err == nil {
104 cfg.tabStop = uint(n)
105 args = args[2:]
106 continue
107 }
108 os.Stderr.WriteString(args[1] + " isn't a valid number\n")
109 os.Exit(1)
110 return
111 }
112
113 if hasAnyPrefix(args[0], `-j=`, `--j=`, `-join=`, `--join=`) {
114 cfg.join = args[0][strings.IndexByte(args[0], '=')+1:]
115 cfg.tabStop = 0
116 args = args[1:]
117 continue
118 }
119
120 break
121 }
122
123 if len(args) > 0 {
124 if n, err := strconv.ParseUint(args[0], 10, 64); err == nil {
125 cfg.tabStop = uint(n)
126 args = args[1:]
127 }
128 }
129
130 if len(args) > 0 && args[0] == `--` {
131 args = args[1:]
132 }
133
134 if cfg.liveLines {
135 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
136 cfg.liveLines = false
137 }
138 }
139
140 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
141 os.Stderr.WriteString(`detab: `)
142 os.Stderr.WriteString(err.Error())
143 os.Stderr.WriteString("\n")
144 os.Exit(1)
145 return
146 }
147 }
148
149 func hasAnyPrefix(s string, prefixes ...string) bool {
150 for _, pre := range prefixes {
151 if strings.HasPrefix(s, pre) {
152 return true
153 }
154 }
155 return false
156 }
157
158 func run(w io.Writer, args []string, cfg config) error {
159 bw := bufio.NewWriterSize(w, 32*1024)
160 defer bw.Flush()
161
162 dashes := 0
163 for _, name := range args {
164 if name == `-` {
165 dashes++
166 }
167 if dashes > 1 {
168 return errors.New(`can't read stdin (dash) more than once`)
169 }
170 }
171
172 if len(args) == 0 {
173 return detab(bw, os.Stdin, cfg)
174 }
175
176 for _, name := range args {
177 if name == `-` {
178 if err := detab(bw, os.Stdin, cfg); err != nil {
179 return err
180 }
181 continue
182 }
183
184 if err := handleFile(bw, name, cfg); err != nil {
185 return err
186 }
187 }
188 return nil
189 }
190
191 func handleFile(w *bufio.Writer, name string, cfg config) error {
192 if name == `` || name == `-` {
193 return detab(w, os.Stdin, cfg)
194 }
195
196 f, err := os.Open(name)
197 if err != nil {
198 return errors.New(`can't read from file named "` + name + `"`)
199 }
200 defer f.Close()
201
202 return detab(w, f, cfg)
203 }
204
205 func detab(w *bufio.Writer, r io.Reader, cfg config) error {
206 if cfg.join != `` {
207 return replace(w, r, cfg)
208 }
209 if cfg.tabStop == 0 {
210 return ignore(w, r, cfg)
211 }
212 return expand(w, r, cfg)
213 }
214
215 func expand(w *bufio.Writer, r io.Reader, cfg config) error {
216 const gb = 1024 * 1024 * 1024
217 sc := bufio.NewScanner(r)
218 sc.Buffer(nil, 8*gb)
219
220 var buf []byte
221 maxSpaces := int(cfg.tabStop)
222
223 for i := 0; sc.Scan(); i++ {
224 s := sc.Bytes()
225 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
226 s = s[3:]
227 }
228
229 if maxSpaces < 1 {
230 w.Write(s)
231 } else if cfg.initial {
232 tabs := 0
233 for len(s) > 0 && s[0] == '\t' {
234 tabs++
235 s = s[1:]
236 }
237 writeSpaces(w, maxSpaces*tabs)
238 w.Write(s)
239 } else if bytes.IndexByte(s, '\t') < 0 {
240 w.Write(s)
241 } else {
242 buf = expandTabs(buf[:0], s, maxSpaces)
243 w.Write(buf)
244 }
245
246 if w.WriteByte('\n') != nil {
247 return io.EOF
248 }
249
250 if cfg.liveLines && w.Flush() != nil {
251 return io.EOF
252 }
253 }
254
255 return sc.Err()
256 }
257
258 func ignore(w *bufio.Writer, r io.Reader, cfg config) error {
259 const gb = 1024 * 1024 * 1024
260 sc := bufio.NewScanner(r)
261 sc.Buffer(nil, 8*gb)
262
263 for i := 0; sc.Scan(); i++ {
264 s := sc.Bytes()
265 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
266 s = s[3:]
267 }
268
269 if bytes.IndexByte(s, '\t') < 0 {
270 w.Write(s)
271 } else {
272 for len(s) > 0 {
273 j := bytes.IndexByte(s, '\t')
274 if j < 0 {
275 w.Write(s)
276 break
277 }
278 w.Write(s[:j])
279 s = s[j+1:]
280 }
281 }
282
283 if w.WriteByte('\n') != nil {
284 return io.EOF
285 }
286
287 if cfg.liveLines && w.Flush() != nil {
288 return io.EOF
289 }
290 }
291
292 return sc.Err()
293 }
294
295 func replace(w *bufio.Writer, r io.Reader, cfg config) error {
296 const gb = 1024 * 1024 * 1024
297 sc := bufio.NewScanner(r)
298 sc.Buffer(nil, 8*gb)
299
300 var buf []byte
301
302 for i := 0; sc.Scan(); i++ {
303 s := sc.Bytes()
304 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
305 s = s[3:]
306 }
307
308 if bytes.IndexByte(s, '\t') < 0 {
309 w.Write(s)
310 } else {
311 buf = replaceTabs(buf[:0], s, cfg.join)
312 w.Write(buf)
313 }
314
315 if w.WriteByte('\n') != nil {
316 return io.EOF
317 }
318
319 if cfg.liveLines && w.Flush() != nil {
320 return io.EOF
321 }
322 }
323
324 return sc.Err()
325 }
326
327 func expandTabs(dst []byte, src []byte, tabStop int) []byte {
328 n := 0
329
330 if tabStop < 1 {
331 return append(dst, src...)
332 }
333
334 for len(src) > 0 {
335 r, size := utf8.DecodeRune(src)
336
337 if r != '\t' {
338 dst = append(dst, src[:size]...)
339 n++
340 } else {
341 spaces := tabStop - n%tabStop
342 dst = appendSpaces(dst, spaces)
343 n += spaces
344 }
345
346 src = src[size:]
347 }
348
349 return dst
350 }
351
352 func appendSpaces(dst []byte, n int) []byte {
353 const (
354 half = ` `
355 spaces = half + half
356 )
357
358 for n >= len(spaces) {
359 dst = append(dst, spaces...)
360 n -= len(spaces)
361 }
362 if n > 0 {
363 dst = append(dst, spaces[:n]...)
364 }
365 return dst
366 }
367
368 func replaceTabs(dst []byte, src []byte, with string) []byte {
369 for len(src) > 0 {
370 i := bytes.IndexByte(src, '\t')
371 if i < 0 {
372 dst = append(dst, src...)
373 break
374 }
375 r, size := utf8.DecodeRune(src)
376
377 if r != '\t' {
378 dst = append(dst, src[:size]...)
379 } else {
380 dst = append(dst, with...)
381 }
382
383 src = src[size:]
384 }
385
386 return dst
387 }
388
389 func writeSpaces(w *bufio.Writer, n int) {
390 const (
391 half = ` `
392 spaces = half + half
393 )
394
395 for n >= len(spaces) {
396 w.WriteString(spaces)
397 n -= len(spaces)
398 }
399 if n > 0 {
400 w.WriteString(spaces[:n])
401 }
402 }
File: ./dtsv/dtsv.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package dtsv
26
27 import (
28 "bufio"
29 "os"
30 "sort"
31 "strconv"
32 "strings"
33 )
34
35 const info = `
36 dtsv [options...] [columns/indices...]
37
38 Drop Tab-Separated Values avoids the columns using the arguments given, no
39 matter the order given. Arguments are matched to columns, first exactly,
40 failing that case-insensitively, and failing even that, as 1-based indices,
41 with negative numbers counting backward from the last column.
42
43 Input is TSV (tab-separated values) lines from the standard input, where the
44 first/header line has all the column names.
45
46 The options are, available both in single and double-dash versions
47
48 -h, -help show this help message
49 `
50
51 func Main() {
52 args := os.Args[1:]
53
54 if len(args) > 0 {
55 switch args[0] {
56 case `-h`, `--h`, `-help`, `--help`:
57 os.Stdout.WriteString(info[1:])
58 return
59 }
60 }
61
62 if len(args) > 0 && args[0] == `--` {
63 args = args[1:]
64 }
65
66 // if len(args) == 0 {
67 // os.Stderr.WriteString(info[1:])
68 // os.Exit(1)
69 // return
70 // }
71
72 var items []string
73 indices := make([]int, 0, len(args))
74
75 sc := bufio.NewScanner(os.Stdin)
76 sc.Buffer(nil, 8*1024*1024*1024)
77
78 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
79 defer bw.Flush()
80
81 // the number of columns, which is determined by the first/header line
82 cols := 0
83
84 for i := 0; sc.Scan(); i++ {
85 line := sc.Text()
86
87 if i == 0 {
88 if strings.HasPrefix(line, "\xef\xbb\xbf") {
89 line = line[3:]
90 }
91
92 items = appendTSV(items[:0], line)
93
94 for _, s := range args {
95 if j := findColumn(items, s); j >= 0 {
96 indices = append(indices, j)
97 }
98 }
99 sort.Ints(indices)
100
101 cols = len(items) - len(indices)
102 } else {
103 items = appendTSV(items[:0], line)
104 }
105
106 if emit(bw, items, indices, cols) != nil {
107 return
108 }
109 }
110 }
111
112 func appendTSV(to []string, line string) []string {
113 for len(line) > 0 {
114 i := strings.IndexByte(line, '\t')
115 if i < 0 {
116 return append(to, line)
117 }
118
119 to = append(to, line[:i])
120 line = line[i+1:]
121 }
122
123 return to
124 }
125
126 // emit writes the current items which aren't meant to be dropped; indices
127 // given are assumed to be sorted in increasing order; the max number of
128 // columns is also needed to pad rows with too few items in them
129 func emit(w *bufio.Writer, items []string, indices []int, cols int) error {
130 n := 0
131
132 for i, s := range items {
133 if len(indices) > 0 && i == indices[0] {
134 indices = indices[1:]
135 continue
136 }
137
138 if n > 0 {
139 w.WriteByte('\t')
140 }
141 n++
142 w.WriteString(s)
143 }
144
145 // pad missing columns with empty values
146 for cols > n {
147 w.WriteByte('\t')
148 cols--
149 }
150
151 return w.WriteByte('\n')
152 }
153
154 // findColumn tries to find the 0-based column-index, given the first/header
155 // row, and a name/integer-index
156 func findColumn(items []string, name string) int {
157 // try exact match
158 for i, s := range items {
159 if s == name {
160 return i
161 }
162 }
163
164 // try case-insensitive match
165 for i, s := range items {
166 if strings.EqualFold(s, name) {
167 return i
168 }
169 }
170
171 // try a non-zero numeric index
172 i, err := strconv.Atoi(name)
173 if err != nil || i == 0 {
174 return -1
175 }
176
177 // make indices 0-based, and correct negative indices
178 if i > 0 {
179 i--
180 } else if i < 0 {
181 i += len(items)
182 }
183
184 // see if numeric index is in range
185 if 0 <= i && i < len(items) {
186 return i
187 }
188
189 return -1
190 }
File: ./ecoli/ecoli.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package ecoli
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "regexp"
33 )
34
35 const info = `
36 ecoli [options...] [regex/style pairs...]
37
38
39 Expressions COloring LInes tries to match each line read from the standard
40 input to the regexes given, coloring/styling with the named-style paired
41 to the first matching regex, if any. Lines not matching any regex stay the
42 same.
43
44 The options are, available both in single and double-dash versions
45
46 -h, -help show this help message
47 -i, -ins, -insensitive match the regexes given case-insensitively
48
49 Some of the colors/styles available are:
50
51 blue blueback
52 bold
53 gray grayback
54 green greenback
55 inverse
56 magenta magentaback
57 orange orangeback
58 purple purpleback
59 red redback
60 underline
61
62 Some style aliases are:
63
64 b blue bb blueback
65 g green gb greenback
66 m magenta mb magentaback
67 o orange ob orangeback
68 p purple pb purpleback
69 r red rb redback
70 i inverse (highlight)
71 u underline
72 `
73
74 var styleAliases = map[string]string{
75 `b`: `blue`,
76 `g`: `green`,
77 `m`: `magenta`,
78 `o`: `orange`,
79 `p`: `purple`,
80 `r`: `red`,
81 `u`: `underline`,
82
83 `bb`: `blueback`,
84 `bg`: `greenback`,
85 `bm`: `magentaback`,
86 `bo`: `orangeback`,
87 `bp`: `purpleback`,
88 `br`: `redback`,
89
90 `gb`: `greenback`,
91 `mb`: `magentaback`,
92 `ob`: `orangeback`,
93 `pb`: `purpleback`,
94 `rb`: `redback`,
95
96 `hi`: `inverse`,
97 `inv`: `inverse`,
98 `mag`: `magenta`,
99
100 `du`: `doubleunderline`,
101
102 `flip`: `inverse`,
103 `swap`: `inverse`,
104
105 `reset`: `plain`,
106 `highlight`: `inverse`,
107 `hilite`: `inverse`,
108 `invert`: `inverse`,
109 `inverted`: `inverse`,
110 `swapped`: `inverse`,
111
112 `dunderline`: `doubleunderline`,
113 `dunderlined`: `doubleunderline`,
114
115 `strikethrough`: `strike`,
116 `strikethru`: `strike`,
117 `struck`: `strike`,
118
119 `underlined`: `underline`,
120
121 `bblue`: `blueback`,
122 `bgray`: `grayback`,
123 `bgreen`: `greenback`,
124 `bmagenta`: `magentaback`,
125 `borange`: `orangeback`,
126 `bpurple`: `purpleback`,
127 `bred`: `redback`,
128
129 `bgblue`: `blueback`,
130 `bggray`: `grayback`,
131 `bggreen`: `greenback`,
132 `bgmag`: `magentaback`,
133 `bgmagenta`: `magentaback`,
134 `bgorange`: `orangeback`,
135 `bgpurple`: `purpleback`,
136 `bgred`: `redback`,
137
138 `bluebg`: `blueback`,
139 `graybg`: `grayback`,
140 `greenbg`: `greenback`,
141 `magbg`: `magentaback`,
142 `magentabg`: `magentaback`,
143 `orangebg`: `orangeback`,
144 `purplebg`: `purpleback`,
145 `redbg`: `redback`,
146
147 `backblue`: `blueback`,
148 `backgray`: `grayback`,
149 `backgreen`: `greenback`,
150 `backmag`: `magentaback`,
151 `backmagenta`: `magentaback`,
152 `backorange`: `orangeback`,
153 `backpurple`: `purpleback`,
154 `backred`: `redback`,
155 }
156
157 var styles = map[string]string{
158 `blue`: "\x1b[38;2;0;95;215m",
159 `bold`: "\x1b[1m",
160 `doubleunderline`: "\x1b[21m",
161 `gray`: "\x1b[38;2;168;168;168m",
162 `green`: "\x1b[38;2;0;135;95m",
163 `inverse`: "\x1b[7m",
164 `magenta`: "\x1b[38;2;215;0;255m",
165 `orange`: "\x1b[38;2;215;95;0m",
166 `plain`: "\x1b[0m",
167 `purple`: "\x1b[38;2;135;95;255m",
168 `red`: "\x1b[38;2;204;0;0m",
169 `strike`: "\x1b[9m",
170 `underline`: "\x1b[4m",
171
172 `blueback`: "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
173 `grayback`: "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
174 `greenback`: "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
175 `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
176 `orangeback`: "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
177 `purpleback`: "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
178 `redback`: "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
179 }
180
181 // pair has a regular-expression and its associated ANSI-code style together
182 type pair struct {
183 expr *regexp.Regexp
184 style string
185 }
186
187 func Main() {
188 buffered := false
189 insensitive := false
190 args := os.Args[1:]
191
192 for len(args) > 0 {
193 switch args[0] {
194 case `-b`, `--b`, `-buffered`, `--buffered`:
195 buffered = true
196 args = args[1:]
197 continue
198
199 case `-h`, `--h`, `-help`, `--help`:
200 os.Stdout.WriteString(info[1:])
201 return
202
203 case `-i`, `--i`, `-ins`, `--ins`:
204 insensitive = true
205 args = args[1:]
206 continue
207 }
208
209 break
210 }
211
212 if len(args) > 0 && args[0] == `--` {
213 args = args[1:]
214 }
215
216 if len(args)%2 != 0 {
217 const msg = "you forgot the style-name for/after the last regex\n"
218 os.Stderr.WriteString(msg)
219 os.Exit(1)
220 return
221 }
222
223 nerr := 0
224 pairs := make([]pair, 0, len(args)/2)
225
226 for len(args) >= 2 {
227 src := args[0]
228 sname := args[1]
229
230 var err error
231 var exp *regexp.Regexp
232 if insensitive {
233 exp, err = regexp.Compile(`(?i)` + src)
234 } else {
235 exp, err = regexp.Compile(src)
236 }
237 if err != nil {
238 os.Stderr.WriteString(`ecoli: `)
239 os.Stderr.WriteString(err.Error())
240 os.Stderr.WriteString("\n")
241 nerr++
242 }
243
244 if alias, ok := styleAliases[sname]; ok {
245 sname = alias
246 }
247
248 style, ok := styles[sname]
249 if !ok {
250 os.Stderr.WriteString("no style named `")
251 os.Stderr.WriteString(args[1])
252 os.Stderr.WriteString("`\n")
253 nerr++
254 }
255
256 pairs = append(pairs, pair{expr: exp, style: style})
257 args = args[2:]
258 }
259
260 if nerr > 0 {
261 os.Exit(1)
262 return
263 }
264
265 liveLines := !buffered
266 if !buffered {
267 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
268 liveLines = false
269 }
270 }
271
272 sc := bufio.NewScanner(os.Stdin)
273 sc.Buffer(nil, 8*1024*1024*1024)
274 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
275 var plain []byte
276
277 for i := 0; sc.Scan(); i++ {
278 s := sc.Bytes()
279 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
280 s = s[3:]
281 }
282 plain = appendPlain(plain[:0], s)
283
284 if err := handleLine(bw, s, noANSI(plain), pairs); err != nil {
285 return
286 }
287
288 if !liveLines {
289 continue
290 }
291
292 if err := bw.Flush(); err != nil {
293 return
294 }
295 }
296 }
297
298 // appendPlain extends the slice given using the non-ANSI parts of a string
299 func appendPlain(dst []byte, src []byte) []byte {
300 for len(src) > 0 {
301 i, j := indexEscapeSequence(src)
302 if i < 0 {
303 dst = append(dst, src...)
304 break
305 }
306 if j < 0 {
307 j = len(src)
308 }
309
310 if i > 0 {
311 dst = append(dst, src[:i]...)
312 }
313
314 src = src[j:]
315 }
316
317 return dst
318 }
319
320 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
321 // the multi-byte sequences starting with ESC[; the result is a pair of slice
322 // indices which can be independently negative when either the start/end of
323 // a sequence isn't found; given their fairly-common use, even the hyperlink
324 // ESC]8 sequences are supported
325 func indexEscapeSequence(s []byte) (int, int) {
326 var prev byte
327
328 for i, b := range s {
329 if prev == '\x1b' && b == '[' {
330 j := indexLetter(s[i+1:])
331 if j < 0 {
332 return i, -1
333 }
334 return i - 1, i + 1 + j + 1
335 }
336
337 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
338 j := indexPair(s[i+1:], '\x1b', '\\')
339 if j < 0 {
340 return i, -1
341 }
342 return i - 1, i + 1 + j + 2
343 }
344
345 prev = b
346 }
347
348 return -1, -1
349 }
350
351 func indexLetter(s []byte) int {
352 for i, b := range s {
353 upper := b &^ 32
354 if 'A' <= upper && upper <= 'Z' {
355 return i
356 }
357 }
358
359 return -1
360 }
361
362 func indexPair(s []byte, x byte, y byte) int {
363 var prev byte
364
365 for i, b := range s {
366 if prev == x && b == y && i > 0 {
367 return i
368 }
369 prev = b
370 }
371
372 return -1
373 }
374
375 // noANSI ensures arguments to func handleLine are given in the right order
376 type noANSI []byte
377
378 // handleLine styles the current line given to it using the first matching
379 // regex, keeping it as given if none of the regexes match; it's given 2
380 // strings: the first is the original line, the latter is its plain-text
381 // version (with no ANSI codes) and is used for the regex-matching, since
382 // ANSI codes use a mix of numbers and letters, which can themselves match
383 func handleLine(w *bufio.Writer, s []byte, plain noANSI, pairs []pair) error {
384 for _, p := range pairs {
385 if p.expr.Match(plain) {
386 w.WriteString(p.style)
387 w.Write(s)
388 w.WriteString("\x1b[0m")
389 return w.WriteByte('\n')
390 }
391 }
392
393 w.Write(s)
394 return w.WriteByte('\n')
395 }
File: ./erase/erase.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package erase
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "regexp"
33 )
34
35 const info = `
36 erase [options...] [regexes...]
37
38
39 Ignore/remove all occurrences of all regex matches along lines read from the
40 standard input. The regular-expression mode used is "re2", which is a superset
41 of the commonly-used "extended-mode".
42
43 All ANSI-style sequences are removed before trying to match-remove things, to
44 avoid messing those up. Each regex erases all its occurrences on the current
45 line in the order given among the arguments, so regex-order matters.
46
47 The options are, available both in single and double-dash versions
48
49 -h, -help show this help message
50 -i, -ins match regexes case-insensitively
51 `
52
53 func Main() {
54 args := os.Args[1:]
55 buffered := false
56 insensitive := false
57
58 for len(args) > 0 {
59 switch args[0] {
60 case `-b`, `--b`, `-buffered`, `--buffered`:
61 buffered = true
62 args = args[1:]
63 continue
64
65 case `-h`, `--h`, `-help`, `--help`:
66 os.Stdout.WriteString(info[1:])
67 return
68
69 case `-i`, `--i`, `-ins`, `--ins`:
70 insensitive = true
71 args = args[1:]
72 continue
73 }
74
75 break
76 }
77
78 if len(args) > 0 && args[0] == `--` {
79 args = args[1:]
80 }
81
82 exprs := make([]*regexp.Regexp, 0, len(args))
83
84 for _, s := range args {
85 var err error
86 var exp *regexp.Regexp
87
88 if insensitive {
89 exp, err = regexp.Compile(`(?i)` + s)
90 } else {
91 exp, err = regexp.Compile(s)
92 }
93
94 if err != nil {
95 os.Stderr.WriteString(`erase: `)
96 os.Stderr.WriteString(err.Error())
97 os.Stderr.WriteString("\n")
98 continue
99 }
100
101 exprs = append(exprs, exp)
102 }
103
104 // quit right away when given invalid regexes
105 if len(exprs) < len(args) {
106 os.Exit(1)
107 return
108 }
109
110 liveLines := !buffered
111 if !buffered {
112 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
113 liveLines = false
114 }
115 }
116
117 err := run(os.Stdout, os.Stdin, exprs, liveLines)
118 if err != nil && err != io.EOF {
119 os.Stderr.WriteString(`erase: `)
120 os.Stderr.WriteString(err.Error())
121 os.Stderr.WriteString("\n")
122 os.Exit(1)
123 return
124 }
125 }
126
127 func run(w io.Writer, r io.Reader, exprs []*regexp.Regexp, live bool) error {
128 var buf []byte
129 sc := bufio.NewScanner(r)
130 sc.Buffer(nil, 8*1024*1024*1024)
131 bw := bufio.NewWriterSize(w, 32*1024)
132 defer bw.Flush()
133
134 src := make([]byte, 8*1024)
135 dst := make([]byte, 8*1024)
136
137 for i := 0; sc.Scan(); i++ {
138 line := sc.Bytes()
139 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
140 line = line[3:]
141 }
142
143 s := line
144 if bytes.IndexByte(s, '\x1b') >= 0 {
145 buf = plain(buf[:0], s)
146 s = buf
147 }
148
149 if len(exprs) > 0 {
150 src = append(src[:0], s...)
151 for _, exp := range exprs {
152 dst = erase(dst[:0], src, exp)
153 src = append(src[:0], dst...)
154 }
155 bw.Write(dst)
156 } else {
157 bw.Write(s)
158 }
159
160 if bw.WriteByte('\n') != nil {
161 return io.EOF
162 }
163
164 if live && bw.Flush() != nil {
165 return io.EOF
166 }
167 }
168
169 return sc.Err()
170 }
171
172 func erase(dst []byte, src []byte, with *regexp.Regexp) []byte {
173 for len(src) > 0 {
174 span := with.FindIndex(src)
175 // also ignore empty regex matches to avoid infinite outer loops,
176 // as skipping empty slices isn't advancing at all, leaving the
177 // string stuck to being empty-matched forever by the same regex
178 if len(span) != 2 || span[0] == span[1] || span[0] < 0 {
179 return append(dst, src...)
180 }
181
182 start, end := span[0], span[1]
183 dst = append(dst, src[:start]...)
184 // avoid infinite loops caused by empty regex matches
185 if start == end && end < len(src) {
186 dst = append(dst, src[end])
187 end++
188 }
189 src = src[end:]
190 }
191
192 return dst
193 }
194
195 func plain(dst []byte, src []byte) []byte {
196 for len(src) > 0 {
197 i, j := indexEscapeSequence(src)
198 if i < 0 {
199 dst = append(dst, src...)
200 break
201 }
202 if j < 0 {
203 j = len(src)
204 }
205
206 if i > 0 {
207 dst = append(dst, src[:i]...)
208 }
209
210 src = src[j:]
211 }
212
213 return dst
214 }
215
216 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
217 // the multi-byte sequences starting with ESC[; the result is a pair of slice
218 // indices which can be independently negative when either the start/end of
219 // a sequence isn't found; given their fairly-common use, even the hyperlink
220 // ESC]8 sequences are supported
221 func indexEscapeSequence(s []byte) (int, int) {
222 var prev byte
223
224 for i, b := range s {
225 if prev == '\x1b' && b == '[' {
226 j := indexLetter(s[i+1:])
227 if j < 0 {
228 return i, -1
229 }
230 return i - 1, i + 1 + j + 1
231 }
232
233 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
234 j := indexPair(s[i+1:], '\x1b', '\\')
235 if j < 0 {
236 return i, -1
237 }
238 return i - 1, i + 1 + j + 2
239 }
240
241 prev = b
242 }
243
244 return -1, -1
245 }
246
247 func indexLetter(s []byte) int {
248 for i, b := range s {
249 upper := b &^ 32
250 if 'A' <= upper && upper <= 'Z' {
251 return i
252 }
253 }
254
255 return -1
256 }
257
258 func indexPair(s []byte, x byte, y byte) int {
259 var prev byte
260
261 for i, b := range s {
262 if prev == x && b == y && i > 0 {
263 return i
264 }
265 prev = b
266 }
267
268 return -1
269 }
File: ./expand/expand.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package expand
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 "strconv"
34 "strings"
35 "unicode/utf8"
36 )
37
38 const info = `
39 expand [options...] [files...]
40
41 Expand each tab into up to the number of spaces given, or up to 4 spaces by
42 default.
43
44 All (optional) leading options start with either single or double-dash:
45
46 -h, -help show this help message
47 -i, -initial don't convert tabs after non-blanks
48 -t [tab-stop] change the tap-stop, or the max spaces for tab expansion
49 `
50
51 type config struct {
52 tabStop uint
53 initial bool
54 liveLines bool
55 }
56
57 func Main() {
58 var cfg config
59 cfg.tabStop = 4
60 cfg.liveLines = true
61 args := os.Args[1:]
62
63 for len(args) > 0 {
64 switch args[0] {
65 case `-b`, `--b`, `-buffered`, `--buffered`:
66 cfg.liveLines = false
67 args = args[1:]
68 continue
69
70 case `-h`, `--h`, `-help`, `--help`:
71 os.Stdout.WriteString(info[1:])
72 return
73
74 case `-i`, `--i`, `-initial`, `--initial`:
75 cfg.initial = true
76 args = args[1:]
77 continue
78
79 case `-t`, `--t`:
80 if len(args) < 2 {
81 os.Stderr.WriteString("forgot the tab-stop number\n")
82 os.Exit(1)
83 return
84 }
85
86 if n, err := strconv.ParseUint(args[1], 10, 64); err == nil {
87 cfg.tabStop = uint(n)
88 }
89 args = args[2:]
90 continue
91 }
92
93 if strings.HasPrefix(args[0], `--tabs=`) {
94 s := strings.TrimPrefix(args[0], `--tabs=`)
95 if n, err := strconv.ParseUint(s, 10, 64); err == nil {
96 cfg.tabStop = uint(n)
97 } else {
98 os.Stderr.WriteString("forgot the tab-stop number\n")
99 os.Exit(1)
100 return
101 }
102 args = args[1:]
103 continue
104 }
105
106 break
107 }
108
109 if len(args) > 0 && args[0] == `--` {
110 args = args[1:]
111 }
112
113 if cfg.liveLines {
114 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
115 cfg.liveLines = false
116 }
117 }
118
119 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
120 os.Stderr.WriteString(`expand: `)
121 os.Stderr.WriteString(err.Error())
122 os.Stderr.WriteString("\n")
123 os.Exit(1)
124 return
125 }
126 }
127
128 func run(w io.Writer, args []string, cfg config) error {
129 bw := bufio.NewWriterSize(w, 32*1024)
130 defer bw.Flush()
131
132 dashes := 0
133 for _, name := range args {
134 if name == `-` {
135 dashes++
136 }
137 if dashes > 1 {
138 return errors.New(`can't read stdin (dash) more than once`)
139 }
140 }
141
142 if len(args) == 0 {
143 return detab(bw, os.Stdin, cfg)
144 }
145
146 for _, name := range args {
147 if name == `-` {
148 if err := detab(bw, os.Stdin, cfg); err != nil {
149 return err
150 }
151 continue
152 }
153
154 if err := handleFile(bw, name, cfg); err != nil {
155 return err
156 }
157 }
158 return nil
159 }
160
161 func handleFile(w *bufio.Writer, name string, cfg config) error {
162 if name == `` || name == `-` {
163 return detab(w, os.Stdin, cfg)
164 }
165
166 f, err := os.Open(name)
167 if err != nil {
168 return errors.New(`can't read from file named "` + name + `"`)
169 }
170 defer f.Close()
171
172 return detab(w, f, cfg)
173 }
174
175 func detab(w *bufio.Writer, r io.Reader, cfg config) error {
176 const gb = 1024 * 1024 * 1024
177 sc := bufio.NewScanner(r)
178 sc.Buffer(nil, 8*gb)
179
180 var buf []byte
181 maxSpaces := int(cfg.tabStop)
182
183 for i := 0; sc.Scan(); i++ {
184 s := sc.Bytes()
185 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
186 s = s[3:]
187 }
188
189 if maxSpaces < 1 {
190 w.Write(s)
191 } else if cfg.initial {
192 tabs := 0
193 for len(s) > 0 && s[0] == '\t' {
194 tabs++
195 s = s[1:]
196 }
197 writeSpaces(w, maxSpaces*tabs)
198 w.Write(s)
199 } else if bytes.IndexByte(s, '\t') < 0 {
200 w.Write(s)
201 } else {
202 buf = expandTabs(buf[:0], s, maxSpaces)
203 w.Write(buf)
204 }
205
206 if w.WriteByte('\n') != nil {
207 return io.EOF
208 }
209
210 if cfg.liveLines && w.Flush() != nil {
211 return io.EOF
212 }
213 }
214
215 return sc.Err()
216 }
217
218 func expandTabs(dst []byte, src []byte, tabStop int) []byte {
219 n := 0
220
221 if tabStop < 1 {
222 return append(dst, src...)
223 }
224
225 for len(src) > 0 {
226 r, size := utf8.DecodeRune(src)
227
228 if r != '\t' {
229 dst = append(dst, src[:size]...)
230 n++
231 } else {
232 spaces := tabStop - n%tabStop
233 dst = appendSpaces(dst, spaces)
234 n += spaces
235 }
236
237 src = src[size:]
238 }
239
240 return dst
241 }
242
243 func appendSpaces(dst []byte, n int) []byte {
244 const (
245 half = ` `
246 spaces = half + half
247 )
248
249 for n >= len(spaces) {
250 dst = append(dst, spaces...)
251 n -= len(spaces)
252 }
253 if n > 0 {
254 dst = append(dst, spaces[:n]...)
255 }
256 return dst
257 }
258
259 func writeSpaces(w *bufio.Writer, n int) {
260 const (
261 half = ` `
262 spaces = half + half
263 )
264
265 for n >= len(spaces) {
266 w.WriteString(spaces)
267 n -= len(spaces)
268 }
269 if n > 0 {
270 w.WriteString(spaces[:n])
271 }
272 }
File: ./factor/factor.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package factor
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "math"
32 "math/bits"
33 "os"
34 "strconv"
35 "strings"
36 )
37
38 const info = `
39 factor [options...] [numbers...]
40
41 Find all prime factors for the numbers given. If no numbers are given, the
42 numbers to factor are read as space-separated fields from each line from the
43 standard input.
44
45 Options
46
47 --help show this help message
48 `
49
50 func Main() {
51 args := os.Args[1:]
52
53 if len(args) > 0 {
54 switch args[0] {
55 case `-h`, `--h`, `-help`, `--help`:
56 os.Stderr.WriteString(info[1:])
57 return
58 }
59 }
60
61 if len(args) > 0 && args[0] == `--` {
62 args = args[1:]
63 }
64
65 if err := run(args); err != nil && err != io.EOF {
66 os.Stderr.WriteString(`factor: `)
67 os.Stderr.WriteString(err.Error())
68 os.Stderr.WriteString("\n")
69 os.Exit(1)
70 return
71 }
72 }
73
74 func run(args []string) error {
75 w := bufio.NewWriterSize(os.Stdout, 32*1024)
76 defer w.Flush()
77
78 if len(args) == 0 {
79 return factorLines(w)
80 }
81
82 for _, s := range args {
83 n, ok := parseInt64(s)
84 if !ok {
85 return errors.New(s + ` isn't a valid integer`)
86 }
87
88 if err := factor(w, int64(n)); err != nil {
89 return err
90 }
91 }
92 return nil
93 }
94
95 func factor(w *bufio.Writer, n int64) error {
96 var buf [24]byte
97
98 w.Write(strconv.AppendInt(buf[:0], n, 10))
99 w.WriteByte(':')
100
101 for n > 0 && n%2 == 0 {
102 w.WriteByte(' ')
103 w.WriteByte('2')
104 n /= 2
105 }
106
107 // avoid O(n**2) time-complexity by only checking up to the square-root
108 max := uint64(math.Ceil(math.Sqrt(float64(n))))
109
110 for div := uint64(3); div <= max; div += 2 {
111 quo, rem := bits.Div64(0, uint64(n), div)
112 if rem == 0 {
113 s := strconv.AppendInt(append(buf[:0], ' '), int64(div), 10)
114
115 w.Write(s)
116 n = int64(quo)
117
118 for {
119 quo, rem := bits.Div64(0, uint64(n), div)
120 if rem != 0 {
121 break
122 }
123
124 w.Write(s)
125 n = int64(quo)
126 }
127 }
128 }
129
130 if n > 1 {
131 w.WriteByte(' ')
132 w.Write(strconv.AppendInt(buf[:0], int64(n), 10))
133 }
134
135 if w.WriteByte('\n') != nil {
136 return io.EOF
137 }
138 return nil
139 }
140
141 func factorLines(w *bufio.Writer) error {
142 const gb = 1024 * 1024 * 1024
143 sc := bufio.NewScanner(os.Stdin)
144 sc.Buffer(nil, 8*gb)
145
146 for sc.Scan() {
147 line := strings.TrimSpace(sc.Text())
148
149 for len(line) > 0 {
150 item, rest := advance(line)
151 line = rest
152
153 n, ok := parseInt64(item)
154 if !ok {
155 return errors.New(item + ` isn't a valid integer`)
156 }
157
158 if err := factor(w, int64(n)); err != nil {
159 return err
160 }
161
162 if w.Flush() != nil {
163 return io.EOF
164 }
165 }
166 }
167
168 return sc.Err()
169 }
170
171 func advance(s string) (lead string, rest string) {
172 for len(s) > 0 && (s[0] == ' ' || s[0] == '\t' || s[0] == '\r') {
173 s = s[1:]
174 }
175
176 for i, r := range s {
177 if r == ' ' || r == '\t' || r == '\r' {
178 return s[:i], s[i:]
179 }
180 }
181
182 return s, ``
183 }
184
185 func parseInt64(s string) (n int64, ok bool) {
186 // n, err := strconv.ParseInt(s, 10, 64)
187 // return n, err == nil
188
189 // handle an optional leading sign
190 sign := int64(1)
191 if len(s) > 0 {
192 if s[0] == '-' {
193 sign = -1
194 s = s[1:]
195 } else if s[0] == '+' {
196 s = s[1:]
197 }
198 }
199
200 if len(s) == 0 {
201 return 0, false
202 }
203
204 digits := 0
205
206 for len(s) > 0 {
207 // less-than-0 byte-wraps around into some bigger-than-9 value
208 if d := s[0] - '0'; d <= 9 {
209 digits++
210 n *= 10
211 n += int64(d)
212 s = s[1:]
213 continue
214 }
215
216 // ignore underscores, which make long numbers easier to type right
217 if s[0] == '_' {
218 s = s[1:]
219 continue
220 }
221
222 return sign * n, false
223 }
224
225 return sign * n, digits > 0
226 }
File: ./factor/factor_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package factor
26
27 import (
28 "math/rand"
29 "strconv"
30 "strings"
31 "testing"
32 "time"
33 )
34
35 var tests = []string{
36 ``,
37 `0`,
38 `0e`,
39 `123456789012`,
40 `+123456789012`,
41 `-123456789012`,
42 `00000123456789012`,
43 `+00000123456789012`,
44 `-00000123456789012`,
45
46 `123_456_789_012`,
47 `+123_456_789_012`,
48 `-123_456_789_012`,
49 `00000123_456_789_012`,
50 `+00000123_456_789_012`,
51 `-00000123_456_789_012`,
52 }
53
54 func testCustomParser(t *testing.T, s string) {
55 got, ok := parseInt64(s)
56 expected, err := strconv.ParseInt(strings.Replace(s, `_`, ``, -1), 10, 64)
57
58 if ok != (err == nil) {
59 if err == nil {
60 t.Fatalf("unexpectedly successful parse")
61 } else {
62 t.Fatalf("unexpectedly failed to parse")
63 }
64 return
65 }
66
67 if err == nil && got != expected {
68 t.Fatalf("expected %v, got %v instead", expected, got)
69 }
70 }
71
72 func TestParseResults(t *testing.T) {
73 for _, s := range tests {
74 t.Run(s, func(t *testing.T) {
75 testCustomParser(t, s)
76 })
77 }
78 }
79
80 func TestFuzzParseResults(t *testing.T) {
81 const max = 1_000_000_000
82 r := rand.New(rand.NewSource(time.Now().UnixNano()))
83
84 for i := 0; i < 1_000_000; i++ {
85 n := int64(r.Intn(max) - 2*max)
86 testCustomParser(t, strconv.FormatInt(n, 10))
87 }
88 }
89
90 func BenchmarkCustomParsing(b *testing.B) {
91 for i := 0; i < b.N; i++ {
92 for _, s := range tests {
93 _, _ = parseInt64(s)
94 }
95 }
96 }
97
98 func BenchmarkParseInt(b *testing.B) {
99 for i := 0; i < b.N; i++ {
100 for _, s := range tests {
101 _, _ = strconv.ParseInt(s, 10, 64)
102 }
103 }
104 }
105
106 func BenchmarkParseAtoi(b *testing.B) {
107 for i := 0; i < b.N; i++ {
108 for _, s := range tests {
109 _, _ = strconv.Atoi(s)
110 }
111 }
112 }
File: ./fail/fail.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fail
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "os/exec"
33 "strconv"
34 )
35
36 const info = `
37 fail [options...] [status code...] [command...] [command args...]
38
39
40 Just quit with the status code/number given as an argument; if no status
41 code/number was given, quit using code 1 by default. If given 0 for the
42 status code, it will paradoxically succeed, as 0 is the success code.
43
44 If given a command after the (optional) error/exit code, that command is
45 run before failing, ignoring the command's own success/failure.
46
47 The options are, available both in single and double-dash versions
48
49 -h, -help show this help message
50 `
51
52 func Main() {
53 buffered := false
54 args := os.Args[1:]
55 code := 1
56
57 for len(args) > 0 {
58 switch args[0] {
59 case `-b`, `--b`, `-buffered`, `--buffered`:
60 buffered = true
61 args = args[1:]
62 continue
63
64 case `-h`, `--h`, `-help`, `--help`:
65 os.Stdout.WriteString(info[1:])
66 return
67 }
68
69 break
70 }
71
72 if len(args) > 0 && args[0] == `--` {
73 args = args[1:]
74 }
75
76 if len(args) > 0 {
77 if n, err := strconv.ParseInt(args[0], 10, 64); err == nil {
78 code = int(n)
79 args = args[1:]
80 }
81 }
82
83 liveLines := !buffered
84 if len(args) > 0 && !buffered {
85 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
86 liveLines = false
87 }
88 }
89
90 if err := run(args, liveLines); err != nil {
91 os.Stderr.WriteString(`fail: `)
92 os.Stderr.WriteString(err.Error())
93 os.Stderr.WriteString("\n")
94 }
95
96 os.Exit(code)
97 }
98
99 func run(args []string, liveLines bool) error {
100 if len(args) == 0 {
101 return nil
102 }
103
104 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
105 defer bw.Flush()
106
107 cmd := exec.Command(args[0], args[1:]...)
108 cmd.Stdin = os.Stdin
109 if liveLines {
110 cmd.Stdout = lineFlusher{bw}
111 } else {
112 cmd.Stdout = bw
113 }
114 cmd.Stderr = os.Stderr
115
116 bw.Flush()
117
118 if err := cmd.Start(); err != nil {
119 os.Stderr.WriteString(`fail: `)
120 os.Stderr.WriteString(err.Error())
121 os.Stderr.WriteString("\n")
122 os.Exit(1)
123 return nil
124 }
125
126 err := cmd.Wait()
127 if _, ok := err.(*exec.ExitError); ok {
128 err = nil
129 }
130
131 if err != nil {
132 os.Stderr.WriteString(`fail: `)
133 os.Stderr.WriteString(err.Error())
134 os.Stderr.WriteString("\n")
135 }
136
137 bw.Flush()
138
139 return nil
140 }
141
142 type lineFlusher struct {
143 bw *bufio.Writer
144 }
145
146 func (lf lineFlusher) Write(p []byte) (n int, err error) {
147 n, err = lf.bw.Write(p)
148 if err != nil {
149 return n, err
150 }
151
152 if bytes.IndexByte(p, '\n') >= 0 {
153 err = lf.bw.Flush()
154 }
155
156 return n, err
157 }
File: ./fh/config.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fh
26
27 import (
28 "errors"
29 "fmt"
30 "image/color"
31 "math"
32 "os"
33 "strconv"
34 "strings"
35 )
36
37 const (
38 // all output formats as constants, to prevent typos
39 pngOutput = `png`
40 pngFastOutput = `fast-png`
41 pngSmallestOutput = `smallest-png`
42 pngUncompressedOutput = `uncompressed-png`
43 bmpOutput = `bmp`
44 jpegOutput = `jpeg`
45
46 // all colorscales as constants, to prevent typos
47 magmaScale = `magma`
48 parulaScale = `parula`
49 viridisScale = `viridis`
50 grayScale = `gray`
51 binaryScale = `binary`
52 signScale = `sign`
53 )
54
55 // fmtAliases normalizes values for the output-format option
56 var fmtAliases = map[string]string{
57 `b`: bmpOutput,
58 `bitmap`: bmpOutput,
59 `bmp`: bmpOutput,
60 `j`: jpegOutput,
61 `jpeg`: jpegOutput,
62 `jpg`: jpegOutput,
63 `p`: pngOutput,
64 `ping`: pngOutput,
65 `png`: pngOutput,
66
67 `f`: pngFastOutput,
68 `fast`: pngFastOutput,
69 `fast-png`: pngFastOutput,
70 `fp`: pngFastOutput,
71 `fpng`: pngFastOutput,
72 `s`: pngSmallestOutput,
73 `small`: pngSmallestOutput,
74 `smallest-png`: pngSmallestOutput,
75 `small-png`: pngSmallestOutput,
76 `sp`: pngSmallestOutput,
77 `spng`: pngSmallestOutput,
78 `u`: pngUncompressedOutput,
79 `unc`: pngUncompressedOutput,
80 `uncompressed-png`: pngUncompressedOutput,
81 }
82
83 // paletteAliases normalizes values for the colorscale/palette option
84 var paletteAliases = map[string]string{
85 `b`: binaryScale,
86 `bin`: binaryScale,
87 `binary`: binaryScale,
88
89 `g`: grayScale,
90 `gr`: grayScale,
91 `gray`: grayScale,
92
93 `m`: magmaScale,
94 `mag`: magmaScale,
95 `magma`: magmaScale,
96
97 `s`: signScale,
98 `sgn`: signScale,
99 `sign`: signScale,
100
101 `py`: viridisScale,
102 `python`: viridisScale,
103 `numpy`: viridisScale,
104 `v`: viridisScale,
105 `vir`: viridisScale,
106 `viridis`: viridisScale,
107
108 `matlab`: parulaScale,
109 `p`: parulaScale,
110 `par`: parulaScale,
111 `parula`: parulaScale,
112 }
113
114 // outputSize is the value type for the resAliases lookup table
115 type outputSize struct {
116 Width int
117 Height int
118 }
119
120 // resAliases normalizes values for option -res
121 var resAliases = map[string]outputSize{
122 `sq`: {2160, 2160},
123 `sqr`: {2160, 2160},
124 `square`: {2160, 2160},
125 `squared`: {2160, 2160},
126
127 `4k`: {3840, 2160},
128 `2160`: {3840, 2160},
129 `2160p`: {3840, 2160},
130 `3840`: {3840, 2160},
131
132 `2.5k`: {2560, 1440},
133 `1440`: {2560, 1440},
134 `1440p`: {2560, 1440},
135 `2560`: {2560, 1440},
136
137 `2k`: {1920, 1080},
138 `hd`: {1920, 1080},
139 `fhd`: {1920, 1080},
140 `fullhd`: {1920, 1080},
141 `1080`: {1920, 1080},
142 `1080p`: {1920, 1080},
143 `1920`: {1920, 1080},
144
145 `720`: {1280, 720},
146 `720p`: {1280, 720},
147
148 `480p`: {640, 480},
149 `480`: {640, 480},
150
151 `2ks`: {1080, 1080},
152 `4ks`: {2160, 2160},
153 `2160s`: {2160, 2160},
154 `1440s`: {1440, 1440},
155 `1080s`: {1080, 1080},
156 `720s`: {720, 720},
157 `480s`: {480, 480},
158 }
159
160 // config has all parsed cmd-line arguments
161 type config struct {
162 Width int
163 Height int
164
165 XMin float64
166 XMax float64
167 YMin float64
168 YMax float64
169
170 Formula string
171 Output string
172
173 Palette func(float64) color.RGBA
174 Bad color.RGBA
175
176 Integers bool
177 }
178
179 // parseFlags is the constructor for type config
180 func parseFlags(usage string) (config, error) {
181 cfg := config{
182 Width: 3840,
183 Height: 2160,
184
185 XMin: 0,
186 XMax: 1,
187 YMin: 0,
188 YMax: 1,
189
190 Output: pngOutput,
191 }
192
193 cfg.Output = pngOutput
194 pal := palettes[parulaScale]
195 cfg.Palette = pal.Func
196 cfg.Bad = pal.Bad
197
198 args := os.Args[1:]
199 if len(args) == 0 {
200 fmt.Fprint(os.Stderr, usage)
201 os.Exit(0)
202 return cfg, nil
203 }
204
205 for _, s := range args {
206 switch s {
207 case `help`, `-h`, `--h`, `-help`, `--help`:
208 fmt.Fprint(os.Stdout, usage)
209 os.Exit(0)
210 return cfg, nil
211 }
212
213 err := cfg.handleArg(s)
214 if err != nil {
215 return cfg, err
216 }
217 }
218
219 if cfg.Integers {
220 cfg.XMin = math.Ceil(float64(cfg.XMin))
221 cfg.XMax = math.Floor(float64(cfg.XMax))
222 cfg.YMin = math.Ceil(float64(cfg.YMin))
223 cfg.YMax = math.Floor(float64(cfg.YMax))
224 }
225
226 if strings.TrimSpace(cfg.Formula) == `` {
227 return cfg, errors.New(`no main formula given`)
228 }
229 return cfg, nil
230 }
231
232 // handleArg parses/uses the cmd-line argument given, except for the help
233 // option and its aliases, which can only be detected separately
234 func (c *config) handleArg(s string) error {
235 switch s {
236 case `int`, `ints`, `integers`:
237 c.Integers = true
238 return nil
239 }
240
241 lcDotless := strings.TrimPrefix(strings.ToLower(s), `.`)
242 if alias, ok := fmtAliases[lcDotless]; ok {
243 c.Output = alias
244 return nil
245 }
246
247 if w, h, ok := parseResolution(s); ok {
248 c.Width = w
249 c.Height = h
250 return nil
251 }
252
253 if colors, ok := paletteAliases[s]; ok {
254 pal := palettes[colors]
255 c.Palette = pal.Func
256 c.Bad = pal.Bad
257 return nil
258 }
259
260 varname, min, max, err := parseDomain(s)
261 if err != nil {
262 return err
263 }
264
265 switch varname {
266 case ``:
267 // no variable name means it's the main formula
268 if c.Formula != `` {
269 const fs = `%q: can't use more than 1 main formula`
270 return fmt.Errorf(fs, s)
271 }
272 c.Formula = s
273 return nil
274
275 case `x`:
276 c.XMin = min
277 c.XMax = max
278 return nil
279
280 case `y`:
281 c.YMin = min
282 c.YMax = max
283 return nil
284
285 case `xy`:
286 c.XMin = min
287 c.XMax = max
288 c.YMin = min
289 c.YMax = max
290 return nil
291
292 default:
293 const fs = "domain variable %q isn't any of `x`, `y`, or `xy`"
294 return fmt.Errorf(fs, varname)
295 }
296 }
297
298 // parseResolution tries to get a width/height resolution out of the
299 // cmd-line argument given to it
300 func parseResolution(s string) (width int, height int, ok bool) {
301 if res, ok := resAliases[s]; ok {
302 return res.Width, res.Height, true
303 }
304
305 i := strings.IndexByte(s, 'x')
306 if i < 0 {
307 return 0, 0, false
308 }
309
310 w, werr := strconv.ParseInt(s[:i], 10, 64)
311 h, herr := strconv.ParseInt(s[i+1:], 10, 64)
312 if werr == nil && herr == nil && w > 0 && h > 0 {
313 return int(w), int(h), true
314 }
315 return 0, 0, false
316 }
317
318 func (c config) IntegerSize() (w, h int) {
319 w = int(math.Abs(c.XMax - c.XMin + 1))
320 h = int(math.Abs(c.YMax - c.YMin + 1))
321 return w, h
322 }
323
324 // parseDomain tries to parse domain/variable-range formulas of the form(s)
325 //
326 // - x:=a..b
327 // - y:=a..b
328 // - xy:=a..b
329 //
330 // where a and b represent valid floating-point numbers; when an empty is
331 // returned, it means the strings given wasn't recognized as a variable's
332 // domain, suggesting it may be another option, or the main formula instead
333 func parseDomain(s string) (string, float64, float64, error) {
334 i := strings.Index(s, `:=`)
335 if i < 0 {
336 return ``, 0, 0, nil
337 }
338
339 v := strings.TrimSpace(s[:i])
340 rng := strings.TrimSpace(s[i+2:])
341 min, max, err := parseSpan(rng)
342 return v, min, max, err
343 }
344
345 // parseSpan tries to parse a pair of numbers with `..` between them
346 func parseSpan(s string) (float64, float64, error) {
347 pair := strings.Split(s, `..`)
348 if len(pair) != 2 {
349 const fs = "missing `..` in domain-span %s"
350 return 0, 1, fmt.Errorf(fs, s)
351 }
352
353 a, err := strconv.ParseFloat(pair[0], 64)
354 if err != nil {
355 const fs = `can't parse %q in domain-span %s`
356 return 0, 1, fmt.Errorf(fs, pair[0], s)
357 }
358 b, err := strconv.ParseFloat(pair[1], 64)
359 if err != nil {
360 const fs = `can't parse %q in domain-span %s`
361 return 0, 1, fmt.Errorf(fs, pair[1], s)
362 }
363 return a, b, nil
364 }
File: ./fh/config_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fh
26
27 import "testing"
28
29 func TestTables(t *testing.T) {
30 for _, kind := range fmtAliases {
31 // check all canonical format names are in the table
32 if _, ok := fmtAliases[kind]; !ok {
33 const fs = `format %q itself isn't in the format-table`
34 t.Fatalf(fs, kind)
35 return
36 }
37
38 if _, ok := encoders[kind]; !ok {
39 const fs = `no encoder for %q`
40 t.Fatalf(fs, kind)
41 return
42 }
43 }
44
45 for _, kind := range paletteAliases {
46 // check all canonical colorscale names are in the table
47 if _, ok := paletteAliases[kind]; !ok {
48 const fs = `format %q itself isn't in the format-table`
49 t.Fatalf(fs, kind)
50 return
51 }
52
53 if _, ok := palettes[kind]; !ok {
54 const fs = `no palette for %q`
55 t.Fatalf(fs, kind)
56 return
57 }
58 }
59 }
File: ./fh/examples.txt
1 # ripples
2 fh xy:=-3..3 'exp(-0.5 * sin(2 * hypot(x - 2, y + 1))) + exp(-0.5 * sin(10 * hypot(x + 2, y - 3.4)))' | si
3
4 # floor lights
5 fh x:=-5..5 y:=1..5 'x.sin.abs / y**1.4' | si
6
7 # beta gradient
8 fh x:=-5..5 y:=1..5 'lbeta(x + 5.1, y + 5.1)' | si
9
10 # hot bars / horizontal bars
11 fh xy:=-5..5 'x.abs + sqrt(abs(sin(2*y)))' | si
12 fh xy:=-5..5 'x.abs + sqrt(abs(sin(2*y)))' | si
13
14 # domain hole
15 fh xy:=-5..5 'log1p(sin(x + y) + (x - y)**2 - 1.5*x + 2.5*y + 1)' | si
16
17 # crazy grids
18 fh xy:=-10..10 'sin(x.sin+y.cos) + cos(sin(x*y)+cos(y*y))' | si
19
20 # panda / smiling ghost
21 fh xy:=-5..5 'log1p(((x - 1)**2 + y*y - 4)*((x + 1)**2 + y*y - 4)*(x*x + (y - sqrt(3))**2 - 4) - 5)' | si
22
23 # lcm 200
24 fh xy:=0.01..199.99 'lcm(x.ceil, y.ceil)' | si
25
26 # light tiles
27 fh 'gauss(2*(sin(50.0*x)*cos(50.0*9/16*y) + 1.0)/2.0)' | si
28
29 # shaky results... at least for me
30 fh 'cos(160*tau*x) + sin(90*tau*y)' | si
31
32 # 90-degree square tiles
33 fh 'sign(cos(160*tau*x) + sin(90*tau*y))' | si
File: ./fh/info.txt
1 fh [options...] [x/y ranges...] formula
2
3
4 Function Heatmapper emits a picture showing a heatmap view of the function
5 f(x, y) implied by the math expression given. Plenty of math functions and
6 constants are available, all their names being lowercase; the syntax is
7 almost identical to Python/JavaScript's math notation, and has no keywords.
8
9 For convenience, you can treat any 1-input func as a fake-property of its
10 only input; you can also pretend all functions are fake-methods, where the
11 1st input comes before the dot preceding the func name, followed by all the
12 other args to it. All values and functions are global: without namespaces
13 of any kind.
14
15 Ranges for variables `x` and `y` are 0 to 1 by default, but you can change
16 them via the special syntax shown on some of the examples below. Using the
17 keyword `int`, `ints`, or `integers` enables integer-mode, where both `x`
18 and `y` values are only sampled as integers: in that case, formula results
19 will be used to fill whole tiles, instead of single pixels.
20
21 By default, output is PNG-encoded using a good tradeoff between encoding
22 speed and final payload size. Output resolutions can be as shown below, or
23 consist of the width, followed by `x`, followed by the height wanted, such
24 as `1024x768`, for example.
25
26 Options have no flags/prefixes, and are accepted in any order.
27
28
29 Options
30
31 resolution resolution
32
33 4k 3840x2160 4ks 2160x2160
34 hd 1920x1080 hds 1080x1080
35
36 2160p 3840x2160 2160s 2160x2160
37 1440p 2560x1440 1440s 1440x1440
38 1080p 1920x1080 1080s 1080x1080
39 720p 1280x720 720s 720x720
40
41
42 output aliases colorscale aliases
43
44 png magma mag, m
45 bmp bitmap parula par, p
46 jpg jpeg viridis vir, v
47
48
49 Concrete Examples
50
51
52 fh 'x/(x+y)' > corner-fan-1.png
53
54 fh 'y/(x+y)' > corner-fan-2.png
55
56 fh 4k x:=-5..5 y:=1..5 'x.sin.abs / y**1.4' > floor-lights.png
57
58 fh vir x:=-5..5 y:=1..5 'lbeta(x + 5.1, y + 5.1)' > beta-gradient.png
59
60 fh mag 4k xy:=0.01..199.99 'lcm(x.ceil, y.ceil)' > lcm-200.png
61
62 fh par 4k xy:=-5..5 'x.abs + sqrt(abs(sin(2*y)))' > bars.png
63
64 fh x:=-1.5..0.5 y:=-1..1 'mandel(16/9*x, y)' > mandelbrot.png
65
66 fh x:=-1.5..0.5 y:=-1..1 'absmandel(16/9*x, y)' > wobbly-mandelbrot.png
67
68 fh 4k 'sign(cos(160*tau*x) + sin(90*tau*y))' > 90-deg-square-tiles.png
69
70 fh xy:=-10..10 'sin(x.sin+y.cos) + cos(sin(x*y)+cos(y*y))' > crazy-grids.png
71
72 fh 'gauss(sin(50*x) * cos(50*9/16*y) + 1)' > light-tiles.png
73
74 fh xy:=-2..3 'sgn(log((x*x-1)*(x-2-y)/(x*x+2+2*y)))' > abstract-shapes.png
75
76 fh xy:=-10..10 square 'sinc(0.55 * hypot(x, y))' > central-ripple.png
File: ./fh/main.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fh
26
27 import (
28 "bufio"
29 "fmt"
30 "image"
31 "os"
32
33 _ "embed"
34 )
35
36 //go:embed info.txt
37 var usage string
38
39 func Main() {
40 cfg, err := parseFlags(usage)
41 if err != nil {
42 fmt.Fprintln(os.Stderr, err.Error())
43 os.Exit(1)
44 return
45 }
46
47 if _, ok := encoders[cfg.Output]; !ok {
48 const fs = "unsupported output format %s\n"
49 fmt.Fprintf(os.Stderr, fs, cfg.Output)
50 os.Exit(1)
51 return
52 }
53
54 addDetermFuncs()
55
56 if err := run(cfg); err != nil {
57 fmt.Fprintln(os.Stderr, err.Error())
58 os.Exit(1)
59 return
60 }
61 }
62
63 func run(cfg config) error {
64 // f, err := os.Create(`fh.prof`)
65 // if err != nil {
66 // return err
67 // }
68 // defer f.Close()
69
70 // pprof.StartCPUProfile(f)
71 // defer pprof.StopCPUProfile()
72
73 encode, ok := encoders[cfg.Output]
74 if !ok {
75 const fs = `unsupported output format %q`
76 return fmt.Errorf(fs, cfg.Output)
77 }
78
79 if cfg.Integers {
80 w, h := cfg.IntegerSize()
81 cfg.Width = w
82 cfg.Height = h
83 }
84
85 // allow runner to use up to 32 cores
86 r, err := newRunner(cfg, 32)
87 if err != nil {
88 return err
89 }
90
91 res, err := r.Run(cfg)
92 if err != nil {
93 return err
94 }
95
96 img := image.NewRGBA(image.Rectangle{
97 Min: image.Point{X: 0, Y: 0},
98 Max: image.Point{X: cfg.Width, Y: cfg.Height},
99 })
100
101 w := bufio.NewWriterSize(os.Stdout, 64*1024)
102 defer w.Flush()
103
104 // give back a blank picture if results aren't usable
105 if !res.isValid() {
106 return encode(w, img, cfg)
107 }
108
109 // handle integers-only coordinate-inputs
110 if cfg.Integers {
111 width, height := cfg.IntegerSize()
112 fillExpandedImage(img, res, cfg, width, height)
113 return encode(w, img, cfg)
114 }
115
116 // handle domain-sampled images
117 fillImage(img, res, cfg)
118 return encode(w, img, cfg)
119 }
File: ./fh/output.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fh
26
27 import (
28 "bufio"
29 "encoding/binary"
30 "image"
31 "image/color"
32 "image/jpeg"
33 "image/png"
34 "math"
35
36 "../colorplus"
37 )
38
39 var (
40 // red is the invalid color for all palettes with dark/black colors
41 red = color.RGBA{R: 255, G: 0, B: 0, A: 255}
42
43 // black is the invalid color for the more colorful palettes
44 black = color.RGBA{R: 0, G: 0, B: 0, A: 255}
45 )
46
47 // paletteSettings describes the full behavior of a palette
48 type paletteSettings struct {
49 Func func(float64) color.RGBA
50 Bad color.RGBA
51 }
52
53 // palettes completely describes the behavior of all supported palettes
54 var palettes = map[string]paletteSettings{
55 grayScale: {gray, red},
56 magmaScale: {colorplus.Magmify, red},
57 viridisScale: {colorplus.Viridize, black},
58 parulaScale: {colorplus.Parulate, black},
59 binaryScale: {colorBinary, black},
60 signScale: {colorSign, black},
61 }
62
63 // gray implements the grayscale coloring option, and is meant to be paired
64 // with a red color for invalid inputs, such as NaNs
65 func gray(x float64) color.RGBA {
66 // restrict input to range 0..1
67 if x < 0 {
68 x = 0
69 } else if x > 1 {
70 x = 1
71 }
72
73 v := uint8(math.Round(255 * x))
74 return color.RGBA{R: v, G: v, B: v, A: 255}
75 }
76
77 // colorBinary assigns 2 colors, thresholding the number given on 0.5
78 func colorBinary(x float64) color.RGBA {
79 if x < 0.5 {
80 return color.RGBA{R: 234, G: 85, B: 58, A: 255}
81 }
82 return color.RGBA{R: 0, G: 95, B: 0, A: 255}
83 }
84
85 // colorSign assigns 3 colors, depending on the sign of the number given
86 func colorSign(x float64) color.RGBA {
87 if x > 0 {
88 return color.RGBA{R: 0, G: 95, B: 0, A: 255}
89 }
90 if x < 0 {
91 return color.RGBA{R: 234, G: 85, B: 58, A: 255}
92 }
93 return color.RGBA{R: 0, G: 135, B: 215, A: 255}
94 }
95
96 // encoders translates output-format settings into the right func to call
97 var encoders = map[string]func(*bufio.Writer, *image.RGBA, config) error{
98 pngOutput: encodePNG,
99 bmpOutput: encodeBMP,
100 jpegOutput: encodeJPEG,
101
102 pngFastOutput: encodeFastPNG,
103 pngSmallestOutput: encodeSmallestPNG,
104 pngUncompressedOutput: encodeUncompressedPNG,
105 }
106
107 // fillImage fills/renders an image using previously calculated values
108 func fillImage(img *image.RGBA, res result, cfg config) {
109 k := 0
110 f := cfg.Palette
111
112 for i := 0; i < cfg.Height; i++ {
113 for j := 0; j < cfg.Width; j++ {
114 v := res.Values[k]
115
116 var c color.RGBA
117 if math.IsNaN(v) || math.IsInf(v, 0) {
118 c = cfg.Bad
119 } else {
120 c = f(colorplus.Wrap(v, res.Min, res.Max))
121 }
122
123 img.SetRGBA(j, i, c)
124 k++
125 }
126 }
127 }
128
129 // fillExpandedImage is like func fillImage, but rendering stretches what
130 // would otherwise be single pixels into rectangles, representing regions
131 // where the integer-parts of x/y inputs stay the same
132 func fillExpandedImage(img *image.RGBA, res result, cfg config, w, h int) {
133 width := img.Rect.Max.X
134 xmax := float64(img.Rect.Max.X)
135 ymax := float64(img.Rect.Max.Y)
136
137 f := cfg.Palette
138 dx := float64(w) / xmax
139 dy := float64(h) / ymax
140
141 for i := 0; i < cfg.Height; i++ {
142 y := int(dy * float64(i))
143 for j := 0; j < cfg.Width; j++ {
144 x := int(dx * float64(j))
145 k := y*width + x
146 v := res.Values[k]
147
148 var c color.RGBA
149 if math.IsNaN(v) || math.IsInf(v, 0) {
150 c = cfg.Bad
151 } else {
152 c = f(colorplus.Wrap(v, res.Min, res.Max))
153 }
154 img.SetRGBA(j, i, c)
155 }
156 }
157 }
158
159 // encodePNG seems a good default both for its main format (PNG), as well as
160 // its reasonable default tradeoff between speed and output size, compared
161 // to the PNG-encoding alternatives available
162 func encodePNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
163 var enc png.Encoder
164 return enc.Encode(w, img)
165 }
166
167 // encodeFastPNG may not always be much faster than the default PNG encoder
168 func encodeFastPNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
169 var enc png.Encoder
170 enc.CompressionLevel = png.BestSpeed
171 return enc.Encode(w, img)
172 }
173
174 // encodeSmallestPNG is substantially slower than the other PNG encoders
175 func encodeSmallestPNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
176 var enc png.Encoder
177 enc.CompressionLevel = png.BestCompression
178 return enc.Encode(w, img)
179 }
180
181 // encodeUncompressedPNG is mostly to compare it to BMP output: it turns out
182 // BMP is slightly smaller than this
183 func encodeUncompressedPNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
184 var enc png.Encoder
185 enc.CompressionLevel = png.NoCompression
186 return enc.Encode(w, img)
187 }
188
189 // encodeJPEG encodes result at max JPEG setting: this usually results in
190 // highly-detailed results for substantially-fewer bytes, compared to PNG
191 // output
192 func encodeJPEG(w *bufio.Writer, img *image.RGBA, cfg config) error {
193 opt := jpeg.Options{Quality: 100}
194 return jpeg.Encode(w, img, &opt)
195 }
196
197 // https://en.wikipedia.org/wiki/BMP_file_format
198
199 // encodeBMP encodes as BMP/bitmap, a simple uncompressed format, which has
200 // been widely supported for many decades
201 func encodeBMP(w *bufio.Writer, img *image.RGBA, cfg config) error {
202 const (
203 dibsize = 40 // the DIB is the 2nd header
204 hdrsize = 14 + dibsize // total size of all headers
205 )
206 imgsize := 3 * cfg.Width * cfg.Height
207
208 w.WriteString(`BM`)
209 binary.Write(w, binary.LittleEndian, uint32(hdrsize+imgsize))
210 binary.Write(w, binary.LittleEndian, uint16(0))
211 binary.Write(w, binary.LittleEndian, uint16(0))
212 binary.Write(w, binary.LittleEndian, uint32(hdrsize))
213 binary.Write(w, binary.LittleEndian, uint32(dibsize))
214 binary.Write(w, binary.LittleEndian, int32(cfg.Width))
215 binary.Write(w, binary.LittleEndian, int32(cfg.Height))
216
217 // 1 color plane
218 binary.Write(w, binary.LittleEndian, uint16(1))
219 // 24 bits per pixel
220 binary.Write(w, binary.LittleEndian, uint16(24))
221 // no compression
222 binary.Write(w, binary.LittleEndian, uint32(0))
223 // number of bytes for the pixels
224 binary.Write(w, binary.LittleEndian, uint32(imgsize))
225 // horizontal & vertical pixels/m
226 binary.Write(w, binary.LittleEndian, int32(0))
227 binary.Write(w, binary.LittleEndian, int32(0))
228 // 2**n palette colors
229 binary.Write(w, binary.LittleEndian, uint32(0))
230 // all colors are important
231 binary.Write(w, binary.LittleEndian, uint32(0))
232
233 stride := img.Stride
234 // rows/lines are apparently stored bottom-to-top
235 for y := cfg.Height - 1; y >= 0; y-- {
236 start := y * stride
237 buf := img.Pix[start : start+stride]
238
239 for len(buf) >= 3 {
240 // color-channel order seems to be BGR, instead of RGB
241 w.WriteByte(buf[2])
242 w.WriteByte(buf[1])
243 err := w.WriteByte(buf[0])
244 if err != nil {
245 // use errors to quit immediately: chances are
246 // the error is the result of a closed-pipe
247 return nil
248 }
249
250 // also skip the alpha channel
251 buf = buf[4:]
252 }
253 }
254 return nil
255 }
File: ./fh/scripts.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fh
26
27 import (
28 "math"
29 "math/cmplx"
30 "math/rand"
31 "runtime"
32 "sync"
33 "time"
34
35 "../fmscripts"
36 "../mathplus"
37 )
38
39 // result has all results, including a summary of the range of values, so the
40 // image renderer can normalize values accordingly
41 type result struct {
42 // Values has all results, which can be normalized into 0..1 using
43 // fields Min and Max.
44 Values []float64
45
46 // Min is the lowest value in Values.
47 Min float64
48
49 // Max is the highest value in Values.
50 Max float64
51 }
52
53 // isValid checks if the result should be a non-blank picture
54 func (r result) isValid() bool {
55 return r.Min <= r.Max && !math.IsInf(r.Min, 0) && !math.IsInf(r.Max, 0)
56 }
57
58 // runner has various twin script-runners, and automatically multicore-splits
59 // the load among tasks along alternating groups of lines, in a striping
60 // manner
61 type runner struct {
62 numTasks int
63
64 // values can have its items updated concurrently, since each vertical
65 // image line is changed by a single task.
66 values []float64
67
68 programs []fmscripts.Program
69 }
70
71 // newRunner is the constructor for type runner
72 func newRunner(cfg config, maxtasks int) (runner, error) {
73 numtasks := runtime.NumCPU()
74 if maxtasks > 0 && numtasks > maxtasks {
75 numtasks = maxtasks
76 }
77 progs := make([]fmscripts.Program, 0, numtasks)
78
79 for i := 0; i < numtasks; i++ {
80 // compiling the same formula multiple times seems wasteful, but
81 // each compilation is very quick; this repetition is necessary
82 // to isolate each task's input variables and pseudo-random state,
83 // anyway
84 p, err := compile(cfg.Formula, cfg, time.Now().UnixNano())
85 if err != nil {
86 return runner{}, err
87 }
88 progs = append(progs, p)
89 }
90
91 return runner{
92 numTasks: numtasks,
93 values: make([]float64, cfg.Width*cfg.Height),
94 programs: progs,
95 }, nil
96 }
97
98 // Run is the entry-point func which handles everything from start to finish.
99 func (r *runner) Run(cfg config) (res result, err error) {
100 var wg sync.WaitGroup
101 wg.Add(r.numTasks)
102
103 // fully allocate min/max slices, as appending is concurrently unsafe
104 lmin := make([]float64, r.numTasks)
105 lmax := make([]float64, r.numTasks)
106
107 // run parallel tasks: updating the shared value-slice works, as long
108 // as each process sticks to its own index and output lines
109 for i := 0; i < r.numTasks; i++ {
110 go func(i int) {
111 defer wg.Done()
112 min, max := r.runSlice(i, cfg)
113 lmin[i] = min
114 lmax[i] = max
115 }(i)
116 }
117 wg.Wait()
118
119 // get overall min/max
120 min := math.Inf(+1)
121 max := math.Inf(-1)
122 for i := range lmin {
123 min = math.Min(min, lmin[i])
124 max = math.Max(max, lmax[i])
125 }
126 return result{Values: r.values, Min: min, Max: max}, nil
127 }
128
129 // runSlice handles the task a specific core is supposed to handle: call
130 // run instead of this func directly
131 func (r *runner) runSlice(task int, cfg config) (min, max float64) {
132 p := r.programs[task]
133 x, _ := p.Get(`x`)
134 y, _ := p.Get(`y`)
135 zs := r.values
136
137 w := cfg.Width
138 h := cfg.Height
139 n := r.numTasks
140 xmin := math.Min(cfg.XMin, cfg.XMax)
141 ymax := math.Max(cfg.YMax, cfg.YMin)
142 wf := float64(w)
143
144 zmin := math.Inf(+1)
145 zmax := math.Inf(-1)
146 dx := math.Abs(cfg.XMax-cfg.XMin) / float64(cfg.Width-1)
147 dy := math.Abs(cfg.YMax-cfg.YMin) / float64(cfg.Height-1)
148
149 for i := task; i < h; i += n {
150 k := w * i
151 *y = ymax - dy*float64(i)
152
153 for j := 0.0; j < wf; j++ {
154 *x = dx*j + xmin
155 z := p.Run()
156 zs[k] = z
157 k++
158
159 if !math.IsNaN(z) {
160 zmin = math.Min(zmin, z)
161 zmax = math.Max(zmax, z)
162 }
163 }
164 }
165 return zmin, zmax
166 }
167
168 // compile extends the built-in fast-math script functionality by adding
169 // pseudo-random generators initialized with the seed number given
170 func compile(src string, cfg config, seed int64) (fmscripts.Program, error) {
171 r := rand.New(rand.NewSource(seed))
172 rand01 := func() float64 {
173 return fmscripts.Random(r)
174 }
175 rint := func(min, max float64) float64 {
176 return fmscripts.RandomInt(r, min, max)
177 }
178 runif := func(min, max float64) float64 {
179 return fmscripts.RandomUnif(r, min, max)
180 }
181 rexp := func(scale float64) float64 {
182 return fmscripts.RandomExp(r, scale)
183 }
184 rnorm := func(mu, sigma float64) float64 {
185 return fmscripts.RandomNorm(r, mu, sigma)
186 }
187 rgamma := func(scale float64) float64 {
188 return fmscripts.RandomGamma(r, scale)
189 }
190 rbeta := func(a, b float64) float64 {
191 return fmscripts.RandomBeta(r, a, b)
192 }
193
194 var c fmscripts.Compiler
195 return c.Compile(src, map[string]any{
196 `x`: 0.0,
197 `y`: 0.0,
198
199 `w`: float64(cfg.Width),
200 `h`: float64(cfg.Height),
201 `ar`: float64(cfg.Width) / float64(cfg.Height),
202 `aspratio`: float64(cfg.Width) / float64(cfg.Height),
203
204 `rand`: rand01,
205 `rbeta`: rbeta,
206 `rexp`: rexp,
207 `rgamma`: rgamma,
208 `rint`: rint,
209 `rnorm`: rnorm,
210 `runif`: runif,
211
212 `randbeta`: rbeta,
213 `randexp`: rexp,
214 `randexpo`: rexp,
215 `randgam`: rgamma,
216 `randgamma`: rgamma,
217 `randint`: rint,
218 `randnorm`: rnorm,
219 `randunif`: runif,
220
221 `random`: rand01,
222 `rbet`: rbeta,
223 `rgam`: rgamma,
224 `rnd`: rand01,
225 })
226 }
227
228 // addDetermFuncs does what it says, ensuring these funcs are optimizable when
229 // they're given all-constant expressions as inputs
230 func addDetermFuncs() {
231 fmscripts.DefineDetFuncs(map[string]any{
232 `ascale`: mathplus.AnchoredScale,
233 `awrap`: mathplus.AnchoredWrap,
234 `choose`: comb,
235 `clamp`: mathplus.Clamp,
236 `comb`: comb,
237 `dbinom`: dbinom,
238 `dnorm`: mathplus.NormalDensity,
239 `epa`: mathplus.Epanechnikov,
240 `epanechnikov`: mathplus.Epanechnikov,
241 `etamag`: etamag,
242 `etamagcap`: etamagcap,
243 `fract`: mathplus.Fract,
244 `gauss`: mathplus.Gauss,
245 `gcd`: gcd,
246 `horner`: mathplus.Polyval,
247 `ieta`: etaimag,
248 `isprime`: isPrime,
249 `lcm`: lcm,
250 `logistic`: mathplus.Logistic,
251 `mageta`: etamag,
252 `magetacap`: etamagcap,
253 `magzeta`: zetamag,
254 `magzetacap`: zetamagcap,
255 `mix`: mathplus.Mix,
256 `perm`: perm,
257 `pbinom`: pbinom,
258 `pnorm`: mathplus.CumulativeNormalDensity,
259 `polyval`: mathplus.Polyval,
260 `reta`: etare,
261 `scale`: mathplus.Scale,
262 `sign`: mathplus.Sign,
263 `sinc`: mathplus.Sinc,
264 `smoothstep`: mathplus.SmoothStep,
265 `step`: mathplus.Step,
266 `tricube`: mathplus.Tricube,
267 `unwrap`: mathplus.Unwrap,
268 `wrap`: mathplus.Wrap,
269 `zetamag`: zetamag,
270 `zetamagcap`: zetamagcap,
271
272 `absmandel`: absmandel,
273 `absmandelcap`: absmandelcap,
274 `itermandel`: itermandel,
275 `itermandelcap`: itermandelcap,
276 `mandel`: itermandel,
277 })
278 }
279
280 // absmandel returns the abs value of the complex number used in the mandelbrot
281 // recurrence relation; recurrence is automatically truncated to a default
282 // threshold and/or max number of loops
283 func absmandel(x, y float64) float64 {
284 return absmandelcap(x, y, 50)
285 }
286
287 // absmandelcap is like func absmandel, except the cap/threshold is an explicit
288 // parameter
289 func absmandelcap(x, y, threshold float64) float64 {
290 z := 0 + 0i
291 c := complex(x, y)
292 const max = 1000
293 // using the threshold's square to avoid using sqrt
294 ts := threshold * threshold
295
296 for n := 0.0; n < max; n++ {
297 sqmag := real(z)*real(z) + imag(z)*imag(z)
298 if sqmag > ts {
299 return math.Sqrt(sqmag)
300 }
301 z = z*z + c
302 }
303 return cmplx.Abs(z)
304 }
305
306 // itermandel returns the number of iterations used in the mandelbrot
307 // recurrence relation; recurrence is automatically truncated to a default
308 // threshold and/or max number of loops
309 func itermandel(x, y float64) float64 {
310 return itermandelcap(x, y, 50)
311 }
312
313 // itermandelcap returns the number of mandelbrot recurrence iterations like
314 // func itermandel, except the cap/threshold is an explicit parameter
315 func itermandelcap(x, y, threshold float64) float64 {
316 z := 0 + 0i
317 c := complex(x, y)
318 const max = 1000
319 // using the threshold's square to avoid using sqrt
320 ts := threshold * threshold
321
322 for n := 0.0; n < max; n++ {
323 sqmag := real(z)*real(z) + imag(z)*imag(z)
324 if sqmag > ts {
325 return n
326 }
327 z = z*z + c
328 }
329 return max
330 }
331
332 func comb(x, y float64) float64 {
333 return float64(mathplus.Choose(int(x), int(y)))
334 }
335
336 func perm(x, y float64) float64 {
337 return float64(mathplus.Perm(int(x), int(y)))
338 }
339
340 func gcd(x, y float64) float64 {
341 return float64(mathplus.GCD(int64(x), int64(y)))
342 }
343
344 func lcm(x, y float64) float64 {
345 return float64(mathplus.LCM(int64(x), int64(y)))
346 }
347
348 func dbinom(x, n, p float64) float64 {
349 return mathplus.BinomialMass(int(x), int(n), p)
350 }
351
352 func pbinom(x, n, p float64) float64 {
353 return mathplus.CumulativeBinomialDensity(int(x), int(n), p)
354 }
355
356 func isPrime(x float64) float64 {
357 if mathplus.IsPrime(int64(x)) {
358 return 1
359 }
360 return 0
361 }
362
363 const (
364 // etaTrunc is when the summation for the eta funcs stops by default
365 etaTrunc = 50
366
367 // zetaTrunc is when the summation for the zeta funcs stops by default
368 zetaTrunc = 50
369 )
370
371 // etamag call func etamagcap with a default truncation
372 func etamag(x, y float64) float64 {
373 return cmplx.Abs(eta(complex(x, y)))
374 }
375
376 // etamagcap is the real-valued magnitude of the truncated approx. of func eta
377 func etamagcap(x, y float64, max float64) float64 {
378 return cmplx.Abs(etacap(complex(x, y), int(max)))
379 }
380
381 // etare is the real part of the truncated approx. of func eta
382 func etare(x, y float64) float64 { return real(eta(complex(x, y))) }
383
384 // etaimag is the imaginary part of the truncated approx. of func eta
385 func etaimag(x, y float64) float64 { return imag(eta(complex(x, y))) }
386
387 // eta approximates the dirichlet eta function by truncation
388 func eta(x complex128) complex128 { return etacap(x, etaTrunc) }
389
390 // etacap accepts a cap/max iteration value for the eta func truncation
391 func etacap(x complex128, max int) complex128 {
392 y := 0 + 0i
393 v := 1 + 0i
394 sign := 1 + 0i
395
396 for n := 1; n <= max; n++ {
397 y += sign * v
398 sign *= -1
399 v /= x
400 }
401 return y
402 }
403
404 // zetamag call func zetamagcap with a default truncation
405 func zetamag(x, y float64) float64 {
406 return cmplx.Abs(zetacap(complex(x, y), zetaTrunc))
407 }
408
409 // etamagcap is the real-valued magnitude of the truncated approx. of func eta
410 func zetamagcap(x, y float64, max float64) float64 {
411 return cmplx.Abs(etacap(complex(x, y), int(max)))
412 }
413
414 // zetacap accepts a cap/max iteration value for the zeta func truncation
415 func zetacap(x complex128, max int) complex128 {
416 y := 0 + 0i
417 v := 1 + 0i
418
419 for n := 1; n <= max; n++ {
420 y += v
421 v /= x
422 }
423 return y
424 }
File: ./files/files.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package files
26
27 import (
28 "bufio"
29 "io"
30 "io/fs"
31 "os"
32 "path/filepath"
33 "strings"
34 )
35
36 const info = `
37 files [options...] [files/folders...]
38
39 Find/list all files in the folders given, without repetitions.
40
41 All (optional) leading options start with either single or double-dash:
42
43 -h, -help show this help message
44 -t, -top turn off recursive behavior; top-level entries only
45 `
46
47 func Main() {
48 top := false
49 buffered := false
50 args := os.Args[1:]
51
52 for len(args) > 0 {
53 switch args[0] {
54 case `-b`, `--b`, `-buffered`, `--buffered`:
55 buffered = true
56 args = args[1:]
57 continue
58
59 case `-h`, `--h`, `-help`, `--help`:
60 os.Stdout.WriteString(info[1:])
61 return
62
63 case `-t`, `--t`, `-top`, `--top`:
64 top = true
65 args = args[1:]
66 continue
67 }
68
69 break
70 }
71
72 if len(args) > 0 && args[0] == `--` {
73 args = args[1:]
74 }
75
76 liveLines := !buffered
77 if !buffered {
78 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
79 liveLines = false
80 }
81 }
82
83 var cfg config
84 if top {
85 cfg.skipSubfolder = fs.SkipDir
86 }
87 cfg.liveLines = liveLines
88
89 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
90 os.Stderr.WriteString(`files: `)
91 os.Stderr.WriteString(err.Error())
92 os.Stderr.WriteString("\n")
93 os.Exit(1)
94 return
95 }
96 }
97
98 type config struct {
99 skipSubfolder error
100 liveLines bool
101 }
102
103 func run(w io.Writer, paths []string, cfg config) error {
104 bw := bufio.NewWriterSize(w, 32*1024)
105 defer bw.Flush()
106
107 got := make(map[string]struct{})
108
109 if len(paths) == 0 {
110 paths = []string{`.`}
111 }
112
113 // handle is the callback for func filepath.WalkDir
114 handle := func(path string, e fs.DirEntry, err error) error {
115 if err != nil {
116 return err
117 }
118
119 if _, ok := got[path]; ok {
120 return nil
121 }
122 got[path] = struct{}{}
123
124 if e.IsDir() {
125 return cfg.skipSubfolder
126 }
127
128 return handleEntry(bw, path, cfg.liveLines)
129 }
130
131 for _, path := range paths {
132 if _, ok := got[path]; ok {
133 continue
134 }
135 got[path] = struct{}{}
136
137 st, err := os.Stat(path)
138 if err != nil {
139 return err
140 }
141
142 if st.IsDir() {
143 if !strings.HasSuffix(path, `/`) {
144 path = path + `/`
145 }
146 got[path] = struct{}{}
147
148 if err := filepath.WalkDir(path, handle); err != nil {
149 return err
150 }
151 continue
152 }
153
154 if err := handleEntry(bw, path, cfg.liveLines); err != nil {
155 return err
156 }
157 }
158
159 return nil
160 }
161
162 func handleEntry(w *bufio.Writer, path string, live bool) error {
163 abs, err := filepath.Abs(path)
164 if err != nil {
165 return err
166 }
167
168 w.WriteString(abs)
169 w.WriteByte('\n')
170
171 if live && w.Flush() != nil {
172 return io.EOF
173 }
174 return nil
175 }
File: ./filesizes/filesizes.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package filesizes
26
27 import (
28 "bufio"
29 "io"
30 "io/fs"
31 "os"
32 "path/filepath"
33 "sort"
34 "strconv"
35 "strings"
36 )
37
38 const info = `
39 filesizes [options...] [files/folders...]
40
41 Find/list all files in the folders given, without repetitions, also showing
42 their size in bytes. Output is lines of tab-separated values, starting with
43 a header line with the column names.
44
45 All (optional) leading options start with either single or double-dash:
46
47 -h, -help show this help message
48 -s, -sort, -sorted backward-sort entries largest to smallest
49 -t, -top turn off recursive behavior; top-level entries only
50 `
51
52 func Main() {
53 var cfg config
54 cfg.recursive = true
55 cfg.sorted = false
56 cfg.blockSize = 4
57 buffered := false
58 args := os.Args[1:]
59
60 for len(args) > 0 {
61 if size, ok := parseBlockSizeOption(args[0]); ok {
62 cfg.blockSize = size
63 args = args[1:]
64 continue
65 }
66
67 switch args[0] {
68 case `-b`, `--b`, `-buffered`, `--buffered`:
69 buffered = true
70 args = args[1:]
71 continue
72
73 case `-h`, `--h`, `-help`, `--help`:
74 os.Stdout.WriteString(info[1:])
75 return
76
77 case `-s`, `--s`, `-sort`, `--sort`, `-sorted`, `--sorted`:
78 cfg.sorted = true
79 args = args[1:]
80 continue
81
82 case `-t`, `--t`, `-top`, `--top`:
83 cfg.recursive = false
84 args = args[1:]
85 continue
86 }
87
88 break
89 }
90
91 if len(args) > 0 && args[0] == `--` {
92 args = args[1:]
93 }
94
95 cfg.liveLines = !buffered && !cfg.sorted
96 if !buffered && !cfg.sorted {
97 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
98 cfg.liveLines = false
99 }
100 }
101
102 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
103 os.Stderr.WriteString(`filesizes: `)
104 os.Stderr.WriteString(err.Error())
105 os.Stderr.WriteString("\n")
106 os.Exit(1)
107 return
108 }
109 }
110
111 func parseBlockSizeOption(s string) (size int, ok bool) {
112 if !strings.HasPrefix(s, `-`) {
113 return 0, false
114 }
115 if len(s) > 0 && s[0] == '-' {
116 s = s[1:]
117 }
118 if len(s) > 0 && s[0] == '-' {
119 s = s[1:]
120 }
121
122 if !strings.HasSuffix(s, `k`) && !strings.HasSuffix(s, `K`) {
123 return 0, false
124 }
125 s = s[:len(s)-1]
126
127 if n, err := strconv.ParseInt(s, 10, 64); err == nil && n > 0 {
128 return int(n), true
129 }
130 return 0, false
131 }
132
133 type config struct {
134 blockSize int
135 sorted bool
136 recursive bool
137 liveLines bool
138 }
139
140 type entry struct {
141 path string
142 size int64
143 }
144
145 type handlers struct {
146 w *bufio.Writer
147 entries []entry
148 blockSize int
149 skipSubfolder error
150
151 file func(h *handlers, path string, size int64) error
152
153 liveLines bool
154 }
155
156 func (h handlers) countBlocks(size int64) int64 {
157 bs := int64(h.blockSize)
158 n := size / (1024 * bs)
159 if size%(1024*bs) != 0 {
160 n++
161 }
162 return n * bs
163 }
164
165 func run(w io.Writer, paths []string, cfg config) error {
166 bw := bufio.NewWriterSize(w, 32*1024)
167 defer bw.Flush()
168
169 bw.WriteString("name\tbytes\tblocks\n")
170
171 var h handlers
172 h.w = bw
173 h.blockSize = cfg.blockSize
174 h.skipSubfolder = nil
175 if !cfg.recursive {
176 h.skipSubfolder = fs.SkipDir
177 }
178 h.file = emitEntry
179 if cfg.sorted {
180 h.file = keepEntry
181 }
182
183 got := make(map[string]struct{})
184
185 if len(paths) == 0 {
186 paths = []string{`.`}
187 }
188
189 // handle is the callback for func filepath.WalkDir
190 handle := func(path string, e fs.DirEntry, err error) error {
191 if err != nil {
192 return err
193 }
194
195 if _, ok := got[path]; ok {
196 return nil
197 }
198 got[path] = struct{}{}
199
200 if e.IsDir() {
201 return h.skipSubfolder
202 }
203
204 info, err := e.Info()
205 if err != nil {
206 return err
207 }
208
209 return h.file(&h, path, info.Size())
210 }
211
212 for _, path := range paths {
213 if _, ok := got[path]; ok {
214 continue
215 }
216 got[path] = struct{}{}
217
218 st, err := os.Stat(path)
219 if err != nil {
220 return err
221 }
222
223 if st.IsDir() {
224 if !strings.HasSuffix(path, `/`) {
225 path = path + `/`
226 }
227 got[path] = struct{}{}
228
229 if err := filepath.WalkDir(path, handle); err != nil {
230 return err
231 }
232 continue
233 }
234
235 var size int64
236 if path != `/dev/stdin` {
237 size = st.Size()
238 } else {
239 size = countBytes(os.Stdin)
240 }
241
242 if err := h.file(&h, path, size); err != nil {
243 return err
244 }
245 }
246
247 if !cfg.sorted {
248 return nil
249 }
250
251 sort.Slice(h.entries, func(i, j int) bool {
252 return h.entries[i].size > h.entries[j].size
253 })
254
255 for _, e := range h.entries {
256 if err := writeEntry(bw, e, h); err != nil {
257 return err
258 }
259 }
260
261 return nil
262 }
263
264 func countBytes(r io.Reader) (count int64) {
265 var buf [32 * 1024]byte
266
267 for {
268 got, err := r.Read(buf[:])
269 if got > 0 {
270 count += int64(got)
271 }
272 if err != nil {
273 break
274 }
275 }
276
277 return count
278 }
279
280 func emitEntry(h *handlers, path string, size int64) error {
281 return writeEntry(h.w, entry{path, size}, *h)
282 }
283
284 func keepEntry(h *handlers, path string, size int64) error {
285 h.entries = append(h.entries, entry{path, size})
286 return nil
287 }
288
289 func writeEntry(w *bufio.Writer, e entry, h handlers) error {
290 abs, err := filepath.Abs(e.path)
291 if err != nil {
292 return err
293 }
294
295 var buf [24]byte
296 w.WriteString(abs)
297 w.WriteByte('\t')
298 w.Write(strconv.AppendInt(buf[:0], e.size, 10))
299 w.WriteByte('\t')
300 w.Write(strconv.AppendInt(buf[:0], h.countBlocks(e.size), 10))
301 w.WriteByte('\n')
302
303 if h.liveLines && w.Flush() != nil {
304 return io.EOF
305 }
306 return nil
307 }
File: ./filetypes/filetypes.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package filetypes
26
27 import "bytes"
28
29 // nameToMIME tries to match a MIME type to a filename, dotted file extension,
30 // or a dot-less filetype/extension given
31 func nameToMIME(fname string) (mimeType string, ok bool) {
32 // handle dotless file types and filenames alike
33 kind, ok := type2mime[makeDotless(fname)]
34 return kind, ok
35 }
36
37 // DetectMIME guesses the first appropriate MIME type from the first few
38 // data bytes given: 24 bytes are enough to detect all supported types
39 func DetectMIME(b []byte) (mimeType string, ok bool) {
40 if t, ok := detectType(b); ok {
41 return t, true
42 }
43 return ``, false
44 }
45
46 // detectType guesses the first appropriate file type for the data given:
47 // here the type is a a filename extension without the leading dot
48 func detectType(b []byte) (dotlessExt string, ok bool) {
49 // empty data, so there's no way to detect anything
50 if len(b) == 0 {
51 return ``, false
52 }
53
54 // check for plain-text web-document formats case-insensitively
55 kind, ok := checkDoc(b)
56 if ok {
57 return kind, true
58 }
59
60 // check data formats which allow any byte at the start
61 kind, ok = checkSpecial(b)
62 if ok {
63 return kind, true
64 }
65
66 // check all other supported data formats
67 headers := hdrDispatch[b[0]]
68 for _, t := range headers {
69 if hasPrefixPattern(b[1:], t.Header[1:], cba) {
70 return t.Type, true
71 }
72 }
73
74 // unrecognized data format
75 return ``, false
76 }
77
78 // checkDoc tries to guess if the bytes given are the start of HTML, SVG,
79 // XML, or JSON data
80 func checkDoc(b []byte) (kind string, ok bool) {
81 // ignore leading whitespaces
82 b = trimLeadingWhitespace(b)
83
84 // can't detect anything with empty data
85 if len(b) == 0 {
86 return ``, false
87 }
88
89 // handle XHTML documents which don't start with a doctype declaration
90 if bytes.Contains(b, doctypeHTML) {
91 return html, true
92 }
93
94 // handle HTML/SVG/XML documents
95 if hasPrefixByte(b, '<') {
96 if hasPrefixFold(b, []byte{'<', '?', 'x', 'm', 'l'}) {
97 if bytes.Contains(b, []byte{'<', 's', 'v', 'g'}) {
98 return svg, true
99 }
100 return xml, true
101 }
102
103 headers := hdrDispatch['<']
104 for _, v := range headers {
105 if hasPrefixFold(b, v.Header) {
106 return v.Type, true
107 }
108 }
109 return ``, false
110 }
111
112 // handle JSON with top-level arrays
113 if hasPrefixByte(b, '[') {
114 // match [", or [[, or [{, ignoring spaces between
115 b = trimLeadingWhitespace(b[1:])
116 if len(b) > 0 {
117 switch b[0] {
118 case '"', '[', '{':
119 return json, true
120 }
121 }
122 return ``, false
123 }
124
125 // handle JSON with top-level objects
126 if hasPrefixByte(b, '{') {
127 // match {", ignoring spaces between: after {, the only valid syntax
128 // which can follow is the opening quote for the expected object-key
129 b = trimLeadingWhitespace(b[1:])
130 if hasPrefixByte(b, '"') {
131 return json, true
132 }
133 return ``, false
134 }
135
136 // checking for a quoted string, any of the JSON keywords, or even a
137 // number seems too ambiguous to declare the data valid JSON
138
139 // no web-document format detected
140 return ``, false
141 }
142
143 // checkSpecial handles special file-format headers, which should be checked
144 // before the normal file-type headers, since the first-byte dispatch algo
145 // doesn't work for these
146 func checkSpecial(b []byte) (kind string, ok bool) {
147 if len(b) >= 8 && bytes.Index(b, []byte{'f', 't', 'y', 'p'}) == 4 {
148 for _, t := range specialHeaders {
149 if hasPrefixPattern(b[4:], t.Header[4:], cba) {
150 return t.Type, true
151 }
152 }
153 }
154 return ``, false
155 }
156
157 // hasPrefixPattern works like bytes.HasPrefix, except it allows for a special
158 // value to signal any byte is allowed on specific spots
159 func hasPrefixPattern(what []byte, pat []byte, wildcard byte) bool {
160 // if the data are shorter than the pattern to match, there's no match
161 if len(what) < len(pat) {
162 return false
163 }
164
165 // use a slice which ensures the pattern length is never exceeded
166 what = what[:len(pat)]
167
168 for i, x := range what {
169 y := pat[i]
170 if x != y && y != wildcard {
171 return false
172 }
173 }
174 return true
175 }
File: ./filetypes/filetypes_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package filetypes
26
27 import (
28 "bytes"
29 "testing"
30 )
31
32 func TestCheckDoc(t *testing.T) {
33 const (
34 lf = "\n"
35 crlf = "\r\n"
36 tab = "\t"
37 xmlIntro = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`
38 )
39
40 tests := []struct {
41 Input string
42 Expected string
43 }{
44 {``, ``},
45 {`{"abc":123}`, json},
46 {`[` + lf + ` {"abc":123}`, json},
47 {`[` + lf + ` {"abc":123}`, json},
48 {`[` + crlf + tab + `{"abc":123}`, json},
49
50 {``, ``},
51 {`<?xml?>`, xml},
52 {`<?xml?><records>`, xml},
53 {`<?xml?>` + lf + `<records>`, xml},
54 {`<?xml?><svg>`, svg},
55 {`<?xml?>` + crlf + `<svg>`, svg},
56 {xmlIntro + lf + `<svg`, svg},
57 {xmlIntro + crlf + `<svg`, svg},
58 }
59
60 for _, tc := range tests {
61 t.Run(tc.Input, func(t *testing.T) {
62 res, _ := checkDoc([]byte(tc.Input))
63 if res != tc.Expected {
64 t.Fatalf(`got %v, expected %v instead`, res, tc.Expected)
65 }
66 })
67 }
68 }
69
70 func TestHasPrefixPattern(t *testing.T) {
71 var (
72 data = []byte{
73 'R', 'I', 'F', 'F', 0xf0, 0xba, 0xc8, 0x2b, 'A', 'V', 'I', ' ',
74 }
75 pat = []byte{
76 'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' ',
77 }
78 )
79
80 if !hasPrefixPattern(data, pat, cba) {
81 t.Fatal(`wildcard pattern not working`)
82 }
83 }
84
85 func BenchmarkHasPrefixMatch(b *testing.B) {
86 var (
87 data = []byte{
88 'R', 'I', 'F', 'F', 0xf0, 0xba, 0xc8, 0x2b, 'A', 'V', 'I', ' ',
89 }
90 pat = []byte{
91 'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' ',
92 }
93 )
94
95 b.ReportAllocs()
96 b.ResetTimer()
97
98 for i := 0; i < b.N; i++ {
99 if !bytes.HasPrefix(data, pat) {
100 b.Fatal(`pattern was specifically chosen to match, but didn't`)
101 }
102 }
103 }
104
105 func BenchmarkHasPrefixPatternMatch(b *testing.B) {
106 var (
107 data = []byte{
108 'R', 'I', 'F', 'F', 0xf0, 0xba, 0xc8, 0x2b, 'A', 'V', 'I', ' ',
109 }
110 pat = []byte{
111 'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' ',
112 }
113 )
114
115 b.ReportAllocs()
116 b.ResetTimer()
117
118 for i := 0; i < b.N; i++ {
119 if !hasPrefixPattern(data, pat, cba) {
120 b.Fatal(`pattern was specifically chosen to match, but didn't`)
121 }
122 }
123 }
File: ./filetypes/mimedata.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package filetypes
26
27 // all the MIME types used/recognized in this package
28 const (
29 aiff = `audio/aiff`
30 au = `audio/basic`
31 avi = `video/avi`
32 avif = `image/avif`
33 bmp = `image/x-bmp`
34 caf = `audio/x-caf`
35 cur = `image/vnd.microsoft.icon`
36 css = `text/css`
37 csv = `text/csv`
38 djvu = `image/x-djvu`
39 elf = `application/x-elf`
40 exe = `application/vnd.microsoft.portable-executable`
41 flac = `audio/x-flac`
42 gif = `image/gif`
43 gz = `application/gzip`
44 heic = `image/heic`
45 htm = `text/html`
46 html = `text/html`
47 ico = `image/x-icon`
48 iso = `application/octet-stream`
49 jpg = `image/jpeg`
50 jpeg = `image/jpeg`
51 js = `application/javascript`
52 json = `application/json`
53 m4a = `audio/aac`
54 m4v = `video/x-m4v`
55 mid = `audio/midi`
56 mov = `video/quicktime`
57 mp4 = `video/mp4`
58 mp3 = `audio/mpeg`
59 mpg = `video/mpeg`
60 ogg = `audio/ogg`
61 opus = `audio/opus`
62 pdf = `application/pdf`
63 png = `image/png`
64 ps = `application/postscript`
65 psd = `image/vnd.adobe.photoshop`
66 rtf = `application/rtf`
67 sqlite3 = `application/x-sqlite3`
68 svg = `image/svg+xml`
69 text = `text/plain`
70 tiff = `image/tiff`
71 tsv = `text/tsv`
72 wasm = `application/wasm`
73 wav = `audio/x-wav`
74 webp = `image/webp`
75 webm = `video/webm`
76 xml = `application/xml`
77 zip = `application/zip`
78 zst = `application/zstd`
79 )
80
81 // type2mime turns dotless format-names into MIME types
82 var type2mime = map[string]string{
83 `aiff`: aiff,
84 `wav`: wav,
85 `avi`: avi,
86 `jpg`: jpg,
87 `jpeg`: jpeg,
88 `m4a`: m4a,
89 `mp4`: mp4,
90 `m4v`: m4v,
91 `mov`: mov,
92 `png`: png,
93 `avif`: avif,
94 `webp`: webp,
95 `gif`: gif,
96 `tiff`: tiff,
97 `psd`: psd,
98 `flac`: flac,
99 `webm`: webm,
100 `mpg`: mpg,
101 `zip`: zip,
102 `gz`: gz,
103 `zst`: zst,
104 `mp3`: mp3,
105 `opus`: opus,
106 `bmp`: bmp,
107 `mid`: mid,
108 `ogg`: ogg,
109 `html`: html,
110 `htm`: htm,
111 `svg`: svg,
112 `xml`: xml,
113 `rtf`: rtf,
114 `pdf`: pdf,
115 `ps`: ps,
116 `au`: au,
117 `ico`: ico,
118 `cur`: cur,
119 `caf`: caf,
120 `heic`: heic,
121 `sqlite3`: sqlite3,
122 `elf`: elf,
123 `exe`: exe,
124 `wasm`: wasm,
125 `iso`: iso,
126 `txt`: text,
127 `css`: css,
128 `csv`: csv,
129 `tsv`: tsv,
130 `js`: js,
131 `json`: json,
132 `geojson`: json,
133 }
134
135 // formatDescriptor ties a file-header pattern to its data-format type
136 type formatDescriptor struct {
137 Header []byte
138 Type string
139 }
140
141 // can be anything: ensure this value differs from all other literal bytes
142 // in the generic-headers table: failing that, its value could cause subtle
143 // type-misdetection bugs
144 const cba = 0xFD // 253, which is > 127, the highest-valued ascii symbol
145
146 // dash-streamed m4a format
147 var m4aDash = []byte{
148 cba, cba, cba, cba, 'f', 't', 'y', 'p', 'd', 'a', 's', 'h',
149 000, 000, 000, 000, 'i', 's', 'o', '6', 'm', 'p', '4', '1',
150 }
151
152 // format markers with leading wildcards, which should be checked before the
153 // normal ones: this is to prevent mismatches with the latter types, even
154 // though you can make probabilistic arguments which suggest these mismatches
155 // should be very unlikely in practice
156 var specialHeaders = []formatDescriptor{
157 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', ' '}, m4a},
158 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', 000}, m4a},
159 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', 'S', 'N', 'V'}, mp4},
160 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'}, mp4},
161 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'm', 'p', '4', '2'}, m4v},
162 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'q', 't', ' ', ' '}, mov},
163 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'h', 'e', 'i', 'c'}, heic},
164 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'a', 'v', 'i', 'f'}, avif},
165 {m4aDash, m4a},
166 }
167
168 // sqlite3 database format
169 var sqlite3db = []byte{
170 'S', 'Q', 'L', 'i', 't', 'e', ' ',
171 'f', 'o', 'r', 'm', 'a', 't', ' ', '3',
172 000,
173 }
174
175 // windows-variant bitmap file-header, which is followed by a byte-counter for
176 // the 40-byte infoheader which follows that
177 var winbmp = []byte{
178 'B', 'M', cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, 40,
179 }
180
181 // deja-vu document format
182 var djv = []byte{
183 'A', 'T', '&', 'T', 'F', 'O', 'R', 'M', cba, cba, cba, cba, 'D', 'J', 'V',
184 }
185
186 var doctypeHTML = []byte{
187 '<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E', ' ', 'h', 't', 'm', 'l',
188 }
189
190 // hdrDispatch groups format-description-groups by their first byte, thus
191 // shortening total lookups for some data header: notice how the `ftyp` data
192 // formats aren't handled here, since these can start with any byte, instead
193 // of the literal value of the any-byte markers they use
194 var hdrDispatch = [256][]formatDescriptor{
195 {
196 {[]byte{000, 000, 001, 0xBA}, mpg},
197 {[]byte{000, 000, 001, 0xB3}, mpg},
198 {[]byte{000, 000, 001, 000}, ico},
199 {[]byte{000, 000, 002, 000}, cur},
200 {[]byte{000, 'a', 's', 'm'}, wasm},
201 }, // 0
202 nil, // 1
203 nil, // 2
204 nil, // 3
205 nil, // 4
206 nil, // 5
207 nil, // 6
208 nil, // 7
209 nil, // 8
210 nil, // 9
211 nil, // 10
212 nil, // 11
213 nil, // 12
214 nil, // 13
215 nil, // 14
216 nil, // 15
217 nil, // 16
218 nil, // 17
219 nil, // 18
220 nil, // 19
221 nil, // 20
222 nil, // 21
223 nil, // 22
224 nil, // 23
225 nil, // 24
226 nil, // 25
227 {
228 {[]byte{0x1A, 0x45, 0xDF, 0xA3}, webm},
229 }, // 26
230 nil, // 27
231 nil, // 28
232 nil, // 29
233 nil, // 30
234 {
235 // {[]byte{0x1F, 0x8B, 0x08, 0x08}, gz},
236 {[]byte{0x1F, 0x8B, 0x08}, gz},
237 }, // 31
238 nil, // 32
239 nil, // 33 !
240 nil, // 34 "
241 {
242 {[]byte{'#', '!', ' '}, text},
243 {[]byte{'#', '!', '/'}, text},
244 }, // 35 #
245 nil, // 36 $
246 {
247 {[]byte{'%', 'P', 'D', 'F'}, pdf},
248 {[]byte{'%', '!', 'P', 'S'}, ps},
249 }, // 37 %
250 nil, // 38 &
251 nil, // 39 '
252 {
253 {[]byte{0x28, 0xB5, 0x2F, 0xFD}, zst},
254 }, // 40 (
255 nil, // 41 )
256 nil, // 42 *
257 nil, // 43 +
258 nil, // 44 ,
259 nil, // 45 -
260 {
261 {[]byte{'.', 's', 'n', 'd'}, au},
262 }, // 46 .
263 nil, // 47 /
264 nil, // 48 0
265 nil, // 49 1
266 nil, // 50 2
267 nil, // 51 3
268 nil, // 52 4
269 nil, // 53 5
270 nil, // 54 6
271 nil, // 55 7
272 {
273 {[]byte{'8', 'B', 'P', 'S'}, psd},
274 }, // 56 8
275 nil, // 57 9
276 nil, // 58 :
277 nil, // 59 ;
278 {
279 // func checkDoc is better for these, since it's case-insensitive
280 {doctypeHTML, html},
281 {[]byte{'<', 's', 'v', 'g'}, svg},
282 {[]byte{'<', 'h', 't', 'm', 'l', '>'}, html},
283 {[]byte{'<', 'h', 'e', 'a', 'd', '>'}, html},
284 {[]byte{'<', 'b', 'o', 'd', 'y', '>'}, html},
285 {[]byte{'<', '?', 'x', 'm', 'l'}, xml},
286 }, // 60 <
287 nil, // 61 =
288 nil, // 62 >
289 nil, // 63 ?
290 nil, // 64 @
291 {
292 {djv, djvu},
293 }, // 65 A
294 {
295 {winbmp, bmp},
296 }, // 66 B
297 nil, // 67 C
298 nil, // 68 D
299 nil, // 69 E
300 {
301 {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'F'}, aiff},
302 {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'C'}, aiff},
303 }, // 70 F
304 {
305 {[]byte{'G', 'I', 'F', '8', '7', 'a'}, gif},
306 {[]byte{'G', 'I', 'F', '8', '9', 'a'}, gif},
307 }, // 71 G
308 nil, // 72 H
309 {
310 {[]byte{'I', 'D', '3', 2}, mp3}, // ID3-format metadata
311 {[]byte{'I', 'D', '3', 3}, mp3}, // ID3-format metadata
312 {[]byte{'I', 'D', '3', 4}, mp3}, // ID3-format metadata
313 {[]byte{'I', 'I', '*', 000}, tiff},
314 }, // 73 I
315 nil, // 74 J
316 nil, // 75 K
317 nil, // 76 L
318 {
319 {[]byte{'M', 'M', 000, '*'}, tiff},
320 {[]byte{'M', 'T', 'h', 'd'}, mid},
321 {[]byte{'M', 'Z', cba, 000, cba, 000}, exe},
322 // {[]byte{'M', 'Z', 0x90, 000, 003, 000}, exe},
323 // {[]byte{'M', 'Z', 0x78, 000, 001, 000}, exe},
324 // {[]byte{'M', 'Z', 'P', 000, 002, 000}, exe},
325 }, // 77 M
326 nil, // 78 N
327 {
328 {[]byte{'O', 'g', 'g', 'S'}, ogg},
329 }, // 79 O
330 {
331 {[]byte{'P', 'K', 003, 004}, zip},
332 }, // 80 P
333 nil, // 81 Q
334 {
335 {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'E', 'B', 'P'}, webp},
336 {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'A', 'V', 'E'}, wav},
337 {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' '}, avi},
338 }, // 82 R
339 {
340 {sqlite3db, sqlite3},
341 }, // 83 S
342 nil, // 84 T
343 nil, // 85 U
344 nil, // 86 V
345 nil, // 87 W
346 nil, // 88 X
347 nil, // 89 Y
348 nil, // 90 Z
349 nil, // 91 [
350 nil, // 92 \
351 nil, // 93 ]
352 nil, // 94 ^
353 nil, // 95 _
354 nil, // 96 `
355 nil, // 97 a
356 nil, // 98 b
357 {
358 {[]byte{'c', 'a', 'f', 'f', 000, 001, 000, 000}, caf},
359 }, // 99 c
360 nil, // 100 d
361 nil, // 101 e
362 {
363 {[]byte{'f', 'L', 'a', 'C'}, flac},
364 }, // 102 f
365 nil, // 103 g
366 nil, // 104 h
367 nil, // 105 i
368 nil, // 106 j
369 nil, // 107 k
370 nil, // 108 l
371 nil, // 109 m
372 nil, // 110 n
373 nil, // 111 o
374 nil, // 112 p
375 nil, // 113 q
376 nil, // 114 r
377 nil, // 115 s
378 nil, // 116 t
379 nil, // 117 u
380 nil, // 118 v
381 nil, // 119 w
382 nil, // 120 x
383 nil, // 121 y
384 nil, // 122 z
385 {
386 {[]byte{'{', '\\', 'r', 't', 'f'}, rtf},
387 }, // 123 {
388 nil, // 124 |
389 nil, // 125 }
390 nil, // 126
391 {
392 {[]byte{127, 'E', 'L', 'F'}, elf},
393 }, // 127
394 nil, // 128
395 nil, // 129
396 nil, // 130
397 nil, // 131
398 nil, // 132
399 nil, // 133
400 nil, // 134
401 nil, // 135
402 nil, // 136
403 {
404 {[]byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, png},
405 }, // 137
406 nil, // 138
407 nil, // 139
408 nil, // 140
409 nil, // 141
410 nil, // 142
411 nil, // 143
412 nil, // 144
413 nil, // 145
414 nil, // 146
415 nil, // 147
416 nil, // 148
417 nil, // 149
418 nil, // 150
419 nil, // 151
420 nil, // 152
421 nil, // 153
422 nil, // 154
423 nil, // 155
424 nil, // 156
425 nil, // 157
426 nil, // 158
427 nil, // 159
428 nil, // 160
429 nil, // 161
430 nil, // 162
431 nil, // 163
432 nil, // 164
433 nil, // 165
434 nil, // 166
435 nil, // 167
436 nil, // 168
437 nil, // 169
438 nil, // 170
439 nil, // 171
440 nil, // 172
441 nil, // 173
442 nil, // 174
443 nil, // 175
444 nil, // 176
445 nil, // 177
446 nil, // 178
447 nil, // 179
448 nil, // 180
449 nil, // 181
450 nil, // 182
451 nil, // 183
452 nil, // 184
453 nil, // 185
454 nil, // 186
455 nil, // 187
456 nil, // 188
457 nil, // 189
458 nil, // 190
459 nil, // 191
460 nil, // 192
461 nil, // 193
462 nil, // 194
463 nil, // 195
464 nil, // 196
465 nil, // 197
466 nil, // 198
467 nil, // 199
468 nil, // 200
469 nil, // 201
470 nil, // 202
471 nil, // 203
472 nil, // 204
473 nil, // 205
474 nil, // 206
475 nil, // 207
476 nil, // 208
477 nil, // 209
478 nil, // 210
479 nil, // 211
480 nil, // 212
481 nil, // 213
482 nil, // 214
483 nil, // 215
484 nil, // 216
485 nil, // 217
486 nil, // 218
487 nil, // 219
488 nil, // 220
489 nil, // 221
490 nil, // 222
491 nil, // 223
492 nil, // 224
493 nil, // 225
494 nil, // 226
495 nil, // 227
496 nil, // 228
497 nil, // 229
498 nil, // 230
499 nil, // 231
500 nil, // 232
501 nil, // 233
502 nil, // 234
503 nil, // 235
504 nil, // 236
505 nil, // 237
506 nil, // 238
507 nil, // 239
508 nil, // 240
509 nil, // 241
510 nil, // 242
511 nil, // 243
512 nil, // 244
513 nil, // 245
514 nil, // 246
515 nil, // 247
516 nil, // 248
517 nil, // 249
518 nil, // 250
519 nil, // 251
520 nil, // 252
521 nil, // 253
522 nil, // 254
523 {
524 {[]byte{0xFF, 0xD8, 0xFF}, jpg},
525 {[]byte{0xFF, 0xF3, 0x48, 0xC4, 0x00}, mp3},
526 {[]byte{0xFF, 0xFB}, mp3},
527 }, // 255
528 }
File: ./filetypes/mimedata_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package filetypes
26
27 import (
28 "strconv"
29 "testing"
30 )
31
32 func TestData(t *testing.T) {
33 t.Run(`could-be-anything constant`, func(t *testing.T) {
34 if len(hdrDispatch[cba]) != 0 {
35 const fs = `chosen constant %d collides with header entries`
36 t.Fatalf(fs, cba)
37 }
38 })
39
40 for i, v := range hdrDispatch {
41 t.Run(`dispatch @ `+strconv.Itoa(i), func(t *testing.T) {
42 const fs = `expected leading byte to be %d, but got %d instead`
43 for _, e := range v {
44 if e.Header[0] != byte(i) {
45 t.Fatalf(fs, i, e.Header[0])
46 return
47 }
48 }
49 })
50 }
51 }
File: ./filetypes/strings.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package filetypes
26
27 import (
28 "bytes"
29 "strings"
30 )
31
32 // makeDotless is similar to filepath.Ext, except its results never start
33 // with a dot
34 func makeDotless(s string) string {
35 if i := strings.LastIndexByte(s, '.'); i >= 0 {
36 return s[(i + 1):]
37 }
38 return s
39 }
40
41 // hasPrefixByte is a simpler, single-byte version of bytes.HasPrefix
42 func hasPrefixByte(b []byte, prefix byte) bool {
43 return len(b) > 0 && b[0] == prefix
44 }
45
46 // hasPrefixFold is a case-insensitive bytes.HasPrefix
47 func hasPrefixFold(s []byte, prefix []byte) bool {
48 n := len(prefix)
49 return len(s) >= n && bytes.EqualFold(s[:n], prefix)
50 }
51
52 // trimLeadingWhitespace ignores leading space-like symbols: this is useful
53 // to handle text-based data formats more flexibly
54 func trimLeadingWhitespace(b []byte) []byte {
55 for len(b) > 0 {
56 switch b[0] {
57 case ' ', '\t', '\n', '\r':
58 b = b[1:]
59 default:
60 return b
61 }
62 }
63
64 // an empty slice is all that's left, at this point
65 return nil
66 }
File: ./filetypes/strings_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package filetypes
26
27 import (
28 "bytes"
29 "testing"
30 )
31
32 func TestHasPrefixByte(t *testing.T) {
33 tests := []struct {
34 Data []byte
35 Prefix byte
36 Expected bool
37 }{
38 {nil, 'x', false},
39 {[]byte(`x`), 'x', true},
40 {[]byte(` x`), 'x', false},
41 {[]byte(`xyz`), 'a', false},
42 {[]byte(`abcxyz`), 'a', true},
43 }
44
45 for _, tc := range tests {
46 t.Run(string(tc.Data), func(t *testing.T) {
47 got := hasPrefixByte(tc.Data, tc.Prefix)
48 if got != tc.Expected {
49 const fs = `expected %v, but got %v instead`
50 t.Fatalf(fs, tc.Expected, got)
51 }
52 })
53 }
54 }
55
56 func TestHasPrefixFold(t *testing.T) {
57 tests := []struct {
58 Data []byte
59 Prefix []byte
60 Expected bool
61 }{
62 {[]byte("<!docTYPE html>\n<html>"), []byte(`<!doctype HTML`), true},
63 }
64
65 for _, tc := range tests {
66 t.Run("", func(t *testing.T) {
67 got := hasPrefixFold(tc.Data, tc.Prefix)
68 if got != tc.Expected {
69 const fs = `expected %v, but got %v instead`
70 t.Fatalf(fs, tc.Expected, got)
71 }
72 })
73 }
74 }
75
76 func TestTrimLeadingWhitespaces(t *testing.T) {
77 tests := []struct {
78 Data []byte
79 Expected []byte
80 }{
81 {[]byte(`abc`), []byte(`abc`)},
82 {[]byte(" \t"), nil},
83 {[]byte(" \tabc"), []byte(`abc`)},
84 {[]byte("\r\nabc"), []byte(`abc`)},
85 }
86
87 for _, tc := range tests {
88 t.Run("", func(t *testing.T) {
89 got := trimLeadingWhitespace(tc.Data)
90 if !bytes.Equal(got, tc.Expected) {
91 const fs = `expected %#v, but got %#v instead`
92 t.Fatalf(fs, tc.Expected, got)
93 }
94 })
95 }
96 }
File: ./finfo/config.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package finfo
26
27 import (
28 "flag"
29 "fmt"
30 "strings"
31 )
32
33 type config struct {
34 To string // output format: any of TSV, JSON, or HTML
35 SortBy string // how to sort results
36 Title string // title to use when emitting HTML
37
38 Bytes bool // show file sizes in bytes
39 KiB bool // show file sizes in kib
40 MiB bool // show file sizes in mib
41 GiB bool // show file sizes in gib
42
43 Lines bool // calculate and show lines (treating all files as plain-text)
44 Text bool // calculate and show plain-text-related info (treating all files as such)
45 Duration bool // calculate and show playing duration (for supported media files)
46 HMS bool // also show playing duration in hour-minute-seconds format
47 Picture bool // find and show picture widths and heights (for supported files)
48 Type bool // show file types (its extension without the starting dot)
49 MIMEType bool // find and show MIME file types
50 Ext bool // show the filename extension
51 Folder bool // show the folders files are in
52 }
53
54 const (
55 picResUsage = "show width, height, and bits per pixel for supported picture files"
56 linesUsage = "show the number of lines by treating files as plain-text ones"
57 textUsage = "show plain-text-related info, treating all files as plain-text"
58 durationUsage = "show the playing duration for supported media files"
59 hmsUsage = "also show the playing duration in hour-minute-seconds format"
60 typeUsage = "show file types using dotless filename extensions"
61 )
62
63 func parseFlags(usage string) config {
64 flag.Usage = func() {
65 fmt.Fprintf(flag.CommandLine.Output(), "%s\n\nOptions\n\n", usage)
66 flag.PrintDefaults()
67 }
68
69 cfg := config{
70 To: "tsv",
71 SortBy: "bytes",
72 Title: "File Info",
73
74 Type: true,
75 Bytes: true,
76 }
77
78 flag.StringVar(&cfg.To, "to", cfg.To, "output format: one of tsv, json, or html")
79 flag.StringVar(&cfg.SortBy, "sort", cfg.SortBy, "what to (reverse-)sort results by")
80 flag.StringVar(&cfg.Title, "title", cfg.Title, "title to use when emitting HTML")
81 flag.BoolVar(&cfg.Bytes, "bytes", cfg.Bytes, "show file sizes in bytes")
82 flag.BoolVar(&cfg.KiB, "kib", cfg.KiB, "show file sizes in KiBs")
83 flag.BoolVar(&cfg.MiB, "mib", cfg.MiB, "show file sizes in MiBs")
84 flag.BoolVar(&cfg.GiB, "gib", cfg.GiB, "show file sizes in GiBs")
85 flag.BoolVar(&cfg.Lines, "l", cfg.Lines, "alias for option -lines")
86 flag.BoolVar(&cfg.Duration, "d", cfg.Duration, "alias for option -duration")
87 flag.BoolVar(&cfg.Picture, "res", cfg.Picture, "alias for option -resolution")
88 flag.BoolVar(&cfg.Picture, "resolution", cfg.Picture, picResUsage)
89 flag.BoolVar(&cfg.Lines, "lines", cfg.Lines, linesUsage)
90 flag.BoolVar(&cfg.Text, "text", cfg.Text, textUsage)
91 flag.BoolVar(&cfg.Duration, "duration", cfg.Duration, durationUsage)
92 flag.BoolVar(&cfg.HMS, "hms", cfg.HMS, hmsUsage)
93 flag.BoolVar(&cfg.Type, "type", cfg.Type, typeUsage)
94 flag.BoolVar(&cfg.MIMEType, "mime", cfg.MIMEType, "show the file's MIME type")
95 flag.BoolVar(&cfg.Folder, "folder", cfg.Folder, "show folder names")
96 flag.Parse()
97
98 // normalize values for option `-to`
99 cfg.To = strings.ToLower(strings.TrimPrefix(cfg.To, "."))
100
101 // normalize value aliases for option -sort and auto-enable any settings these imply
102 switch strings.ToLower(cfg.SortBy) {
103 case "", "byte", "bytes", "size", "kb", "mb", "b", "s":
104 cfg.SortBy = "bytes"
105 case "line", "lines", "ln", "l":
106 cfg.SortBy = "lines"
107 // auto-enable the line-counter if sorting by lines
108 cfg.Lines = true
109 case "duration", "time", "dur", "d":
110 cfg.SortBy = "duration"
111 // auto-enable the time-duration-counter if sorting by duration
112 cfg.Duration = true
113 case "column", "columns", "c":
114 cfg.SortBy = "columns"
115 // auto-enable text-specific info
116 cfg.Text = true
117 case "cr":
118 cfg.SortBy = "cr"
119 // auto-enable text-specific info
120 cfg.Text = true
121 case "bom":
122 cfg.SortBy = "bom"
123 // auto-enable text-specific info
124 cfg.Text = true
125 case "name", "n":
126 cfg.SortBy = "name"
127 }
128
129 // auto-enable duration when asked to also show it in HH:MM:SS foramt
130 if cfg.HMS {
131 cfg.Duration = true
132 }
133 return cfg
134 }
135
136 func (c config) NeedsExtraInfo() bool {
137 return c.Lines || c.Text || c.Duration || c.Picture || c.MIMEType
138 }
File: ./finfo/fileinfo.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package finfo
26
27 import (
28 "io"
29 "os"
30 "path/filepath"
31 "strings"
32
33 "../filetypes"
34 "../mediainfo"
35 )
36
37 // FileInfo has all sorts of summary stats about files, including its auto-detected
38 // type and multimedia properties when these are available.
39 type FileInfo struct {
40 // general info
41 Name string `json:"name"`
42 Folder string `json:"folder"`
43 Size int `json:"size"`
44
45 // plain-text stats
46 Lines int `json:"lines"`
47 Columns int `json:"columns"`
48 Separator rune `json:"separator"`
49 CarriageReturns int `json:"cr"`
50 ByteOrderMarks int `json:"bom"`
51
52 // sound/image stats
53 Duration float64 `json:"duration"`
54 Width int `json:"width"`
55 Height int `json:"height"`
56 BitsPerPixel int `json:"bpp"`
57
58 MIMEType string `json:"mime"`
59 Problem error `json:"-"`
60 }
61
62 func newFileInfo(fname string, size int) FileInfo {
63 return FileInfo{
64 Name: fname,
65 Folder: filepath.Dir(fname),
66 Size: size,
67
68 Lines: -1,
69 Columns: -1,
70 CarriageReturns: -1,
71 ByteOrderMarks: -1,
72
73 Duration: -1,
74 Width: -1,
75 Height: -1,
76 BitsPerPixel: -1,
77
78 MIMEType: "",
79 Problem: nil,
80 }
81 }
82
83 func (fi *FileInfo) hasLines() bool {
84 switch fi.MIMEType {
85 case "", "application/xml", "application/json":
86 return true
87 }
88 return strings.HasPrefix(fi.MIMEType, "text/") || strings.HasPrefix(fi.MIMEType, "image/svg")
89 }
90
91 func (r *FileInfo) calculateStats(cfg config) {
92 // empty files have no lines and last no time: no need to open a file in this case
93 if r.Size == 0 {
94 return
95 }
96
97 f, err := os.Open(r.Name)
98 if err != nil {
99 r.Problem = err
100 return
101 }
102 defer f.Close()
103
104 if cfg.Duration {
105 d, err := mediainfo.FileDuration(f)
106 if err != nil {
107 r.Problem = err
108 } else {
109 r.Duration = d
110 }
111
112 // later readers must start from the beginning of the file
113 f.Seek(0, io.SeekStart)
114 }
115
116 if cfg.Picture {
117 w, h, bd, err := mediainfo.Resolution(f, r.Name)
118 if err == nil {
119 r.Width = w
120 r.Height = h
121 r.BitsPerPixel = bd
122 } else {
123 r.Problem = err
124 }
125
126 // later readers must start from the beginning of the file
127 f.Seek(0, io.SeekStart)
128 }
129
130 if cfg.MIMEType || cfg.Text {
131 var b [128]byte
132 n, err := f.Read(b[:])
133 mime, ok := filetypes.DetectMIME(b[:n])
134 if !ok || (err != nil && err != io.EOF) {
135 mime = ""
136 }
137 r.MIMEType = mime
138
139 // later readers must start from the beginning of the file
140 f.Seek(0, io.SeekStart)
141 }
142
143 // don't count lines for most mime types
144 if (cfg.Lines || cfg.Text) && r.hasLines() {
145 st, err := summarizePlainText(f)
146 // csv files need special handling to count their # of columns and autodetect their separator
147 if strings.HasSuffix(r.Name, ".csv") || strings.HasSuffix(r.Name, ".CSV") {
148 f.Seek(0, io.SeekStart)
149 adjustForCSV(f, &st)
150 }
151
152 if err == nil {
153 r.Lines = st.Lines
154 // non-empty text files without newlines count as having 1 line
155 if r.Lines == 0 && r.Size > 0 {
156 r.Lines = 1
157 }
158 // non-empty text files with a detected field-separator count as having at least 1 column
159 r.Columns = st.Columns
160 if r.Columns == 0 && r.Size > 0 && st.EmptyLines < r.Lines {
161 r.Columns = 1
162 }
163 r.Separator = st.Separator
164 r.CarriageReturns = st.CarriageReturns
165 r.ByteOrderMarks = st.ByteOrderMarks
166 } else {
167 r.Problem = err
168 }
169
170 // xml and json data have no separators nor columns to count
171 if strings.HasPrefix(r.MIMEType, "application/") {
172 r.Columns = -1
173 r.Separator = rune(0)
174 }
175 }
176 }
File: ./finfo/grouping.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package finfo
26
27 import (
28 "path/filepath"
29 "strings"
30 )
31
32 type FolderInfo struct {
33 Name string `json:"name"`
34 Path string `json:"path"`
35 Folders []FolderInfo `json:"folders"`
36 Files []FileInfo `json:"files"`
37
38 NumItems int `json:"items"`
39 Size int `json:"size"`
40 Lines int `json:"lines"`
41 CarriageReturns int `json:"cr"`
42 ByteOrderMarks int `json:"bom"`
43 Duration float64 `json:"duration"`
44 }
45
46 func (fi *FolderInfo) Update() {
47 fi.NumItems = len(fi.Files)
48 fi.Size = 0
49 fi.Lines = 0
50 fi.CarriageReturns = 0
51 fi.ByteOrderMarks = 0
52 fi.Duration = 0
53
54 for i := range fi.Folders {
55 fi.Folders[i].Update()
56 }
57
58 for _, v := range fi.Folders {
59 fi.NumItems += v.NumItems
60 fi.Size += v.Size
61 fi.Lines += v.Lines
62 fi.CarriageReturns += v.CarriageReturns
63 fi.ByteOrderMarks += v.ByteOrderMarks
64 fi.Duration += v.Duration
65 }
66
67 for _, v := range fi.Files {
68 fi.Size += v.Size
69 fi.Lines += v.Lines
70 fi.CarriageReturns += v.CarriageReturns
71 fi.ByteOrderMarks += v.ByteOrderMarks
72 fi.Duration += v.Duration
73 }
74
75 if fi.Size < 0 {
76 fi.Size = 0
77 }
78 if fi.Lines < 0 {
79 fi.Lines = 0
80 }
81 if fi.CarriageReturns < 0 {
82 fi.CarriageReturns = 0
83 }
84 if fi.ByteOrderMarks < 0 {
85 fi.ByteOrderMarks = 0
86 }
87 if fi.Duration < 0 {
88 fi.Duration = 0
89 }
90 }
91
92 func (fi *FolderInfo) FindFolder(path string) *FolderInfo {
93 parent := fi
94 parts := strings.Split(path, string(filepath.Separator))
95 for _, s := range parts {
96 parent = parent.findFolder(path, s)
97 }
98 return parent
99 }
100
101 func (fi *FolderInfo) findFolder(path, s string) *FolderInfo {
102 for i, v := range fi.Folders {
103 if v.Name == s {
104 return &fi.Folders[i]
105 }
106 }
107
108 fi.Folders = append(fi.Folders, FolderInfo{Name: s, Path: path})
109 return &fi.Folders[len(fi.Folders)-1]
110 }
111
112 func (fi *FolderInfo) Inspect(f func(fi *FolderInfo)) {
113 f(fi)
114 for i := range fi.Folders {
115 f(&fi.Folders[i])
116 }
117 }
118
119 func group(files []FileInfo) FolderInfo {
120 var res FolderInfo
121 cache := make(map[string]*FolderInfo)
122
123 for _, v := range files {
124 parent, _ := filepath.Split(v.Name)
125 parent = strings.TrimSuffix(parent, "\\")
126 parent = strings.TrimSuffix(parent, "/")
127
128 ptr, ok := cache[parent]
129 if !ok {
130 ptr = res.FindFolder(parent)
131 cache[parent] = ptr
132 }
133 ptr.Files = append(ptr.Files, v)
134 }
135
136 res.Update()
137 return res
138 }
File: ./finfo/info.txt
1 finfo [options...] [files/folders...]
2
3 Show various file info, mainly filesizes in decreasing order (biggest to
4 smallest): all folders given are explored recursively to find all files in
5 them.
6
7 When exploring files in the current folder, use a dot as the folder name;
8 when not given any file/folder names, it reads those from standard input one
9 name per line, so file paths with spaces don't cause any problem.
10
11 Besides file size and name, it can show other info
12
13 - line counts, except for recognized media files
14 - column counts, in the context of delimiter-separated tabular text data
15 - carriage-return and byte-order-mark counts, except for known media types
16 - width, height, and bits per pixel for pictures and video files
17 - duration/play-length in seconds for all common audio/video files
18 - path of containing folder
19 - extension
20 - MIME type
21
22 Results can also be reverse-sorted by line count, by duration, or any other
23 numeric option/column: there's no way to increase-sort for any of the numeric
24 ranking options.
File: ./finfo/json.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package finfo
26
27 import (
28 "fmt"
29 "io"
30 "strings"
31 )
32
33 func folders2JSON(w io.Writer, x []FolderInfo) error {
34 fmt.Fprint(w, "[")
35 for i, v := range x {
36 if i > 0 {
37 fmt.Fprint(w, ", ")
38 }
39 if err := folder2JSON(w, v); err != nil {
40 return err
41 }
42 }
43 _, err := fmt.Fprint(w, "]")
44 return err
45 }
46
47 func folder2JSON(w io.Writer, fi FolderInfo) error {
48 // return json.NewEncoder(w).Encode(fi)
49
50 fmt.Fprint(w, "{")
51 writeStringJSON(w, "name", unixPath(fi.Name))
52 writeStringJSON(w, "path", unixPath(fi.Path))
53 fmt.Fprintf(w, `"folders": [`)
54 for i, v := range fi.Folders {
55 if i > 0 {
56 fmt.Fprint(w, ", ")
57 }
58 if err := folder2JSON(w, v); err != nil {
59 return err
60 }
61 }
62 fmt.Fprint(w, "], ")
63 fmt.Fprintf(w, `"files": [`)
64 for i, v := range fi.Files {
65 if i > 0 {
66 fmt.Fprint(w, ", ")
67 }
68 if err := file2JSON(w, v); err != nil {
69 return err
70 }
71 }
72 fmt.Fprint(w, "]")
73 writeIntJSON(w, "items", fi.NumItems)
74 writeIntJSON(w, "size", fi.Size)
75 writeIntJSON(w, "lines", fi.Lines)
76 writeIntJSON(w, "cr", fi.CarriageReturns)
77 writeIntJSON(w, "bom", fi.ByteOrderMarks)
78 if fi.Duration >= 0 {
79 fmt.Fprintf(w, `, "duration": %.2f`, fi.Duration)
80 }
81 _, err := fmt.Fprint(w, "}")
82 return err
83 }
84
85 func file2JSON(w io.Writer, fi FileInfo) error {
86 // return json.NewEncoder(w).Encode(fi)
87 fmt.Fprint(w, "{")
88 writeStringJSON(w, "name", unixPath(fi.Name))
89 writeStringJSON(w, "folder", unixPath(fi.Folder))
90 writeStringJSON(w, "mime", fi.MIMEType)
91 fmt.Fprintf(w, `"size": %d`, fi.Size)
92 writeIntJSON(w, "lines", fi.Lines)
93 writeIntJSON(w, "columns", fi.Columns)
94 // fi.Separator
95 writeIntJSON(w, "cr", fi.CarriageReturns)
96 writeIntJSON(w, "bom", fi.ByteOrderMarks)
97 if fi.Duration >= 0 {
98 fmt.Fprintf(w, `, "duration": %.2f`, fi.Duration)
99 }
100 writeIntJSON(w, "width", fi.Width)
101 writeIntJSON(w, "height", fi.Height)
102 writeIntJSON(w, "bpp", fi.BitsPerPixel)
103 _, err := fmt.Fprint(w, "}")
104 return err
105 }
106
107 func writeStringJSON(w io.Writer, k, s string) {
108 s = strings.TrimSpace(s)
109 if strings.Contains(s, `"`) {
110 s = strings.ReplaceAll(s, `"`, `\"`)
111 }
112 if strings.Contains(s, `\`) {
113 s = strings.ReplaceAll(s, `\`, `\\`)
114 }
115 fmt.Fprintf(w, `"%s": "%s", `, k, s)
116 }
117
118 func writeIntJSON(w io.Writer, k string, n int) {
119 if n >= 0 {
120 fmt.Fprintf(w, `, "%s": %d`, k, n)
121 }
122 }
123
124 func unixPath(s string) string {
125 if strings.Contains(s, `\`) {
126 return strings.ReplaceAll(s, `\`, `/`)
127 }
128 return s
129 }
File: ./finfo/main.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package finfo
26
27 import (
28 "bufio"
29 "flag"
30 "fmt"
31 "os"
32 "sort"
33
34 _ "embed"
35 )
36
37 //go:embed info.txt
38 var usage string
39
40 // //go:embed style.css
41 // var css string
42
43 const maxbufsize = 8 * 1024 * 1024 * 1024
44
45 func Main() {
46 cfg := parseFlags(usage)
47 if err := run(cfg); err != nil {
48 fmt.Fprintln(os.Stderr, err.Error())
49 os.Exit(1)
50 return
51 }
52 }
53
54 func run(cfg config) error {
55 w := bufio.NewWriterSize(os.Stdout, 32*1024)
56 defer w.Flush()
57
58 switch cfg.To {
59 case "tsv":
60 td := newTableDisplay(cfg)
61 // show header immediately to reassure user in case scanning/sorting is taking a while
62 td.FprintlnHeader(os.Stdout)
63
64 data := scan(flag.Args(), cfg)
65 sortItems(data, cfg.SortBy)
66 for _, e := range data {
67 if err := td.Fprintln(w, e); err != nil {
68 return nil // probably a pipe was closed
69 }
70 }
71 return nil
72
73 case "json":
74 data := scan(flag.Args(), cfg)
75 grouped := group(data)
76 grouped.Inspect(func(fi *FolderInfo) {
77 sortItems(fi.Files, cfg.SortBy)
78
79 // avoid null values anywhere
80 if fi.Folders == nil {
81 fi.Folders = []FolderInfo{}
82 }
83 if fi.Files == nil {
84 fi.Files = []FileInfo{}
85 }
86 })
87
88 folders2JSON(w, grouped.Folders)
89 w.Write([]byte("\n"))
90 return nil
91
92 default:
93 return fmt.Errorf("unknown output-type %q", cfg.To)
94 }
95 }
96
97 func scan(args []string, cfg config) []FileInfo {
98 // get all file/folder names to check, then check them all: if no arguments
99 // were given, use stdin to read them line by line
100 if len(args) == 0 {
101 sc := bufio.NewScanner(os.Stdin)
102 sc.Buffer(nil, maxbufsize)
103 for sc.Scan() {
104 args = append(args, sc.Text())
105 }
106 }
107
108 // get file sizes, count lines, count media duration, etc.
109 agg := newAggregator(cfg)
110 agg.Scan(args)
111 data := agg.Results()
112 return data
113 }
114
115 func sortItems(data []FileInfo, by string) {
116 // show the list sorted by decreasing size, text lines, time duration, # of columns,
117 // # of carriage-returns, # of byte-order marks, or forward-sorted by filename
118 switch by {
119 case "lines":
120 sort.Sort(sortableLineCountInfo(data))
121 case "duration":
122 sort.Sort(sortableDurationInfo(data))
123 case "columns":
124 sort.Sort(sortableColumnCountInfo(data))
125 case "cr":
126 sort.Sort(sortableCarriageReturnInfo(data))
127 case "bom":
128 sort.Sort(sortableByteOrderMarkInfo(data))
129 case "name":
130 sort.Sort(sortableNameInfo(data))
131 default:
132 sort.Sort(sortableSizeInfo(data))
133 }
134 }
File: ./finfo/plaintext.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package finfo
26
27 import (
28 "bufio"
29 "bytes"
30 "encoding/csv"
31 "io"
32 "strings"
33 )
34
35 // 0xefbbbf
36 var bom = []byte{0xef, 0xbb, 0xbf}
37
38 type plainTextStats struct {
39 Lines int
40 Columns int
41 CarriageReturns int
42 ByteOrderMarks int
43 EmptyLines int
44 AlphanumASCII int
45 Separator rune
46 }
47
48 func summarizePlainText(f io.Reader) (plainTextStats, error) {
49 st := plainTextStats{}
50 sc := bufio.NewScanner(f)
51 sc.Buffer(nil, maxbufsize)
52 sc.Split(splitUnixLines)
53
54 nonempty := 0
55 for ; sc.Scan(); st.Lines++ {
56 err := sc.Err()
57 if err != nil {
58 return st, err
59 }
60
61 if st.Lines == 0 && bytes.HasPrefix(sc.Bytes(), bom) {
62 st.ByteOrderMarks = 1
63 }
64
65 line := sc.Text()
66 // ignore empty lines
67 if line == "" {
68 st.EmptyLines++
69 continue
70 }
71 nonempty++
72
73 // first line: count tabs, pipes, and colons to update # columns
74 if nonempty == 1 {
75 summarizePlainTextHeader(&st, line)
76 continue
77 }
78
79 for _, r := range line {
80 if r == '\r' {
81 st.CarriageReturns++
82 } else if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || ('0' <= r && r <= '9') {
83 st.AlphanumASCII++
84 }
85 }
86 }
87
88 return st, nil
89 }
90
91 // used only in function summarizePlainText
92 func summarizePlainTextHeader(st *plainTextStats, line string) {
93 tabs := 0
94 pipes := 0
95 colons := 0
96 for _, r := range line {
97 switch r {
98 case '\t':
99 tabs++
100 case '|':
101 pipes++
102 case ':':
103 colons++
104 case '\r':
105 st.CarriageReturns++
106 }
107 }
108
109 // # of columns is # of tabs + 1
110 if tabs > 0 && tabs+1 > st.Columns {
111 st.Columns = tabs + 1
112 st.Separator = '\t'
113 return
114 }
115
116 if pipes > 0 && pipes+1 > st.Columns {
117 st.Columns = pipes + 1
118 st.Separator = '|'
119 return
120 }
121
122 // 2 as the minimum avoids file patterns (text or not) where there are no colon separators:
123 // in practice they're only used in unix-like config files where there are many fields anyway
124
125 // note: still use 1 as the minimum count for now
126 if colons > 0 && colons+1 > st.Columns {
127 st.Columns = colons + 1
128 st.Separator = ':'
129 }
130
131 if st.Columns == 0 && st.Separator != 0 {
132 st.Columns = 1
133 }
134 }
135
136 // used only in function summarizePlainText
137 func splitUnixLines(b []byte, eof bool) (int, []byte, error) {
138 if eof && len(b) == 0 {
139 return 0, nil, nil
140 }
141 i := bytes.IndexByte(b, '\n')
142 if i >= 0 {
143 return i + 1, b[0:i], nil
144 }
145 // last line
146 if eof {
147 return len(b), b, nil
148 }
149 return 0, nil, nil
150 }
151
152 func adjustForCSV(f io.Reader, st *plainTextStats) {
153 // get the first nonempty line
154 line := ""
155 sc := bufio.NewScanner(f)
156 sc.Buffer(nil, maxbufsize)
157 for sc.Scan() {
158 if sc.Err() != nil {
159 break
160 }
161 line = sc.Text()
162 if line != "" {
163 break
164 }
165 }
166
167 w := 0
168 if st.Separator != rune(0) {
169 w = csvCountHeader(strings.NewReader(line), st.Separator)
170 }
171 wc := csvCountHeader(strings.NewReader(line), ',')
172 ws := csvCountHeader(strings.NewReader(line), ';')
173 if w > wc && w > ws {
174 st.Columns = w
175 } else if wc > w && wc > ws {
176 st.Columns = wc
177 st.Separator = ','
178 } else if ws > w && ws > wc {
179 st.Columns = ws
180 st.Separator = ';'
181 }
182 }
183
184 func csvCountHeader(f io.Reader, sep rune) int {
185 sc := csv.NewReader(f)
186 sc.LazyQuotes = true
187 sc.ReuseRecord = true
188 if sep == rune(0) {
189 sep = ','
190 }
191 sc.Comma = sep
192 for {
193 row, err := sc.Read()
194 if err != nil {
195 return 0
196 }
197 // just use the first non-empty line to estimate the # of columns
198 if len(row) > 0 {
199 return len(row)
200 }
201 }
202 }
File: ./finfo/sorting.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package finfo
26
27 import (
28 "strings"
29 )
30
31 // allow reverse-sorting by size in bytes
32 type sortableSizeInfo []FileInfo
33
34 func (r sortableSizeInfo) Len() int { return len(r) }
35 func (r sortableSizeInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
36 func (r sortableSizeInfo) Less(i, j int) bool {
37 if diff := r[i].Size - r[j].Size; diff != 0 {
38 return diff > 0
39 }
40 return strings.Compare(r[i].Name, r[j].Name) < 0
41 }
42
43 // allow reverse-sorting by lines of text counted
44 type sortableLineCountInfo []FileInfo
45
46 func (r sortableLineCountInfo) Len() int { return len(r) }
47 func (r sortableLineCountInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
48 func (r sortableLineCountInfo) Less(i, j int) bool {
49 if diff := r[i].Lines - r[j].Lines; diff != 0 {
50 return diff > 0
51 }
52 return strings.Compare(r[i].Name, r[j].Name) < 0
53 }
54
55 // allow reverse-sorting by time duration
56 type sortableDurationInfo []FileInfo
57
58 func (r sortableDurationInfo) Len() int { return len(r) }
59 func (r sortableDurationInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
60 func (r sortableDurationInfo) Less(i, j int) bool {
61 v1 := r[i].Duration >= 0 //!math.IsNaN(r[i].Duration)
62 v2 := r[j].Duration >= 0 //!math.IsNaN(r[j].Duration)
63 if v1 && v2 {
64 if diff := r[i].Duration - r[j].Duration; diff != 0 {
65 return diff > 0
66 }
67 return strings.Compare(r[i].Name, r[j].Name) < 0
68 }
69
70 // treat nan < valid
71 if !v1 && v2 {
72 return false
73 }
74 if v1 && !v2 {
75 return true
76 }
77
78 // if neither has a time duration, sort by size
79 if diff := r[i].Size - r[j].Size; diff != 0 {
80 return diff > 0
81 }
82 return strings.Compare(r[i].Name, r[j].Name) < 0
83 }
84
85 // allow reverse-sorting by data columns counted
86 type sortableColumnCountInfo []FileInfo
87
88 func (r sortableColumnCountInfo) Len() int { return len(r) }
89 func (r sortableColumnCountInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
90 func (r sortableColumnCountInfo) Less(i, j int) bool {
91 if diff := r[i].Columns - r[j].Columns; diff != 0 {
92 return diff > 0
93 }
94 return strings.Compare(r[i].Name, r[j].Name) < 0
95 }
96
97 // allow reverse-sorting by carriage-returns counted
98 type sortableCarriageReturnInfo []FileInfo
99
100 func (r sortableCarriageReturnInfo) Len() int { return len(r) }
101 func (r sortableCarriageReturnInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
102 func (r sortableCarriageReturnInfo) Less(i, j int) bool {
103 if diff := r[i].CarriageReturns - r[j].CarriageReturns; diff != 0 {
104 return diff > 0
105 }
106 return strings.Compare(r[i].Name, r[j].Name) < 0
107 }
108
109 // allow reverse-sorting by byte-order marks counted
110 type sortableByteOrderMarkInfo []FileInfo
111
112 func (r sortableByteOrderMarkInfo) Len() int { return len(r) }
113 func (r sortableByteOrderMarkInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
114 func (r sortableByteOrderMarkInfo) Less(i, j int) bool {
115 if diff := r[i].ByteOrderMarks - r[j].ByteOrderMarks; diff != 0 {
116 return diff > 0
117 }
118 return strings.Compare(r[i].Name, r[j].Name) < 0
119 }
120
121 // allow forward-sorting by filename
122 type sortableNameInfo []FileInfo
123
124 func (r sortableNameInfo) Len() int { return len(r) }
125 func (r sortableNameInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
126 func (r sortableNameInfo) Less(i, j int) bool {
127 return strings.Compare(r[i].Name, r[j].Name) < 0
128 }
File: ./finfo/tables.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package finfo
26
27 import (
28 "fmt"
29 "io"
30 "math"
31 "path/filepath"
32 "strings"
33 )
34
35 // tableDisplay handles the final display of all the info gathered
36 type tableDisplay struct {
37 Headers []string
38 Formats []string
39 Conditions []bool
40
41 values []any // to minimize memory allocations
42 }
43
44 func newTableDisplay(c config) tableDisplay {
45 return tableDisplay{
46 Headers: []string{
47 "bytes", "KiB", "MiB", "GiB", // file size
48 "duration", "hh:mm:ss", "width", "height", "bpp", // sound/picture info
49 "lines", "columns", "cells", "CR", "BOM", "sep", // plain-text-related
50 "name", "folder", "ext", "MIME", // name and type
51 },
52 Formats: []string{
53 "%d", "%.2f", "%.2f", "%.2f", // file size
54 "%.2f", "%v", "%d", "%d", "%d", // sound/picture info
55 "%d", "%d", "%d", "%d", "%d", "%s", // plain-text-related
56 "%s", "%s", "%s", "%s", // name and type
57 },
58 Conditions: []bool{
59 c.Bytes, c.KiB, c.MiB, c.GiB, // file size
60 c.Duration, c.HMS, c.Picture, c.Picture, c.Picture, // sound/picture info
61 c.Lines || c.Text, c.Text, c.Text, c.Text, c.Text, c.Text, // plain-text-related
62 true, c.Folder, c.Type, c.MIMEType, // name and type
63 },
64 values: make([]any, 0, 20),
65 }
66 }
67
68 func (c tableDisplay) FprintlnHeader(w io.Writer) {
69 first := true
70 for i, cond := range c.Conditions {
71 if !cond {
72 continue
73 }
74 if !first {
75 fmt.Fprint(w, "\t")
76 }
77 first = false
78 fmt.Fprint(w, c.Headers[i])
79 }
80 fmt.Fprintln(w)
81 }
82
83 func (c tableDisplay) Fprintln(w io.Writer, e FileInfo) error {
84 kib := float64(e.Size) / 1024
85 mib := kib / 1024
86 gib := mib / 1024
87 name := strings.ReplaceAll(e.Name, "\\", "/") // show unix-style folder separators
88 ext := strings.TrimLeft(filepath.Ext(e.Name), ".") // file extension with no leading dot
89 ext = strings.ToLower(ext) // handle uppercase file extensions
90 folder := strings.ReplaceAll(e.Folder, "\\", "/")
91 sep := ""
92 switch e.Separator {
93 case '\t':
94 sep = "tab"
95 case rune(0):
96 sep = ""
97 case ',':
98 sep = "comma"
99 case ';':
100 sep = "semicolon"
101 case '|':
102 sep = "pipe"
103 case ':':
104 sep = "colon"
105 default:
106 sep = string(e.Separator)
107 }
108
109 hms := ""
110 if c.Conditions[5] {
111 hms = s2hms(e.Duration)
112 }
113 ncells := e.Lines * e.Columns
114 if e.Lines < 2 || e.Columns < 2 {
115 ncells = -1
116 }
117
118 c.values = c.values[:0]
119 c.values = append(c.values,
120 e.Size, kib, mib, gib, // file size
121 e.Duration, hms, e.Width, e.Height, e.BitsPerPixel, // sound/picture info
122 e.Lines, e.Columns, ncells, e.CarriageReturns, e.ByteOrderMarks, sep, // plain-text-related
123 name, folder, ext, e.MIMEType, // name and type
124 )
125
126 first := true
127 for i, cond := range c.Conditions {
128 if !cond {
129 continue
130 }
131 if !first {
132 fmt.Fprint(w, "\t")
133 }
134 first = false
135
136 v := c.values[i]
137 // avoid emitting nan values as "NaN"
138 f, ok := v.(float64)
139 if ok && (math.IsNaN(f) || f < 0) {
140 continue
141 }
142 // negative integer counters result from errors
143 n, ok := v.(int)
144 if ok && n < 0 {
145 continue
146 }
147 fmt.Fprintf(w, c.Formats[i], v)
148 }
149
150 _, err := fmt.Fprintln(w)
151 return err
152 }
153
154 func s2hms(t float64) string {
155 if t < 0 || math.IsNaN(t) {
156 return ""
157 }
158 h := math.Floor(t / 3600)
159 m := math.Floor(math.Mod(t, 3600) / 60)
160 s := math.Mod(t, 60)
161 return fmt.Sprintf("%02d:%02d:%05.2f", int(h), int(m), s)
162 }
File: ./finfo/tasks.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package finfo
26
27 import (
28 "fmt"
29 "io/fs"
30 "os"
31 "path/filepath"
32 "runtime"
33 "sync"
34 )
35
36 // a parallel results-collector
37 type aggregator struct {
38 cfg config
39 seen map[string]struct{} // avoid duplicate results for files
40 res []FileInfo
41 }
42
43 func newAggregator(cfg config) aggregator {
44 return aggregator{
45 cfg: cfg,
46 seen: make(map[string]struct{}),
47 res: make([]FileInfo, 0),
48 }
49 }
50
51 func (a *aggregator) Results() []FileInfo {
52 return a.res
53 }
54
55 func (a *aggregator) Scan(filenames []string) {
56 for _, fname := range filenames {
57 info, err := os.Stat(fname)
58 if err != nil {
59 fmt.Fprintln(os.Stderr, err.Error())
60 continue
61 }
62
63 if info.IsDir() {
64 err = a.handleFolder(fname)
65 if err != nil {
66 fmt.Fprintln(os.Stderr, err.Error())
67 continue
68 }
69 continue
70 }
71 a.addFileEntry(fname, info)
72 }
73
74 // no need to open files when only name and file size are going to be shown
75 if !a.cfg.NeedsExtraInfo() {
76 return
77 }
78
79 var wg sync.WaitGroup
80 wg.Add(len(a.res))
81
82 // calculate stats/results asynchronously when told to; use a channel to limit how many
83 // file-stats calculations are running at the same time: the limit is the number of cores
84 exitTickets := make(chan struct{}, runtime.NumCPU())
85 defer close(exitTickets)
86 for i := range a.res {
87 exitTickets <- struct{}{}
88 go func(i int) {
89 defer func() {
90 <-exitTickets
91 wg.Done()
92 }()
93 a.res[i].calculateStats(a.cfg)
94 }(i)
95 }
96
97 // ensure all jobs are finished before returning
98 wg.Wait()
99 }
100
101 func (a *aggregator) addFileEntry(fname string, info os.FileInfo) {
102 // avoid going over the same places more than once
103 _, ok := a.seen[fname]
104 if ok {
105 return
106 }
107 a.seen[fname] = struct{}{}
108 a.res = append(a.res, newFileInfo(fname, int(info.Size())))
109 }
110
111 func (a *aggregator) handleFolder(fpath string) error {
112 return filepath.WalkDir(fpath, func(path string, d fs.DirEntry, err error) error {
113 // nothing to do when there's either an error or it's a folder
114 if err != nil {
115 return err
116 }
117 if d.IsDir() {
118 return nil
119 }
120
121 info, err := d.Info()
122 if err != nil {
123 return err
124 }
125 a.addFileEntry(path, info)
126 return nil
127 })
128 }
File: ./first/first.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package first
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 "strconv"
34 "strings"
35 )
36
37 const info = `
38 first [options...] [max lines...] [files...]
39
40 Keep only up to the first n lines from the input. If not given an explicit
41 number, the default is to only keep the first line.
42
43 All (optional) leading options start with either single or double-dash:
44
45 -h, -help show this help message
46 `
47
48 func Main() {
49 buffered := false
50 args := os.Args[1:]
51
52 for len(args) > 0 {
53 switch args[0] {
54 case `-b`, `--b`, `-buffered`, `--buffered`:
55 buffered = true
56 args = args[1:]
57 continue
58
59 case `-h`, `--h`, `-help`, `--help`:
60 os.Stdout.WriteString(info[1:])
61 return
62 }
63
64 break
65 }
66
67 linesLeft := 1
68 if len(args) > 0 {
69 s := strings.Replace(args[0], `_`, ``, -1)
70 n, err := strconv.ParseInt(s, 10, 64)
71 if err == nil {
72 linesLeft = int(n)
73 args = args[1:]
74 }
75 }
76
77 if len(args) > 0 && args[0] == `--` {
78 args = args[1:]
79 }
80
81 if linesLeft < 1 {
82 return
83 }
84
85 liveLines := !buffered
86 if !buffered {
87 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
88 liveLines = false
89 }
90 }
91
92 err := run(os.Stdout, args, linesLeft, liveLines)
93 if err != nil && err != io.EOF {
94 os.Stderr.WriteString(`first: `)
95 os.Stderr.WriteString(err.Error())
96 os.Stderr.WriteString("\n")
97 os.Exit(1)
98 return
99 }
100 }
101
102 func run(w io.Writer, paths []string, linesLeft int, liveLines bool) error {
103 bw := bufio.NewWriterSize(w, 32*1024)
104 defer bw.Flush()
105
106 for _, path := range paths {
107 if err := handleFile(bw, path, &linesLeft, liveLines); err != nil {
108 return err
109 }
110 }
111
112 if len(paths) == 0 {
113 return first(bw, os.Stdin, &linesLeft, liveLines)
114 }
115 return nil
116 }
117
118 func handleFile(w *bufio.Writer, name string, left *int, live bool) error {
119 if name == `` || name == `-` {
120 return first(w, os.Stdin, left, live)
121 }
122
123 f, err := os.Open(name)
124 if err != nil {
125 return errors.New(`can't read from file named "` + name + `"`)
126 }
127 defer f.Close()
128
129 return first(w, f, left, live)
130 }
131
132 func first(w *bufio.Writer, r io.Reader, left *int, live bool) error {
133 if *left < 1 {
134 return io.EOF
135 }
136
137 const gb = 1024 * 1024 * 1024
138 sc := bufio.NewScanner(r)
139 sc.Buffer(nil, 8*gb)
140
141 for i := 0; sc.Scan(); i++ {
142 s := sc.Bytes()
143 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
144 s = s[3:]
145 }
146
147 w.Write(s)
148 if w.WriteByte('\n') != nil {
149 return io.EOF
150 }
151
152 if live && w.Flush() != nil {
153 return io.EOF
154 }
155
156 *left--
157 if *left < 1 {
158 break
159 }
160 }
161
162 return sc.Err()
163 }
File: ./fixlines/fixlines.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fixlines
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 )
34
35 const info = `
36 fixlines [options...] [filepaths...]
37
38
39 This tool fixes lines in UTF-8 text, ignoring leading UTF-8 BOMs, trailing
40 carriage-returns on all lines, and ensures no lines across inputs are
41 accidentally joined, since all lines it outputs end with line-feeds,
42 even when the original files don't.
43
44 The options are, available both in single and double-dash versions
45
46 -h, -help show this help message
47
48 -detrail, -trimtrail, -trim-trail, -trimtrails, -trim-trails
49 ignore trailing spaces on lines
50
51 -squeeze aggressively trim lines, ignoring both leading and
52 trailing spaces, spaces around tabs, and turn runs
53 of multiple spaces into single ones
54 `
55
56 type config struct {
57 fix func(w *bufio.Writer, r io.Reader, live bool) error
58 liveLines bool
59 }
60
61 func Main() {
62 buffered := false
63 args := os.Args[1:]
64 var cfg config
65 cfg.fix = detrail
66
67 if len(args) > 0 {
68 switch args[0] {
69 case `-b`, `--b`, `-buffered`, `--buffered`:
70 buffered = true
71 args = args[1:]
72
73 case
74 `-detrail`, `--detrail`, `-trimtrail`, `--trimtrail`,
75 `-trim-trail`, `--trim-trail`, `-trimtrails`, `--trimtrails`,
76 `-trim-trails`, `--trim-trails`:
77 cfg.fix = detrail
78 args = args[1:]
79
80 case `-h`, `--h`, `-help`, `--help`:
81 os.Stdout.WriteString(info[1:])
82 return
83
84 case
85 `-keeptrail`, `--keeptrail`, `-keept-rail`, `--keep-trail`,
86 `-keeptrails`, `--keeptrails`, `-keept-rails`, `--keep-trails`:
87 cfg.fix = catl
88 args = args[1:]
89
90 case `-s`, `--s`, `-squeeze`, `--squeeze`:
91 buffered = true
92 args = args[1:]
93 }
94 }
95
96 if len(args) > 0 && args[0] == `--` {
97 args = args[1:]
98 }
99
100 cfg.liveLines = !buffered
101 if !buffered {
102 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
103 cfg.liveLines = false
104 }
105 }
106
107 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
108 os.Stderr.WriteString(`fixlines: `)
109 os.Stderr.WriteString(err.Error())
110 os.Stderr.WriteString("\n")
111 os.Exit(1)
112 return
113 }
114 }
115
116 func run(w io.Writer, args []string, cfg config) error {
117 dashes := 0
118 for _, name := range args {
119 if name == `-` {
120 dashes++
121 }
122 if dashes > 1 {
123 return errors.New(`can't use stdin (dash) more than once`)
124 }
125 }
126
127 bw := bufio.NewWriterSize(w, 32*1024)
128 defer bw.Flush()
129
130 if len(args) == 0 {
131 return cfg.fix(bw, os.Stdin, cfg.liveLines)
132 }
133
134 for _, name := range args {
135 if err := handleFile(bw, name, cfg); err != nil {
136 return err
137 }
138 }
139 return nil
140 }
141
142 func handleFile(w *bufio.Writer, name string, cfg config) error {
143 if name == `` || name == `-` {
144 return cfg.fix(w, os.Stdin, cfg.liveLines)
145 }
146
147 f, err := os.Open(name)
148 if err != nil {
149 return errors.New(`can't read from file named "` + name + `"`)
150 }
151 defer f.Close()
152
153 return cfg.fix(w, f, cfg.liveLines)
154 }
155
156 func catl(w *bufio.Writer, r io.Reader, live bool) error {
157 const gb = 1024 * 1024 * 1024
158 sc := bufio.NewScanner(r)
159 sc.Buffer(nil, 8*gb)
160
161 for i := 0; sc.Scan(); i++ {
162 s := sc.Bytes()
163 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
164 s = s[3:]
165 }
166
167 w.Write(s)
168 if w.WriteByte('\n') != nil {
169 return io.EOF
170 }
171
172 if live && w.Flush() != nil {
173 return io.EOF
174 }
175 }
176
177 return sc.Err()
178 }
179
180 func detrail(w *bufio.Writer, r io.Reader, live bool) error {
181 const gb = 1024 * 1024 * 1024
182 sc := bufio.NewScanner(r)
183 sc.Buffer(nil, 8*gb)
184
185 for i := 0; sc.Scan(); i++ {
186 s := sc.Bytes()
187
188 // ignore leading UTF-8 BOM on the first line
189 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
190 s = s[3:]
191 }
192
193 // trim trailing spaces on the current line
194 for len(s) > 0 && s[len(s)-1] == ' ' {
195 s = s[:len(s)-1]
196 }
197
198 w.Write(s)
199 if w.WriteByte('\n') != nil {
200 return io.EOF
201 }
202
203 if live && w.Flush() != nil {
204 return io.EOF
205 }
206 }
207
208 return sc.Err()
209 }
210
211 func squeeze(w *bufio.Writer, r io.Reader, live bool) error {
212 const gb = 1024 * 1024 * 1024
213 sc := bufio.NewScanner(r)
214 sc.Buffer(nil, 8*gb)
215
216 for i := 0; sc.Scan(); i++ {
217 s := sc.Bytes()
218 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
219 s = s[3:]
220 }
221
222 writeSqueezed(w, s)
223 if w.WriteByte('\n') != nil {
224 return io.EOF
225 }
226
227 if live && w.Flush() != nil {
228 return io.EOF
229 }
230 }
231
232 return sc.Err()
233 }
234
235 func writeSqueezed(w *bufio.Writer, s []byte) {
236 // ignore leading spaces
237 for len(s) > 0 && s[0] == ' ' {
238 s = s[1:]
239 }
240
241 // ignore trailing spaces
242 for len(s) > 0 && s[len(s)-1] == ' ' {
243 s = s[:len(s)-1]
244 }
245
246 space := false
247
248 for len(s) > 0 {
249 switch s[0] {
250 case ' ':
251 s = s[1:]
252 space = true
253
254 case '\t':
255 s = s[1:]
256 space = false
257 for len(s) > 0 && s[0] == ' ' {
258 s = s[1:]
259 }
260 w.WriteByte('\t')
261
262 default:
263 if space {
264 w.WriteByte(' ')
265 space = false
266 }
267 w.WriteByte(s[0])
268 s = s[1:]
269 }
270 }
271 }
File: ./fmscripts/benchmarks_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fmscripts
26
27 import (
28 "math"
29 "testing"
30 )
31
32 const (
33 interferingRipples = "" +
34 "exp(-0.5 * sin(2 * hypot(x - 2, y + 1))) + " +
35 "exp(-0.5 * sin(10 * hypot(x + 2, y - 3.4)))"
36
37 smilingGhost = "" +
38 "log1p(((x - 1)**2 + y*y - 4)*((x + 1)**2 + y*y - 4)*" +
39 "(x*x + (y - sqrt(3))**2 - 4) - 5)"
40 )
41
42 var benchmarks = []struct {
43 Name string
44 Script string
45 Native func(float64, float64) float64
46 }{
47 {"Load", "x", func(x, y float64) float64 { return x }},
48 {"Constant", "4.25", func(x, y float64) float64 { return 4.25 }},
49 {"Constant Addition", "1+2", func(x, y float64) float64 { return 1 + 2 }},
50 {"0 Additions", "x", func(x, y float64) float64 { return x }},
51 {"1 Additions", "x+x", func(x, y float64) float64 { return x + x }},
52 {"2 Additions", "x+x+x", func(x, y float64) float64 { return x + x + x }},
53 {"0 Multiplications", "x", func(x, y float64) float64 { return x }},
54 {"1 Multiplications", "x*x", func(x, y float64) float64 { return x * x }},
55 {"2 Multiplications", "x*x*x", func(x, y float64) float64 { return x * x * x }},
56 {"0 Divisions", "x", func(x, y float64) float64 { return x }},
57 {"1 Divisions", "x/x", func(x, y float64) float64 { return x / x }},
58 {"2 Divisions", "x/x/x", func(x, y float64) float64 { return x / x / x }},
59 {"e+pi", "e+pi", func(x, y float64) float64 { return math.E + math.Pi }},
60 {"1-2", "1-2", func(x, y float64) float64 { return 1 - 2 }},
61 {"e-pi", "e-pi", func(x, y float64) float64 { return math.E - math.Pi }},
62 {"Add 0", "x+0", func(x, y float64) float64 { return x + 0 }},
63 {"x*1", "x*1", func(x, y float64) float64 { return x * 1 }},
64 {"Abs Func", "abs(-x)", func(x, y float64) float64 { return math.Abs(-x) }},
65 {"Abs Syntax", "&(-x)", func(x, y float64) float64 { return math.Abs(-x) }},
66 {
67 "Square (pow)",
68 "pow(x, 2)",
69 func(x, y float64) float64 { return math.Pow(x, 2) },
70 },
71 {"Square", "square(x)", func(x, y float64) float64 { return x * x }},
72 {"Square Syntax", "*x", func(x, y float64) float64 { return x * x }},
73 {
74 "Linear",
75 "3.4 * x - 0.2",
76 func(x, y float64) float64 { return 3.4*x - 0.2 },
77 },
78 {"Direct Square", "x*x", func(x, y float64) float64 { return x * x }},
79 {
80 "Cube (pow)",
81 "pow(x, 3)",
82 func(x, y float64) float64 { return math.Pow(x, 3) },
83 },
84 {"Cube", "cube(x)", func(x, y float64) float64 { return x * x * x }},
85 {"Direct Cube", "x*x*x", func(x, y float64) float64 { return x * x * x }},
86 {
87 "Reciprocal (pow)",
88 "pow(x, -1)",
89 func(x, y float64) float64 { return math.Pow(x, -1) },
90 },
91 {
92 "Reciprocal Func",
93 "reciprocal(x)",
94 func(x, y float64) float64 { return 1 / x },
95 },
96 {"Direct Reciprocal", "1/x", func(x, y float64) float64 { return 1 / x }},
97 {"Variable Negation", "-x", func(x, y float64) float64 { return -x }},
98 {"Constant Multip.", "1*2", func(x, y float64) float64 { return 1 * 2 }},
99 {"e*pi", "e*pi", func(x, y float64) float64 { return math.E * math.Pi }},
100 {
101 "Variable y = mx + k",
102 "2.1*x + 3.4",
103 func(x, y float64) float64 { return 2.1*x + 3.4 },
104 },
105 {"sin(x)", "sin(x)", func(x, y float64) float64 { return math.Sin(x) }},
106 {"cos(x)", "cos(x)", func(x, y float64) float64 { return math.Cos(x) }},
107 {"exp(x)", "exp(x)", func(x, y float64) float64 { return math.Exp(x) }},
108 {"expm1(x)", "expm1(x)", func(x, y float64) float64 { return math.Expm1(x) }},
109 {"ln(x)", "ln(x)", func(x, y float64) float64 { return math.Log(x) }},
110 {"log2(x)", "log2(x)", func(x, y float64) float64 { return math.Log2(x) }},
111 {"log10(x)", "log10(x)", func(x, y float64) float64 { return math.Log10(x) }},
112 {"1/2", "1/2", func(x, y float64) float64 { return 1.0 / 2.0 }},
113 {"e/pi", "e/pi", func(x, y float64) float64 { return math.E / math.Pi }},
114 {"exp(2)", "exp(2)", func(x, y float64) float64 { return math.Exp(2) }},
115 {
116 "Club Beat Pulse",
117 "sin(10*tau * exp(-20*x)) * exp(-2*x)",
118 func(x, y float64) float64 {
119 return math.Sin(10*2*math.Pi*math.Exp(-20*x)) * math.Exp(-2*x)
120 },
121 },
122 {
123 "Interfering Ripples",
124 interferingRipples,
125 func(x, y float64) float64 {
126 return math.Exp(-0.5*math.Sin(2*math.Hypot(x-2, y+1))) +
127 math.Exp(-0.5*math.Sin(10*math.Hypot(x+2, y-3.4)))
128 },
129 },
130 {
131 "Floor Lights",
132 "abs(sin(x)) / y**1.4",
133 func(x, y float64) float64 {
134 return math.Abs(math.Sin(x)) / math.Pow(y, 1.4)
135 },
136 },
137 {
138 "Domain Hole",
139 "log1p(sin(x + y) + (x - y)**2 - 1.5*x + 2.5*y + 1)",
140 func(x, y float64) float64 {
141 v := x - y
142 return math.Log1p(math.Sin(x+y) + v*v - 1.5*x + 2.5*y + 1)
143 },
144 },
145 {
146 "Beta Gradient",
147 "lbeta(x + 5.1, y + 5.1)",
148 func(x, y float64) float64 { return lnBeta(x+5.1, y+5.1) },
149 },
150 {
151 "Hot Bars",
152 "abs(x) + sqrt(abs(sin(2*y)))",
153 func(x, y float64) float64 {
154 return math.Abs(x) + math.Sqrt(math.Abs(math.Sin(2*y)))
155 },
156 },
157 {
158 "Grid Pattern",
159 "sin(sin(x)+cos(y)) + cos(sin(x*y)+cos(y**2))",
160 func(x, y float64) float64 {
161 return math.Sin(math.Sin(x)+math.Cos(y)) +
162 math.Cos(math.Sin(x*y)+math.Cos(y*y))
163 },
164 },
165 {
166 "Smiling Ghost",
167 smilingGhost,
168 func(x, y float64) float64 {
169 xm1 := x - 1
170 xp1 := x + 1
171 v := y - math.Sqrt(3)
172 w := (xm1*xm1 + y*y - 4) * (xp1*xp1 + y*y - 4) * (x*x + v*v - 4)
173 return math.Log1p(w - 5)
174 },
175 },
176 {
177 "Forgot its Name",
178 "1 / (1 + x.abs/y*y)",
179 func(x, y float64) float64 {
180 return 1 / (1 + math.Abs(x)/y*y)
181 },
182 },
183 }
184
185 func BenchmarkEmpty(b *testing.B) {
186 b.Run("empty program", func(b *testing.B) {
187 var p Program
188 b.ReportAllocs()
189 b.ResetTimer()
190
191 for i := 0; i < b.N; i++ {
192 _ = p.Run()
193 }
194 })
195
196 f := func(float64, float64) float64 {
197 return math.NaN()
198 }
199
200 b.Run("empty function", func(b *testing.B) {
201 b.ReportAllocs()
202 b.ResetTimer()
203
204 for i := 0; i < b.N; i++ {
205 _ = f(0, 0)
206 }
207 })
208 }
209
210 func BenchmarkBasicProgram(b *testing.B) {
211 for _, tc := range benchmarks {
212 var c Compiler
213 defs := map[string]any{
214 "x": 0.05,
215 "y": 0,
216 "z": 0,
217 }
218
219 b.Run(tc.Name, func(b *testing.B) {
220 p, err := c.Compile(tc.Script, defs)
221 if err != nil {
222 const fs = "while compiling %q, got error %q"
223 b.Fatalf(fs, tc.Script, err.Error())
224 return
225 }
226
227 x, _ := p.Get("x")
228 _, _ = p.Get("y")
229 _, _ = p.Get("z")
230
231 b.ResetTimer()
232 for i := 0; i < b.N; i++ {
233 _ = p.Run()
234 *x++
235 }
236 })
237 }
238 }
239
240 func BenchmarkBasicFunc(b *testing.B) {
241 for _, tc := range benchmarks {
242 b.Run(tc.Name, func(b *testing.B) {
243 x := 0.05
244 y := 0.0
245 fn := tc.Native
246 b.ResetTimer()
247
248 for i := 0; i < b.N; i++ {
249 fn(x, y)
250 x++
251 y++
252 }
253 })
254 }
255 }
256
257 func BenchmarkSoundProgram(b *testing.B) {
258 var soundBenchmarkTests = []struct {
259 Name string
260 Script string
261 }{
262 {
263 "Sine Wave",
264 "sin(1000 * tau * x)",
265 },
266 {
267 "Laser Pulse",
268 "sin(100 * tau * exp(-40 * u))",
269 },
270 {
271 "Club Beat Pulse",
272 "sin(10 * tau * exp(-20 * x)) * exp(-2 * x)",
273 },
274 }
275
276 for _, tc := range soundBenchmarkTests {
277 var c Compiler
278 defs := map[string]any{
279 "t": 0.05,
280 "u": 0,
281 }
282
283 const seconds = 2
284 const sampleRate = 48_000
285 dt := 1.0 / float64(sampleRate)
286 // buf is the destination buffer for all calculated samples
287 buf := make([]float64, 0, seconds*sampleRate)
288
289 b.Run(tc.Name, func(b *testing.B) {
290 p, err := c.Compile(tc.Script, defs)
291 if err != nil {
292 const fs = "while compiling %q, got error %q"
293 b.Fatalf(fs, tc.Script, err.Error())
294 return
295 }
296
297 // input parameters for the program
298 t, _ := p.Get("t")
299 u, _ := p.Get("u")
300
301 b.ResetTimer()
302 for i := 0; i < b.N; i++ {
303 // avoid buffer expansions after the first run
304 buf = buf[:0]
305
306 // benchmark 1 second of generated sound
307 for j := 0; j < seconds*sampleRate; j++ {
308 // calculate time in seconds from current sample index
309 v := float64(j) * dt
310 *t = v
311 _, *u = math.Modf(v)
312
313 // calculate a mono sample
314 buf = append(buf, p.Run())
315 }
316 }
317 })
318 }
319 }
320
321 func BenchmarkNativeSoundProgram(b *testing.B) {
322 const tau = 2 * math.Pi
323
324 var soundBenchmarkTests = []struct {
325 Name string
326 Fun func(float64, float64) float64
327 }{
328 {
329 "Sine Wave",
330 func(t, u float64) float64 {
331 return math.Sin(1000 * tau * t)
332 },
333 },
334 {
335 "Laser Pulse",
336 func(t, u float64) float64 {
337 return math.Sin(100 * tau * math.Exp(-40*u))
338 },
339 },
340 {
341 "Club Beat Pulse",
342 func(t, u float64) float64 {
343 return math.Sin(10*tau*math.Exp(-20*t)) * math.Exp(-2*t)
344 },
345 },
346 }
347
348 for _, tc := range soundBenchmarkTests {
349 const seconds = 2
350 const sampleRate = 48_000
351 dt := 1.0 / float64(sampleRate)
352 // buf is the destination buffer for all calculated samples
353 buf := make([]float64, 0, seconds*sampleRate)
354
355 b.Run(tc.Name, func(b *testing.B) {
356 // input parameters for the program
357 t := 0.05
358 u := 0.00
359 fn := tc.Fun
360 b.ResetTimer()
361
362 for i := 0; i < b.N; i++ {
363 // avoid buffer expansions after the first run
364 buf = buf[:0]
365
366 // benchmark 1 second of generated sound
367 for j := 0; j < seconds*sampleRate; j++ {
368 // calculate time in seconds from current sample index
369 v := float64(j) * dt
370 t = v
371 _, u = math.Modf(v)
372
373 // calculate a mono sample
374 buf = append(buf, fn(t, u))
375 }
376 }
377 })
378 }
379 }
380
381 func BenchmarkImageProgram(b *testing.B) {
382 const (
383 // use part of a full HD pic to give more runs for each test, giving
384 // greater statistical-stability/comparability across benchmark runs
385 w = 1920 / 4
386 h = 1080 / 4
387
388 intRipples = "" +
389 "exp(-0.5 * sin(2 * hypot(x - 2, y + 1))) + " +
390 "exp(-0.5 * sin(10 * hypot(x + 2, y - 3.4)))"
391 domainHole = "" +
392 "log1p(sin(x + y) + pow(x - y, 2) - 1.5*x + 2.5*y + 1)"
393 gridPatPow = "" +
394 "sin(sin(x)+cos(y)) + cos(sin(x*y)+cos(pow(y, 2)))"
395 gridPatSquare = "" +
396 "sin(sin(x)+cos(y)) + cos(sin(x*y)+cos(square(y)))"
397 smilingGhostFunc = "" +
398 "log1p((pow(x - 1, 2) + y*y - 4)*" +
399 "(pow(x + 1, 2) + y*y - 4)*" +
400 "(x*x + pow(y - sqrt(3), 2) - 4) - 5)"
401 smilingGhostSyntax = "" +
402 "log1p((*(x - 1) + *y - 4)*" +
403 "(*(x + 1) + *y - 4)*" +
404 "(*x + *(y - sqrt(3)) - 4) - 5)"
405 )
406
407 var imageBenchmarkTests = []struct {
408 Name string
409 Width int
410 Height int
411 Script string
412 }{
413 {"Horizontal Linear", w, h, "x"},
414 {"Multiplication", w, h, "x*y"},
415 {"Star", w, h, "exp(-0.5 * hypot(x, y))"},
416 {"Interfering Ripples", w, h, intRipples},
417 {"Floor Lights", w, h, "abs(sin(x)) / pow(y, 1.4)"},
418 {"Domain Hole", w, h, domainHole},
419 {"Beta Gradient", w, h, "lbeta(x + 5.1, y + 5.1)"},
420 {"Hot Bars", w, h, "abs(x) + sqrt(abs(sin(2*y)))"},
421 {"Grid Pattern (pow)", w, h, gridPatPow},
422 {"Grid Pattern (square)", w, h, gridPatSquare},
423 {"Smiling Ghost (pow)", w, h, smilingGhostFunc},
424 {"Smiling Ghost (square syntax)", w, h, smilingGhostSyntax},
425 }
426
427 for _, tc := range imageBenchmarkTests {
428 var c Compiler
429 defs := map[string]any{
430 "x": 0,
431 "y": 0,
432 }
433
434 // 4k-resolution results buffer
435 buf := make([]float64, 1920*1080*4)
436
437 b.Run(tc.Name, func(b *testing.B) {
438 p, err := c.Compile(tc.Script, defs)
439 if err != nil {
440 const fs = "while compiling %q, got error %q"
441 b.Fatalf(fs, tc.Script, err.Error())
442 return
443 }
444
445 // input parameters for the program
446 x, _ := p.Get("x")
447 y, _ := p.Get("y")
448
449 w := tc.Width
450 h := tc.Height
451 dx := 1.0 / float64(w)
452 dy := 1.0 / float64(h)
453 xmin := -5.2
454 ymax := 23.91
455 b.ResetTimer()
456 for i := 0; i < b.N; i++ {
457 // avoid buffer expansions after the first run
458 buf = buf[:0]
459
460 for j := 0; j < h; j++ {
461 *y = ymax - float64(j)*dy
462 for i := 0; i < w; i++ {
463 *x = float64(i)*dx + xmin
464 buf = append(buf, p.Run())
465 }
466 }
467 }
468 })
469 }
470 }
471
472 func BenchmarkCompiler(b *testing.B) {
473 f := func(name string, src string) {
474 b.Run(name, func(b *testing.B) {
475 for i := 0; i < b.N; i++ {
476 var c Compiler
477 _, err := c.Compile(src, map[string]any{
478 "x": 0.05,
479 "y": 0,
480 "z": 0,
481 })
482
483 if err != nil {
484 const fs = "while compiling %q, got error %q"
485 b.Fatalf(fs, src, err.Error())
486 }
487 }
488 })
489 }
490
491 for _, tc := range mathCorrectnessTests {
492 f(tc.Name, tc.Script)
493 }
494
495 for _, tc := range benchmarks {
496 f(tc.Name, tc.Script)
497 }
498 }
File: ./fmscripts/compilers.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fmscripts
26
27 import (
28 "fmt"
29 "math"
30 "strconv"
31 )
32
33 // unary2op turns unary operators into their corresponding basic operations;
34 // some entries are only for the optimizer, and aren't accessible directly
35 // from valid source code
36 var unary2op = map[string]opcode{
37 "-": neg,
38 "!": not,
39 "&": abs,
40 "*": square,
41 "^": square,
42 "/": rec,
43 "%": mod1,
44 }
45
46 // binary2op turns binary operators into their corresponding basic operations
47 var binary2op = map[string]opcode{
48 "+": add,
49 "-": sub,
50 "*": mul,
51 "/": div,
52 "%": mod,
53 "&&": and,
54 "&": and,
55 "||": or,
56 "|": or,
57 "==": equal,
58 "!=": notequal,
59 "<>": notequal,
60 "<": less,
61 "<=": lessoreq,
62 ">": more,
63 ">=": moreoreq,
64 "**": pow,
65 "^": pow,
66 }
67
68 // Compiler lets you create Program objects, which you can then run. The whole
69 // point of this is to create quicker-to-run numeric scripts. You can even add
70 // variables with initial values, as well as functions for the script to use.
71 //
72 // Common math funcs and constants are automatically detected, and constant
73 // results are optimized away, unless builtins are redefined. In other words,
74 // the optimizer is effectively disabled for all (sub)expressions containing
75 // redefined built-ins, as there's no way to be sure those values won't change
76 // from one run to the next.
77 //
78 // See the comment for type Program for more details.
79 //
80 // # Example
81 //
82 // var c fmscripts.Compiler
83 //
84 // defs := map[string]any{
85 // "x": 0, // define `x`, and initialize it to 0
86 // "k": 4.25, // define `k`, and initialize it to 4.25
87 // "b": true, // define `b`, and initialize it to 1.0
88 // "n": -23, // define `n`, and initialize it to -23.0
89 // "pi": 3, // define `pi`, overriding the default constant named `pi`
90 //
91 // "f": numericKernel // type is func ([]float64) float64
92 // "g": otherFunc // type is func (float64) float64
93 // }
94 //
95 // prog, err := c.Compile("log10(k) + f(sqrt(k) * exp(-x), 45, -0.23)", defs)
96 // // ...
97 //
98 // x, _ := prog.Get("x") // Get returns (*float64, bool)
99 // y, _ := prog.Get("y") // a useless pointer, since program doesn't use `y`
100 // // ...
101 //
102 // for i := 0; i < n; i++ {
103 // *x = float64(i)*dx + minx // you update inputs in place using pointers
104 // f := prog.Run() // method Run gives you a float64 back
105 // // ...
106 // }
107 type Compiler struct {
108 maxStack int // the exact stack size the resulting program needs
109 ops []numOp // the program's operations
110
111 vaddr map[string]int // address lookup for values
112 faddr map[string]int // address lookup for functions
113 ftype map[string]any // keeps track of func types during compilation
114
115 values []float64 // variables and constants available to programs
116 funcs []any // funcs available to programs
117 }
118
119 // Compile parses the script given and generates a fast float64-only Program
120 // made only of sequential steps: any custom funcs you provide it can use
121 // their own internal looping and/or conditional logic, of course.
122 func (c *Compiler) Compile(src string, defs map[string]any) (Program, error) {
123 // turn source code into an abstract syntax-tree
124 root, err := parse(src)
125 if err != nil {
126 return Program{}, err
127 }
128
129 // generate operations
130 if err := c.reset(defs); err != nil {
131 return Program{}, err
132 }
133 if err = c.compile(root, 0); err != nil {
134 return Program{}, err
135 }
136
137 // create the resulting program
138 var p Program
139 p.stack = make([]float64, c.maxStack)
140 p.values = make([]float64, len(c.values))
141 copy(p.values, c.values)
142 p.ops = make([]numOp, len(c.ops))
143 copy(p.ops, c.ops)
144 p.funcs = make([]any, len(c.ftype))
145 copy(p.funcs, c.funcs)
146 p.names = make(map[string]int, len(c.vaddr))
147
148 // give the program's Get method access only to all allocated variables
149 for k, v := range c.vaddr {
150 // avoid exposing numeric constants on the program's name-lookup
151 // table, which would allow users to change literals across runs
152 if _, err := strconv.ParseFloat(k, 64); err == nil {
153 continue
154 }
155 // expose only actual variable names
156 p.names[k] = v
157 }
158
159 return p, nil
160 }
161
162 // reset prepares a compiler by satisfying internal preconditions func
163 // compileExpr relies on, when given an abstract syntax-tree
164 func (c *Compiler) reset(defs map[string]any) error {
165 // reset the compiler's internal state
166 c.maxStack = 0
167 c.ops = c.ops[:0]
168 c.vaddr = make(map[string]int)
169 c.faddr = make(map[string]int)
170 c.ftype = make(map[string]any)
171 c.values = c.values[:0]
172 c.funcs = c.funcs[:0]
173
174 // allocate vars and funcs
175 for k, v := range defs {
176 if err := c.allocEntry(k, v); err != nil {
177 return err
178 }
179 }
180 return nil
181 }
182
183 // allocEntry simplifies the control-flow of func reset
184 func (c *Compiler) allocEntry(k string, v any) error {
185 const (
186 maxExactRound = 2 << 52
187 rangeErrFmt = "%d is outside range of exact float64 integers"
188 typeErrFmt = "got value of unsupported type %T"
189 )
190
191 switch v := v.(type) {
192 case float64:
193 _, err := c.allocValue(k, v)
194 return err
195
196 case int:
197 if math.Abs(float64(v)) > maxExactRound {
198 return fmt.Errorf(rangeErrFmt, v)
199 }
200 _, err := c.allocValue(k, float64(v))
201 return err
202
203 case bool:
204 _, err := c.allocValue(k, debool(v))
205 return err
206
207 default:
208 if isSupportedFunc(v) {
209 c.ftype[k] = v
210 _, err := c.allocFunc(k, v)
211 return err
212 }
213 return fmt.Errorf(typeErrFmt, v)
214 }
215 }
216
217 // allocValue ensures there's a place to match the name given, returning its
218 // index when successful; only programs using unreasonably many values will
219 // cause this func to fail
220 func (c *Compiler) allocValue(s string, f float64) (int, error) {
221 if len(c.values) >= maxOpIndex {
222 const fs = "programs can only use up to %d distinct float64 values"
223 return -1, fmt.Errorf(fs, maxOpIndex+1)
224 }
225
226 i := len(c.values)
227 c.vaddr[s] = i
228 c.values = append(c.values, f)
229 return i, nil
230 }
231
232 // valueIndex returns the index reserved for an allocated value/variable:
233 // all unallocated values/variables are allocated here on first access
234 func (c *Compiler) valueIndex(s string, f float64) (int, error) {
235 // name is found: return the index of the already-allocated var
236 if i, ok := c.vaddr[s]; ok {
237 return i, nil
238 }
239
240 // name not found, but it's a known math constant
241 if f, ok := mathConst[s]; ok {
242 return c.allocValue(s, f)
243 }
244 // name not found, and it's not of a known math constant
245 return c.allocValue(s, f)
246 }
247
248 // constIndex allocates a constant as a variable named as its own string
249 // representation, which avoids multiple entries for repeated uses of the
250 // same constant value
251 func (c *Compiler) constIndex(f float64) (int, error) {
252 // constants have no name, so use a canonical string representation
253 return c.valueIndex(strconv.FormatFloat(f, 'f', 16, 64), f)
254 }
255
256 // funcIndex returns the index reserved for an allocated function: all
257 // unallocated functions are allocated here on first access
258 func (c *Compiler) funcIndex(name string) (int, error) {
259 // check if func was already allocated
260 if i, ok := c.faddr[name]; ok {
261 return i, nil
262 }
263
264 // if name is reserved allocate an index for its matching func
265 if fn, ok := c.ftype[name]; ok {
266 return c.allocFunc(name, fn)
267 }
268
269 // if name wasn't reserved, see if it's a standard math func name
270 if fn := c.autoFuncLookup(name); fn != nil {
271 return c.allocFunc(name, fn)
272 }
273
274 // name isn't even a standard math func's
275 return -1, fmt.Errorf("function not found")
276 }
277
278 // allocFunc ensures there's a place for the function name given, returning its
279 // index when successful; only funcs of unsupported types will cause failure
280 func (c *Compiler) allocFunc(name string, fn any) (int, error) {
281 if isSupportedFunc(fn) {
282 i := len(c.funcs)
283 c.faddr[name] = i
284 c.ftype[name] = fn
285 c.funcs = append(c.funcs, fn)
286 return i, nil
287 }
288 return -1, fmt.Errorf("can't use a %T as a number-crunching function", fn)
289 }
290
291 // autoFuncLookup checks built-in deterministic funcs for the name given: its
292 // result is nil only if there's no match
293 func (c *Compiler) autoFuncLookup(name string) any {
294 if fn, ok := determFuncs[name]; ok {
295 return fn
296 }
297 return nil
298 }
299
300 // genOp generates/adds a basic operation to the program, while keeping track
301 // of the maximum depth the stack can reach
302 func (c *Compiler) genOp(op numOp, depth int) {
303 // add 2 defensively to ensure stack space for the inputs of binary ops
304 n := depth + 2
305 if c.maxStack < n {
306 c.maxStack = n
307 }
308 c.ops = append(c.ops, op)
309 }
310
311 // compile is a recursive expression evaluator which does the actual compiling
312 // as it goes along
313 func (c *Compiler) compile(expr any, depth int) error {
314 expr = c.optimize(expr)
315
316 switch expr := expr.(type) {
317 case float64:
318 return c.compileLiteral(expr, depth)
319 case string:
320 return c.compileVariable(expr, depth)
321 case []any:
322 return c.compileCombo(expr, depth)
323 case unaryExpr:
324 return c.compileUnary(expr.op, expr.x, depth)
325 case binaryExpr:
326 return c.compileBinary(expr.op, expr.x, expr.y, depth)
327 case callExpr:
328 return c.compileCall(expr, depth)
329 case assignExpr:
330 return c.compileAssign(expr, depth)
331 default:
332 return fmt.Errorf("unsupported expression type %T", expr)
333 }
334 }
335
336 // compileLiteral generates a load operation for the constant value given
337 func (c *Compiler) compileLiteral(f float64, depth int) error {
338 i, err := c.constIndex(f)
339 if err != nil {
340 return err
341 }
342 c.genOp(numOp{What: load, Index: opindex(i)}, depth)
343 return nil
344 }
345
346 // compileVariable generates a load operation for the variable name given
347 func (c *Compiler) compileVariable(name string, depth int) error {
348 // handle names which aren't defined, but are known math constants
349 if _, ok := c.vaddr[name]; !ok {
350 if f, ok := mathConst[name]; ok {
351 return c.compileLiteral(f, depth)
352 }
353 }
354
355 // handle actual variables
356 i, err := c.valueIndex(name, 0)
357 if err != nil {
358 return err
359 }
360 c.genOp(numOp{What: load, Index: opindex(i)}, depth)
361 return nil
362 }
363
364 // compileCombo handles a sequence of expressions
365 func (c *Compiler) compileCombo(exprs []any, depth int) error {
366 for _, v := range exprs {
367 err := c.compile(v, depth)
368 if err != nil {
369 return err
370 }
371 }
372 return nil
373 }
374
375 // compileUnary handles unary expressions
376 func (c *Compiler) compileUnary(op string, x any, depth int) error {
377 err := c.compile(x, depth+1)
378 if err != nil {
379 return err
380 }
381
382 if op, ok := unary2op[op]; ok {
383 c.genOp(numOp{What: op}, depth)
384 return nil
385 }
386 return fmt.Errorf("unary operation %q is unsupported", op)
387 }
388
389 // compileBinary handles binary expressions
390 func (c *Compiler) compileBinary(op string, x, y any, depth int) error {
391 switch op {
392 case "===", "!==":
393 // handle binary expressions with no matching basic operation, by
394 // treating them as aliases for function calls
395 return c.compileCall(callExpr{name: op, args: []any{x, y}}, depth)
396 }
397
398 err := c.compile(x, depth+1)
399 if err != nil {
400 return err
401 }
402 err = c.compile(y, depth+2)
403 if err != nil {
404 return err
405 }
406
407 if op, ok := binary2op[op]; ok {
408 c.genOp(numOp{What: op}, depth)
409 return nil
410 }
411 return fmt.Errorf("binary operation %q is unsupported", op)
412 }
413
414 // compileCall handles function-call expressions
415 func (c *Compiler) compileCall(expr callExpr, depth int) error {
416 // lookup function name
417 index, err := c.funcIndex(expr.name)
418 if err != nil {
419 return fmt.Errorf("%s: %s", expr.name, err.Error())
420 }
421 // get the func value, as its type determines the calling op to emit
422 v, ok := c.ftype[expr.name]
423 if !ok {
424 return fmt.Errorf("%s: function is undefined", expr.name)
425 }
426
427 // figure which type of function operation to use
428 op, ok := func2op(v)
429 if !ok {
430 return fmt.Errorf("%s: unsupported function type %T", expr.name, v)
431 }
432
433 // ensure number of args given to the func makes sense for the func type
434 err = checkArgCount(func2info[op], expr.name, len(expr.args))
435 if err != nil {
436 return err
437 }
438
439 // generate operations to evaluate all args
440 for i, v := range expr.args {
441 err := c.compile(v, depth+i+1)
442 if err != nil {
443 return err
444 }
445 }
446
447 // generate func-call operation
448 given := len(expr.args)
449 next := numOp{What: op, NumArgs: opargs(given), Index: opindex(index)}
450 c.genOp(next, depth)
451 return nil
452 }
453
454 // checkArgCount does what it says, returning informative errors when arg
455 // counts are wrong
456 func checkArgCount(info funcTypeInfo, name string, nargs int) error {
457 if info.AtLeast < 0 && info.AtMost < 0 {
458 return nil
459 }
460
461 if info.AtLeast == info.AtMost && nargs != info.AtMost {
462 const fs = "%s: expected %d args, got %d instead"
463 return fmt.Errorf(fs, name, info.AtMost, nargs)
464 }
465
466 if info.AtLeast >= 0 && info.AtMost >= 0 {
467 const fs = "%s: expected between %d and %d args, got %d instead"
468 if nargs < info.AtLeast || nargs > info.AtMost {
469 return fmt.Errorf(fs, name, info.AtLeast, info.AtMost, nargs)
470 }
471 }
472
473 if info.AtLeast >= 0 && nargs < info.AtLeast {
474 const fs = "%s: expected at least %d args, got %d instead"
475 return fmt.Errorf(fs, name, info.AtLeast, nargs)
476 }
477
478 if info.AtMost >= 0 && nargs > info.AtMost {
479 const fs = "%s: expected at most %d args, got %d instead"
480 return fmt.Errorf(fs, name, info.AtMost, nargs)
481 }
482
483 // all is good
484 return nil
485 }
486
487 // compileAssign generates a store operation for the expression given
488 func (c *Compiler) compileAssign(expr assignExpr, depth int) error {
489 err := c.compile(expr.expr, depth)
490 if err != nil {
491 return err
492 }
493
494 i, err := c.allocValue(expr.name, 0)
495 if err != nil {
496 return err
497 }
498
499 c.genOp(numOp{What: store, Index: opindex(i)}, depth)
500 return nil
501 }
502
503 // func sameFunc(x, y any) bool {
504 // if x == nil || y == nil {
505 // return false
506 // }
507 // return reflect.ValueOf(x).Pointer() == reflect.ValueOf(y).Pointer()
508 // }
509
510 // isSupportedFunc checks if a value's type is a supported func type
511 func isSupportedFunc(fn any) bool {
512 _, ok := func2op(fn)
513 return ok
514 }
515
516 // funcTypeInfo is an entry in the func2info lookup table
517 type funcTypeInfo struct {
518 // AtLeast is the minimum number of inputs the func requires: negative
519 // values are meant to be ignored
520 AtLeast int
521
522 // AtMost is the maximum number of inputs the func requires: negative
523 // values are meant to be ignored
524 AtMost int
525 }
526
527 // func2info is a lookup table to check the number of inputs funcs are given
528 var func2info = map[opcode]funcTypeInfo{
529 call0: {AtLeast: +0, AtMost: +0},
530 call1: {AtLeast: +1, AtMost: +1},
531 call2: {AtLeast: +2, AtMost: +2},
532 call3: {AtLeast: +3, AtMost: +3},
533 call4: {AtLeast: +4, AtMost: +4},
534 call5: {AtLeast: +5, AtMost: +5},
535 callv: {AtLeast: -1, AtMost: -1},
536 call1v: {AtLeast: +1, AtMost: -1},
537 }
538
539 // func2op tries to match a func type into a corresponding opcode
540 func func2op(v any) (op opcode, ok bool) {
541 switch v.(type) {
542 case func() float64:
543 return call0, true
544 case func(float64) float64:
545 return call1, true
546 case func(float64, float64) float64:
547 return call2, true
548 case func(float64, float64, float64) float64:
549 return call3, true
550 case func(float64, float64, float64, float64) float64:
551 return call4, true
552 case func(float64, float64, float64, float64, float64) float64:
553 return call5, true
554 case func(...float64) float64:
555 return callv, true
556 case func(float64, ...float64) float64:
557 return call1v, true
558 default:
559 return 0, false
560 }
561 }
File: ./fmscripts/compilers_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fmscripts
26
27 import (
28 "testing"
29 )
30
31 func TestBuiltinFuncs(t *testing.T) {
32 list := []map[string]any{
33 determFuncs,
34 }
35
36 for _, kv := range list {
37 for k, v := range kv {
38 t.Run(k, func(t *testing.T) {
39 if !isSupportedFunc(v) {
40 t.Fatalf("%s: invalid function type %T", k, v)
41 }
42 })
43 }
44 }
45 }
46
47 var mathCorrectnessTests = []struct {
48 Name string
49 Script string
50 Expected float64
51 Error string
52 }{
53 {
54 Name: "value",
55 Script: "-45.2",
56 Expected: -45.2,
57 Error: "",
58 },
59 {
60 Name: "simple",
61 Script: "1 + 3",
62 Expected: 4,
63 Error: "",
64 },
65 {
66 Name: "fancier",
67 Script: "1*8 + 3*2**3",
68 Expected: 32,
69 Error: "",
70 },
71 {
72 Name: "function call",
73 Script: "log2(1024)",
74 Expected: 10,
75 Error: "",
76 },
77 {
78 Name: "function call (2 args)",
79 Script: "pow(3, 3)",
80 Expected: 27,
81 Error: "",
82 },
83 {
84 Name: "function call (3 args)",
85 Script: "fma(3.5, 56, -1.52)",
86 Expected: 194.48,
87 Error: "",
88 },
89 {
90 Name: "vararg-function call",
91 Script: "min(log10(10_000), log10(1_000_000), log2(4_096), 100 - 130)",
92 Expected: -30,
93 Error: "",
94 },
95 {
96 Name: "square shortcut",
97 Script: "*-3",
98 Expected: 9,
99 Error: "",
100 },
101 {
102 Name: "abs shortcut",
103 Script: "&-3",
104 Expected: 3,
105 Error: "",
106 },
107 {
108 Name: "negative constant",
109 Script: "(-3)",
110 Expected: -3,
111 Error: "",
112 },
113 {
114 Name: "power syntax",
115 Script: "2**4",
116 Expected: 16,
117 Error: "",
118 },
119 {
120 Name: "power syntax order",
121 Script: "3*2**4",
122 Expected: 48,
123 Error: "",
124 },
125 {
126 Script: "2*3**4",
127 Expected: 162,
128 Error: "",
129 },
130 {
131 Script: "2**3*4",
132 Expected: 32,
133 Error: "",
134 },
135 {
136 Script: "(2*3)**4",
137 Expected: 1_296,
138 Error: "",
139 },
140 {
141 Script: "*2*2",
142 Expected: 8,
143 Error: "",
144 },
145 {
146 Script: "2*2*2",
147 Expected: 8,
148 Error: "",
149 },
150 {
151 Script: "3 == 3 ? 10 : -1",
152 Expected: 10,
153 Error: "",
154 },
155 {
156 Script: "3 == 4 ? 10 : -1",
157 Expected: -1,
158 Error: "",
159 },
160 {
161 Script: "log10(-1) ?? 4",
162 Expected: 4,
163 Error: "",
164 },
165 {
166 Script: "log10(10) ?? 4",
167 Expected: 1,
168 Error: "",
169 },
170 {
171 Script: "abc = 123; abc",
172 Expected: 123,
173 Error: "",
174 },
175 {
176 Name: "calling func(float64, ...float64) float64",
177 Script: "horner(2.5, 1, 2, 3)",
178 Expected: 14.25,
179 Error: "",
180 },
181 }
182
183 func TestCompiler(t *testing.T) {
184 for _, tc := range mathCorrectnessTests {
185 name := tc.Name
186 if len(name) == 0 {
187 name = tc.Script
188 }
189
190 t.Run(name, func(t *testing.T) {
191 var c Compiler
192 p, err := c.Compile(tc.Script, map[string]any{"x": 1})
193 if err != nil {
194 t.Fatalf("got compiler error %q", err.Error())
195 }
196
197 f := p.Run()
198 if (err != nil || tc.Error != "") && err.Error() != tc.Error {
199 const fs = "expected error %q, got error %q instead"
200 t.Fatalf(fs, err.Error(), tc.Error)
201 }
202
203 if f != tc.Expected {
204 const fs = "expected result to be %f, got %f instead"
205 t.Fatalf(fs, tc.Expected, f)
206 }
207 })
208 }
209 }
File: ./fmscripts/doc.go
1 /*
2 # Floating-point Math Scripts
3
4 This self-contained package gives you a compiler/interpreter combo to run
5 number-crunching scripts. These are limited to float64 values, but they run
6 surprisingly quickly: various simple benchmarks suggest it's between 1/2 and
7 1/4 the speed of native funcs.
8
9 There are several built-in numeric functions, and the compiler makes it easy
10 to add your custom Go functions to further speed-up any operations specific to
11 your app/domain.
12
13 Finally, notice how float64 data don't really limit you as much as you might
14 think at first, since they can act as booleans (treating 0 as false, and non-0
15 as true), or as exact integers in the extremely-wide range [-2**53, +2**53].
16 */
17
18 package fmscripts
File: ./fmscripts/math.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fmscripts
26
27 import (
28 "math"
29 )
30
31 const (
32 // the maximum integer a float64 can represent exactly; -maxflint is the
33 // minimum such integer, since floating-point values allow such symmetries
34 maxflint = 2 << 52
35
36 // base-2 versions of size multipliers
37 kilobyte = 1024 * 1.0
38 megabyte = 1024 * kilobyte
39 gigabyte = 1024 * megabyte
40 terabyte = 1024 * gigabyte
41 petabyte = 1024 * terabyte
42
43 // unit-conversion multipliers
44 mi2kmMult = 1 / 0.6213712
45 nm2kmMult = 1.852
46 nmi2kmMult = 1.852
47 yd2mtMult = 1 / 1.093613
48 ft2mtMult = 1 / 3.28084
49 in2cmMult = 2.54
50 lb2kgMult = 0.4535924
51 ga2ltMult = 1 / 0.2199692
52 gal2lMult = 1 / 0.2199692
53 oz2mlMult = 29.5735295625
54 cup2lMult = 0.2365882365
55 mpg2kplMult = 0.2199692 / 0.6213712
56 ton2kgMult = 1 / 907.18474
57 psi2paMult = 1 / 0.00014503773800722
58 deg2radMult = math.Pi / 180
59 rad2degMult = 180 / math.Pi
60 )
61
62 // default math constants
63 var mathConst = map[string]float64{
64 "e": math.E,
65 "pi": math.Pi,
66 "tau": 2 * math.Pi,
67 "phi": math.Phi,
68 "nan": math.NaN(),
69 "inf": math.Inf(+1),
70
71 "minint": -float64(maxflint - 1),
72 "maxint": +float64(maxflint - 1),
73 "minsafeint": -float64(maxflint - 1),
74 "maxsafeint": +float64(maxflint - 1),
75
76 "false": 0.0,
77 "true": 1.0,
78 "f": 0.0,
79 "t": 1.0,
80
81 // conveniently-named multipliers
82 "femto": 1e-15,
83 "pico": 1e-12,
84 "nano": 1e-09,
85 "micro": 1e-06,
86 "milli": 1e-03,
87 "kilo": 1e+03,
88 "mega": 1e+06,
89 "giga": 1e+09,
90 "tera": 1e+12,
91 "peta": 1e+15,
92
93 // unit-conversion multipliers
94 "mi2km": mi2kmMult,
95 "nm2km": nm2kmMult,
96 "nmi2km": nmi2kmMult,
97 "yd2mt": yd2mtMult,
98 "ft2mt": ft2mtMult,
99 "in2cm": in2cmMult,
100 "lb2kg": lb2kgMult,
101 "ga2lt": ga2ltMult,
102 "gal2l": gal2lMult,
103 "oz2ml": oz2mlMult,
104 "cup2l": cup2lMult,
105 "mpg2kpl": mpg2kplMult,
106 "ton2kg": ton2kgMult,
107 "psi2pa": psi2paMult,
108 "deg2rad": deg2radMult,
109 "rad2deg": rad2degMult,
110
111 // base-2 versions of size multipliers
112 "kb": kilobyte,
113 "mb": megabyte,
114 "gb": gigabyte,
115 "tb": terabyte,
116 "pb": petabyte,
117 "binkilo": kilobyte,
118 "binmega": megabyte,
119 "bingiga": gigabyte,
120 "bintera": terabyte,
121 "binpeta": petabyte,
122
123 // physical constants
124 "c": 299_792_458, // speed of light in m/s
125 "g": 6.67430e-11, // gravitational constant in N m2/kg2
126 "h": 6.62607015e-34, // planck constant in J s
127 "ec": 1.602176634e-19, // elementary charge in C
128 "e0": 8.8541878128e-12, // vacuum permittivity in C2/(Nm2)
129 "mu0": 1.25663706212e-6, // vacuum permeability in T m/A
130 "k": 1.380649e-23, // boltzmann constant in J/K
131 "mu": 1.66053906660e-27, // atomic mass constant in kg
132 "me": 9.1093837015e-31, // electron mass in kg
133 "mp": 1.67262192369e-27, // proton mass in kg
134 "mn": 1.67492749804e-27, // neutron mass in kg
135
136 // float64s can only vaguely approx. avogadro's mole (6.02214076e23)
137 }
138
139 // deterministic math functions lookup-table generated using the command
140 //
141 // go doc math | awk '/func/ { gsub(/func |\(.*/, ""); printf("\"%s\": math.%s,\n", tolower($0), $0) }'
142 //
143 // then hand-edited to remove funcs, or to use adapter funcs when needed: removed
144 // funcs either had multiple returns (like SinCos) or dealt with float32 values
145 var determFuncs = map[string]any{
146 "abs": math.Abs,
147 "acos": math.Acos,
148 "acosh": math.Acosh,
149 "asin": math.Asin,
150 "asinh": math.Asinh,
151 "atan": math.Atan,
152 "atan2": math.Atan2,
153 "atanh": math.Atanh,
154 "cbrt": math.Cbrt,
155 "ceil": math.Ceil,
156 "copysign": math.Copysign,
157 "cos": math.Cos,
158 "cosh": math.Cosh,
159 "dim": math.Dim,
160 "erf": math.Erf,
161 "erfc": math.Erfc,
162 "erfcinv": math.Erfcinv,
163 "erfinv": math.Erfinv,
164 "exp": math.Exp,
165 "exp2": math.Exp2,
166 "expm1": math.Expm1,
167 "fma": math.FMA,
168 "floor": math.Floor,
169 "gamma": math.Gamma,
170 "inf": inf,
171 "isinf": isInf,
172 "isnan": isNaN,
173 "j0": math.J0,
174 "j1": math.J1,
175 "jn": jn,
176 "ldexp": ldexp,
177 "lgamma": lgamma,
178 "log10": math.Log10,
179 "log1p": math.Log1p,
180 "log2": math.Log2,
181 "logb": math.Logb,
182 "mod": math.Mod,
183 "nan": math.NaN,
184 "nextafter": math.Nextafter,
185 "pow": math.Pow,
186 "pow10": pow10,
187 "remainder": math.Remainder,
188 "round": math.Round,
189 "roundtoeven": math.RoundToEven,
190 "signbit": signbit,
191 "sin": math.Sin,
192 "sinh": math.Sinh,
193 "sqrt": math.Sqrt,
194 "tan": math.Tan,
195 "tanh": math.Tanh,
196 "trunc": math.Trunc,
197 "y0": math.Y0,
198 "y1": math.Y1,
199 "yn": yn,
200
201 // a few aliases for the standard math funcs: some of the single-letter
202 // aliases are named after the ones in `bc`, the basic calculator tool
203 "a": math.Abs,
204 "c": math.Cos,
205 "ceiling": math.Ceil,
206 "cosine": math.Cos,
207 "e": math.Exp,
208 "isinf0": isAnyInf,
209 "isinfinite": isAnyInf,
210 "l": math.Log,
211 "ln": math.Log,
212 "lg": math.Log2,
213 "modulus": math.Mod,
214 "power": math.Pow,
215 "rem": math.Remainder,
216 "s": math.Sin,
217 "sine": math.Sin,
218 "t": math.Tan,
219 "tangent": math.Tan,
220 "truncate": math.Trunc,
221 "truncated": math.Trunc,
222
223 // not from standard math: these custom funcs were added manually
224 "beta": beta,
225 "bool": num2bool,
226 "clamp": clamp,
227 "cond": cond, // vector-arg if-else chain
228 "cube": cube,
229 "cubed": cube,
230 "degrees": degrees,
231 "deinf": deInf,
232 "denan": deNaN,
233 "factorial": factorial,
234 "fract": fract,
235 "horner": polyval,
236 "hypot": hypot,
237 "if": ifElse,
238 "ifelse": ifElse,
239 "inv": reciprocal,
240 "isanyinf": isAnyInf,
241 "isbad": isBad,
242 "isfin": isFinite,
243 "isfinite": isFinite,
244 "isgood": isGood,
245 "isinteger": isInteger,
246 "lbeta": lnBeta,
247 "len": length,
248 "length": length,
249 "lnbeta": lnBeta,
250 "log": math.Log,
251 "logistic": logistic,
252 "mag": length,
253 "max": max,
254 "min": min,
255 "neg": negate,
256 "negate": negate,
257 "not": notBool,
258 "polyval": polyval,
259 "radians": radians,
260 "range": rangef, // vector-arg max - min
261 "reciprocal": reciprocal,
262 "rev": revalue,
263 "revalue": revalue,
264 "scale": scale,
265 "sgn": sign,
266 "sign": sign,
267 "sinc": sinc,
268 "sq": sqr,
269 "sqmin": solveQuadMin,
270 "sqmax": solveQuadMax,
271 "square": sqr,
272 "squared": sqr,
273 "unwrap": unwrap,
274 "wrap": wrap,
275
276 // a few aliases for the custom funcs
277 "deg": degrees,
278 "isint": isInteger,
279 "rad": radians,
280
281 // a few quicker 2-value versions of vararg funcs: the optimizer depends
282 // on these to rewrite 2-input uses of their vararg counterparts
283 "hypot2": math.Hypot,
284 "max2": math.Max,
285 "min2": math.Min,
286
287 // a few entries to enable custom syntax: the parser depends on these to
288 // rewrite binary expressions into func calls
289 "??": deNaN,
290 "?:": ifElse,
291 "===": same,
292 "!==": notSame,
293 }
294
295 // DefineDetFuncs adds more deterministic funcs to the default set. Such funcs
296 // are considered optimizable, since calling them with the same constant inputs
297 // is supposed to return the same constant outputs, as the name `deterministic`
298 // suggests.
299 //
300 // Only call this before compiling any scripts, and ensure all funcs given are
301 // supported and are deterministic. Random-output funcs certainly won't fit
302 // the bill here.
303 func DefineDetFuncs(funcs map[string]any) {
304 for k, v := range funcs {
305 determFuncs[k] = v
306 }
307 }
308
309 func sqr(x float64) float64 {
310 return x * x
311 }
312
313 func cube(x float64) float64 {
314 return x * x * x
315 }
316
317 func num2bool(x float64) float64 {
318 if x == 0 {
319 return 0
320 }
321 return 1
322 }
323
324 func logistic(x float64) float64 {
325 return 1 / (1 + math.Exp(-x))
326 }
327
328 func sign(x float64) float64 {
329 if math.IsNaN(x) {
330 return x
331 }
332 if x > 0 {
333 return +1
334 }
335 if x < 0 {
336 return -1
337 }
338 return 0
339 }
340
341 func sinc(x float64) float64 {
342 if x == 0 {
343 return 1
344 }
345 return math.Sin(x) / x
346 }
347
348 func isInteger(x float64) float64 {
349 _, frac := math.Modf(x)
350 if frac == 0 {
351 return 1
352 }
353 return 0
354 }
355
356 func inf(sign float64) float64 {
357 return math.Inf(int(sign))
358 }
359
360 func isInf(x float64, sign float64) float64 {
361 if math.IsInf(x, int(sign)) {
362 return 1
363 }
364 return 0
365 }
366
367 func isAnyInf(x float64) float64 {
368 if math.IsInf(x, 0) {
369 return 1
370 }
371 return 0
372 }
373
374 func isFinite(x float64) float64 {
375 if math.IsInf(x, 0) {
376 return 0
377 }
378 return 1
379 }
380
381 func isNaN(x float64) float64 {
382 if math.IsNaN(x) {
383 return 1
384 }
385 return 0
386 }
387
388 func isGood(x float64) float64 {
389 if math.IsNaN(x) || math.IsInf(x, 0) {
390 return 0
391 }
392 return 1
393 }
394
395 func isBad(x float64) float64 {
396 if math.IsNaN(x) || math.IsInf(x, 0) {
397 return 1
398 }
399 return 0
400 }
401
402 func same(x, y float64) float64 {
403 if math.IsNaN(x) && math.IsNaN(y) {
404 return 1
405 }
406 return debool(x == y)
407 }
408
409 func notSame(x, y float64) float64 {
410 if math.IsNaN(x) && math.IsNaN(y) {
411 return 0
412 }
413 return debool(x != y)
414 }
415
416 func deNaN(x float64, instead float64) float64 {
417 if !math.IsNaN(x) {
418 return x
419 }
420 return instead
421 }
422
423 func deInf(x float64, instead float64) float64 {
424 if !math.IsInf(x, 0) {
425 return x
426 }
427 return instead
428 }
429
430 func revalue(x float64, instead float64) float64 {
431 if !math.IsNaN(x) && !math.IsInf(x, 0) {
432 return x
433 }
434 return instead
435 }
436
437 func pow10(n float64) float64 {
438 return math.Pow10(int(n))
439 }
440
441 func jn(n, x float64) float64 {
442 return math.Jn(int(n), x)
443 }
444
445 func ldexp(frac, exp float64) float64 {
446 return math.Ldexp(frac, int(exp))
447 }
448
449 func lgamma(x float64) float64 {
450 y, s := math.Lgamma(x)
451 if s < 0 {
452 return math.NaN()
453 }
454 return y
455 }
456
457 func signbit(x float64) float64 {
458 if math.Signbit(x) {
459 return 1
460 }
461 return 0
462 }
463
464 func yn(n, x float64) float64 {
465 return math.Yn(int(n), x)
466 }
467
468 func negate(x float64) float64 {
469 return -x
470 }
471
472 func reciprocal(x float64) float64 {
473 return 1 / x
474 }
475
476 func rangef(v ...float64) float64 {
477 min := math.Inf(+1)
478 max := math.Inf(-1)
479 for _, f := range v {
480 min = math.Min(min, f)
481 max = math.Max(max, f)
482 }
483 return max - min
484 }
485
486 func cond(v ...float64) float64 {
487 for {
488 switch len(v) {
489 case 0:
490 // either no values are left, or no values were given at all
491 return math.NaN()
492
493 case 1:
494 // either all previous conditions failed, and this last value is
495 // automatically chosen, or only 1 value was given to begin with
496 return v[0]
497
498 default:
499 // check condition: if true (non-0), return the value after it
500 if v[0] != 0 {
501 return v[1]
502 }
503 // condition was false, so skip the leading pair of values
504 v = v[2:]
505 }
506 }
507 }
508
509 func notBool(x float64) float64 {
510 if x == 0 {
511 return 1
512 }
513 return 0
514 }
515
516 func ifElse(cond float64, yes, no float64) float64 {
517 if cond != 0 {
518 return yes
519 }
520 return no
521 }
522
523 func lnGamma(x float64) float64 {
524 y, s := math.Lgamma(x)
525 if s < 0 {
526 return math.NaN()
527 }
528 return y
529 }
530
531 func lnBeta(x float64, y float64) float64 {
532 return lnGamma(x) + lnGamma(y) - lnGamma(x+y)
533 }
534
535 func beta(x float64, y float64) float64 {
536 return math.Exp(lnBeta(x, y))
537 }
538
539 func factorial(n float64) float64 {
540 return math.Round(math.Gamma(n + 1))
541 }
542
543 func degrees(rad float64) float64 {
544 return rad2degMult * rad
545 }
546
547 func radians(deg float64) float64 {
548 return deg2radMult * deg
549 }
550
551 func fract(x float64) float64 {
552 return x - math.Floor(x)
553 }
554
555 func min(v ...float64) float64 {
556 min := +math.Inf(+1)
557 for _, f := range v {
558 min = math.Min(min, f)
559 }
560 return min
561 }
562
563 func max(v ...float64) float64 {
564 max := +math.Inf(-1)
565 for _, f := range v {
566 max = math.Max(max, f)
567 }
568 return max
569 }
570
571 func hypot(v ...float64) float64 {
572 sumsq := 0.0
573 for _, f := range v {
574 sumsq += f * f
575 }
576 return math.Sqrt(sumsq)
577 }
578
579 func length(v ...float64) float64 {
580 ss := 0.0
581 for _, f := range v {
582 ss += f * f
583 }
584 return math.Sqrt(ss)
585 }
586
587 // solveQuadMin finds the lowest solution of a 2nd-degree polynomial, using
588 // a formula which is more accurate than the textbook one
589 func solveQuadMin(a, b, c float64) float64 {
590 disc := math.Sqrt(b*b - 4*a*c)
591 r1 := 2 * c / (-b - disc)
592 return r1
593 }
594
595 // solveQuadMax finds the highest solution of a 2nd-degree polynomial, using
596 // a formula which is more accurate than the textbook one
597 func solveQuadMax(a, b, c float64) float64 {
598 disc := math.Sqrt(b*b - 4*a*c)
599 r2 := 2 * c / (-b + disc)
600 return r2
601 }
602
603 func wrap(x float64, min, max float64) float64 {
604 return (x - min) / (max - min)
605 }
606
607 func unwrap(x float64, min, max float64) float64 {
608 return (max-min)*x + min
609 }
610
611 func clamp(x float64, min, max float64) float64 {
612 return math.Min(math.Max(x, min), max)
613 }
614
615 func scale(x float64, xmin, xmax, ymin, ymax float64) float64 {
616 k := (x - xmin) / (xmax - xmin)
617 return (ymax-ymin)*k + ymin
618 }
619
620 // polyval runs horner's algorithm on a value, with the polynomial coefficients
621 // given after it, higher-order first
622 func polyval(x float64, v ...float64) float64 {
623 if len(v) == 0 {
624 return 0
625 }
626
627 x0 := x
628 x = 1.0
629 y := 0.0
630 for i := len(v) - 1; i >= 0; i-- {
631 y += v[i] * x
632 x *= x0
633 }
634 return y
635 }
File: ./fmscripts/math_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fmscripts
26
27 import "testing"
28
29 func TestTables(t *testing.T) {
30 for k, v := range determFuncs {
31 t.Run(k, func(t *testing.T) {
32 if !isSupportedFunc(v) {
33 t.Fatalf("unsupported func of type %T", v)
34 }
35 })
36 }
37 }
File: ./fmscripts/optimizers.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fmscripts
26
27 import (
28 "math"
29 )
30
31 // unary2func matches unary operators to funcs the optimizer can use to eval
32 // constant-input unary expressions into their results
33 var unary2func = map[string]func(x float64) float64{
34 // avoid unary +, since it's a no-op and thus there's no instruction for
35 // it, which in turn makes unit-tests for these optimization tables fail
36 // unnecessarily
37 // "+": func(x float64) float64 { return +x },
38
39 "-": func(x float64) float64 { return -x },
40 "!": func(x float64) float64 { return debool(x == 0) },
41 "&": func(x float64) float64 { return math.Abs(x) },
42 "*": func(x float64) float64 { return x * x },
43 "^": func(x float64) float64 { return x * x },
44 "/": func(x float64) float64 { return 1 / x },
45 }
46
47 // binary2func matches binary operators to funcs the optimizer can use to eval
48 // constant-input binary expressions into their results
49 var binary2func = map[string]func(x, y float64) float64{
50 "+": func(x, y float64) float64 { return x + y },
51 "-": func(x, y float64) float64 { return x - y },
52 "*": func(x, y float64) float64 { return x * y },
53 "/": func(x, y float64) float64 { return x / y },
54 "%": func(x, y float64) float64 { return math.Mod(x, y) },
55 "&": func(x, y float64) float64 { return debool(x != 0 && y != 0) },
56 "&&": func(x, y float64) float64 { return debool(x != 0 && y != 0) },
57 "|": func(x, y float64) float64 { return debool(x != 0 || y != 0) },
58 "||": func(x, y float64) float64 { return debool(x != 0 || y != 0) },
59 "==": func(x, y float64) float64 { return debool(x == y) },
60 "!=": func(x, y float64) float64 { return debool(x != y) },
61 "<>": func(x, y float64) float64 { return debool(x != y) },
62 "<": func(x, y float64) float64 { return debool(x < y) },
63 "<=": func(x, y float64) float64 { return debool(x <= y) },
64 ">": func(x, y float64) float64 { return debool(x > y) },
65 ">=": func(x, y float64) float64 { return debool(x >= y) },
66 "**": func(x, y float64) float64 { return math.Pow(x, y) },
67 "^": func(x, y float64) float64 { return math.Pow(x, y) },
68 }
69
70 // func2unary turns built-in func names into built-in unary operators
71 var func2unary = map[string]string{
72 "a": "&",
73 "abs": "&",
74 "inv": "/",
75 "neg": "-",
76 "negate": "-",
77 "reciprocal": "/",
78 "sq": "*",
79 "square": "*",
80 "squared": "*",
81 }
82
83 // func2binary turns built-in func names into built-in binary operators
84 var func2binary = map[string]string{
85 "mod": "%",
86 "modulus": "%",
87 "pow": "**",
88 "power": "**",
89 }
90
91 // vararg2func2 matches variable-argument funcs to their 2-argument versions
92 var vararg2func2 = map[string]string{
93 "hypot": "hypot2",
94 "max": "max2",
95 "min": "min2",
96 }
97
98 // optimize tries to simplify the expression given as much as possible, by
99 // simplifying constants whenever possible, and exploiting known built-in
100 // funcs which are known to behave deterministically
101 func (c *Compiler) optimize(expr any) any {
102 switch expr := expr.(type) {
103 case []any:
104 return c.optimizeCombo(expr)
105
106 case unaryExpr:
107 return c.optimizeUnaryExpr(expr)
108
109 case binaryExpr:
110 return c.optimizeBinaryExpr(expr)
111
112 case callExpr:
113 return c.optimizeCallExpr(expr)
114
115 case assignExpr:
116 expr.expr = c.optimize(expr.expr)
117 return expr
118
119 default:
120 f, ok := c.tryConstant(expr)
121 if ok {
122 return f
123 }
124 return expr
125 }
126 }
127
128 // optimizeCombo handles a sequence of expressions for the optimizer
129 func (c *Compiler) optimizeCombo(exprs []any) any {
130 if len(exprs) == 1 {
131 return c.optimize(exprs[0])
132 }
133
134 // count how many expressions are considered useful: these are
135 // assignments as well as the last expression, since that's what
136 // determines the script's final result
137 useful := 0
138 for i, v := range exprs {
139 _, ok := v.(assignExpr)
140 if ok || i == len(exprs)-1 {
141 useful++
142 }
143 }
144
145 // ignore all expressions which are a waste of time, and optimize
146 // all other expressions
147 res := make([]any, 0, useful)
148 for i, v := range exprs {
149 _, ok := v.(assignExpr)
150 if ok || i == len(exprs)-1 {
151 res = append(res, c.optimize(v))
152 }
153 }
154 return res
155 }
156
157 // optimizeUnaryExpr handles unary expressions for the optimizer
158 func (c *Compiler) optimizeUnaryExpr(expr unaryExpr) any {
159 // recursively optimize input
160 expr.x = c.optimize(expr.x)
161
162 // optimize unary ops on a constant into concrete values
163 if x, ok := expr.x.(float64); ok {
164 if fn, ok := unary2func[expr.op]; ok {
165 return fn(x)
166 }
167 }
168
169 switch expr.op {
170 case "+":
171 // unary plus is an identity operation
172 return expr.x
173
174 default:
175 return expr
176 }
177 }
178
179 // optimizeBinaryExpr handles binary expressions for the optimizer
180 func (c *Compiler) optimizeBinaryExpr(expr binaryExpr) any {
181 // recursively optimize inputs
182 expr.x = c.optimize(expr.x)
183 expr.y = c.optimize(expr.y)
184
185 // optimize binary ops on 2 constants into concrete values
186 if x, ok := expr.x.(float64); ok {
187 if y, ok := expr.y.(float64); ok {
188 if fn, ok := binary2func[expr.op]; ok {
189 return fn(x, y)
190 }
191 }
192 }
193
194 switch expr.op {
195 case "+":
196 if expr.x == 0.0 {
197 // 0+y -> y
198 return expr.y
199 }
200 if expr.y == 0.0 {
201 // x+0 -> x
202 return expr.x
203 }
204
205 case "-":
206 if expr.x == 0.0 {
207 // 0-y -> -y
208 return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.y})
209 }
210 if expr.y == 0.0 {
211 // x-0 -> x
212 return expr.x
213 }
214
215 case "*":
216 if expr.x == 1.0 {
217 // 1*y -> y
218 return expr.y
219 }
220 if expr.y == 1.0 {
221 // x*1 -> x
222 return expr.x
223 }
224 if expr.x == -1.0 {
225 // -1*y -> -y
226 return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.y})
227 }
228 if expr.y == -1.0 {
229 // x*-1 -> -x
230 return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.x})
231 }
232
233 case "/":
234 if expr.x == 1.0 {
235 // 1/y -> reciprocal of y
236 return c.optimizeUnaryExpr(unaryExpr{op: "/", x: expr.y})
237 }
238 if expr.y == 1.0 {
239 // x/1 -> x
240 return expr.x
241 }
242 if expr.y == -1.0 {
243 // x/-1 -> -x
244 return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.x})
245 }
246
247 case "**":
248 switch expr.y {
249 case -1.0:
250 // x**-1 -> 1/x, reciprocal of x
251 return c.optimizeUnaryExpr(unaryExpr{op: "/", x: expr.x})
252 case 1.0:
253 // x**1 -> x
254 return expr.x
255 case 2.0:
256 // x**2 -> *x
257 return c.optimizeUnaryExpr(unaryExpr{op: "*", x: expr.x})
258 case 3.0:
259 // x**3 -> *x*x
260 sq := unaryExpr{op: "*", x: expr.x}
261 return c.optimizeBinaryExpr(binaryExpr{op: "*", x: sq, y: expr.x})
262 }
263 }
264
265 // no simplifiable patterns were detected
266 return expr
267 }
268
269 // optimizeCallExpr optimizes special cases of built-in func calls
270 func (c *Compiler) optimizeCallExpr(call callExpr) any {
271 // recursively optimize all inputs, and keep track if they're all
272 // constants in the end
273 numlit := 0
274 for i, v := range call.args {
275 v = c.optimize(v)
276 call.args[i] = v
277 if _, ok := v.(float64); ok {
278 numlit++
279 }
280 }
281
282 // if func is overridden, there's no guarantee the new func works the same
283 if _, ok := determFuncs[call.name]; c.ftype[call.name] != nil || !ok {
284 return call
285 }
286
287 // from this point on, func is guaranteed to be built-in and deterministic
288
289 // handle all-const inputs, by calling func and using its result
290 if numlit == len(call.args) {
291 in := make([]float64, 0, len(call.args))
292 for _, v := range call.args {
293 f, _ := v.(float64)
294 in = append(in, f)
295 }
296
297 if f, ok := tryCall(determFuncs[call.name], in); ok {
298 return f
299 }
300 }
301
302 switch len(call.args) {
303 case 1:
304 if op, ok := func2unary[call.name]; ok {
305 expr := unaryExpr{op: op, x: call.args[0]}
306 return c.optimizeUnaryExpr(expr)
307 }
308 return call
309
310 case 2:
311 if op, ok := func2binary[call.name]; ok {
312 expr := binaryExpr{op: op, x: call.args[0], y: call.args[1]}
313 return c.optimizeBinaryExpr(expr)
314 }
315 if name, ok := vararg2func2[call.name]; ok {
316 call.name = name
317 return call
318 }
319 return call
320
321 default:
322 return call
323 }
324 }
325
326 // tryConstant tries to optimize the expression given into a constant
327 func (c *Compiler) tryConstant(expr any) (value float64, ok bool) {
328 switch expr := expr.(type) {
329 case float64:
330 return expr, true
331
332 case string:
333 if _, ok := c.vaddr[expr]; !ok {
334 // name isn't explicitly defined
335 if f, ok := mathConst[expr]; ok {
336 // and is a known math constant
337 return f, true
338 }
339 }
340 return 0, false
341
342 default:
343 return 0, false
344 }
345 }
346
347 // tryCall tries to simplify the function expression given into a constant
348 func tryCall(fn any, in []float64) (value float64, ok bool) {
349 switch fn := fn.(type) {
350 case func(float64) float64:
351 if len(in) == 1 {
352 return fn(in[0]), true
353 }
354 return 0, false
355
356 case func(float64, float64) float64:
357 if len(in) == 2 {
358 return fn(in[0], in[1]), true
359 }
360 return 0, false
361
362 case func(float64, float64, float64) float64:
363 if len(in) == 3 {
364 return fn(in[0], in[1], in[2]), true
365 }
366 return 0, false
367
368 case func(float64, float64, float64, float64) float64:
369 if len(in) == 4 {
370 return fn(in[0], in[1], in[2], in[3]), true
371 }
372 return 0, false
373
374 case func(float64, float64, float64, float64, float64) float64:
375 if len(in) == 5 {
376 return fn(in[0], in[1], in[2], in[3], in[4]), true
377 }
378 return 0, false
379
380 case func(...float64) float64:
381 return fn(in...), true
382
383 case func(float64, ...float64) float64:
384 if len(in) >= 1 {
385 return fn(in[0], in[1:]...), true
386 }
387 return 0, false
388
389 default:
390 // type isn't a supported func
391 return 0, false
392 }
393 }
File: ./fmscripts/optimizers_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fmscripts
26
27 import (
28 "math"
29 "reflect"
30 "testing"
31 )
32
33 func TestOptimizerTables(t *testing.T) {
34 for k := range unary2func {
35 t.Run(k, func(t *testing.T) {
36 if _, ok := unary2op[k]; !ok {
37 t.Fatalf("missing unary constant optimizer for %q", k)
38 }
39 })
40 }
41
42 for k := range binary2func {
43 t.Run(k, func(t *testing.T) {
44 if _, ok := binary2op[k]; !ok {
45 t.Fatalf("missing binary constant optimizer for %q", k)
46 }
47 })
48 }
49
50 for k, name := range func2unary {
51 t.Run(k, func(t *testing.T) {
52 const fs = "func(x) optimizer %q has no matching built-in func"
53 if _, ok := determFuncs[k]; !ok {
54 t.Fatalf(fs, k)
55 }
56
57 if _, ok := unary2op[name]; !ok {
58 t.Fatalf("missing unary func optimizer for %q", k)
59 }
60 })
61 }
62
63 for k, name := range func2binary {
64 t.Run(k, func(t *testing.T) {
65 const fs = "func(x, y) optimizer %q has no matching built-in func"
66 if _, ok := determFuncs[k]; !ok {
67 t.Fatalf(fs, k)
68 }
69
70 if _, ok := binary2op[name]; !ok {
71 t.Fatalf("missing binary func optimizer for %q", k)
72 }
73 })
74 }
75
76 for k := range vararg2func2 {
77 t.Run(k, func(t *testing.T) {
78 if _, ok := determFuncs[k]; !ok {
79 const fs = "vararg optimizer %q has no matching built-in func"
80 t.Fatalf(fs, k)
81 }
82 })
83 }
84 }
85
86 func TestOptimizer(t *testing.T) {
87 tests := map[string]any{
88 `1`: 1.0,
89 `3+4*5`: 23.0,
90 `e`: math.E,
91 `pi`: math.Pi,
92 `phi`: math.Phi,
93 `2*pi`: 2 * math.Pi,
94 `4.51*phi-14.23564`: 4.51*math.Phi - 14.23564,
95 `-e`: -math.E,
96 `exp(2*pi)`: math.Exp(2 * math.Pi),
97 `log(2342.55) / log(43.21)`: math.Log(2342.55) / math.Log(43.21),
98 `f(3)`: callExpr{name: `f`, args: []any{3.0}},
99 `min(3, 2, -1.5)`: -1.5,
100
101 `hypot(x, 4)`: callExpr{name: `hypot2`, args: []any{`x`, 4.0}},
102 `max(x, 4)`: callExpr{name: `max2`, args: []any{`x`, 4.0}},
103 `min(x, 4)`: callExpr{name: `min2`, args: []any{`x`, 4.0}},
104 `rand()`: callExpr{name: `rand`},
105
106 `sin(2_000 * x * tau * x)`: callExpr{
107 name: `sin`,
108 args: []any{
109 binaryExpr{
110 `*`,
111 binaryExpr{
112 `*`,
113 binaryExpr{`*`, 2_000.0, `x`},
114 2 * math.Pi,
115 },
116 `x`,
117 },
118 },
119 },
120
121 `sin(10 * tau * exp(-20 * x)) * exp(-2 * x)`: binaryExpr{
122 `*`,
123 // sin(...)
124 callExpr{
125 name: `sin`,
126 args: []any{
127 // 10 * tau * exp(...)
128 binaryExpr{
129 `*`,
130 10 * 2 * math.Pi,
131 // exp(-20 * x)
132 callExpr{
133 name: `exp`,
134 args: []any{
135 binaryExpr{`*`, -20.0, `x`},
136 },
137 },
138 },
139 },
140 },
141 // exp(-2 * x)
142 callExpr{
143 name: `exp`,
144 args: []any{binaryExpr{`*`, -2.0, `x`}},
145 },
146 },
147 }
148
149 defs := map[string]any{
150 `x`: 3.5,
151 `f`: math.Exp,
152 }
153
154 for source, expected := range tests {
155 t.Run(source, func(t *testing.T) {
156 var c Compiler
157 root, err := parse(source)
158 if err != nil {
159 t.Fatal(err)
160 return
161 }
162
163 if err := c.reset(defs); err != nil {
164 t.Fatal(err)
165 return
166 }
167
168 got := c.optimize(root)
169 if !reflect.DeepEqual(got, expected) {
170 const fs = "expected result to be\n%#v\ninstead of\n%#v"
171 t.Fatalf(fs, expected, got)
172 return
173 }
174 })
175 }
176 }
File: ./fmscripts/parsing.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fmscripts
26
27 import (
28 "errors"
29 "fmt"
30 "strconv"
31 "strings"
32 )
33
34 // parse turns source code into an expression type interpreters can use.
35 func parse(src string) (any, error) {
36 tok := newTokenizer(src)
37 par, err := newParser(&tok)
38 if err != nil {
39 return nil, err
40 }
41
42 v, err := par.parse()
43 err = par.improveError(err, src)
44 return v, err
45 }
46
47 // pickLine slices a string, picking one of its lines via a 1-based index:
48 // func improveError uses it to isolate the line of code an error came from
49 func pickLine(src string, linenum int) string {
50 // skip the lines before the target one
51 for i := 0; i < linenum && len(src) > 0; i++ {
52 j := strings.IndexByte(src, '\n')
53 if j < 0 {
54 break
55 }
56 src = src[j+1:]
57 }
58
59 // limit leftover to a single line
60 i := strings.IndexByte(src, '\n')
61 if i >= 0 {
62 return src[:i]
63 }
64 return src
65 }
66
67 // unaryExpr is a unary expression
68 type unaryExpr struct {
69 op string
70 x any
71 }
72
73 // binaryExpr is a binary expression
74 type binaryExpr struct {
75 op string
76 x any
77 y any
78 }
79
80 // callExpr is a function call and its arguments
81 type callExpr struct {
82 name string
83 args []any
84 }
85
86 // assignExpr is a value/variable assignment
87 type assignExpr struct {
88 name string
89 expr any
90 }
91
92 // parser is a parser for JavaScript-like syntax, limited to operations on
93 // 64-bit floating-point numbers
94 type parser struct {
95 tokens []token
96 line int
97 pos int
98 toklen int
99 }
100
101 // newParser is the constructor for type parser
102 func newParser(t *tokenizer) (parser, error) {
103 var p parser
104
105 // get all tokens from the source code
106 for {
107 v, err := t.next()
108 if v.kind != unknownToken {
109 p.tokens = append(p.tokens, v)
110 }
111
112 if err == errEOS {
113 // done scanning/tokenizing
114 return p, nil
115 }
116
117 if err != nil {
118 // handle actual errors
119 return p, err
120 }
121 }
122 }
123
124 // improveError adds more info about where exactly in the source code an error
125 // came from, thus making error messages much more useful
126 func (p *parser) improveError(err error, src string) error {
127 if err == nil {
128 return nil
129 }
130
131 line := pickLine(src, p.line)
132 if len(line) == 0 || p.pos < 1 {
133 const fs = "(line %d: pos %d): %w"
134 return fmt.Errorf(fs, p.line, p.pos, err)
135 }
136
137 ptr := strings.Repeat(" ", p.pos) + "^"
138 const fs = "(line %d: pos %d): %w\n%s\n%s"
139 return fmt.Errorf(fs, p.line, p.pos, err, line, ptr)
140 }
141
142 // parse tries to turn tokens into a compilable abstract syntax tree, and is
143 // the parser's entry point
144 func (p *parser) parse() (any, error) {
145 // source codes with no tokens are always errors
146 if len(p.tokens) == 0 {
147 const msg = "source code is empty, or has no useful expressions"
148 return nil, errors.New(msg)
149 }
150
151 // handle optional excel-like leading equal sign
152 p.acceptSyntax(`=`)
153
154 // ignore trailing semicolons
155 for len(p.tokens) > 0 {
156 t := p.tokens[len(p.tokens)-1]
157 if t.kind != syntaxToken || t.value != `;` {
158 break
159 }
160 p.tokens = p.tokens[:len(p.tokens)-1]
161 }
162
163 // handle single expressions as well as multiple semicolon-separated
164 // expressions: the latter allow value assignments/updates in scripts
165 var res []any
166 for keepGoing := true; keepGoing && len(p.tokens) > 0; {
167 v, err := p.parseExpression()
168 if err != nil && err != errEOS {
169 return v, err
170 }
171
172 res = append(res, v)
173 // handle optional separator/continuation semicolons
174 _, keepGoing = p.acceptSyntax(`;`)
175 }
176
177 // unexpected unparsed trailing tokens are always errors; any trailing
178 // semicolons in the original script are already trimmed
179 if len(p.tokens) > 0 {
180 const fs = "unexpected %s"
181 return res, fmt.Errorf(fs, p.tokens[0].value)
182 }
183
184 // make scripts ending in an assignment also load that value, so they're
185 // useful, as assignments result in no useful value by themselves
186 assign, ok := res[len(res)-1].(assignExpr)
187 if ok {
188 res = append(res, assign.name)
189 }
190
191 // turn 1-item combo expressions into their only expression: some
192 // unit tests may rely on that for convenience
193 if len(res) == 1 {
194 return res[0], nil
195 }
196 return res, nil
197 }
198
199 // acceptSyntax advances the parser on the first syntactic string matched:
200 // notice any number of alternatives/options are allowed, as the syntax
201 // allows/requires at various points
202 func (p *parser) acceptSyntax(syntax ...string) (match string, ok bool) {
203 if len(p.tokens) == 0 {
204 return "", false
205 }
206
207 t := p.tokens[0]
208 if t.kind != syntaxToken {
209 return "", false
210 }
211
212 for _, s := range syntax {
213 if t.value == s {
214 p.advance()
215 return s, true
216 }
217 }
218 return "", false
219 }
220
221 // advance skips the current leading token, if there are still any left
222 func (p *parser) advance() {
223 if len(p.tokens) == 0 {
224 return
225 }
226
227 t := p.tokens[0]
228 p.tokens = p.tokens[1:]
229 p.line = t.line
230 p.pos = t.pos
231 p.toklen = len(t.value)
232 }
233
234 // acceptNumeric advances the parser on a numeric value, but only if it's
235 // the leading token: conversely, any other type of token doesn't advance
236 // the parser; when matches happen the resulting strings need parsing via
237 // func parseNumber
238 func (p *parser) acceptNumeric() (numliteral string, ok bool) {
239 if len(p.tokens) == 0 {
240 return "", false
241 }
242
243 t := p.tokens[0]
244 if t.kind == numberToken {
245 p.advance()
246 return t.value, true
247 }
248 return "", false
249 }
250
251 // demandSyntax imposes a specific syntactic element to follow, or else it's
252 // an error
253 func (p *parser) demandSyntax(syntax string) error {
254 if len(p.tokens) == 0 {
255 return fmt.Errorf("expected %s instead of the end of source", syntax)
256 }
257
258 first := p.tokens[0]
259 if first.kind == syntaxToken && first.value == syntax {
260 p.advance()
261 return nil
262 }
263 return fmt.Errorf("expected %s instead of %s", syntax, first.value)
264 }
265
266 func (p *parser) parseExpression() (any, error) {
267 x, err := p.parseComparison()
268 if err != nil {
269 return x, err
270 }
271
272 // handle assignment statements
273 if _, ok := p.acceptSyntax(`=`, `:=`); ok {
274 varname, ok := x.(string)
275 if !ok {
276 const fs = "expected a variable name, instead of a %T"
277 return nil, fmt.Errorf(fs, x)
278 }
279
280 x, err := p.parseExpression()
281 expr := assignExpr{name: varname, expr: x}
282 return expr, err
283 }
284
285 // handle and/or logical chains
286 for {
287 if op, ok := p.acceptSyntax(`&&`, `||`, `&`, `|`); ok {
288 y, err := p.parseExpression()
289 if err != nil {
290 return y, err
291 }
292 x = binaryExpr{op: op, x: x, y: y}
293 continue
294 }
295 break
296 }
297
298 // handle maybe-properties
299 if _, ok := p.acceptSyntax(`??`); ok {
300 y, err := p.parseExpression()
301 expr := callExpr{name: "??", args: []any{x, y}}
302 return expr, err
303 }
304
305 // handle choice/ternary operator
306 if _, ok := p.acceptSyntax(`?`); ok {
307 y, err := p.parseExpression()
308 if err != nil {
309 expr := callExpr{name: "?:", args: []any{x, nil, nil}}
310 return expr, err
311 }
312
313 if _, ok := p.acceptSyntax(`:`); ok {
314 z, err := p.parseExpression()
315 expr := callExpr{name: "?:", args: []any{x, y, z}}
316 return expr, err
317 }
318
319 if len(p.tokens) == 0 {
320 expr := callExpr{name: "?:", args: []any{x, y, nil}}
321 return expr, errors.New("expected `:`")
322 }
323
324 s := p.tokens[0].value
325 expr := callExpr{name: "?:", args: []any{x, y, nil}}
326 err = fmt.Errorf("expected `:`, but got %q instead", s)
327 return expr, err
328 }
329
330 // expression was just a comparison, or simpler
331 return x, nil
332 }
333
334 func (p *parser) parseComparison() (any, error) {
335 x, err := p.parseTerm()
336 if err != nil {
337 return x, err
338 }
339
340 op, ok := p.acceptSyntax(`==`, `!=`, `<`, `>`, `<=`, `>=`, `<>`, `===`, `!==`)
341 if ok {
342 y, err := p.parseTerm()
343 return binaryExpr{op: op, x: x, y: y}, err
344 }
345 return x, err
346 }
347
348 // parseBinary handles binary operations, by recursing depth-first on the left
349 // side of binary expressions; going tail-recursive on these would reverse the
350 // order of arguments instead, which is obviously wrong
351 func (p *parser) parseBinary(parse func() (any, error), syntax ...string) (any, error) {
352 x, err := parse()
353 if err != nil {
354 return x, err
355 }
356
357 for {
358 op, ok := p.acceptSyntax(syntax...)
359 if !ok {
360 return x, nil
361 }
362
363 y, err := parse()
364 x = binaryExpr{op: op, x: x, y: y}
365 if err != nil {
366 return x, err
367 }
368 }
369 }
370
371 func (p *parser) parseTerm() (any, error) {
372 return p.parseBinary(func() (any, error) {
373 return p.parseProduct()
374 }, `+`, `-`, `^`)
375 }
376
377 func (p *parser) parseProduct() (any, error) {
378 return p.parseBinary(func() (any, error) {
379 return p.parsePower()
380 }, `*`, `/`, `%`)
381 }
382
383 func (p *parser) parsePower() (any, error) {
384 return p.parseBinary(func() (any, error) {
385 return p.parseValue()
386 }, `**`, `^`)
387 }
388
389 func (p *parser) parseValue() (any, error) {
390 // handle unary operators which can also be considered part of numeric
391 // literals, and thus should be simplified away
392 if op, ok := p.acceptSyntax(`+`, `-`); ok {
393 if s, ok := p.acceptNumeric(); ok {
394 x, err := strconv.ParseFloat(s, 64)
395 if err != nil {
396 return nil, err
397 }
398 if simpler, ok := simplifyNumber(op, x); ok {
399 return simpler, nil
400 }
401 return unaryExpr{op: op, x: x}, nil
402 }
403
404 x, err := p.parsePower()
405 return unaryExpr{op: op, x: x}, err
406 }
407
408 // handle all other unary operators
409 if op, ok := p.acceptSyntax(`!`, `&`, `*`, `^`); ok {
410 x, err := p.parsePower()
411 return unaryExpr{op: op, x: x}, err
412 }
413
414 // handle subexpression in parentheses
415 if _, ok := p.acceptSyntax(`(`); ok {
416 x, err := p.parseExpression()
417 if err != nil {
418 return x, err
419 }
420
421 if err := p.demandSyntax(`)`); err != nil {
422 return x, err
423 }
424 return p.parseAccessors(x)
425 }
426
427 // handle subexpression in square brackets: it's just an alternative to
428 // using parentheses for subexpressions
429 if _, ok := p.acceptSyntax(`[`); ok {
430 x, err := p.parseExpression()
431 if err != nil {
432 return x, err
433 }
434
435 if err := p.demandSyntax(`]`); err != nil {
436 return x, err
437 }
438 return p.parseAccessors(x)
439 }
440
441 // handle all other cases
442 x, err := p.parseSimpleValue()
443 if err != nil {
444 return x, err
445 }
446
447 // handle arbitrarily-long chains of accessors
448 return p.parseAccessors(x)
449 }
450
451 // parseSimpleValue handles a numeric literal or a variable/func name, also
452 // known as identifier
453 func (p *parser) parseSimpleValue() (any, error) {
454 if len(p.tokens) == 0 {
455 return nil, errEOS
456 }
457 t := p.tokens[0]
458
459 switch t.kind {
460 case identifierToken:
461 p.advance()
462 // handle func calls, such as f(...)
463 if _, ok := p.acceptSyntax(`(`); ok {
464 args, err := p.parseList(`)`)
465 expr := callExpr{name: t.value, args: args}
466 return expr, err
467 }
468 // handle func calls, such as f[...]
469 if _, ok := p.acceptSyntax(`[`); ok {
470 args, err := p.parseList(`]`)
471 expr := callExpr{name: t.value, args: args}
472 return expr, err
473 }
474 return t.value, nil
475
476 case numberToken:
477 p.advance()
478 return strconv.ParseFloat(t.value, 64)
479
480 default:
481 const fs = "unexpected %s (token type %d)"
482 return nil, fmt.Errorf(fs, t.value, t.kind)
483 }
484 }
485
486 // parseAccessors handles an arbitrarily-long chain of accessors
487 func (p *parser) parseAccessors(x any) (any, error) {
488 for {
489 s, ok := p.acceptSyntax(`.`, `@`)
490 if !ok {
491 // dot-chain is over
492 return x, nil
493 }
494
495 // handle property/method accessors
496 v, err := p.parseDot(s, x)
497 if err != nil {
498 return v, err
499 }
500 x = v
501 }
502 }
503
504 // parseDot handles what follows a syntactic dot, as opposed to a dot which
505 // may be part of a numeric literal
506 func (p *parser) parseDot(after string, x any) (any, error) {
507 if len(p.tokens) == 0 {
508 const fs = "unexpected end of source after a %q"
509 return x, fmt.Errorf(fs, after)
510 }
511
512 t := p.tokens[0]
513 p.advance()
514 if t.kind != identifierToken {
515 const fs = "expected a valid property name, but got %s instead"
516 return x, fmt.Errorf(fs, t.value)
517 }
518
519 if _, ok := p.acceptSyntax(`(`); ok {
520 items, err := p.parseList(`)`)
521 args := append([]any{x}, items...)
522 return callExpr{name: t.value, args: args}, err
523 }
524
525 if _, ok := p.acceptSyntax(`[`); ok {
526 items, err := p.parseList(`]`)
527 args := append([]any{x}, items...)
528 return callExpr{name: t.value, args: args}, err
529 }
530
531 return callExpr{name: t.value, args: []any{x}}, nil
532 }
533
534 // parseList handles the argument-list following a `(` or a `[`
535 func (p *parser) parseList(end string) ([]any, error) {
536 var arr []any
537 for len(p.tokens) > 0 {
538 if _, ok := p.acceptSyntax(`,`); ok {
539 // ensure extra/trailing commas are allowed/ignored
540 continue
541 }
542
543 if _, ok := p.acceptSyntax(end); ok {
544 return arr, nil
545 }
546
547 v, err := p.parseExpression()
548 if err != nil {
549 return arr, err
550 }
551 arr = append(arr, v)
552 }
553
554 // return an appropriate error for the unexpected end of the source
555 return arr, p.demandSyntax(`)`)
556 }
557
558 // simplifyNumber tries to simplify a few trivial unary operations on
559 // numeric constants
560 func simplifyNumber(op string, x any) (v any, ok bool) {
561 if x, ok := x.(float64); ok {
562 switch op {
563 case `+`:
564 return x, true
565 case `-`:
566 return -x, true
567 default:
568 return x, false
569 }
570 }
571 return x, false
572 }
File: ./fmscripts/programs.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fmscripts
26
27 import (
28 "math"
29 )
30
31 // So far, my apps relying on this package are
32 //
33 // - fh, a Function Heatmapper which color-codes f(x, y), seen from above
34 // - star, a STAtistical Resampler to calculate stats from tabular data
35 // - waveout, an app to emit sample-by-sample wave-audio by formula
36
37 // Quick Notes on Performance
38 //
39 // Note: while these microbenchmarks are far from proper benchmarks, Club Pulse
40 // can still give a general idea of what relative performance to expect.
41 //
42 // While funscripts.Interpreter can do anything a Program can do, and much more,
43 // a fmscripts.Program is way faster for float64-only tasks: typical speed-ups
44 // are between 20-to-50 times. Various simple benchmarks suggest running speed
45 // is between 1/2 and 1/4 of native funcs.
46 //
47 // Reimplementations of the Club Pulse benchmark, when run 50 million times,
48 // suggest this package is starting to measure the relative speed of various
49 // trigonometric func across JIT-ted script runners, running somewhat slower
50 // than NodeJS/V8, faster than PyPy, and much faster than Python.
51 //
52 // Other scripted tests, which aren't as trigonometry/exponential-heavy, hint
53 // at even higher speed-ups compared to Python and PyPy, as well as being
54 // almost as fast as NodeJS/V8.
55 //
56 // Being so close to Node's V8 (0.6x - 0.8x its speed) is a surprisingly good
57 // result for the relatively little effort this package took.
58
59 const (
60 maxArgsLen = 1<<16 - 1 // max length for the []float64 input of callv
61 maxOpIndex = 1<<32 - 1 // max index for values
62 )
63
64 // types to keep compiler code the same, while changing sizes of numOp fields
65 type (
66 opcode uint16
67 opargs uint16 // opcode directly affects max correct value of maxArgsLen
68 opindex uint32 // opcode directly affects max correct value of maxOpIndex
69 )
70
71 // numOp is a numeric operation in a program
72 type numOp struct {
73 What opcode // what to do
74 NumArgs opargs // vector-length only used for callv and call1v
75 Index opindex // index to load a value or call a function
76 }
77
78 // Since the go 1.19 compiler started compiling dense switch statements using
79 // jump tables, adding more basic ops doesn't slow things down anymore when
80 // reaching the next power-of-2 thresholds in the number of cases.
81
82 const (
83 // load float64 value to top of the stack
84 load opcode = iota
85
86 // pop top of stack and store it into value
87 store opcode = iota
88
89 // unary operations
90 neg opcode = iota
91 not opcode = iota
92 abs opcode = iota
93 square opcode = iota
94 // 1/x, the reciprocal/inverse value
95 rec opcode = iota
96 // x%1, but faster than math.Mod(x, 1)
97 mod1 opcode = iota
98
99 // arithmetic and logic operations: 2 float64 inputs, 1 float64 output
100
101 add opcode = iota
102 sub opcode = iota
103 mul opcode = iota
104 div opcode = iota
105 mod opcode = iota
106 pow opcode = iota
107 and opcode = iota
108 or opcode = iota
109
110 // binary comparisons: 2 float64 inputs, 1 booleanized float64 output
111
112 equal opcode = iota
113 notequal opcode = iota
114 less opcode = iota
115 lessoreq opcode = iota
116 more opcode = iota
117 moreoreq opcode = iota
118
119 // function callers with 1..5 float64 inputs and a float64 result
120
121 call0 opcode = iota
122 call1 opcode = iota
123 call2 opcode = iota
124 call3 opcode = iota
125 call4 opcode = iota
126 call5 opcode = iota
127
128 // var-arg input function callers
129
130 callv opcode = iota
131 call1v opcode = iota
132 )
133
134 // Program runs a sequence of float64 operations, with no explicit control-flow:
135 // implicit control-flow is available in the float64-only functions you make
136 // available to such programs. Such custom funcs must all return a float64, and
137 // either take from 0 to 5 float64 arguments, or a single float64 array.
138 //
139 // As the name suggests, don't create such objects directly, but instead use
140 // Compiler.Compile to create them. Only a Compiler lets you register variables
141 // and functions, which then become part of your numeric Program.
142 //
143 // A Program lets you change values before each run, using pointers from method
144 // Program.Get: such pointers are guaranteed never to change before or across
145 // runs. Just ensure you get a variable defined in the Compiler used to make the
146 // Program, or the pointer will be to a dummy place which has no effect on final
147 // results.
148 //
149 // Expressions using literals are automatically optimized into their results:
150 // this also applies to func calls with standard math functions and constants.
151 //
152 // The only way to limit such optimizations is to redefine common math funcs and
153 // const values explicitly when compiling: after doing so, all those value-names
154 // will stand for externally-updatable values which can change from one run to
155 // the next. Similarly, there's no guarantee the (re)defined functions will be
156 // deterministic, like the defaults they replaced.
157 //
158 // # Example
159 //
160 // var c fmscripts.Compiler
161 //
162 // defs := map[string]any{
163 // "x": 0, // define `x`, and initialize it to 0
164 // "k": 4.25, // define `k`, and initialize it to 4.25
165 // "b": true, // define `b`, and initialize it to 1.0
166 // "n": -23, // define `n`, and initialize it to -23.0
167 // "pi": 3, // define `pi`, overriding the automatic constant named `pi`
168 //
169 // "f": numericKernel // type is func ([]float64) float64
170 // "g": otherFunc // type is func (float64) float64
171 // }
172 //
173 // prog, err := c.Compile("log10(k) + f(sqrt(k) * exp(-x), 45, -0.23)", defs)
174 // // ...
175 //
176 // x, _ := prog.Get("x") // Get returns (*float64, bool)
177 // y, _ := prog.Get("y") // a useless pointer, since program doesn't use `y`
178 // // ...
179 //
180 // for i := 0; i < n; i++ {
181 // *x = float64(i)*dx + minx // you update inputs in place using pointers
182 // f := prog.Run() // method Run gives you a float64 back
183 // // ...
184 // }
185 type Program struct {
186 sp int // stack pointer, even though it's an index
187
188 stack []float64 // pre-allocated by compiler to max length needed by program
189 values []float64 // holds all values, whether literals, or variables
190
191 ops []numOp // all sequential operations for each run
192 funcs []any // all funcs used by the program
193
194 names map[string]int // variable-name to index lookup
195 dummy float64 // all pointers for undefined variables point here
196
197 // data []float64
198 }
199
200 // Get lets you change parameters/variables before each time you run a program,
201 // since it doesn't return a value, but a pointer to it, so you can update it
202 // in place.
203 //
204 // If the name given isn't available, the result is a pointer to a dummy place:
205 // this ensures non-nil pointers, which are always safe to use, even though
206 // updates to the dummy destination have no effect on program results.
207 func (p *Program) Get(name string) (ptr *float64, useful bool) {
208 if i, ok := p.names[name]; ok {
209 return &p.values[i], true
210 }
211 return &p.dummy, false
212 }
213
214 // Clone creates an exact copy of a Program with all values in their current
215 // state: this is useful when dispatching embarassingly-parallel tasks to
216 // multiple programs.
217 func (p Program) Clone() Program {
218 // can't share the stack nor the values
219 stack := make([]float64, len(p.stack))
220 values := make([]float64, len(p.values))
221 copy(stack, p.stack)
222 copy(values, p.values)
223 p.stack = stack
224 p.values = values
225
226 // can share everything else as is
227 return p
228 }
229
230 // Memo: the command to show all bound checks is
231 // go test -gcflags="-d=ssa/check_bce/debug=1" fmscripts/programs.go
232
233 // Discussion about go compiler optimizing switch statements into jump tables
234 // https://go-review.googlesource.com/c/go/+/357330/
235
236 // Run executes the program once. Before each run, update input values using
237 // pointers from method Get.
238 func (p *Program) Run() float64 {
239 // Check for empty programs: these happen either when a compilation error
240 // was ignored, or when a program was explicitly declared as a variable.
241 if len(p.ops) == 0 {
242 return math.NaN()
243 }
244
245 p.sp = 0
246 p.runAllOps()
247 return p.stack[p.sp]
248 }
249
250 type func4 = func(float64, float64, float64, float64) float64
251 type func5 = func(float64, float64, float64, float64, float64) float64
252
253 // runAllOps runs all operations in a loop: when done, the program's result is
254 // ready as the only item left in the stack
255 func (p *Program) runAllOps() {
256 for _, op := range p.ops {
257 // shortcut for the current stack pointer
258 sp := p.sp
259
260 // Preceding binary ops and func calls by _ = p.stack[i-n] prevents
261 // an extra bound check for the lhs of assignments, but a quick
262 // statistical summary of benchmarks doesn't show clear speedups,
263 // let alone major ones.
264 //
265 // Separating different func types into different arrays to avoid
266 // type-checking at runtime doesn't seem to be worth it either,
267 // and makes the compiler more complicated.
268
269 switch op.What {
270 case load:
271 // store above top of stack
272 p.stack[sp+1] = p.values[op.Index]
273 p.sp++
274 case store:
275 // store from top of the stack
276 p.values[op.Index] = p.stack[sp]
277 p.sp--
278
279 // unary operations
280 case neg:
281 p.stack[sp] = -p.stack[sp]
282 case not:
283 p.stack[sp] = deboolNot(p.stack[sp])
284 case abs:
285 p.stack[sp] = math.Abs(p.stack[sp])
286 case square:
287 p.stack[sp] *= p.stack[sp]
288 case rec:
289 p.stack[sp] = 1 / p.stack[sp]
290
291 // binary arithmetic ops
292 case add:
293 p.stack[sp-1] += p.stack[sp]
294 p.sp--
295 case sub:
296 p.stack[sp-1] -= p.stack[sp]
297 p.sp--
298 case mul:
299 p.stack[sp-1] *= p.stack[sp]
300 p.sp--
301 case div:
302 p.stack[sp-1] /= p.stack[sp]
303 p.sp--
304 case mod:
305 p.stack[sp-1] = math.Mod(p.stack[sp-1], p.stack[sp])
306 p.sp--
307 case pow:
308 p.stack[sp-1] = math.Pow(p.stack[sp-1], p.stack[sp])
309 p.sp--
310
311 // binary boolean ops / binary comparisons
312 case and:
313 p.stack[sp-1] = deboolAnd(p.stack[sp-1], p.stack[sp])
314 p.sp--
315 case or:
316 p.stack[sp-1] = deboolOr(p.stack[sp-1], p.stack[sp])
317 p.sp--
318 case equal:
319 p.stack[sp-1] = debool(p.stack[sp-1] == p.stack[sp])
320 p.sp--
321 case notequal:
322 p.stack[sp-1] = debool(p.stack[sp-1] != p.stack[sp])
323 p.sp--
324 case less:
325 p.stack[sp-1] = debool(p.stack[sp-1] < p.stack[sp])
326 p.sp--
327 case lessoreq:
328 p.stack[sp-1] = debool(p.stack[sp-1] <= p.stack[sp])
329 p.sp--
330 case more:
331 p.stack[sp-1] = debool(p.stack[sp-1] > p.stack[sp])
332 p.sp--
333 case moreoreq:
334 p.stack[sp-1] = debool(p.stack[sp-1] >= p.stack[sp])
335 p.sp--
336
337 // function calls
338 case call0:
339 f := p.funcs[op.Index].(func() float64)
340 // store above top of stack
341 p.stack[sp+1] = f()
342 p.sp++
343 case call1:
344 f := p.funcs[op.Index].(func(float64) float64)
345 p.stack[sp] = f(p.stack[sp])
346 case call2:
347 f := p.funcs[op.Index].(func(float64, float64) float64)
348 p.stack[sp-1] = f(p.stack[sp-1], p.stack[sp])
349 p.sp--
350 case call3:
351 f := p.funcs[op.Index].(func(float64, float64, float64) float64)
352 p.stack[sp-2] = f(p.stack[sp-2], p.stack[sp-1], p.stack[sp])
353 p.sp -= 2
354 case call4:
355 f := p.funcs[op.Index].(func4)
356 st := p.stack
357 p.stack[sp-3] = f(st[sp-3], st[sp-2], st[sp-1], st[sp])
358 p.sp -= 3
359 case call5:
360 f := p.funcs[op.Index].(func5)
361 st := p.stack
362 p.stack[sp-4] = f(st[sp-4], st[sp-3], st[sp-2], st[sp-1], st[sp])
363 p.sp -= 4
364 case callv:
365 i := sp - int(op.NumArgs) + 1
366 f := p.funcs[op.Index].(func(...float64) float64)
367 p.stack[sp-i+1] = f(p.stack[i : sp+1]...)
368 p.sp = sp - i + 1
369 case call1v:
370 i := sp - int(op.NumArgs) + 1
371 f := p.funcs[op.Index].(func(float64, ...float64) float64)
372 p.stack[sp-i+1] = f(p.stack[i], p.stack[i+1:sp+1]...)
373 p.sp = sp - i + 1
374 }
375 }
376 }
377
378 // debool is only used to turn boolean values used in comparison operations
379 // into float64s, since those are the only type accepted on a program stack
380 func debool(b bool) float64 {
381 if b {
382 return 1
383 }
384 return 0
385 }
386
387 // deboolNot runs the basic `not` operation
388 func deboolNot(x float64) float64 {
389 return debool(x == 0)
390 }
391
392 // deboolAnd runs the basic `and` operation
393 func deboolAnd(x, y float64) float64 {
394 return debool(x != 0 && y != 0)
395 }
396
397 // deboolOr runs the basic `or` operation
398 func deboolOr(x, y float64) float64 {
399 return debool(x != 0 || y != 0)
400 }
File: ./fmscripts/programs_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fmscripts
26
27 import "testing"
28
29 func TestCallVarFuncs(t *testing.T) {
30 tests := []struct {
31 name string
32 src string
33 exp float64
34 }{
35 {"check 1 var arg", "numargs(34)", 1},
36 {"check 2 var arg", "numargs(34, 33)", 2},
37 {"check 3 var arg", "numargs(34, 322, 0.3)", 3},
38 {"check 4 var arg", "numargs(34, -1, 553, 42)", 4},
39 {"check 5 var arg", "numargs(34, 3, 4, 5, 1)", 5},
40 }
41
42 numargs := func(args ...float64) float64 { return float64(len(args)) }
43
44 for _, tc := range tests {
45 t.Run(tc.name, func(t *testing.T) {
46 var c Compiler
47 p, err := c.Compile(tc.src, map[string]any{
48 "numargs": numargs,
49 })
50 if err != nil {
51 t.Fatal(err)
52 return
53 }
54
55 got := p.Run()
56 const fs = "failed check (var-arg): expected %f, got %f instead"
57 if got != tc.exp {
58 t.Fatalf(fs, tc.exp, got)
59 return
60 }
61 })
62 }
63 }
File: ./fmscripts/random.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fmscripts
26
27 import (
28 "math"
29 "math/rand"
30 )
31
32 // These funcs are kept separate from the larger lookup table so they aren't
33 // mistaken for deterministic funcs which can be optimized away.
34 //
35 // var randomFuncs = map[string]any{
36 // "rand": Random,
37 // "rint": RandomInt,
38 // "runif": RandomUnif,
39 // "rexp": RandomExp,
40 // "rnorm": RandomNorm,
41 // "rgamma": RandomGamma,
42 // "rbeta": RandomBeta,
43 // }
44
45 func Random(r *rand.Rand) float64 {
46 return r.Float64()
47 }
48
49 func RandomInt(r *rand.Rand, min, max float64) float64 {
50 fmin := math.Trunc(min)
51 fmax := math.Trunc(max)
52 if fmin == fmax {
53 return fmin
54 }
55
56 diff := math.Abs(fmax - fmin)
57 return float64(r.Intn(int(diff)+1)) + fmin
58 }
59
60 func RandomUnif(r *rand.Rand, min, max float64) float64 {
61 return (max-min)*r.Float64() + min
62 }
63
64 func RandomExp(r *rand.Rand, scale float64) float64 {
65 return scale * r.ExpFloat64()
66 }
67
68 func RandomNorm(r *rand.Rand, mu, sigma float64) float64 {
69 return sigma*r.NormFloat64() + mu
70 }
71
72 // Gamma generates a gamma-distributed real value, using a scale parameter.
73 //
74 // The algorithm is from Marsaglia and Tsang, as described in
75 //
76 // A simple method for generating gamma variables
77 // https://dl.acm.org/doi/10.1145/358407.358414
78 func RandomGamma(r *rand.Rand, scale float64) float64 {
79 d := scale - 1.0/3.0
80 c := 1 / math.Sqrt(9/d)
81
82 for {
83 // generate candidate value
84 var x, v float64
85 for {
86 x = r.NormFloat64()
87 v = 1 + c*x
88 if v > 0 {
89 break
90 }
91 }
92 v = v * v * v
93
94 // accept or reject candidate value
95 x2 := x * x
96 u := r.Float64()
97 if u < 1-0.0331*x2*x2 {
98 return d * v
99 }
100 if math.Log(u) < 0.5*x2+d*(1-v+math.Log(v)) {
101 return d * v
102 }
103 }
104 }
105
106 // Beta generates a beta-distributed real value.
107 func RandomBeta(r *rand.Rand, a, b float64) float64 {
108 return RandomGamma(r, a) / (RandomGamma(r, a) + RandomGamma(r, b))
109 }
File: ./fmscripts/tokens.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fmscripts
26
27 import (
28 "errors"
29 "fmt"
30 "strings"
31 "unicode"
32 "unicode/utf8"
33 )
34
35 // tokenType is the specific type of a token; tokens never represent
36 // whitespace-like text between recognized tokens
37 type tokenType int
38
39 const (
40 // default zero-value type for tokens
41 unknownToken tokenType = iota
42
43 // a name
44 identifierToken tokenType = iota
45
46 // a literal numeric value
47 numberToken tokenType = iota
48
49 // any syntactic element made of 1 or more runes
50 syntaxToken tokenType = iota
51 )
52
53 // errEOS signals the end of source code, and is the only token-related error
54 // which the parser should ignore
55 var errEOS = errors.New(`no more source code`)
56
57 // token is either a name, value, or syntactic element coming from a script's
58 // source code
59 type token struct {
60 kind tokenType
61 value string
62 line int
63 pos int
64 }
65
66 // tokenizer splits a string into a stream tokens, via its `next` method
67 type tokenizer struct {
68 cur string
69 linenum int
70 linepos int
71 }
72
73 // newTokenizer is the constructor for type tokenizer
74 func newTokenizer(src string) tokenizer {
75 return tokenizer{
76 cur: src,
77 linenum: 1,
78 linepos: 1,
79 }
80 }
81
82 // next advances the tokenizer, giving back a token, unless it's done
83 func (t *tokenizer) next() (token, error) {
84 // label to allow looping back after skipping comments, and thus avoid
85 // an explicit tail-recursion for each commented line
86 rerun:
87
88 // always ignore any whitespace-like source
89 if err := t.skipWhitespace(); err != nil {
90 return token{}, err
91 }
92
93 if len(t.cur) == 0 {
94 return token{}, errEOS
95 }
96
97 // remember starting position, in case of error
98 line := t.linenum
99 pos := t.linepos
100
101 // use the leading rune to probe what's next
102 r, _ := utf8.DecodeRuneInString(t.cur)
103
104 switch r {
105 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
106 s, err := t.scanNumber()
107 return token{kind: numberToken, value: s, line: line, pos: pos}, err
108
109 case '(', ')', '[', ']', '{', '}', ',', '+', '-', '%', '^', '~', '@', ';':
110 s := t.cur[:1]
111 t.cur = t.cur[1:]
112 t.linepos++
113 res := token{kind: syntaxToken, value: s, line: line, pos: pos}
114 return res, t.eos()
115
116 case ':':
117 s, err := t.tryPrefixes(`:=`, `:`)
118 return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
119
120 case '*':
121 s, err := t.tryPrefixes(`**`, `*`)
122 return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
123
124 case '/':
125 s, err := t.tryPrefixes(`//`, `/*`, `/`)
126 // double-slash starts a comment until the end of the line
127 if s == `//` {
128 t.skipLine()
129 goto rerun
130 }
131 // handle comments which can span multiple lines
132 if s == `/*` {
133 err := t.skipComment()
134 if err != nil {
135 return token{}, err
136 }
137 goto rerun
138 }
139 // handle division
140 return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
141
142 case '#':
143 // even hash starts a comment until the end of the line, making the
144 // syntax more Python-like
145 t.skipLine()
146 goto rerun
147
148 case '&':
149 s, err := t.tryPrefixes(`&&`, `&`)
150 return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
151
152 case '|':
153 s, err := t.tryPrefixes(`||`, `|`)
154 return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
155
156 case '?':
157 s, err := t.tryPrefixes(`??`, `?.`, `?`)
158 return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
159
160 case '.':
161 r, _ := utf8.DecodeRuneInString(t.cur[1:])
162 if '0' <= r && r <= '9' {
163 s, err := t.scanNumber()
164 res := token{kind: numberToken, value: s, line: line, pos: pos}
165 return res, err
166 }
167 s, err := t.tryPrefixes(`...`, `..`, `.?`, `.`)
168 return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
169
170 case '=':
171 // triple-equal makes the syntax even more JavaScript-like
172 s, err := t.tryPrefixes(`===`, `==`, `=>`, `=`)
173 return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
174
175 case '!':
176 // not-double-equal makes the syntax even more JavaScript-like
177 s, err := t.tryPrefixes(`!==`, `!=`, `!`)
178 return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
179
180 case '<':
181 // the less-more/diamond syntax is a SQL-like way to say not equal
182 s, err := t.tryPrefixes(`<=`, `<>`, `<`)
183 return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
184
185 case '>':
186 s, err := t.tryPrefixes(`>=`, `>`)
187 return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
188
189 default:
190 if isIdentStartRune(r) {
191 s, err := t.scanIdentifier()
192 res := token{kind: identifierToken, value: s, line: line, pos: pos}
193 return res, err
194 }
195 const fs = `line %d: pos %d: unexpected symbol %c`
196 return token{}, fmt.Errorf(fs, t.linenum, t.linepos, r)
197 }
198 }
199
200 // tryPrefixes tries to greedily match any prefix in the order given: when all
201 // candidates fail, an empty string is returned; when successful, the tokenizer
202 // updates its state to account for the matched prefix
203 func (t *tokenizer) tryPrefixes(prefixes ...string) (string, error) {
204 for _, pre := range prefixes {
205 if strings.HasPrefix(t.cur, pre) {
206 t.linepos += len(pre)
207 t.cur = t.cur[len(pre):]
208 return pre, t.eos()
209 }
210 }
211
212 return ``, t.eos()
213 }
214
215 // skipWhitespace updates the tokenizer to ignore runs of consecutive whitespace
216 // symbols: these are the likes of space, tab, newline, carriage return, etc.
217 func (t *tokenizer) skipWhitespace() error {
218 for len(t.cur) > 0 {
219 r, size := utf8.DecodeRuneInString(t.cur)
220 if !unicode.IsSpace(r) {
221 // no more spaces to skip
222 return nil
223 }
224
225 t.cur = t.cur[size:]
226 if r == '\n' {
227 // reached the next line
228 t.linenum++
229 t.linepos = 1
230 continue
231 }
232 // continuing on the same line
233 t.linepos++
234 }
235
236 // source code ended
237 return errEOS
238 }
239
240 // skipLine updates the tokenizer to the end of the current line, or the end of
241 // the source code, if it's the last line
242 func (t *tokenizer) skipLine() error {
243 for len(t.cur) > 0 {
244 r, size := utf8.DecodeRuneInString(t.cur)
245 t.cur = t.cur[size:]
246 if r == '\n' {
247 // reached the next line, as expected
248 t.linenum++
249 t.linepos = 1
250 return nil
251 }
252 }
253
254 // source code ended
255 t.linenum++
256 t.linepos = 1
257 return errEOS
258 }
259
260 // skipComment updates the tokenizer to the end of the comment started with a
261 // `/*` and ending with a `*/`, or to the end of the source code
262 func (t *tokenizer) skipComment() error {
263 var prev rune
264 for len(t.cur) > 0 {
265 r, size := utf8.DecodeRuneInString(t.cur)
266 t.cur = t.cur[size:]
267
268 if r == '\n' {
269 t.linenum++
270 t.linepos = 1
271 } else {
272 t.linepos++
273 }
274
275 if prev == '*' && r == '/' {
276 return nil
277 }
278 prev = r
279 }
280
281 // source code ended
282 const msg = "comment not ended with a `*/` sequence"
283 return errors.New(msg)
284 }
285
286 func (t *tokenizer) scanIdentifier() (string, error) {
287 end := 0
288 for len(t.cur) > 0 {
289 r, size := utf8.DecodeRuneInString(t.cur[end:])
290 if (end == 0 && !isIdentStartRune(r)) || !isIdentRestRune(r) {
291 // identifier ended, and there's more source code after it
292 name := t.cur[:end]
293 t.cur = t.cur[end:]
294 return name, nil
295 }
296 end += size
297 t.linepos++
298 }
299
300 // source code ended with an identifier name
301 name := t.cur
302 t.cur = ``
303 return name, nil
304 }
305
306 func (t *tokenizer) scanNumber() (string, error) {
307 dots := 0
308 end := 0
309 var prev rune
310
311 for len(t.cur) > 0 {
312 r, size := utf8.DecodeRuneInString(t.cur[end:])
313
314 switch r {
315 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_', 'e':
316 end += size
317 t.linepos++
318 prev = r
319
320 case '+', '-':
321 if end > 0 && prev != 'e' {
322 // number ended, and there's more source code after it
323 num := t.cur[:end]
324 t.cur = t.cur[end:]
325 return num, nil
326 }
327 end += size
328 t.linepos++
329 prev = r
330
331 case '.':
332 nr, _ := utf8.DecodeRuneInString(t.cur[end+size:])
333 if dots == 1 || isIdentStartRune(nr) || unicode.IsSpace(nr) || nr == '.' {
334 // number ended, and there's more source code after it
335 num := t.cur[:end]
336 t.cur = t.cur[end:]
337 return num, nil
338 }
339 dots++
340 end += size
341 t.linepos++
342 prev = r
343
344 default:
345 // number ended, and there's more source code after it
346 num := t.cur[:end]
347 t.cur = t.cur[end:]
348 return num, nil
349 }
350 }
351
352 // source code ended with a number
353 name := t.cur
354 t.cur = ``
355 return name, nil
356 }
357
358 // eos checks if the source-code is over: if so, it returns an end-of-file error,
359 // or a nil error otherwise
360 func (t *tokenizer) eos() error {
361 if len(t.cur) == 0 {
362 return errEOS
363 }
364 return nil
365 }
366
367 func isIdentStartRune(r rune) bool {
368 return ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || r == '_'
369 }
370
371 func isIdentRestRune(r rune) bool {
372 return isIdentStartRune(r) || ('0' <= r && r <= '9')
373 }
File: ./fmscripts/tokens_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package fmscripts
26
27 import (
28 "reflect"
29 "testing"
30 )
31
32 func TestTokenizer(t *testing.T) {
33 tests := map[string][]string{
34 ``: nil,
35 `3.`: []string{`3.`},
36 `3.2`: []string{`3.2`},
37 `-3.2`: []string{`-`, `3.2`},
38 `-3.2+56`: []string{`-`, `3.2`, `+`, `56`},
39 }
40
41 for script, expected := range tests {
42 t.Run(script, func(t *testing.T) {
43 tok := newTokenizer(script)
44 par, err := newParser(&tok)
45 if err != nil {
46 t.Fatal(err)
47 return
48 }
49
50 var got []string
51 for _, v := range par.tokens {
52 got = append(got, v.value)
53 }
54
55 if !reflect.DeepEqual(got, expected) {
56 const fs = "from %s\nexpected\n%#v\nbut got\n%#v\ninstead"
57 t.Fatalf(fs, script, expected, got)
58 return
59 }
60 })
61 }
62 }
File: ./folders/folders.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package folders
26
27 import (
28 "bufio"
29 "io"
30 "io/fs"
31 "os"
32 "path/filepath"
33 "strings"
34 )
35
36 const info = `
37 folders [options...] [folders...]
38
39 Find/list all folders in the folders given, without repetitions.
40
41 All (optional) leading options start with either single or double-dash:
42
43 -h, -help show this help message
44 -t, -top turn off recursive behavior; top-level entries only
45 `
46
47 func Main() {
48 top := false
49 buffered := false
50 args := os.Args[1:]
51
52 for len(args) > 0 {
53 switch args[0] {
54 case `-b`, `--b`, `-buffered`, `--buffered`:
55 buffered = true
56 args = args[1:]
57 continue
58
59 case `-h`, `--h`, `-help`, `--help`:
60 os.Stdout.WriteString(info[1:])
61 return
62
63 case `-t`, `--t`, `-top`, `--top`:
64 top = true
65 args = args[1:]
66 continue
67 }
68
69 break
70 }
71
72 if len(args) > 0 && args[0] == `--` {
73 args = args[1:]
74 }
75
76 liveLines := !buffered
77 if !buffered {
78 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
79 liveLines = false
80 }
81 }
82
83 var cfg config
84 cfg.got = make(map[string]struct{})
85 cfg.recursive = !top
86 cfg.liveLines = liveLines
87
88 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
89 os.Stderr.WriteString(`folders: `)
90 os.Stderr.WriteString(err.Error())
91 os.Stderr.WriteString("\n")
92 os.Exit(1)
93 return
94 }
95 }
96
97 type config struct {
98 got map[string]struct{}
99 recursive bool
100 liveLines bool
101 }
102
103 func run(w io.Writer, paths []string, cfg config) error {
104 bw := bufio.NewWriterSize(w, 32*1024)
105 defer bw.Flush()
106
107 if len(paths) == 0 {
108 paths = []string{`.`}
109 }
110
111 if cfg.recursive {
112 return runRecursive(bw, paths, cfg)
113 }
114 return runFlat(bw, paths, cfg)
115 }
116
117 func runRecursive(w *bufio.Writer, paths []string, cfg config) error {
118 if len(paths) == 0 {
119 paths = []string{`.`}
120 }
121
122 // handle is the callback for func filepath.WalkDir
123 handle := func(path string, e fs.DirEntry, err error) error {
124 if err != nil {
125 return err
126 }
127
128 if _, ok := cfg.got[path]; ok {
129 return nil
130 }
131 cfg.got[path] = struct{}{}
132
133 if e.IsDir() {
134 if err := handleEntry(w, path, cfg.liveLines); err != nil {
135 return err
136 }
137 }
138
139 return nil
140 }
141
142 for _, path := range paths {
143 if _, ok := cfg.got[path]; ok {
144 continue
145 }
146 cfg.got[path] = struct{}{}
147
148 st, err := os.Stat(path)
149 if err != nil {
150 return err
151 }
152
153 if !strings.HasSuffix(path, `/`) {
154 path = path + `/`
155 cfg.got[path] = struct{}{}
156 }
157
158 if !st.IsDir() {
159 continue
160 }
161
162 if err := filepath.WalkDir(path, handle); err != nil {
163 return err
164 }
165 }
166
167 return nil
168 }
169
170 func runFlat(w *bufio.Writer, paths []string, cfg config) error {
171 if len(paths) == 0 {
172 paths = []string{`.`}
173 }
174
175 for _, path := range paths {
176 if _, ok := cfg.got[path]; ok {
177 continue
178 }
179 cfg.got[path] = struct{}{}
180
181 st, err := os.Stat(path)
182 if err != nil {
183 return err
184 }
185
186 if !strings.HasSuffix(path, `/`) {
187 path = path + `/`
188 cfg.got[path] = struct{}{}
189 }
190
191 if !st.IsDir() {
192 continue
193 }
194
195 entries, err := os.ReadDir(path)
196 if err != nil {
197 return err
198 }
199
200 for _, e := range entries {
201 if !e.IsDir() {
202 continue
203 }
204
205 path := filepath.Join(path, e.Name())
206 if err := handleEntry(w, path, cfg.liveLines); err != nil {
207 return err
208 }
209 }
210 }
211
212 return nil
213 }
214
215 func handleEntry(w *bufio.Writer, path string, live bool) error {
216 abs, err := filepath.Abs(path)
217 if err != nil {
218 return err
219 }
220
221 w.WriteString(abs)
222 w.WriteByte('\n')
223
224 if live && w.Flush() != nil {
225 return io.EOF
226 }
227 return nil
228 }
File: ./gsub/gsub.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package gsub
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "regexp"
33 "strings"
34 )
35
36 const info = `
37 gsub [options...] [regex / replacement pairs...]
38
39
40 Named after the AWK function 'gsub' (global substitute), this tool replaces
41 all matches found with the substitution pattern associated to it.
42
43 Regexes and replacements are given as pairs. Input always comes from standard
44 input. The regular-expression mode used is "re2", which is a superset of the
45 commonly-used "extended-mode".
46
47 All ANSI-style sequences are removed before trying to replace all matches, to
48 avoid messing those up. Each regex replaces all its occurrences on the current
49 line in the order given among the arguments, so regex-order matters.
50
51 As with the AWK function 'gsub', any '&' symbols substitute into the substring
52 matched, except for any '&' preceded by a backslash.
53
54 The options are, available both in single and double-dash versions
55
56 -h, -help show this help message
57 -e, -erase all arguments are regexes which are replaced with nothing
58 -i, -ins match regexes case-insensitively
59 `
60
61 type pair struct {
62 expr *regexp.Regexp
63 repl []string
64 }
65
66 func Main() {
67 args := os.Args[1:]
68 erase := false
69 buffered := false
70 insensitive := false
71
72 for len(args) > 0 {
73 switch args[0] {
74 case `-b`, `--b`, `-buffered`, `--buffered`:
75 buffered = true
76 args = args[1:]
77 continue
78
79 case `-e`, `--e`, `-erase`, `--erase`:
80 erase = true
81 args = args[1:]
82 continue
83
84 case `-h`, `--h`, `-help`, `--help`:
85 os.Stdout.WriteString(info[1:])
86 return
87
88 case `-i`, `--i`, `-ins`, `--ins`:
89 insensitive = true
90 args = args[1:]
91 continue
92 }
93
94 break
95 }
96
97 if len(args) > 0 && args[0] == `--` {
98 args = args[1:]
99 }
100
101 errcount := 0
102 pairs := make([]pair, 0, (len(args)+1)/2)
103
104 for len(args) > 0 {
105 var err error
106 var what *regexp.Regexp
107
108 s := args[0]
109 if insensitive {
110 what, err = regexp.Compile(`(?i)` + s)
111 } else {
112 what, err = regexp.Compile(s)
113 }
114
115 if err != nil {
116 os.Stderr.WriteString(`gsub: `)
117 os.Stderr.WriteString(err.Error())
118 os.Stderr.WriteString("\n")
119 errcount++
120 continue
121 }
122
123 args = args[1:]
124 var with []string
125 if !erase && len(args) > 0 {
126 with = splitReplacement(args[0])
127 args = args[1:]
128 }
129 pairs = append(pairs, pair{what, with})
130 }
131
132 // quit right away when given invalid regexes
133 if errcount > 0 {
134 os.Exit(1)
135 return
136 }
137
138 liveLines := !buffered
139 if !buffered {
140 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
141 liveLines = false
142 }
143 }
144
145 err := run(os.Stdout, os.Stdin, pairs, liveLines)
146 if err != nil && err != io.EOF {
147 os.Stderr.WriteString(`gsub: `)
148 os.Stderr.WriteString(err.Error())
149 os.Stderr.WriteString("\n")
150 os.Exit(1)
151 return
152 }
153 }
154
155 func splitReplacement(s string) []string {
156 if s == `` {
157 return nil
158 }
159
160 n := 1
161 var prev rune
162 for _, r := range s {
163 if prev != '\\' && r == '&' {
164 n += 2
165 }
166 prev = r
167 }
168
169 repl := make([]string, 0, n)
170
171 for len(s) > 0 {
172 i := strings.IndexByte(s, '&')
173 if i == 0 || (i > 0 && s[i-1] != '\\') {
174 repl = append(repl, unescapeBackslashes(s[:i]))
175 repl = append(repl, ``)
176 s = s[i+1:]
177 continue
178 }
179 break
180 }
181
182 if len(s) > 0 {
183 repl = append(repl, unescapeBackslashes(s))
184 }
185 return repl
186 }
187
188 func unescapeBackslashes(s string) string {
189 if strings.IndexByte(s, '\\') < 0 {
190 return s
191 }
192
193 var prev byte
194 unesc := make([]byte, 0, len(s))
195
196 for i := range s {
197 b := s[i]
198
199 if prev != '\\' {
200 if b != '\\' {
201 unesc = append(unesc, s[i])
202 }
203 prev = b
204 continue
205 }
206
207 switch b {
208 case 'e':
209 unesc = append(unesc, '\x1b')
210 case 'n':
211 unesc = append(unesc, '\n')
212 case 'r':
213 unesc = append(unesc, '\r')
214 case 't':
215 unesc = append(unesc, '\t')
216 case 'v':
217 unesc = append(unesc, '\v')
218 default:
219 unesc = append(unesc, s[i])
220 }
221
222 prev = b
223 }
224
225 return string(unesc)
226 }
227
228 func run(w io.Writer, r io.Reader, pairs []pair, live bool) error {
229 var buf []byte
230 sc := bufio.NewScanner(r)
231 sc.Buffer(nil, 8*1024*1024*1024)
232 bw := bufio.NewWriterSize(w, 32*1024)
233 defer bw.Flush()
234
235 src := make([]byte, 8*1024)
236 dst := make([]byte, 8*1024)
237
238 for i := 0; sc.Scan(); i++ {
239 line := sc.Bytes()
240 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
241 line = line[3:]
242 }
243
244 s := line
245 if bytes.IndexByte(s, '\x1b') >= 0 {
246 buf = plain(buf[:0], s)
247 s = buf
248 }
249
250 if len(pairs) > 0 {
251 src = append(src[:0], s...)
252 for _, p := range pairs {
253 dst = gsub(dst[:0], src, p.expr, p.repl)
254 src = append(src[:0], dst...)
255 }
256 bw.Write(dst)
257 } else {
258 bw.Write(s)
259 }
260
261 if bw.WriteByte('\n') != nil {
262 return io.EOF
263 }
264
265 if live && bw.Flush() != nil {
266 return io.EOF
267 }
268 }
269
270 return sc.Err()
271 }
272
273 func gsub(dst []byte, src []byte, what *regexp.Regexp, with []string) []byte {
274 for len(src) > 0 {
275 span := what.FindIndex(src)
276 // also ignore empty regex matches to avoid infinite outer loops,
277 // as skipping empty slices isn't advancing at all, leaving the
278 // string stuck to being empty-matched forever by the same regex
279 if len(span) != 2 || span[0] == span[1] || span[0] < 0 {
280 return append(dst, src...)
281 }
282
283 start, end := span[0], span[1]
284 dst = append(dst, src[:start]...)
285 // avoid infinite loops caused by empty regex matches
286 if start == end {
287 if end >= len(src) {
288 break
289 }
290 dst = append(dst, src[end])
291 end++
292 src = src[end:]
293 continue
294 }
295
296 match := src[start:end]
297 for _, sub := range with {
298 if sub == `` {
299 dst = append(dst, match...)
300 continue
301 }
302 dst = append(dst, sub...)
303 }
304
305 src = src[end:]
306 }
307
308 return dst
309 }
310
311 func plain(dst []byte, src []byte) []byte {
312 for len(src) > 0 {
313 i, j := indexEscapeSequence(src)
314 if i < 0 {
315 dst = append(dst, src...)
316 break
317 }
318 if j < 0 {
319 j = len(src)
320 }
321
322 if i > 0 {
323 dst = append(dst, src[:i]...)
324 }
325
326 src = src[j:]
327 }
328
329 return dst
330 }
331
332 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
333 // the multi-byte sequences starting with ESC[; the result is a pair of slice
334 // indices which can be independently negative when either the start/end of
335 // a sequence isn't found; given their fairly-common use, even the hyperlink
336 // ESC]8 sequences are supported
337 func indexEscapeSequence(s []byte) (int, int) {
338 var prev byte
339
340 for i, b := range s {
341 if prev == '\x1b' && b == '[' {
342 j := indexLetter(s[i+1:])
343 if j < 0 {
344 return i, -1
345 }
346 return i - 1, i + 1 + j + 1
347 }
348
349 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
350 j := indexPair(s[i+1:], '\x1b', '\\')
351 if j < 0 {
352 return i, -1
353 }
354 return i - 1, i + 1 + j + 2
355 }
356
357 prev = b
358 }
359
360 return -1, -1
361 }
362
363 func indexLetter(s []byte) int {
364 for i, b := range s {
365 upper := b &^ 32
366 if 'A' <= upper && upper <= 'Z' {
367 return i
368 }
369 }
370
371 return -1
372 }
373
374 func indexPair(s []byte, x byte, y byte) int {
375 var prev byte
376
377 for i, b := range s {
378 if prev == x && b == y && i > 0 {
379 return i
380 }
381 prev = b
382 }
383
384 return -1
385 }
File: ./head/head.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package head
26
27 import (
28 "bufio"
29 "io"
30 "os"
31 "strconv"
32 "strings"
33 )
34
35 const info = `
36 head [options...] [files...]
37
38 Keep at most the first n lines, or keep the first 10 lines by default. When
39 not given any filepaths, the standard input is used instead.
40
41 Options
42
43 -n [number] change max number of lines (default is 10)
44 `
45
46 type config struct {
47 max int
48 liveLines bool
49 }
50
51 func Main() {
52 var cfg config
53 cfg.max = 10
54 cfg.liveLines = true
55
56 args := os.Args[1:]
57 for len(args) > 0 {
58 switch args[0] {
59 case `-b`, `--b`, `-buffered`, `--buffered`:
60 cfg.liveLines = false
61 args = args[1:]
62 continue
63
64 case `-n`:
65 args = args[1:]
66 if len(args) == 0 {
67 os.Stderr.WriteString("missing number of lines\n")
68 os.Exit(1)
69 return
70 }
71
72 s := strings.Replace(args[0], `_`, ``, -1)
73 n, err := strconv.ParseInt(s, 10, 64)
74 if err != nil {
75 os.Stderr.WriteString(`head: invalid number: `)
76 os.Stderr.WriteString(err.Error())
77 os.Stderr.WriteString("\n")
78 os.Exit(1)
79 return
80 }
81
82 args = args[1:]
83 cfg.max = int(n)
84 continue
85
86 case `--help`:
87 os.Stderr.WriteString(info[1:])
88 return
89 }
90
91 break
92 }
93
94 if len(args) > 0 && args[0] == `--` {
95 args = args[1:]
96 }
97
98 if cfg.max <= 0 {
99 return
100 }
101
102 if cfg.liveLines {
103 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
104 cfg.liveLines = false
105 }
106 }
107
108 if err := run(args, &cfg); err != nil && err != io.EOF {
109 os.Stderr.WriteString(`head: `)
110 os.Stderr.WriteString(err.Error())
111 os.Stderr.WriteString("\n")
112 os.Exit(1)
113 return
114 }
115 }
116
117 func run(paths []string, cfg *config) error {
118 w := bufio.NewWriterSize(os.Stdout, 32*1024)
119 defer w.Flush()
120
121 for _, path := range paths {
122 if cfg.max <= 0 {
123 return io.EOF
124 }
125 if err := handleFile(w, path, cfg); err != nil {
126 return err
127 }
128 }
129
130 if len(paths) == 0 {
131 if err := head(w, os.Stdin, cfg); err != nil {
132 return err
133 }
134 }
135 return nil
136 }
137
138 func handleFile(w *bufio.Writer, path string, cfg *config) error {
139 f, err := os.Open(path)
140 if err != nil {
141 return err
142 }
143 defer f.Close()
144 return head(w, f, cfg)
145 }
146
147 func head(w *bufio.Writer, r io.Reader, cfg *config) error {
148 const gb = 1024 * 1024 * 1024
149 sc := bufio.NewScanner(r)
150 sc.Buffer(nil, 8*gb)
151
152 for sc.Scan() {
153 if cfg.max <= 0 {
154 return io.EOF
155 }
156
157 w.Write(sc.Bytes())
158 if w.WriteByte('\n') != nil {
159 return io.EOF
160 }
161
162 cfg.max--
163
164 if cfg.liveLines && w.Flush() != nil {
165 return io.EOF
166 }
167 }
168
169 return sc.Err()
170 }
File: ./hima/hima.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package hima
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "regexp"
33 "strings"
34 )
35
36 const info = `
37 hima [options...] [regexes...]
38
39
40 HIlight MAtches ANSI-styles matching regular expressions along lines read
41 from the standard input. The regular-expression mode used is "re2", which
42 is a superset of the commonly-used "extended-mode".
43
44 Regexes always avoid matching any ANSI-style sequences, to avoid messing
45 those up. Also, multiple matches in a line never overlap: at each step
46 along a line, the earliest-starting match among the regexes always wins,
47 as the order regexes are given among the arguments never matters.
48
49 The options are, available both in single and double-dash versions
50
51 -f, -filter filter out (ignore) lines with no matches
52 -h, -help show this help message
53 -i, -ins match regexes case-insensitively
54 -l, -line, -w, -whole highlight whole lines when matching
55 `
56
57 const highlightStyle = "\x1b[7m"
58
59 type config struct {
60 filter bool
61 insensitive bool
62 whole bool
63 liveLines bool
64 }
65
66 func Main() {
67 var cfg config
68 buffered := false
69 args := os.Args[1:]
70
71 for len(args) > 0 {
72 switch args[0] {
73 case `-b`, `--b`, `-buffered`, `--buffered`:
74 buffered = true
75 args = args[1:]
76 continue
77
78 case `-f`, `--f`, `-filter`, `--filter`, `-filtered`, `--filtered`:
79 cfg.filter = true
80 args = args[1:]
81 continue
82
83 case `-h`, `--h`, `-help`, `--help`:
84 os.Stdout.WriteString(info[1:])
85 return
86
87 case `-i`, `--i`, `-ins`, `--ins`, `-insensitive`, `--insensitive`:
88 cfg.insensitive = true
89 args = args[1:]
90 continue
91
92 case `-l`, `--l`, `-line`, `--line`, `-w`, `--w`, `-whole`, `--whole`:
93 cfg.whole = true
94 args = args[1:]
95 continue
96 }
97
98 if s := args[0]; s == `-` || s == `--` || !strings.HasPrefix(s, `-`) {
99 break
100 }
101
102 for s := args[0][1:]; len(s) > 0; s = s[1:] {
103 switch s[0] {
104 case 'b':
105 buffered = true
106 continue
107 case 'f':
108 cfg.filter = true
109 continue
110 case 'h':
111 os.Stdout.WriteString(info[1:])
112 return
113 case 'i':
114 cfg.insensitive = true
115 continue
116 case 'l':
117 cfg.whole = true
118 continue
119 case 'w':
120 cfg.whole = true
121 continue
122 default:
123 os.Stderr.WriteString(`hima: unsupported option `)
124 os.Stderr.WriteString(args[0])
125 os.Stderr.WriteString("\n")
126 os.Exit(1)
127 }
128 }
129
130 args = args[1:]
131 }
132
133 if len(args) > 0 && args[0] == `--` {
134 args = args[1:]
135 }
136
137 patterns := make([]pattern, 0, len(args))
138
139 for _, s := range args {
140 var err error
141 var pat pattern
142
143 if cfg.insensitive {
144 pat, err = compile(`(?i)` + s)
145 } else {
146 pat, err = compile(s)
147 }
148
149 if err != nil {
150 os.Stderr.WriteString(`hima: `)
151 os.Stderr.WriteString(err.Error())
152 os.Stderr.WriteString("\n")
153 continue
154 }
155
156 patterns = append(patterns, pat)
157 }
158
159 // quit right away when given invalid regexes
160 if len(patterns) < len(args) {
161 os.Exit(1)
162 return
163 }
164
165 cfg.liveLines = !buffered
166 if !buffered {
167 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
168 cfg.liveLines = false
169 }
170 }
171
172 ok, err := run(os.Stdout, os.Stdin, patterns, cfg)
173 if err != nil && err != io.EOF {
174 os.Stderr.WriteString(`hima: `)
175 os.Stderr.WriteString(err.Error())
176 os.Stderr.WriteString("\n")
177 os.Exit(1)
178 return
179 }
180
181 if !ok {
182 os.Exit(1)
183 return
184 }
185 }
186
187 // pattern is a regular-expression pattern which distinguishes between the
188 // start/end of a line and those of the chunks it can be used to match
189 type pattern struct {
190 // expr is the regular-expression
191 expr *regexp.Regexp
192
193 // begin is whether the regexp refers to the start of a line
194 begin bool
195
196 // end is whether the regexp refers to the end of a line
197 end bool
198 }
199
200 func compile(src string) (pattern, error) {
201 expr, err := regexp.Compile(src)
202
203 var pat pattern
204 pat.expr = expr
205 pat.begin = strings.HasPrefix(src, `^`) || strings.HasPrefix(src, `(?i)^`)
206 pat.end = strings.HasSuffix(src, `$`) && !strings.HasSuffix(src, `\$`)
207 return pat, err
208 }
209
210 func (p pattern) findIndex(s []byte, i int, last int) (start int, stop int) {
211 if i > 0 && p.begin {
212 return -1, -1
213 }
214 if i != last && p.end {
215 return -1, -1
216 }
217
218 span := p.expr.FindIndex(s)
219 // also ignore empty regex matches to avoid infinite outer loops,
220 // as skipping empty slices isn't advancing at all, leaving the
221 // string stuck to being empty-matched forever by the same regex
222 if len(span) != 2 || span[0] == span[1] {
223 return -1, -1
224 }
225
226 return span[0], span[1]
227 }
228
229 func run(w io.Writer, r io.Reader, pats []pattern, cfg config) (bool, error) {
230 sc := bufio.NewScanner(r)
231 sc.Buffer(nil, 8*1024*1024*1024)
232 bw := bufio.NewWriterSize(w, 32*1024)
233 defer bw.Flush()
234
235 ok := !cfg.filter
236
237 for i := 0; sc.Scan(); i++ {
238 s := sc.Bytes()
239 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
240 s = s[3:]
241 }
242
243 n := 0
244 last := countChunks(s) - 1
245 if last < 0 {
246 last = 0
247 }
248
249 anyMatches := matches(s, pats, last)
250
251 if cfg.filter && !anyMatches {
252 continue
253 }
254 if cfg.filter {
255 ok = true
256 }
257
258 if cfg.whole && anyMatches {
259 handleWhole(bw, s)
260 s = nil
261 }
262
263 for len(s) > 0 {
264 i, j := indexEscapeSequence(s)
265 if i < 0 {
266 handleChunk(bw, s, pats, n, last)
267 break
268 }
269 if j < 0 {
270 j = len(s)
271 }
272
273 handleChunk(bw, s[:i], pats, n, last)
274 if i > 0 {
275 n++
276 }
277
278 bw.Write(s[i:j])
279
280 s = s[j:]
281 }
282
283 if bw.WriteByte('\n') != nil {
284 return ok, io.EOF
285 }
286
287 if cfg.liveLines && bw.Flush() != nil {
288 return ok, io.EOF
289 }
290 }
291
292 err := sc.Err()
293 return err == nil && ok, err
294 }
295
296 // matches finds out if any regex matches any substring around ANSI-sequences
297 func matches(s []byte, patterns []pattern, last int) bool {
298 n := 0
299
300 for len(s) > 0 {
301 i, j := indexEscapeSequence(s)
302 if i < 0 {
303 for _, p := range patterns {
304 if begin, _ := p.findIndex(s, n, last); begin >= 0 {
305 return true
306 }
307 }
308 return false
309 }
310
311 if j < 0 {
312 j = len(s)
313 }
314
315 for _, p := range patterns {
316 if begin, _ := p.findIndex(s[:i], n, last); begin >= 0 {
317 return true
318 }
319 }
320
321 if i > 0 {
322 n++
323 }
324
325 s = s[j:]
326 }
327
328 return false
329 }
330
331 func countChunks(s []byte) int {
332 chunks := 0
333
334 for len(s) > 0 {
335 i, j := indexEscapeSequence(s)
336 if i < 0 {
337 break
338 }
339
340 if i > 0 {
341 chunks++
342 }
343
344 if j < 0 {
345 break
346 }
347 s = s[j:]
348 }
349
350 if len(s) > 0 {
351 chunks++
352 }
353 return chunks
354 }
355
356 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
357 // the multi-byte sequences starting with ESC[; the result is a pair of slice
358 // indices which can be independently negative when either the start/end of
359 // a sequence isn't found; given their fairly-common use, even the hyperlink
360 // ESC]8 sequences are supported
361 func indexEscapeSequence(s []byte) (int, int) {
362 var prev byte
363
364 for i, b := range s {
365 if prev == '\x1b' && b == '[' {
366 j := indexLetter(s[i+1:])
367 if j < 0 {
368 return i, -1
369 }
370 return i - 1, i + 1 + j + 1
371 }
372
373 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
374 j := indexPair(s[i+1:], '\x1b', '\\')
375 if j < 0 {
376 return i, -1
377 }
378 return i - 1, i + 1 + j + 2
379 }
380
381 prev = b
382 }
383
384 return -1, -1
385 }
386
387 func indexLetter(s []byte) int {
388 for i, b := range s {
389 upper := b &^ 32
390 if 'A' <= upper && upper <= 'Z' {
391 return i
392 }
393 }
394
395 return -1
396 }
397
398 func indexPair(s []byte, x byte, y byte) int {
399 var prev byte
400
401 for i, b := range s {
402 if prev == x && b == y && i > 0 {
403 return i
404 }
405 prev = b
406 }
407
408 return -1
409 }
410
411 // note: looking at the results of restoring ANSI-styles after style-resets
412 // doesn't seem to be worth it, as a previous version used to do
413
414 // handleChunk handles line-slices around any detected ANSI-style sequences,
415 // or even whole lines, when no ANSI-styles are found in them
416 func handleChunk(w *bufio.Writer, s []byte, with []pattern, n int, last int) {
417 for len(s) > 0 {
418 start, end := -1, -1
419 for _, p := range with {
420 i, j := p.findIndex(s, n, last)
421 if i >= 0 && (i < start || start < 0) {
422 start, end = i, j
423 }
424 }
425
426 if start < 0 {
427 w.Write(s)
428 return
429 }
430
431 w.Write(s[:start])
432 w.WriteString(highlightStyle)
433 w.Write(s[start:end])
434 w.WriteString("\x1b[0m")
435
436 s = s[end:]
437 }
438 }
439
440 func handleWhole(w *bufio.Writer, s []byte) {
441 if len(s) == 0 {
442 return
443 }
444
445 w.WriteString(highlightStyle)
446
447 for len(s) > 0 {
448 i := bytes.Index(s, []byte{0x1b, '[', '0', 'm'})
449 if i < 0 {
450 w.Write(s)
451 break
452 }
453
454 j := i + len("\x1b[0m")
455 w.Write(s[:j])
456 w.WriteString(highlightStyle)
457
458 s = s[j:]
459 }
460
461 w.WriteString("\x1b[0m")
462 }
File: ./htmlify/htmlify.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package htmlify
26
27 import (
28 "bufio"
29 "encoding/base64"
30 "errors"
31 "io"
32 "os"
33 "regexp"
34 "strings"
35 )
36
37 const info = `
38 htmlify [options...] [filepaths/URIs...]
39
40
41 Render plain-text prose into self-contained HTML. Lines which are just a
42 valid data-URI are turned into pictures, audio, or even video elements.
43
44 All HTTP(s) URIs are autodetected and rendered as hyperlinks, even when
45 lines have multiple URIs in them.
46
47 If a title isn't given from the cmd-line options, the first line is used
48 as the title.
49
50 All (optional) leading options start with either single or double-dash,
51 and most of them change the style/color used. Some of the options are,
52 shown in their single-dash form:
53
54 -h, -help show this help message
55 -mono, -monospace use a monospace font for text
56 -t, -title use the next argument as the webpage title
57 `
58
59 var links = regexp.MustCompile(`(?i)(https?|ftps?)://[a-zA-Z0-9_%.,?/&=#-]+`)
60
61 type config struct {
62 Title string
63 GotTitle bool
64 Started bool
65 Monospace bool
66 }
67
68 func Main() {
69 var cfg config
70 args := os.Args[1:]
71
72 for len(args) > 0 {
73 switch args[0] {
74 case `-h`, `--h`, `-help`, `--help`:
75 os.Stdout.WriteString(info[1:])
76 return
77
78 case `-mono`, `--mono`, `-monospace`, `--monospace`:
79 cfg.Monospace = true
80 args = args[1:]
81 continue
82
83 case `-t`, `--t`, `-title`, `--title`:
84 if len(args) >= 2 {
85 cfg.Title = args[1]
86 cfg.GotTitle = true
87 args = args[2:]
88 } else {
89 const m = "title option isn't followed by the actual title\n"
90 os.Stderr.WriteString(m)
91 os.Exit(1)
92 return
93 }
94 continue
95 }
96
97 break
98 }
99
100 if len(args) > 0 && args[0] == `--` {
101 args = args[1:]
102 }
103
104 if err := run(os.Stdout, args, &cfg); err != nil && err != io.EOF {
105 os.Stderr.WriteString(`htmlify: `)
106 os.Stderr.WriteString(err.Error())
107 os.Stderr.WriteString("\n")
108 os.Exit(1)
109 return
110 }
111 }
112
113 func run(w io.Writer, args []string, cfg *config) error {
114 bw := bufio.NewWriterSize(w, 32*1024)
115 defer bw.Flush()
116
117 if len(args) == 0 {
118 if err := handleReader(bw, os.Stdin, cfg); err != nil {
119 return err
120 }
121 }
122
123 for _, name := range args {
124 if err := handleFile(bw, name, cfg); err != nil {
125 return err
126 }
127 }
128
129 if cfg.Started {
130 endPage(bw)
131 }
132 return nil
133 }
134
135 func handleFile(w *bufio.Writer, name string, cfg *config) error {
136 if name == `` || name == `-` {
137 return handleReader(w, os.Stdin, cfg)
138 }
139
140 f, err := os.Open(name)
141 if err != nil {
142 return errors.New(`can't read from file named "` + name + `"`)
143 }
144 defer f.Close()
145
146 return handleReader(w, f, cfg)
147 }
148
149 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
150 const gb = 1024 * 1024 * 1024
151 sc := bufio.NewScanner(r)
152 sc.Buffer(nil, 8*gb)
153 lines := 0
154
155 for sc.Scan() {
156 line := sc.Text()
157
158 if lines == 0 && strings.HasPrefix(line, "\xef\xbb\xbf") {
159 line = line[3:]
160 }
161 if lines == 0 && !cfg.Started {
162 title := line
163 if cfg.GotTitle {
164 title = cfg.Title
165 }
166 startPage(w, title, cfg.Monospace)
167 cfg.Started = true
168 }
169 lines++
170
171 if err := handleLine(w, line, cfg); err != nil {
172 return err
173 }
174 }
175
176 if !cfg.Started && lines > 0 {
177 startPage(w, cfg.Title, cfg.Monospace)
178 cfg.Started = true
179 }
180 return sc.Err()
181 }
182
183 const style = `
184 body {
185 margin: 1rem auto 2rem auto;
186 padding: 0.25rem;
187 font-size: 1.1rem;
188 line-height: 1.8rem;
189 font-family: sans-serif;
190
191 max-width: 95vw;
192 /* width: max-content; */
193 width: fit-content;
194
195 box-sizing: border-box;
196 display: block;
197 }
198
199 a {
200 color: steelblue;
201 text-decoration: none;
202 }
203
204 p {
205 display: block;
206 margin: auto;
207 max-width: 80ch;
208 }
209
210 img {
211 margin: none;
212 }
213
214 audio {
215 width: 60ch;
216 }
217
218 table {
219 margin: 2rem auto;
220 border-collapse: collapse;
221 }
222
223 thead>* {
224 position: sticky;
225 top: 0;
226 background-color: white;
227 }
228
229 tfoot th {
230 user-select: none;
231 }
232
233 th, td {
234 padding: 0.1rem 1ch;
235 min-width: 4ch;
236 border-bottom: solid thin transparent;
237 }
238
239 tr:nth-child(5n) td {
240 border-bottom: solid thin #ccc;
241 }
242
243 .monospace {
244 font-family: monospace;
245 }
246 `
247
248 func startPage(w *bufio.Writer, title string, monospace bool) {
249 w.WriteString("<!DOCTYPE html>\n")
250 w.WriteString("<html lang=\"en\">\n")
251 w.WriteString("\n")
252 w.WriteString("<head>\n")
253 w.WriteString(" <meta charset=\"UTF-8\">\n")
254 w.WriteString(" <meta name=\"viewport\" content=\"width=device-width,")
255 w.WriteString(" initial-scale=1.0\">\n")
256 w.WriteString(" <meta http-equiv=\"X-UA-Compatible\"")
257 w.WriteString(" content=\"ie=edge\">\n")
258 w.WriteString("\n")
259 w.WriteString(" <link rel=\"icon\" href=\"data:,\">\n")
260 w.WriteString(` <title>`)
261 w.WriteString(title)
262 w.WriteString("</title>\n")
263 w.WriteString("\n")
264 w.WriteString("\n")
265 w.WriteString(" <style>\n")
266 w.WriteString(style[1:])
267 w.WriteString(" </style>\n")
268 w.WriteString("</head>\n")
269 if monospace {
270 w.WriteString("<body class=\"monospace\">\n")
271 } else {
272 w.WriteString("<body>\n")
273 }
274 }
275
276 func endPage(w *bufio.Writer) {
277 w.WriteString("</body>\n")
278 w.WriteString("</html>\n")
279 }
280
281 func handleLine(w *bufio.Writer, s string, cfg *config) error {
282 if handleDataURI(w, s) {
283 if w.WriteByte('\n') != nil {
284 return io.EOF
285 }
286 return nil
287 }
288
289 for len(s) > 0 {
290 span := links.FindStringIndex(s)
291 if span != nil && len(span) == 2 {
292 i := span[0]
293 j := span[1]
294 href := s[i:j]
295 handleChunk(w, s[:i])
296 w.WriteString(`<a href="`)
297 w.WriteString(href)
298 w.WriteString(`">`)
299 w.WriteString(href)
300 w.WriteString(`</a>`)
301 s = s[j:]
302 continue
303 }
304
305 handleChunk(w, s)
306 break
307 }
308
309 w.WriteString(`<br>`)
310 if w.WriteByte('\n') != nil {
311 return io.EOF
312 }
313 return nil
314 }
315
316 func handleChunk(w *bufio.Writer, s string) {
317 for len(s) > 0 {
318 switch b := s[0]; b {
319 case '&':
320 w.WriteString("&")
321 case '<':
322 w.WriteString("<")
323 case '>':
324 w.WriteString(">")
325 default:
326 w.WriteByte(b)
327 }
328 s = s[1:]
329 }
330 }
331
332 func handleDataURI(w *bufio.Writer, s string) bool {
333 full := s
334
335 if !strings.HasPrefix(s, `data:`) {
336 return false
337 }
338
339 s = strings.TrimPrefix(s, `data:`)
340 i := strings.Index(s, `;base64,`)
341 if i < 0 {
342 return false
343 }
344
345 kind := s[:i]
346 s = s[i+len(`;base64,`):]
347
348 if strings.HasPrefix(kind, `image/`) {
349 if !isBase64(s) {
350 return false
351 }
352
353 w.WriteString(`<img src="`)
354 w.WriteString(full)
355 w.WriteString(`">`)
356 return true
357 }
358
359 if strings.HasPrefix(kind, `audio/`) {
360 if !isBase64(s) {
361 return false
362 }
363
364 w.WriteString(`<audio controls src="`)
365 w.WriteString(full)
366 w.WriteString(`"></audio>`)
367 return true
368 }
369
370 if strings.HasPrefix(kind, `video/`) {
371 if !isBase64(s) {
372 return false
373 }
374
375 w.WriteString(`<video controls src="`)
376 w.WriteString(full)
377 w.WriteString(`"></video>`)
378 return true
379 }
380
381 return false
382 }
383
384 func handleMedia(w *bufio.Writer, begin string, s string, end string) bool {
385 if !isBase64(s) {
386 return false
387 }
388
389 w.WriteString(begin)
390 w.WriteString(s)
391 w.WriteString(end)
392 return true
393 }
394
395 func isBase64(s string) bool {
396 dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(s))
397 n, err := io.Copy(io.Discard, dec)
398 return n > 0 && err == nil
399 }
File: ./id3pic/id3pic.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package id3pic
26
27 import (
28 "bufio"
29 "encoding/binary"
30 "errors"
31 "io"
32 "os"
33 )
34
35 const info = `
36 id3pic [options...] [file...]
37
38 Extract picture/thumbnail bytes from ID3/MP3 metadata, if available.
39
40 All (optional) leading options start with either single or double-dash:
41
42 -h, -help show this help message
43 `
44
45 // errNoThumb is a generic error to handle lack of thumbnails, in case no
46 // picture-metadata-starters are found at all
47 var errNoThumb = errors.New(`no thumbnail data found`)
48
49 // errInvalidPIC is a generic error for invalid PIC-format pics
50 var errInvalidPIC = errors.New(`invalid PIC-format embedded thumbnail`)
51
52 func Main() {
53 if len(os.Args) > 1 {
54 switch os.Args[1] {
55 case `-h`, `--h`, `-help`, `--help`:
56 os.Stdout.WriteString(info[1:])
57 return
58 }
59 }
60
61 if len(os.Args) > 2 {
62 showError(`can only handle 1 file`)
63 os.Exit(1)
64 return
65 }
66
67 name := `-`
68 if len(os.Args) > 1 {
69 name = os.Args[1]
70 }
71
72 if err := run(os.Stdout, name); err != nil && err != io.EOF {
73 showError(err.Error())
74 os.Exit(1)
75 return
76 }
77 }
78
79 func showError(msg string) {
80 os.Stderr.WriteString(msg)
81 os.Stderr.WriteString("\n")
82 }
83
84 func run(w io.Writer, name string) error {
85 if name == `-` {
86 return id3pic(w, bufio.NewReader(os.Stdin))
87 }
88
89 f, err := os.Open(name)
90 if err != nil {
91 return errors.New(`can't read from file named "` + name + `"`)
92 }
93 defer f.Close()
94
95 return id3pic(w, bufio.NewReader(f))
96 }
97
98 func match(r *bufio.Reader, what []byte) bool {
99 for _, v := range what {
100 b, err := r.ReadByte()
101 if b != v || err != nil {
102 return false
103 }
104 }
105 return true
106 }
107
108 func id3pic(w io.Writer, r *bufio.Reader) error {
109 // match the ID3 mark
110 for {
111 b, err := r.ReadByte()
112 if err == io.EOF {
113 return errNoThumb
114 }
115 if err != nil {
116 return err
117 }
118
119 if b == 'I' && match(r, []byte{'D', '3'}) {
120 break
121 }
122 }
123
124 for {
125 b, err := r.ReadByte()
126 if err == io.EOF {
127 return errNoThumb
128 }
129 if err != nil {
130 return err
131 }
132
133 // handle APIC-type chunks
134 if b == 'A' && match(r, []byte{'P', 'I', 'C'}) {
135 return handleAPIC(w, r)
136 }
137 }
138 }
139
140 func handleAPIC(w io.Writer, r *bufio.Reader) error {
141 // section-size seems stored as 4 big-endian bytes
142 var size uint32
143 err := binary.Read(r, binary.BigEndian, &size)
144 if err != nil {
145 return err
146 }
147
148 n, err := skipThumbnailTypeAPIC(r)
149 if err != nil {
150 return err
151 }
152
153 _, err = io.Copy(w, io.LimitReader(r, int64(int(size)-n)))
154 return err
155 }
156
157 func skipThumbnailTypeAPIC(r *bufio.Reader) (skipped int, err error) {
158 m, err := r.Discard(2)
159 if err != nil || m != 2 {
160 return -1, errors.New(`failed to sync APIC flags`)
161 }
162 skipped += m
163
164 m, err = r.Discard(1)
165 if err != nil || m != 1 {
166 return -1, errors.New(`failed to sync APIC text-encoding`)
167 }
168 skipped += m
169
170 junk, err := r.ReadSlice(0)
171 if err != nil {
172 return -1, errors.New(`failed to sync to APIC thumbnail MIME-type`)
173 }
174 skipped += len(junk)
175
176 m, err = r.Discard(1)
177 if err != nil || m != 1 {
178 return -1, errors.New(`failed to sync APIC picture type`)
179 }
180 skipped += m
181
182 junk, err = r.ReadSlice(0)
183 if err != nil {
184 return -1, errors.New(`failed to sync to APIC thumbnail description`)
185 }
186 skipped += len(junk)
187
188 return skipped, nil
189 }
File: ./ignore/ignore.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package ignore
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 )
33
34 const info = `
35 ignore [options...] [command...] [command args...]
36
37 Ignore the command given, acting as a stdio-passthru instead. This allows
38 quick editing of pipes to temporarily exclude a pipe-step, while still
39 having the disabled step show in the pipe-chain as a memo of sorts.
40
41 All (optional) leading options start with either single or double-dash:
42
43 -h, -help show this help message
44 `
45
46 func Main() {
47 buffered := false
48 args := os.Args[1:]
49
50 if len(args) > 0 {
51 switch args[0] {
52 case `-b`, `--b`, `-buffered`, `--buffered`:
53 buffered = true
54 args = args[1:]
55
56 case `-h`, `--h`, `-help`, `--help`:
57 os.Stdout.WriteString(info[1:])
58 return
59 }
60 }
61
62 if len(args) > 0 && args[0] == `--` {
63 args = args[1:]
64 }
65
66 liveLines := !buffered
67 if !buffered {
68 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
69 liveLines = false
70 }
71 }
72
73 err := run(os.Stdout, os.Stdin, liveLines)
74 if err != nil && err != io.EOF {
75 os.Stderr.WriteString(`ignore: `)
76 os.Stderr.WriteString(err.Error())
77 os.Stderr.WriteString("\n")
78 os.Exit(1)
79 return
80 }
81 }
82
83 func run(w io.Writer, r io.Reader, live bool) error {
84 bw := bufio.NewWriterSize(w, 32*1024)
85 defer bw.Flush()
86
87 return catl(bw, r, live)
88 }
89
90 func catl(w *bufio.Writer, r io.Reader, live bool) error {
91 if !live {
92 return catlFast(w, r)
93 }
94
95 const gb = 1024 * 1024 * 1024
96 sc := bufio.NewScanner(r)
97 sc.Buffer(nil, 8*gb)
98
99 for i := 0; sc.Scan(); i++ {
100 s := sc.Bytes()
101 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
102 s = s[3:]
103 }
104
105 w.Write(s)
106 if w.WriteByte('\n') != nil {
107 return io.EOF
108 }
109
110 if live && w.Flush() != nil {
111 return io.EOF
112 }
113 }
114
115 return sc.Err()
116 }
117
118 func catlFast(w *bufio.Writer, r io.Reader) error {
119 var buf [32 * 1024]byte
120 var last byte = '\n'
121
122 for i := 0; true; i++ {
123 n, err := r.Read(buf[:])
124 if n > 0 && err == io.EOF {
125 err = nil
126 }
127 if err == io.EOF {
128 if last != '\n' {
129 w.WriteByte('\n')
130 }
131 return nil
132 }
133
134 if err != nil {
135 return err
136 }
137
138 chunk := buf[:n]
139 if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
140 chunk = chunk[3:]
141 }
142
143 if len(chunk) >= 1 {
144 if _, err := w.Write(chunk); err != nil {
145 return io.EOF
146 }
147 last = chunk[len(chunk)-1]
148 }
149 }
150
151 return nil
152 }
File: ./indent/indent.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package indent
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 "strconv"
34 )
35
36 const info = `
37 indent [options...] [leading spaces...] [files...]
38
39 Start each non-empty line with extra n spaces: when a leading number isn't
40 given, lines are indented using 4 spaces by default.
41
42 All (optional) leading options start with either single or double-dash:
43
44 -h, -help show this help message
45 `
46
47 func Main() {
48 buffered := false
49 args := os.Args[1:]
50 indent := 4
51
52 if len(args) > 0 {
53 switch args[0] {
54 case `-b`, `--b`, `-buffered`, `--buffered`:
55 buffered = true
56 args = args[1:]
57
58 case `-h`, `--h`, `-help`, `--help`:
59 os.Stdout.WriteString(info[1:])
60 return
61 }
62 }
63
64 if len(args) > 0 && args[0] == `--` {
65 args = args[1:]
66 }
67
68 if len(args) > 0 {
69 if n, err := strconv.Atoi(args[0]); err == nil {
70 indent = n
71 args = args[1:]
72 }
73 }
74
75 liveLines := !buffered
76 if !buffered {
77 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
78 liveLines = false
79 }
80 }
81
82 err := run(os.Stdout, args, indent, liveLines)
83 if err != nil && err != io.EOF {
84 os.Stderr.WriteString(`indent: `)
85 os.Stderr.WriteString(err.Error())
86 os.Stderr.WriteString("\n")
87 os.Exit(1)
88 return
89 }
90 }
91
92 func run(w io.Writer, args []string, level int, live bool) error {
93 bw := bufio.NewWriterSize(w, 32*1024)
94 defer bw.Flush()
95
96 if len(args) == 0 {
97 return indent(bw, os.Stdin, level, live)
98 }
99
100 for _, name := range args {
101 if err := handleFile(bw, name, level, live); err != nil {
102 return err
103 }
104 }
105 return nil
106 }
107
108 func handleFile(w *bufio.Writer, name string, level int, live bool) error {
109 if name == `` || name == `-` {
110 return indent(w, os.Stdin, level, live)
111 }
112
113 f, err := os.Open(name)
114 if err != nil {
115 return errors.New(`can't read from file named "` + name + `"`)
116 }
117 defer f.Close()
118
119 return indent(w, f, level, live)
120 }
121
122 func indent(w *bufio.Writer, r io.Reader, level int, live bool) error {
123 const gb = 1024 * 1024 * 1024
124 sc := bufio.NewScanner(r)
125 sc.Buffer(nil, 8*gb)
126
127 for i := 0; sc.Scan(); i++ {
128 s := sc.Bytes()
129 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
130 s = s[3:]
131 }
132
133 writeSpaces(w, level)
134 w.Write(s)
135
136 if w.WriteByte('\n') != nil {
137 return io.EOF
138 }
139
140 if live && w.Flush() != nil {
141 return io.EOF
142 }
143 }
144
145 return sc.Err()
146 }
147
148 func writeSpaces(w *bufio.Writer, n int) {
149 const (
150 half = ` `
151 spaces = half + half
152 )
153
154 for n >= len(spaces) {
155 w.WriteString(spaces)
156 n -= len(spaces)
157 }
158 if n > 0 {
159 w.WriteString(spaces[:n])
160 }
161 }
File: ./info.txt
1 easybox [options...] [tool] [arguments...]
2
3 This is a collection of many specialized app-like tools, similar to "busybox".
4
5 You can either run it with the tool name as its first argument, or run a link
6 to it whose name is one of those same tools, avoiding the tool-name argument
7 in that case.
8
9 Tool "help" shows you all tools available, as well as all their aliases, and
10 tool "tools" merely lists all main tool-names.
File: ./items/items.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package items
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 )
34
35 const info = `
36 items [options...] [files...]
37
38 Emit each word/item from each input line into its own output line.
39
40 All (optional) leading options start with either single or double-dash:
41
42 -h, -help show this help message
43 -tsv separate items from input-lines using individual tabs
44 `
45
46 type config struct {
47 tsv bool
48 liveLines bool
49 }
50
51 func Main() {
52 var cfg config
53 cfg.liveLines = true
54 args := os.Args[1:]
55
56 for len(args) > 0 {
57 switch args[0] {
58 case `-b`, `--b`, `-buffered`, `--buffered`:
59 cfg.liveLines = false
60 args = args[1:]
61 continue
62
63 case `-h`, `--h`, `-help`, `--help`:
64 os.Stdout.WriteString(info[1:])
65 return
66
67 case `-tsv`, `--tsv`:
68 cfg.tsv = true
69 args = args[1:]
70 continue
71 }
72
73 break
74 }
75
76 if len(args) > 0 && args[0] == `--` {
77 args = args[1:]
78 }
79
80 if cfg.liveLines {
81 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
82 cfg.liveLines = false
83 }
84 }
85
86 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
87 os.Stderr.WriteString(`items: `)
88 os.Stderr.WriteString(err.Error())
89 os.Stderr.WriteString("\n")
90 os.Exit(1)
91 return
92 }
93 }
94
95 func run(w io.Writer, args []string, cfg config) error {
96 bw := bufio.NewWriterSize(w, 32*1024)
97 defer bw.Flush()
98
99 dashes := 0
100 for _, name := range args {
101 if name == `-` {
102 dashes++
103 }
104 if dashes > 1 {
105 return errors.New(`can't read stdin (dash) more than once`)
106 }
107 }
108
109 if len(args) == 0 {
110 return items(bw, os.Stdin, cfg)
111 }
112
113 for _, name := range args {
114 if name == `-` {
115 if err := items(bw, os.Stdin, cfg); err != nil {
116 return err
117 }
118 continue
119 }
120
121 if err := handleFile(bw, name, cfg); err != nil {
122 return err
123 }
124 }
125 return nil
126 }
127
128 func handleFile(w *bufio.Writer, name string, cfg config) error {
129 if name == `` || name == `-` {
130 return items(w, os.Stdin, cfg)
131 }
132
133 f, err := os.Open(name)
134 if err != nil {
135 return errors.New(`can't read from file named "` + name + `"`)
136 }
137 defer f.Close()
138
139 return items(w, f, cfg)
140 }
141
142 func items(w *bufio.Writer, r io.Reader, cfg config) error {
143 const gb = 1024 * 1024 * 1024
144 sc := bufio.NewScanner(r)
145 sc.Buffer(nil, 8*gb)
146
147 handleLine := handleSSV
148 if cfg.tsv {
149 handleLine = handleTSV
150 }
151
152 for i := 0; sc.Scan(); i++ {
153 s := sc.Bytes()
154 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
155 s = s[3:]
156 }
157
158 if handleLine(w, s) != nil {
159 return io.EOF
160 }
161
162 if cfg.liveLines && w.Flush() != nil {
163 return io.EOF
164 }
165 }
166
167 return sc.Err()
168 }
169
170 func handleSSV(w *bufio.Writer, line []byte) error {
171 for len(line) > 0 {
172 if i := indexNonWhitespace(line); i >= 0 {
173 line = line[i:]
174 }
175 if len(line) == 0 {
176 break
177 }
178
179 var item []byte
180 skip := 0
181
182 i := indexWhitespace(line)
183 if i >= 0 {
184 item = line[:i]
185 skip = i + 1
186 } else {
187 item = line
188 skip = len(line)
189 }
190
191 w.Write(item)
192 if w.WriteByte('\n') != nil {
193 return io.EOF
194 }
195
196 line = line[skip:]
197 }
198
199 return nil
200 }
201
202 func handleTSV(w *bufio.Writer, line []byte) error {
203 for len(line) > 0 {
204 var item []byte
205 skip := 0
206
207 i := bytes.IndexByte(line, '\t')
208 if i >= 0 {
209 item = line[:i]
210 skip = i + 1
211 } else {
212 item = line
213 skip = len(line)
214 }
215
216 w.Write(item)
217 if w.WriteByte('\n') != nil {
218 return io.EOF
219 }
220
221 line = line[skip:]
222 }
223
224 return nil
225 }
226
227 func indexNonWhitespace(s []byte) int {
228 for i, b := range s {
229 switch b {
230 case ' ', '\t':
231 continue
232 }
233 return i
234 }
235
236 return -1
237 }
238
239 func indexWhitespace(s []byte) int {
240 for i, b := range s {
241 switch b {
242 case ' ', '\t':
243 return i
244 }
245 }
246
247 return -1
248 }
File: ./items/items_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package items
26
27 import (
28 "bufio"
29 "strings"
30 "testing"
31 )
32
33 func TestSingleLineItems(t *testing.T) {
34 tests := []struct {
35 line string
36 expected []string
37 tsv bool
38 }{
39 {``, nil, false},
40 {``, nil, true},
41 {` abc def `, []string{`abc`, `def`}, false},
42 {` abc def `, []string{` abc def `}, true},
43 {" abc \t def ", []string{` abc `, ` def `}, true},
44 {" abc \t def ", []string{`abc`, `def`}, false},
45 }
46
47 for _, tc := range tests {
48 var prefix string
49 if tc.tsv {
50 prefix = `TSV: `
51 } else {
52 prefix = `SSV: `
53 }
54
55 t.Run(prefix+tc.line, func(t *testing.T) {
56 var sb strings.Builder
57 w := bufio.NewWriter(&sb)
58
59 var cfg config
60 cfg.tsv = tc.tsv
61 if err := items(w, strings.NewReader(tc.line), cfg); err != nil {
62 t.Fatal(err)
63 return
64 }
65 w.Flush()
66
67 out := strings.TrimSuffix(sb.String(), "\n")
68 got := strings.Split(out, "\n")
69 if len(got) == 1 && got[0] == `` {
70 got = nil
71 }
72
73 if len(tc.expected) != len(got) {
74 const fs = "len %d vs. %d: got\n%v\nexp\n%v\n"
75 t.Fatalf(fs, len(got), len(tc.expected), got, tc.expected)
76 return
77 }
78
79 for i, s := range got {
80 if s != tc.expected[i] {
81 t.Logf("#%d: %q vs. %q", i, s, tc.expected[i])
82 t.Fatalf("got\n%s", out)
83 return
84 }
85 }
86 })
87 }
88 }
File: ./json0/json0.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package json0
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "os"
32 "strconv"
33 "unicode"
34 )
35
36 const info = `
37 json0 [options...] [file...]
38
39
40 JSON-0 converts/fixes JSON/pseudo-JSON input into minimal JSON output.
41 Its output is always a single line, which ends with a line-feed.
42
43 Besides minimizing bytes, this tool also adapts almost-JSON input into
44 valid JSON, since it
45
46 - ignores extra/trailing commas in arrays and objects
47 - ignores rest-of-line, multi-line, and even unix-style comments
48 - ignores final 'n' in javascript bigint literals
49 - ignores final trailing semicolon
50 - turns single-quoted strings/keys into double-quoted strings
51 - turns parentheses into proper square-bracket array delimiters
52 - turns PYON (PYthon Object Notation) into its valid JSON counterpart
53 - double-quotes unquoted object keys
54 - changes \x 2-hex-digit into \u 4-hex-digit string-escapes
55
56 All options available can either start with a single or a double-dash
57
58 -h, -help show this help message
59 -jsonl emit JSON Lines, when top-level value is an array
60 -max-rec next argument limits maximum-recursion allowed
61 `
62
63 const (
64 bufSize = 32 * 1024
65 chunkPeekSize = 32
66 )
67
68 // escapedStringBytes helps funcs like handleString treat all strings
69 // quickly and correctly, using their officially-supported JSON escape
70 // sequences
71 //
72 // https://www.rfc-editor.org/rfc/rfc8259#section-7
73 var escapedStringBytes = [128][]byte{
74 {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
75 {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
76 {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
77 {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
78 {'\\', 'b'}, {'\\', 't'},
79 {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
80 {'\\', 'f'}, {'\\', 'r'},
81 {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
82 {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
83 {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
84 {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
85 {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
86 {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
87 {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
88 {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
89 {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
90 {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
91 {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
92 {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
93 {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
94 {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
95 {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
96 {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
97 {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
98 {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
99 {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
100 {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
101 {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
102 }
103
104 type config struct {
105 maxNesting int
106 liveLines bool
107 }
108
109 func Main() {
110 args := os.Args[1:]
111 buffered := true // avoid checking stdout by default, when emitting 1 line
112 handler := json0
113 var cfg config
114
115 for len(args) > 0 {
116 switch args[0] {
117 case `-b`, `--b`, `-buffered`, `--buffered`:
118 buffered = true
119 args = args[1:]
120 continue
121
122 case `-h`, `--h`, `-help`, `--help`:
123 os.Stdout.WriteString(info[1:])
124 return
125
126 case `-jsonl`, `--jsonl`:
127 handler = jsonl
128 args = args[1:]
129 continue
130
131 case
132 `-max-nesting`, `--max-nesting`,
133 `-max-rec`, `--max-rec`,
134 `-max-recursion`, `--max-recursion`:
135 if len(args) >= 2 {
136 s := args[1]
137 n, err := strconv.ParseInt(s, 10, 64)
138 if err != nil {
139 os.Stderr.WriteString("invalid number '" + s + "'\n")
140 os.Exit(1)
141 return
142 }
143
144 cfg.maxNesting = int(n)
145 args = args[2:]
146 continue
147 }
148
149 os.Stderr.WriteString("forgot the max-recursion limit\n")
150 os.Exit(1)
151 return
152 }
153
154 break
155 }
156
157 if len(args) > 0 && args[0] == `--` {
158 args = args[1:]
159 }
160
161 if len(args) > 1 {
162 os.Stderr.WriteString("multiple inputs aren't allowed\n")
163 os.Exit(1)
164 return
165 }
166
167 cfg.liveLines = !buffered
168 if !buffered {
169 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
170 cfg.liveLines = false
171 }
172 }
173
174 name := `-`
175 if len(args) == 1 {
176 name = args[0]
177 }
178
179 err := run(os.Stdout, name, handler, cfg)
180 if err != nil && err != io.EOF {
181 os.Stderr.WriteString(`json0: `)
182 os.Stderr.WriteString(err.Error())
183 os.Stderr.WriteString("\n")
184 os.Exit(1)
185 return
186 }
187 }
188
189 type handlerFunc func(w *bufio.Writer, r *bufio.Reader, cfg config) error
190
191 func run(w io.Writer, name string, handler handlerFunc, cfg config) error {
192 // f, _ := os.Create(`json0.prof`)
193 // defer f.Close()
194 // pprof.StartCPUProfile(f)
195 // defer pprof.StopCPUProfile()
196
197 if name == `` || name == `-` {
198 bw := bufio.NewWriterSize(w, bufSize)
199 br := bufio.NewReaderSize(os.Stdin, bufSize)
200 defer bw.Flush()
201 return handler(bw, br, cfg)
202 }
203
204 f, err := os.Open(name)
205 if err != nil {
206 return errors.New(`can't read from file named "` + name + `"`)
207 }
208 defer f.Close()
209
210 bw := bufio.NewWriterSize(w, bufSize)
211 br := bufio.NewReaderSize(f, bufSize)
212 defer bw.Flush()
213 return handler(bw, br, cfg)
214 }
215
216 var (
217 errCommentEarlyEnd = errors.New(`unexpected early-end of comment`)
218 errInputEarlyEnd = errors.New(`expected end of input data`)
219 errInvalidComment = errors.New(`expected / or *`)
220 errInvalidHex = errors.New(`expected a base-16 digit`)
221 errInvalidRune = errors.New(`invalid UTF-8 bytes`)
222 errInvalidToken = errors.New(`invalid JSON token`)
223 errNoDigits = errors.New(`expected numeric digits`)
224 errNoStringQuote = errors.New(`expected " or '`)
225 errNoArrayComma = errors.New(`missing comma between array values`)
226 errNoObjectComma = errors.New(`missing comma between key-value pairs`)
227 errStringEarlyEnd = errors.New(`unexpected early-end of string`)
228 errTooMuchRecursion = errors.New(`exceeded recursion-limit given`)
229 errExtraBytes = errors.New(`unexpected extra input bytes`)
230 )
231
232 // linePosError is a more descriptive kind of error, showing the source of
233 // the input-related problem, as 1-based a line/pos number pair in front
234 // of the error message
235 type linePosError struct {
236 // line is the 1-based line count from the input
237 line int
238
239 // pos is the 1-based `horizontal` position in its line
240 pos int
241
242 // err is the error message to `decorate` with the position info
243 err error
244 }
245
246 // Error satisfies the error interface
247 func (lpe linePosError) Error() string {
248 where := strconv.Itoa(lpe.line) + `:` + strconv.Itoa(lpe.pos)
249 return where + `: ` + lpe.err.Error()
250 }
251
252 // isIdentifier improves control-flow of func handleKey, when it handles
253 // unquoted object keys
254 var isIdentifier = [256]bool{
255 '_': true,
256
257 '0': true, '1': true, '2': true, '3': true, '4': true,
258 '5': true, '6': true, '7': true, '8': true, '9': true,
259
260 'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true,
261 'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true,
262 'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true,
263 'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
264 'Y': true, 'Z': true,
265
266 'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true,
267 'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true,
268 'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true,
269 's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true,
270 'y': true, 'z': true,
271 }
272
273 // matchHex both figures out if a byte is a valid ASCII hex-digit, by not
274 // being 0, and normalizes letter-case for the hex letters
275 var matchHex = [256]byte{
276 '0': '0', '1': '1', '2': '2', '3': '3', '4': '4',
277 '5': '5', '6': '6', '7': '7', '8': '8', '9': '9',
278 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F',
279 'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F',
280 }
281
282 // json0 converts JSON/pseudo-JSON into (valid) minimal JSON; final boolean
283 // value isn't used, and is just there to match the signature of func jsonl
284 func json0(w *bufio.Writer, r *bufio.Reader, cfg config) error {
285 jr := jsonReader{r: r, line: 1, pos: 1, maxNesting: cfg.maxNesting}
286 defer w.Flush()
287
288 if err := jr.handleLeadingJunk(); err != nil {
289 return err
290 }
291
292 // handle a single top-level JSON value
293 err := handleValue(w, &jr)
294
295 // end the only output-line with a line-feed; this also avoids showing
296 // error messages on the same line as the main output, since JSON-0
297 // output has no line-feeds before its last byte
298 outputByte(w, '\n')
299
300 if err != nil {
301 return err
302 }
303 return jr.handleTrailingJunk()
304 }
305
306 // jsonl converts JSON/pseudo-JSON into (valid) minimal JSON Lines; this func
307 // avoids writing a trailing line-feed, leaving that up to its caller
308 func jsonl(w *bufio.Writer, r *bufio.Reader, cfg config) error {
309 jr := jsonReader{r: r, line: 1, pos: 1, maxNesting: cfg.maxNesting}
310
311 if err := jr.handleLeadingJunk(); err != nil {
312 return err
313 }
314
315 chunk, err := jr.r.Peek(1)
316 if err == nil && len(chunk) >= 1 {
317 switch b := chunk[0]; b {
318 case '[', '(':
319 return handleArrayJSONL(w, &jr, b, cfg.liveLines)
320 }
321 }
322
323 // handle a single top-level JSON value
324 err = handleValue(w, &jr)
325
326 // end the only output-line with a line-feed; this also avoids showing
327 // error messages on the same line as the main output, since JSON-0
328 // output has no line-feeds before its last byte
329 outputByte(w, '\n')
330
331 if err != nil {
332 return err
333 }
334 return jr.handleTrailingJunk()
335 }
336
337 // handleArrayJSONL handles top-level arrays for func jsonl
338 func handleArrayJSONL(w *bufio.Writer, jr *jsonReader, start byte, live bool) error {
339 if err := jr.demandSyntax(start); err != nil {
340 return err
341 }
342
343 var end byte = ']'
344 if start == '(' {
345 end = ')'
346 }
347
348 for n := 0; true; n++ {
349 // there may be whitespace/comments before the next comma
350 if err := jr.seekNext(); err != nil {
351 return err
352 }
353
354 // handle commas between values, as well as trailing ones
355 comma := false
356 b, _ := jr.peekByte()
357 if b == ',' {
358 jr.discardSyntaxByte()
359 comma = true
360
361 // there may be whitespace/comments before an ending ']'
362 if err := jr.seekNext(); err != nil {
363 return err
364 }
365 b, _ = jr.peekByte()
366 }
367
368 // handle end of array
369 if b == end {
370 jr.discardSyntaxByte()
371 if n > 0 {
372 err := outputByte(w, '\n')
373 if live && w.Flush() != nil {
374 return io.EOF
375 }
376 return err
377 }
378 return nil
379 }
380
381 // turn commas between adjacent values into line-feeds, as the
382 // output for this custom func is supposed to be JSON Lines
383 if n > 0 {
384 if !comma {
385 return errNoArrayComma
386 }
387 if err := outputByte(w, '\n'); err != nil {
388 return err
389 }
390 if live && w.Flush() != nil {
391 return io.EOF
392 }
393 }
394
395 // handle the next value
396 if err := jr.seekNext(); err != nil {
397 return err
398 }
399 if err := handleValue(w, jr); err != nil {
400 return err
401 }
402 }
403
404 // make the compiler happy
405 return nil
406 }
407
408 // jsonReader reads data via a buffer, keeping track of the input position:
409 // this in turn allows showing much more useful errors, when these happen
410 type jsonReader struct {
411 // r is the actual reader
412 r *bufio.Reader
413
414 // line is the 1-based line-counter for input bytes, and gives errors
415 // useful position info
416 line int
417
418 // pos is the 1-based `horizontal` position in its line, and gives
419 // errors useful position info
420 pos int
421
422 // maxNesting optionally controls recursion when parsing the input;
423 // checking recursion is disabled when this value is 0 or negative
424 maxNesting int
425
426 // nesting is the current recursive-nesting level, which can only
427 // increase when handling arrays and/or objects
428 nesting int
429 }
430
431 // exitRec decreases the recursion counter: this func is handy to defer
432 func (jr *jsonReader) exitRec() {
433 jr.nesting--
434 }
435
436 // improveError makes any error more useful, by giving it info about the
437 // current input-position, as a 1-based line/within-line-position pair
438 func (jr jsonReader) improveError(err error) error {
439 if _, ok := err.(linePosError); ok {
440 return err
441 }
442
443 if err == io.EOF {
444 return linePosError{jr.line, jr.pos, errInputEarlyEnd}
445 }
446 if err != nil {
447 return linePosError{jr.line, jr.pos, err}
448 }
449 return nil
450 }
451
452 // enterRec increases the recursion counter, checking if new recursion-level
453 // is allowed
454 func (jr *jsonReader) enterRec() error {
455 jr.nesting++
456 if jr.maxNesting > 0 && jr.maxNesting < jr.nesting {
457 return errTooMuchRecursion
458 }
459 return nil
460 }
461
462 func (jr *jsonReader) handleLeadingJunk() error {
463 // input is already assumed to be UTF-8: a leading UTF-8 BOM (byte-order
464 // mark) gives no useful info if present, as UTF-8 leaves no ambiguity
465 // about byte-order by design
466 jr.skipUTF8BOM()
467
468 // ignore leading whitespace and/or comments
469 return jr.seekNext()
470 }
471
472 func (jr *jsonReader) handleTrailingJunk() error {
473 // ignore trailing whitespace and/or comments
474 if err := jr.seekNext(); err != nil {
475 return err
476 }
477
478 // ignore trailing semicolons
479 for {
480 if b, ok := jr.peekByte(); !ok || b != ';' {
481 break
482 }
483
484 jr.discardSyntaxByte()
485 // ignore trailing whitespace and/or comments
486 if err := jr.seekNext(); err != nil {
487 return err
488 }
489 }
490
491 // beyond trailing whitespace and/or comments, any more bytes
492 // make the whole input data invalid JSON
493 if _, ok := jr.peekByte(); ok {
494 return jr.improveError(errExtraBytes)
495 }
496 return nil
497 }
498
499 // demandSyntax fails with an error when the next byte isn't the one given;
500 // when it is, the byte is then read/skipped, and a nil error is returned
501 func (jr *jsonReader) demandSyntax(syntax byte) error {
502 chunk, err := jr.r.Peek(1)
503 if err == io.EOF {
504 return jr.improveError(errInputEarlyEnd)
505 }
506 if err != nil {
507 return jr.improveError(err)
508 }
509
510 if len(chunk) < 1 || chunk[0] != syntax {
511 msg := `expected ` + string(rune(syntax))
512 return jr.improveError(errors.New(msg))
513 }
514
515 jr.discardSyntaxByte()
516 return nil
517 }
518
519 // discardSyntaxByte discards 1 byte, updating the reader's position info
520 func (jr *jsonReader) discardSyntaxByte() {
521 jr.readByte()
522 }
523
524 // peekByte simplifies control-flow for various other funcs
525 func (jr jsonReader) peekByte() (b byte, ok bool) {
526 chunk, err := jr.r.Peek(1)
527 if err == nil && len(chunk) >= 1 {
528 return chunk[0], true
529 }
530 return 0, false
531 }
532
533 // readByte does what it says, updating the reader's position info
534 func (jr *jsonReader) readByte() (b byte, err error) {
535 b, err = jr.r.ReadByte()
536 if err == nil {
537 if b == '\n' {
538 jr.line += 1
539 jr.pos = 1
540 } else {
541 jr.pos++
542 }
543 return b, nil
544 }
545 return b, jr.improveError(err)
546 }
547
548 // readRune does what it says, updating the reader's position info
549 func (jr *jsonReader) readRune() (r rune, err error) {
550 r, _, err = jr.r.ReadRune()
551 if err == nil {
552 if r == '\n' {
553 jr.line += 1
554 jr.pos = 1
555 } else {
556 jr.pos++
557 }
558 return r, nil
559 }
560 return r, jr.improveError(err)
561 }
562
563 // seekNext skips/seeks the next token, ignoring runs of whitespace symbols
564 // and comments, either single-line (starting with //) or general (starting
565 // with /* and ending with */)
566 func (jr *jsonReader) seekNext() error {
567 for {
568 b, err := jr.r.ReadByte()
569 if err != nil {
570 return nil
571 }
572
573 switch b {
574 case '\n':
575 jr.line++
576 jr.pos = 1
577
578 case '#':
579 jr.pos++
580 if err := jr.skipLine(); err != nil {
581 return err
582 }
583
584 case '/':
585 jr.r.UnreadByte()
586 if err := jr.skipComment(); err != nil {
587 return err
588 }
589
590 default:
591 if b <= 32 {
592 // keep skipping whitespace bytes
593 jr.pos++
594 } else {
595 // reached the next token
596 jr.r.UnreadByte()
597 return nil
598 }
599 }
600
601 // after comments, keep looking for more whitespace and/or comments
602 }
603 }
604
605 // skipComment helps func seekNext skip over comments, simplifying the latter
606 // func's control-flow
607 func (jr *jsonReader) skipComment() error {
608 err := jr.demandSyntax('/')
609 if err != nil {
610 return err
611 }
612
613 b, ok := jr.peekByte()
614 if !ok {
615 return nil
616 }
617
618 switch b {
619 case '/':
620 // handle single-line comments
621 return jr.skipLine()
622
623 case '*':
624 // handle (potentially) multi-line comments
625 return jr.skipGeneralComment()
626
627 default:
628 return jr.improveError(errInvalidComment)
629 }
630 }
631
632 // skipLine handles single-line comments for func skipComment
633 func (jr *jsonReader) skipLine() error {
634 for {
635 b, err := jr.readByte()
636 if err == io.EOF {
637 // end of input is fine in this case
638 return nil
639 }
640 if err != nil {
641 return err
642 }
643
644 if b == '\n' {
645 return nil
646 }
647 }
648 }
649
650 // skipGeneralComment handles (potentially) multi-line comments for func
651 // skipComment
652 func (jr *jsonReader) skipGeneralComment() error {
653 var prev byte
654 for {
655 b, err := jr.readByte()
656 if err != nil {
657 return jr.improveError(errCommentEarlyEnd)
658 }
659
660 if prev == '*' && b == '/' {
661 return nil
662 }
663 if b == '\n' {
664 jr.line++
665 jr.pos = 1
666 }
667 prev = b
668 }
669 }
670
671 // skipUTF8BOM does what it says, if a UTF-8 BOM is present
672 func (jr *jsonReader) skipUTF8BOM() {
673 lead, err := jr.r.Peek(3)
674 if err != nil {
675 return
676 }
677
678 if len(lead) > 2 && lead[0] == 0xef && lead[1] == 0xbb && lead[2] == 0xbf {
679 jr.r.Discard(3)
680 jr.pos += 1
681 }
682 }
683
684 // outputByte is a small wrapper on func WriteByte, which adapts any error
685 // into a custom dummy output-error, which is in turn meant to be ignored,
686 // being just an excuse to quit the app immediately and successfully
687 func outputByte(w *bufio.Writer, b byte) error {
688 err := w.WriteByte(b)
689 if err == nil {
690 return nil
691 }
692 return io.EOF
693 }
694
695 // handleArray handles arrays for func handleValue
696 func handleArray(w *bufio.Writer, jr *jsonReader, start byte) error {
697 jr.enterRec()
698 defer jr.exitRec()
699
700 if err := jr.demandSyntax(start); err != nil {
701 return err
702 }
703
704 var end byte = ']'
705 if start == '(' {
706 end = ')'
707 }
708
709 w.WriteByte('[')
710
711 for n := 0; true; n++ {
712 // there may be whitespace/comments before the next comma
713 if err := jr.seekNext(); err != nil {
714 return err
715 }
716
717 // handle commas between values, as well as trailing ones
718 comma := false
719 b, _ := jr.peekByte()
720 if b == ',' {
721 jr.discardSyntaxByte()
722 comma = true
723
724 // there may be whitespace/comments before an ending ']'
725 if err := jr.seekNext(); err != nil {
726 return err
727 }
728 b, _ = jr.peekByte()
729 }
730
731 // handle end of array
732 if b == end {
733 jr.discardSyntaxByte()
734 w.WriteByte(']')
735 return nil
736 }
737
738 // don't forget commas between adjacent values
739 if n > 0 {
740 if !comma {
741 return errNoArrayComma
742 }
743 if err := outputByte(w, ','); err != nil {
744 return err
745 }
746 }
747
748 // handle the next value
749 if err := jr.seekNext(); err != nil {
750 return err
751 }
752 if err := handleValue(w, jr); err != nil {
753 return err
754 }
755 }
756
757 // make the compiler happy
758 return nil
759 }
760
761 // handleDigits helps various number-handling funcs do their job
762 func handleDigits(w *bufio.Writer, jr *jsonReader) error {
763 if trySimpleDigits(w, jr) {
764 return nil
765 }
766
767 for n := 0; true; n++ {
768 b, _ := jr.peekByte()
769
770 // support `nice` long numbers by ignoring their underscores
771 if b == '_' {
772 jr.discardSyntaxByte()
773 continue
774 }
775
776 if '0' <= b && b <= '9' {
777 jr.discardSyntaxByte()
778 w.WriteByte(b)
779 continue
780 }
781
782 if n == 0 {
783 return errNoDigits
784 }
785 return nil
786 }
787
788 // make the compiler happy
789 return nil
790 }
791
792 // trySimpleDigits tries to handle (more quickly) digit-runs where all bytes
793 // are just digits: this is a very common case for numbers; returns whether
794 // it succeeded, so this func's caller knows knows if it needs to do anything,
795 // the slower way
796 func trySimpleDigits(w *bufio.Writer, jr *jsonReader) (gotIt bool) {
797 chunk, _ := jr.r.Peek(chunkPeekSize)
798
799 for i, b := range chunk {
800 if '0' <= b && b <= '9' {
801 continue
802 }
803
804 if i == 0 || b == '_' {
805 return false
806 }
807
808 // bulk-writing the chunk is this func's whole point
809 w.Write(chunk[:i])
810
811 jr.r.Discard(i)
812 jr.pos += i
813 return true
814 }
815
816 // maybe the digits-run is ok, but it's just longer than the chunk
817 return false
818 }
819
820 // handleDot handles pseudo-JSON numbers which start with a decimal dot
821 func handleDot(w *bufio.Writer, jr *jsonReader) error {
822 if err := jr.demandSyntax('.'); err != nil {
823 return err
824 }
825 w.Write([]byte{'0', '.'})
826 return handleDigits(w, jr)
827 }
828
829 // handleKey is used by func handleObjects and generalizes func handleString,
830 // by allowing unquoted object keys; it's not used anywhere else, as allowing
831 // unquoted string values is ambiguous with actual JSON-keyword values null,
832 // false, and true.
833 func handleKey(w *bufio.Writer, jr *jsonReader) error {
834 quote, ok := jr.peekByte()
835 if !ok {
836 return jr.improveError(errStringEarlyEnd)
837 }
838
839 if quote == '"' || quote == '\'' {
840 return handleString(w, jr, quote)
841 }
842
843 w.WriteByte('"')
844 for {
845 if b, _ := jr.peekByte(); isIdentifier[b] {
846 jr.discardSyntaxByte()
847 w.WriteByte(b)
848 continue
849 }
850
851 w.WriteByte('"')
852 return nil
853 }
854 }
855
856 // trySimpleString tries to handle (more quickly) inner-strings where all bytes
857 // are unescaped ASCII symbols: this is a very common case for strings, and is
858 // almost always the case for object keys; returns whether it succeeded, so
859 // this func's caller knows knows if it needs to do anything, the slower way
860 func trySimpleString(w *bufio.Writer, jr *jsonReader, quote byte) (gotIt bool) {
861 end := -1
862 chunk, _ := jr.r.Peek(chunkPeekSize)
863
864 for i, b := range chunk {
865 if b == byte(quote) {
866 end = i
867 break
868 }
869
870 if b < ' ' || b > 127 || b == '\\' || b == '"' {
871 return false
872 }
873 }
874
875 if end < 0 {
876 return false
877 }
878
879 // bulk-writing the chunk is this func's whole point
880 w.WriteByte('"')
881 w.Write(chunk[:end])
882 w.WriteByte('"')
883
884 jr.r.Discard(end + 1)
885 jr.pos += end + 1
886 return true
887 }
888
889 // handleKeyword is used by funcs handleFalse, handleNull, and handleTrue
890 func handleKeyword(w *bufio.Writer, jr *jsonReader, kw []byte) error {
891 for rest := kw; len(rest) > 0; rest = rest[1:] {
892 b, err := jr.readByte()
893 if err == nil && b == rest[0] {
894 // keywords given to this func have no line-feeds
895 jr.pos++
896 continue
897 }
898
899 msg := `expected JSON value ` + string(kw)
900 return jr.improveError(errors.New(msg))
901 }
902
903 w.Write(kw)
904 return nil
905 }
906
907 func replaceKeyword(w *bufio.Writer, jr *jsonReader, kw, with []byte) error {
908 for rest := kw; len(rest) > 0; rest = rest[1:] {
909 b, err := jr.readByte()
910 if err == nil && b == rest[0] {
911 // keywords given to this func have no line-feeds
912 jr.pos++
913 continue
914 }
915
916 msg := `expected JSON value ` + string(kw)
917 return jr.improveError(errors.New(msg))
918 }
919
920 w.Write(with)
921 return nil
922 }
923
924 // handleNegative handles numbers starting with a negative sign for func
925 // handleValue
926 func handleNegative(w *bufio.Writer, jr *jsonReader) error {
927 if err := jr.demandSyntax('-'); err != nil {
928 return err
929 }
930
931 w.WriteByte('-')
932 if b, _ := jr.peekByte(); b == '.' {
933 jr.discardSyntaxByte()
934 w.Write([]byte{'0', '.'})
935 return handleDigits(w, jr)
936 }
937 return handleNumber(w, jr)
938 }
939
940 // handleNumber handles numeric values/tokens, including invalid-JSON cases,
941 // such as values starting with a decimal dot
942 func handleNumber(w *bufio.Writer, jr *jsonReader) error {
943 // handle integer digits
944 if err := handleDigits(w, jr); err != nil {
945 return err
946 }
947
948 switch b, _ := jr.peekByte(); b {
949 case 'n':
950 // handle optional trailing 'n', used in javascript's bigint literals
951 jr.discardSyntaxByte()
952 return nil
953
954 case '.':
955 // handle optional decimal digits, starting with a leading dot
956 jr.discardSyntaxByte()
957 w.WriteByte('.')
958 return handleDigits(w, jr)
959
960 case 'e', 'E':
961 // handle optional exponent digits
962 jr.discardSyntaxByte()
963 w.WriteByte(b)
964 b, _ = jr.peekByte()
965 if b == '+' {
966 jr.discardSyntaxByte()
967 } else if b == '-' {
968 w.WriteByte('-')
969 jr.discardSyntaxByte()
970 }
971 return handleDigits(w, jr)
972
973 default:
974 // nothing special after some digits
975 return nil
976 }
977 }
978
979 // handleObject handles objects for func handleValue
980 func handleObject(w *bufio.Writer, jr *jsonReader) error {
981 jr.enterRec()
982 defer jr.exitRec()
983
984 if err := jr.demandSyntax('{'); err != nil {
985 return err
986 }
987
988 w.WriteByte('{')
989
990 for npairs := 0; true; npairs++ {
991 // there may be whitespace/comments before the next comma
992 if err := jr.seekNext(); err != nil {
993 return err
994 }
995
996 // handle commas between key-value pairs, as well as trailing ones
997 comma := false
998 b, _ := jr.peekByte()
999 if b == ',' {
1000 jr.discardSyntaxByte()
1001 comma = true
1002
1003 // there may be whitespace/comments before an ending '}'
1004 if err := jr.seekNext(); err != nil {
1005 return err
1006 }
1007 b, _ = jr.peekByte()
1008 }
1009
1010 // handle end of object
1011 if b == '}' {
1012 jr.discardSyntaxByte()
1013 w.WriteByte('}')
1014 return nil
1015 }
1016
1017 // don't forget commas between adjacent key-value pairs
1018 if npairs > 0 {
1019 if !comma {
1020 return errNoObjectComma
1021 }
1022 if err := outputByte(w, ','); err != nil {
1023 return err
1024 }
1025 }
1026
1027 // handle the next pair's key
1028 if err := jr.seekNext(); err != nil {
1029 return err
1030 }
1031 if err := handleKey(w, jr); err != nil {
1032 return err
1033 }
1034
1035 // demand a colon right after the key
1036 if err := jr.seekNext(); err != nil {
1037 return err
1038 }
1039 if err := jr.demandSyntax(':'); err != nil {
1040 return err
1041 }
1042 w.WriteByte(':')
1043
1044 // handle the next pair's value
1045 if err := jr.seekNext(); err != nil {
1046 return err
1047 }
1048 if err := handleValue(w, jr); err != nil {
1049 return err
1050 }
1051 }
1052
1053 // make the compiler happy
1054 return nil
1055 }
1056
1057 // handlePositive handles numbers starting with a positive sign for func
1058 // handleValue
1059 func handlePositive(w *bufio.Writer, jr *jsonReader) error {
1060 if err := jr.demandSyntax('+'); err != nil {
1061 return err
1062 }
1063
1064 // valid JSON isn't supposed to have leading pluses on numbers, so
1065 // emit nothing for it, unlike for negative numbers
1066
1067 if b, _ := jr.peekByte(); b == '.' {
1068 jr.discardSyntaxByte()
1069 w.Write([]byte{'0', '.'})
1070 return handleDigits(w, jr)
1071 }
1072 return handleNumber(w, jr)
1073 }
1074
1075 // handleString handles strings for funcs handleValue and handleObject, and
1076 // supports both single-quotes and double-quotes, always emitting the latter
1077 // in the output, of course
1078 func handleString(w *bufio.Writer, jr *jsonReader, quote byte) error {
1079 if quote != '"' && quote != '\'' {
1080 return errNoStringQuote
1081 }
1082
1083 jr.discardSyntaxByte()
1084
1085 // try the quicker no-escapes ASCII handler
1086 if trySimpleString(w, jr, quote) {
1087 return nil
1088 }
1089
1090 // it's a non-trivial inner-string, so handle it byte-by-byte
1091 w.WriteByte('"')
1092 escaped := false
1093
1094 for quote := rune(quote); true; {
1095 r, err := jr.readRune()
1096 if r == unicode.ReplacementChar {
1097 return jr.improveError(errInvalidRune)
1098 }
1099 if err != nil {
1100 if err == io.EOF {
1101 return jr.improveError(errStringEarlyEnd)
1102 }
1103 return jr.improveError(err)
1104 }
1105
1106 if !escaped {
1107 if r == '\\' {
1108 escaped = true
1109 continue
1110 }
1111
1112 // handle end of string
1113 if r == quote {
1114 return outputByte(w, '"')
1115 }
1116
1117 if int(r) <= len(escapedStringBytes) {
1118 w.Write(escapedStringBytes[r])
1119 } else {
1120 w.WriteRune(r)
1121 }
1122 continue
1123 }
1124
1125 // handle escaped items
1126 escaped = false
1127
1128 switch r {
1129 case 'u':
1130 // \u needs exactly 4 hex-digits to follow it
1131 w.Write([]byte{'\\', 'u'})
1132 if err := copyHex(w, 4, jr); err != nil {
1133 return jr.improveError(err)
1134 }
1135
1136 case 'x':
1137 // JSON only supports 4 escaped hex-digits, so pad the 2
1138 // expected hex-digits with 2 zeros
1139 w.Write([]byte{'\\', 'u', '0', '0'})
1140 if err := copyHex(w, 2, jr); err != nil {
1141 return jr.improveError(err)
1142 }
1143
1144 case 't', 'f', 'r', 'n', 'b', '\\', '"':
1145 // handle valid-JSON escaped string sequences
1146 w.WriteByte('\\')
1147 w.WriteByte(byte(r))
1148
1149 case '\'':
1150 // escaped single-quotes aren't standard JSON, but are needed
1151 // when the input uses non-standard single-quoted strings
1152 w.WriteByte('\'')
1153
1154 default:
1155 if int(r) < len(escapedStringBytes) {
1156 w.Write(escapedStringBytes[r])
1157 } else {
1158 w.WriteRune(r)
1159 }
1160 }
1161 }
1162
1163 if escaped {
1164 return jr.improveError(errStringEarlyEnd)
1165 }
1166 return nil
1167 }
1168
1169 // copyHex handles a run of hex-digits for func handleString, starting right
1170 // after the leading `\u` (or `\x`) part; this func doesn't `improve` its
1171 // errors with position info: that's up to the caller
1172 func copyHex(w *bufio.Writer, n int, jr *jsonReader) error {
1173 for i := 0; i < n; i++ {
1174 b, err := jr.readByte()
1175 if err == io.EOF {
1176 return errStringEarlyEnd
1177 }
1178 if err != nil {
1179 return err
1180 }
1181
1182 if b >= 128 {
1183 return errInvalidHex
1184 }
1185
1186 if b := matchHex[b]; b != 0 {
1187 w.WriteByte(b)
1188 continue
1189 }
1190
1191 return errInvalidHex
1192 }
1193
1194 return nil
1195 }
1196
1197 // handleValue is a generic JSON-token handler, which allows the recursive
1198 // behavior to handle any kind of JSON/pseudo-JSON input
1199 func handleValue(w *bufio.Writer, jr *jsonReader) error {
1200 chunk, err := jr.r.Peek(1)
1201 if err == nil && len(chunk) >= 1 {
1202 return handleValueDispatch(w, jr, chunk[0])
1203 }
1204
1205 if err == io.EOF {
1206 return jr.improveError(errInputEarlyEnd)
1207 }
1208 return jr.improveError(errInputEarlyEnd)
1209 }
1210
1211 // handleValueDispatch simplifies control-flow for func handleValue
1212 func handleValueDispatch(w *bufio.Writer, jr *jsonReader, b byte) error {
1213 switch b {
1214 case '#':
1215 return jr.skipLine()
1216 case 'f':
1217 return handleKeyword(w, jr, []byte{'f', 'a', 'l', 's', 'e'})
1218 case 'n':
1219 return handleKeyword(w, jr, []byte{'n', 'u', 'l', 'l'})
1220 case 't':
1221 return handleKeyword(w, jr, []byte{'t', 'r', 'u', 'e'})
1222 case 'F':
1223 return replaceKeyword(w, jr, []byte(`False`), []byte(`false`))
1224 case 'N':
1225 return replaceKeyword(w, jr, []byte(`None`), []byte(`null`))
1226 case 'T':
1227 return replaceKeyword(w, jr, []byte(`True`), []byte(`true`))
1228 case '.':
1229 return handleDot(w, jr)
1230 case '+':
1231 return handlePositive(w, jr)
1232 case '-':
1233 return handleNegative(w, jr)
1234 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
1235 return handleNumber(w, jr)
1236 case '\'', '"':
1237 return handleString(w, jr, b)
1238 case '[', '(':
1239 return handleArray(w, jr, b)
1240 case '{':
1241 return handleObject(w, jr)
1242 default:
1243 return jr.improveError(errInvalidToken)
1244 }
1245 }
File: ./json0/json_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package json0
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "strings"
32 "testing"
33 )
34
35 func TestJSON0(t *testing.T) {
36 tests := map[string]string{
37 `false`: `false`,
38 `null`: `null`,
39 ` true `: `true`,
40
41 `False`: `false`,
42 `None`: `null`,
43 ` True `: `true`,
44
45 `0`: `0`,
46 `1`: `1`,
47 `2`: `2`,
48 `3`: `3`,
49 `4`: `4`,
50 `5`: `5`,
51 `6`: `6`,
52 `7`: `7`,
53 `8`: `8`,
54 `9`: `9`,
55
56 ` .345`: `0.345`,
57 ` -.345`: `-0.345`,
58 ` +.345`: `0.345`,
59 ` +123.345`: `123.345`,
60 ` 123.34523`: `123.34523`,
61 ` 123.34_523`: `123.34523`,
62 ` 123_456.123`: `123456.123`,
63 ` 123n `: `123`,
64 ` +123n `: `123`,
65 ` -123n `: `-123`,
66 ` -123_456n `: `-123456`,
67
68 `""`: `""`,
69 `''`: `""`,
70 `"\""`: `"\""`,
71 `'\"'`: `"\""`,
72 `'\''`: `"'"`,
73 `'abc\u0e9A'`: `"abc\u0E9A"`,
74 `'abc\x1f[0m'`: `"abc\u001F[0m"`,
75 `"abc●def"`: `"abc●def"`,
76
77 `[ ]`: `[]`,
78 `[ , ]`: `[]`,
79 `[.345, false,null , ]`: `[0.345,false,null]`,
80
81 `( )`: `[]`,
82 `( , )`: `[]`,
83 `(.345, false,null , )`: `[0.345,false,null]`,
84
85 `{ }`: `{}`,
86 `{ , }`: `{}`,
87
88 `{ 'abc': .345, "def" : false, 'xyz':null , }`: `{"abc":0.345,"def":false,"xyz":null}`,
89
90 `{0problems:123,}`: `{"0problems":123}`,
91 `{0_problems:123}`: `{"0_problems":123}`,
92 }
93
94 for input, expected := range tests {
95 t.Run(input, func(t *testing.T) {
96 var out strings.Builder
97 w := bufio.NewWriter(&out)
98 r := bufio.NewReader(strings.NewReader(input))
99 if err := json0(w, r, config{}); err != nil && err != io.EOF {
100 t.Fatal(err)
101 return
102 }
103 // don't forget to flush the buffer, or output will be empty
104 w.Flush()
105
106 // output may have a final line-feed: get rid of it, or every
107 // single test-case will fail
108 got := out.String()
109 if len(got) > 0 && got[len(got)-1] == '\n' {
110 got = got[:len(got)-1]
111 }
112
113 if got != expected {
114 t.Fatalf("<got>\n%s\n<expected>\n%s", got, expected)
115 return
116 }
117 })
118 }
119 }
120
121 func TestEscapedStringBytes(t *testing.T) {
122 var escaped = map[rune][]byte{
123 '\x00': {'\\', 'u', '0', '0', '0', '0'},
124 '\x01': {'\\', 'u', '0', '0', '0', '1'},
125 '\x02': {'\\', 'u', '0', '0', '0', '2'},
126 '\x03': {'\\', 'u', '0', '0', '0', '3'},
127 '\x04': {'\\', 'u', '0', '0', '0', '4'},
128 '\x05': {'\\', 'u', '0', '0', '0', '5'},
129 '\x06': {'\\', 'u', '0', '0', '0', '6'},
130 '\x07': {'\\', 'u', '0', '0', '0', '7'},
131 '\x0b': {'\\', 'u', '0', '0', '0', 'b'},
132 '\x0e': {'\\', 'u', '0', '0', '0', 'e'},
133 '\x0f': {'\\', 'u', '0', '0', '0', 'f'},
134 '\x10': {'\\', 'u', '0', '0', '1', '0'},
135 '\x11': {'\\', 'u', '0', '0', '1', '1'},
136 '\x12': {'\\', 'u', '0', '0', '1', '2'},
137 '\x13': {'\\', 'u', '0', '0', '1', '3'},
138 '\x14': {'\\', 'u', '0', '0', '1', '4'},
139 '\x15': {'\\', 'u', '0', '0', '1', '5'},
140 '\x16': {'\\', 'u', '0', '0', '1', '6'},
141 '\x17': {'\\', 'u', '0', '0', '1', '7'},
142 '\x18': {'\\', 'u', '0', '0', '1', '8'},
143 '\x19': {'\\', 'u', '0', '0', '1', '9'},
144 '\x1a': {'\\', 'u', '0', '0', '1', 'a'},
145 '\x1b': {'\\', 'u', '0', '0', '1', 'b'},
146 '\x1c': {'\\', 'u', '0', '0', '1', 'c'},
147 '\x1d': {'\\', 'u', '0', '0', '1', 'd'},
148 '\x1e': {'\\', 'u', '0', '0', '1', 'e'},
149 '\x1f': {'\\', 'u', '0', '0', '1', 'f'},
150
151 '\t': {'\\', 't'},
152 '\f': {'\\', 'f'},
153 '\b': {'\\', 'b'},
154 '\r': {'\\', 'r'},
155 '\n': {'\\', 'n'},
156 '\\': {'\\', '\\'},
157 '"': {'\\', '"'},
158 }
159
160 if n := len(escapedStringBytes); n != 128 {
161 t.Errorf(`expected 128 entries, instead of %d`, n)
162 }
163
164 for i, v := range escapedStringBytes {
165 exp := []byte{byte(i)}
166 if esc, ok := escaped[rune(i)]; ok {
167 exp = esc
168 }
169
170 if !bytes.Equal(v, exp) {
171 t.Errorf("%d: expected %#v, got %#v", i, exp, v)
172 }
173 }
174 }
File: ./json0/tables_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package json0
26
27 import (
28 "bytes"
29 "encoding/json"
30 "testing"
31 )
32
33 func TestTables(t *testing.T) {
34 var buf []byte
35
36 for i := 0; i < 128; i++ {
37 r := rune(i)
38 exp := buf[:0]
39 exp = append(exp, '"')
40 exp = append(exp, escapedStringBytes[i]...)
41 exp = append(exp, '"')
42
43 got, err := json.Marshal(string(r))
44 if err != nil {
45 t.Error(err)
46 continue
47 }
48
49 if bytes.Equal(got, exp) {
50 continue
51 }
52
53 // can't fully rely on the JSON string-encoder from the stdlib
54 switch r {
55 case '&', '<', '>':
56 continue
57 }
58
59 const fs = "escaped-strings entry @ %d: expected %q, got %q"
60 t.Errorf(fs, i, string(exp), string(got))
61 }
62 }
File: ./json2/json2.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package json2
26
27 import (
28 "bufio"
29 "encoding/json"
30 "errors"
31 "io"
32 "os"
33 )
34
35 const info = `
36 json2 [filepath...]
37
38 JSON-2 indents valid JSON input into multi-line JSON which uses 2 spaces for
39 each indentation level.
40 `
41
42 // escapedStringBytes helps funcs like handleString treat all strings
43 // quickly and correctly, using their officially-supported JSON escape
44 // sequences
45 //
46 // https://www.rfc-editor.org/rfc/rfc8259#section-7
47 var escapedStringBytes = [128][]byte{
48 {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
49 {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
50 {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
51 {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
52 {'\\', 'b'}, {'\\', 't'},
53 {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
54 {'\\', 'f'}, {'\\', 'r'},
55 {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
56 {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
57 {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
58 {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
59 {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
60 {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
61 {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
62 {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
63 {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
64 {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
65 {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
66 {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
67 {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
68 {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
69 {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
70 {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
71 {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
72 {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
73 {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
74 {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
75 {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
76 }
77
78 func Main() {
79 args := os.Args[1:]
80
81 if len(args) > 0 {
82 switch args[0] {
83 case `-h`, `--h`, `-help`, `--help`:
84 os.Stdout.WriteString(info[1:])
85 return
86 }
87 }
88
89 if len(args) > 0 && args[0] == `--` {
90 args = args[1:]
91 }
92
93 if len(args) > 1 {
94 const msg = "multiple inputs aren't allowed\n"
95 os.Stderr.WriteString(msg)
96 os.Exit(1)
97 return
98 }
99
100 // figure out whether input should come from a named file or from stdin
101 name := `-`
102 if len(args) == 1 {
103 name = args[0]
104 }
105
106 if err := handleInput(os.Stdout, name); err != nil && err != io.EOF {
107 os.Stderr.WriteString(`json2: `)
108 os.Stderr.WriteString(err.Error())
109 os.Stderr.WriteString("\n")
110 os.Exit(1)
111 return
112 }
113 }
114
115 // handleInput simplifies control-flow for func main
116 func handleInput(w io.Writer, path string) error {
117 if path == `-` {
118 return convert(w, os.Stdin)
119 }
120
121 f, err := os.Open(path)
122 if err != nil {
123 // on windows, file-not-found error messages may mention `CreateFile`,
124 // even when trying to open files in read-only mode
125 return errors.New(`can't open file named ` + path)
126 }
127 defer f.Close()
128 return convert(w, f)
129 }
130
131 // convert simplifies control-flow for func handleInput
132 func convert(w io.Writer, r io.Reader) error {
133 bw := bufio.NewWriterSize(w, 32*1024)
134 defer bw.Flush()
135 return json2(bw, r)
136 }
137
138 // writeSpaces does what it says, minimizing calls to write-like funcs
139 func writeSpaces(w *bufio.Writer, n int) {
140 const spaces = ` `
141 if n < 1 {
142 return
143 }
144
145 for n >= len(spaces) {
146 w.WriteString(spaces)
147 n -= len(spaces)
148 }
149 w.WriteString(spaces[:n])
150 }
151
152 // json2 does it all, given a reader and a writer
153 func json2(w *bufio.Writer, r io.Reader) error {
154 dec := json.NewDecoder(r)
155 // avoid parsing numbers, so unusually-long numbers are kept verbatim,
156 // even if JSON parsers aren't required to guarantee such input-fidelity
157 // for numbers
158 dec.UseNumber()
159
160 t, err := dec.Token()
161 if err == io.EOF {
162 return errors.New(`input has no JSON values`)
163 }
164
165 if err = handleToken(w, dec, t, 0, 0); err != nil {
166 return err
167 }
168 // don't forget ending the last line for the last value
169 w.WriteByte('\n')
170
171 _, err = dec.Token()
172 if err == io.EOF {
173 // input is over, so it's a success
174 return nil
175 }
176
177 if err == nil {
178 // a successful `read` is a failure, as it means there are
179 // trailing JSON tokens
180 return errors.New(`unexpected trailing data`)
181 }
182
183 // any other error, perhaps some invalid-JSON-syntax-type error
184 return err
185 }
186
187 // handleToken handles recursion for func json2
188 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, pre, level int) error {
189 switch t := t.(type) {
190 case json.Delim:
191 switch t {
192 case json.Delim('['):
193 return handleArray(w, dec, pre, level)
194 case json.Delim('{'):
195 return handleObject(w, dec, pre, level)
196 default:
197 return errors.New(`unsupported JSON syntax ` + string(t))
198 }
199
200 case nil:
201 writeSpaces(w, 2*pre)
202 w.WriteString(`null`)
203 return nil
204
205 case bool:
206 writeSpaces(w, 2*pre)
207 if t {
208 w.WriteString(`true`)
209 } else {
210 w.WriteString(`false`)
211 }
212 return nil
213
214 case json.Number:
215 writeSpaces(w, 2*pre)
216 w.WriteString(t.String())
217 return nil
218
219 case string:
220 return handleString(w, t, pre)
221
222 default:
223 // return fmt.Errorf(`unsupported token type %T`, t)
224 return errors.New(`invalid JSON token`)
225 }
226 }
227
228 // handleArray handles arrays for func handleToken
229 func handleArray(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
230 for i := 0; true; i++ {
231 t, err := dec.Token()
232 if err == io.EOF {
233 return errors.New(`end of JSON before array was closed`)
234 }
235 if err != nil {
236 return err
237 }
238
239 if t == json.Delim(']') {
240 if i == 0 {
241 writeSpaces(w, 2*pre)
242 w.WriteByte('[')
243 w.WriteByte(']')
244 } else {
245 w.WriteByte('\n')
246 writeSpaces(w, 2*level)
247 w.WriteByte(']')
248 }
249 return nil
250 }
251
252 if i == 0 {
253 writeSpaces(w, 2*pre)
254 w.WriteByte('[')
255 w.WriteByte('\n')
256 } else {
257 w.WriteByte(',')
258 w.WriteByte('\n')
259 }
260
261 err = handleToken(w, dec, t, level+1, level+1)
262 if err != nil {
263 return err
264 }
265 }
266
267 // make the compiler happy
268 return nil
269 }
270
271 // handleObject handles objects for func handleToken
272 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
273 for i := 0; true; i++ {
274 t, err := dec.Token()
275 if err == io.EOF {
276 return errors.New(`end of JSON before object was closed`)
277 }
278 if err != nil {
279 return err
280 }
281
282 if t == json.Delim('}') {
283 if i == 0 {
284 writeSpaces(w, 2*pre)
285 w.WriteByte('{')
286 w.WriteByte('}')
287 } else {
288 w.WriteByte('\n')
289 writeSpaces(w, 2*level)
290 w.WriteByte('}')
291 }
292 return nil
293 }
294
295 if i == 0 {
296 writeSpaces(w, 2*pre)
297 w.WriteByte('{')
298 w.WriteByte('\n')
299 } else {
300 w.WriteByte(',')
301 w.WriteByte('\n')
302 }
303
304 k, ok := t.(string)
305 if !ok {
306 return errors.New(`expected a string for a key-value pair`)
307 }
308
309 err = handleString(w, k, level+1)
310 if err != nil {
311 return err
312 }
313
314 w.WriteString(": ")
315
316 t, err = dec.Token()
317 if err == io.EOF {
318 return errors.New(`expected a value for a key-value pair`)
319 }
320
321 err = handleToken(w, dec, t, 0, level+1)
322 if err != nil {
323 return err
324 }
325 }
326
327 // make the compiler happy
328 return nil
329 }
330
331 // handleString handles strings for func handleToken, and keys for func
332 // handleObject
333 func handleString(w *bufio.Writer, s string, level int) error {
334 writeSpaces(w, 2*level)
335
336 for _, r := range s {
337 if r < ' ' || r == '"' || r == '\\' {
338 w.WriteByte('"')
339 for _, r := range s {
340 if int(r) < len(escapedStringBytes) {
341 w.Write(escapedStringBytes[r])
342 } else {
343 w.WriteRune(r)
344 }
345 }
346 w.WriteByte('"')
347 return nil
348 }
349 }
350
351 w.WriteByte('"')
352 w.WriteString(s)
353 w.WriteByte('"')
354 return nil
355 }
File: ./jsonl/jsonl.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package jsonl
26
27 import (
28 "bufio"
29 "encoding/json"
30 "errors"
31 "io"
32 "os"
33 )
34
35 const info = `
36 jsonl [options...] [filepaths...]
37
38 JSON Lines turns valid JSON-input arrays into separate JSON lines, one for
39 each top-level item. Non-arrays result in a single JSON-line.
40
41 When not given a filepath to load, standard input is used instead. Every
42 output line is always a single top-level item from the input.
43 `
44
45 func Main() {
46 args := os.Args[1:]
47 buffered := false
48
49 for len(args) > 0 {
50 switch args[0] {
51 case `-b`, `--b`, `-buffered`, `--buffered`:
52 buffered = true
53 args = args[1:]
54 continue
55
56 case `-h`, `--h`, `-help`, `--help`:
57 os.Stdout.WriteString(info[1:])
58 return
59 }
60
61 break
62 }
63
64 if len(args) > 0 && args[0] == `--` {
65 args = args[1:]
66 }
67
68 liveLines := !buffered
69 if !buffered {
70 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
71 liveLines = false
72 }
73 }
74
75 if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
76 os.Stderr.WriteString(`jsonl: `)
77 os.Stderr.WriteString(err.Error())
78 os.Stderr.WriteString("\n")
79 os.Exit(1)
80 return
81 }
82 }
83
84 func run(w io.Writer, args []string, liveLines bool) error {
85 dashes := 0
86 for _, path := range args {
87 if path == `-` {
88 dashes++
89 }
90 if dashes > 1 {
91 return errors.New(`can't use stdin (dash) more than once`)
92 }
93 }
94
95 bw := bufio.NewWriterSize(w, 32*1024)
96 defer bw.Flush()
97
98 if len(args) == 0 {
99 return handleInput(bw, `-`, liveLines)
100 }
101
102 for _, path := range args {
103 if err := handleInput(bw, path, liveLines); err != nil {
104 return err
105 }
106 }
107 return nil
108 }
109
110 // handleInput simplifies control-flow for func main
111 func handleInput(w *bufio.Writer, path string, liveLines bool) error {
112 if path == `-` {
113 return jsonl(w, os.Stdin, liveLines)
114 }
115
116 f, err := os.Open(path)
117 if err != nil {
118 // on windows, file-not-found error messages may mention `CreateFile`,
119 // even when trying to open files in read-only mode
120 return errors.New(`can't open file named ` + path)
121 }
122 defer f.Close()
123 return jsonl(w, f, liveLines)
124 }
125
126 // escapedStringBytes helps funcs like handleString treat all strings
127 // quickly and correctly, using their officially-supported JSON escape
128 // sequences
129 //
130 // https://www.rfc-editor.org/rfc/rfc8259#section-7
131 var escapedStringBytes = [128][]byte{
132 {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
133 {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
134 {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
135 {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
136 {'\\', 'b'}, {'\\', 't'},
137 {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
138 {'\\', 'f'}, {'\\', 'r'},
139 {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
140 {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
141 {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
142 {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
143 {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
144 {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
145 {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
146 {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
147 {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
148 {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
149 {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
150 {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
151 {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
152 {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
153 {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
154 {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
155 {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
156 {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
157 {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
158 {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
159 {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
160 }
161
162 // jsonl does it all, given a reader and a writer
163 func jsonl(w *bufio.Writer, r io.Reader, live bool) error {
164 dec := json.NewDecoder(r)
165 // avoid parsing numbers, so unusually-long numbers are kept verbatim,
166 // even if JSON parsers aren't required to guarantee such input-fidelity
167 // for numbers
168 dec.UseNumber()
169
170 t, err := dec.Token()
171 if err == io.EOF {
172 // return errors.New(`input has no JSON values`)
173 return nil
174 }
175
176 if t == json.Delim('[') {
177 if err := handleTopLevelArray(w, dec, live); err != nil {
178 return err
179 }
180 } else {
181 if err := handleToken(w, dec, t); err != nil {
182 return err
183 }
184 w.WriteByte('\n')
185 }
186
187 _, err = dec.Token()
188 if err == io.EOF {
189 // input is over, so it's a success
190 return nil
191 }
192
193 if err == nil {
194 // a successful `read` is a failure, as it means there are
195 // trailing JSON tokens
196 return errors.New(`unexpected trailing data`)
197 }
198
199 // any other error, perhaps some invalid-JSON-syntax-type error
200 return err
201 }
202
203 // handleToken handles recursion for func json2
204 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token) error {
205 switch t := t.(type) {
206 case json.Delim:
207 switch t {
208 case json.Delim('['):
209 return handleArray(w, dec)
210 case json.Delim('{'):
211 return handleObject(w, dec)
212 default:
213 return errors.New(`unsupported JSON syntax ` + string(t))
214 }
215
216 case nil:
217 w.WriteString(`null`)
218 return nil
219
220 case bool:
221 if t {
222 w.WriteString(`true`)
223 } else {
224 w.WriteString(`false`)
225 }
226 return nil
227
228 case json.Number:
229 w.WriteString(t.String())
230 return nil
231
232 case string:
233 return handleString(w, t)
234
235 default:
236 // return fmt.Errorf(`unsupported token type %T`, t)
237 return errors.New(`invalid JSON token`)
238 }
239 }
240
241 func handleTopLevelArray(w *bufio.Writer, dec *json.Decoder, live bool) error {
242 for i := 0; true; i++ {
243 t, err := dec.Token()
244 if err == io.EOF {
245 return nil
246 }
247
248 if err != nil {
249 return err
250 }
251
252 if t == json.Delim(']') {
253 return nil
254 }
255
256 err = handleToken(w, dec, t)
257 if err != nil {
258 return err
259 }
260
261 if w.WriteByte('\n') != nil {
262 return io.EOF
263 }
264
265 if live && w.Flush() != nil {
266 return io.EOF
267 }
268 }
269
270 // make the compiler happy
271 return nil
272 }
273
274 // handleArray handles arrays for func handleToken
275 func handleArray(w *bufio.Writer, dec *json.Decoder) error {
276 w.WriteByte('[')
277
278 for i := 0; true; i++ {
279 t, err := dec.Token()
280 if err == io.EOF {
281 return errors.New(`end of JSON before array was closed`)
282 }
283 if err != nil {
284 return err
285 }
286
287 if t == json.Delim(']') {
288 w.WriteByte(']')
289 return nil
290 }
291
292 if i > 0 {
293 _, err := w.WriteString(", ")
294 if err != nil {
295 return io.EOF
296 }
297 }
298
299 err = handleToken(w, dec, t)
300 if err != nil {
301 return err
302 }
303 }
304
305 // make the compiler happy
306 return nil
307 }
308
309 // handleObject handles objects for func handleToken
310 func handleObject(w *bufio.Writer, dec *json.Decoder) error {
311 w.WriteByte('{')
312
313 for i := 0; true; i++ {
314 t, err := dec.Token()
315 if err == io.EOF {
316 return errors.New(`end of JSON before object was closed`)
317 }
318 if err != nil {
319 return err
320 }
321
322 if t == json.Delim('}') {
323 w.WriteByte('}')
324 return nil
325 }
326
327 if i > 0 {
328 _, err := w.WriteString(", ")
329 if err != nil {
330 return io.EOF
331 }
332 }
333
334 k, ok := t.(string)
335 if !ok {
336 return errors.New(`expected a string for a key-value pair`)
337 }
338
339 err = handleString(w, k)
340 if err != nil {
341 return err
342 }
343
344 w.WriteString(": ")
345
346 t, err = dec.Token()
347 if err == io.EOF {
348 return errors.New(`expected a value for a key-value pair`)
349 }
350
351 err = handleToken(w, dec, t)
352 if err != nil {
353 return err
354 }
355 }
356
357 // make the compiler happy
358 return nil
359 }
360
361 // handleString handles strings for func handleToken, and keys for func
362 // handleObject
363 func handleString(w *bufio.Writer, s string) error {
364 for _, r := range s {
365 if r < ' ' || r == '"' || r == '\\' {
366 w.WriteByte('"')
367 for _, r := range s {
368 if int(r) < len(escapedStringBytes) {
369 w.Write(escapedStringBytes[r])
370 } else {
371 w.WriteRune(r)
372 }
373 }
374 w.WriteByte('"')
375 return nil
376 }
377 }
378
379 w.WriteByte('"')
380 w.WriteString(s)
381 w.WriteByte('"')
382 return nil
383 }
File: ./jsons/jsons.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package jsons
26
27 import (
28 "bufio"
29 "io"
30 "os"
31 "strings"
32 )
33
34 const info = `
35 jsons [options...] [filenames...]
36
37 JSON Strings turns TSV (tab-separated values) data into a JSON array of
38 objects whose values are strings or nulls, the latter being used for
39 missing trailing values.
40 `
41
42 func Main() {
43 if len(os.Args) > 1 {
44 switch os.Args[1] {
45 case `-h`, `--h`, `-help`, `--help`:
46 os.Stdout.WriteString(info[1:])
47 return
48 }
49 }
50
51 err := run(os.Args[1:])
52 if err == io.EOF {
53 err = nil
54 }
55
56 if err != nil {
57 os.Stderr.WriteString(`jsons: `)
58 os.Stderr.WriteString(err.Error())
59 os.Stderr.WriteString("\n")
60 os.Exit(1)
61 return
62 }
63 }
64
65 type runConfig struct {
66 lines int
67 keys []string
68 }
69
70 func run(paths []string) error {
71 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
72 defer bw.Flush()
73
74 dashes := 0
75 var cfg runConfig
76
77 for _, path := range paths {
78 if path == `-` {
79 dashes++
80 if dashes > 1 {
81 continue
82 }
83
84 if err := handleInput(bw, os.Stdin, &cfg); err != nil {
85 return err
86 }
87
88 continue
89 }
90
91 if err := handleFile(bw, path, &cfg); err != nil {
92 return err
93 }
94 }
95
96 if len(paths) == 0 {
97 if err := handleInput(bw, os.Stdin, &cfg); err != nil {
98 return err
99 }
100 }
101
102 if cfg.lines > 1 {
103 bw.WriteString("\n]\n")
104 } else {
105 bw.WriteString("[]\n")
106 }
107 return nil
108 }
109
110 func handleFile(w *bufio.Writer, path string, cfg *runConfig) error {
111 f, err := os.Open(path)
112 if err != nil {
113 return err
114 }
115 defer f.Close()
116 return handleInput(w, f, cfg)
117 }
118
119 func escapeKeys(line string) []string {
120 var keys []string
121 var sb strings.Builder
122
123 loopTSV(line, func(i int, s string) {
124 sb.WriteByte('"')
125 for _, r := range s {
126 if r == '\\' || r == '"' {
127 sb.WriteByte('\\')
128 }
129 sb.WriteRune(r)
130 }
131 sb.WriteByte('"')
132
133 keys = append(keys, sb.String())
134 sb.Reset()
135 })
136
137 return keys
138 }
139
140 func emitRow(w *bufio.Writer, line string, keys []string) {
141 j := 0
142 w.WriteByte('{')
143
144 loopTSV(line, func(i int, s string) {
145 j = i
146 if i > 0 {
147 w.WriteString(", ")
148 }
149
150 w.WriteString(keys[i])
151 w.WriteString(": \"")
152
153 for _, r := range s {
154 if r == '\\' || r == '"' {
155 w.WriteByte('\\')
156 }
157 w.WriteRune(r)
158 }
159 w.WriteByte('"')
160 })
161
162 for i := j + 1; i < len(keys); i++ {
163 if i > 0 {
164 w.WriteString(", ")
165 }
166 w.WriteString(keys[i])
167 w.WriteString(": null")
168 }
169 w.WriteByte('}')
170 }
171
172 func loopTSV(line string, f func(i int, s string)) {
173 for i := 0; len(line) > 0; i++ {
174 pos := strings.IndexByte(line, '\t')
175 if pos < 0 {
176 f(i, line)
177 return
178 }
179
180 f(i, line[:pos])
181 line = line[pos+1:]
182 }
183 }
184
185 func handleInput(w *bufio.Writer, r io.Reader, cfg *runConfig) error {
186 const gb = 1024 * 1024 * 1024
187 sc := bufio.NewScanner(r)
188 sc.Buffer(nil, 8*gb)
189
190 for i := 0; sc.Scan(); i++ {
191 s := sc.Text()
192 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
193 s = s[3:]
194 }
195
196 if cfg.lines == 0 {
197 cfg.keys = escapeKeys(s)
198 w.WriteByte('[')
199 cfg.lines++
200 continue
201 }
202
203 if cfg.lines == 1 {
204 w.WriteString("\n ")
205 } else {
206 if _, err := w.WriteString(",\n "); err != nil {
207 return io.EOF
208 }
209 }
210
211 emitRow(w, s, cfg.keys)
212 cfg.lines++
213 }
214
215 return sc.Err()
216 }
File: ./junk/junk.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package junk
26
27 import (
28 "crypto/rand"
29 "io"
30 "os"
31 "strconv"
32 "strings"
33 )
34
35 const info = `
36 junk [byte-count...]
37
38 Emit a given count of random bytes, or emit 1024 random bytes by default.
39
40 All options available can either start with a single or a double-dash
41
42 -h, -help show this help message
43 `
44
45 type config struct {
46 all bool
47 long bool
48 }
49
50 func Main() {
51 args := os.Args[1:]
52 if len(args) > 0 {
53 switch args[0] {
54 case `-h`, `--h`, `-help`, `--help`:
55 os.Stdout.WriteString(info[1:])
56 return
57 }
58 }
59
60 n := 1024
61 if len(args) > 0 {
62 s := strings.Replace(args[0], `_`, ``, -1)
63 if v, err := strconv.ParseInt(s, 10, 64); err == nil {
64 n = int(v)
65 args = args[1:]
66 }
67 }
68
69 if err := writeJunk(os.Stdout, n); err != nil && err != io.EOF {
70 os.Stderr.WriteString(`junk: `)
71 os.Stderr.WriteString(err.Error())
72 os.Stderr.WriteString("\n")
73 os.Exit(1)
74 return
75 }
76 }
77
78 func writeJunk(w io.Writer, n int) error {
79 var buf [32 * 1024]byte
80
81 for n > 0 {
82 max := n
83 if max > cap(buf) {
84 max = cap(buf)
85 }
86
87 got, err := io.ReadFull(rand.Reader, buf[:max])
88 if err == io.EOF && got > 0 {
89 err = nil
90 }
91
92 if err != nil {
93 return err
94 }
95
96 if _, err := w.Write(buf[:got]); err != nil {
97 return io.EOF
98 }
99
100 n -= got
101 }
102
103 return nil
104 }
File: ./label/label.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package label
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "unicode/utf8"
33 )
34
35 const info = `
36 label [options...] [words...]
37
38 Emit an ANSI-reverse-styled right-padded line with the words given as
39 command-line arguments, then copies all bytes from the standard input
40 into the standard output.
41
42 The options are, available both in single and double-dash versions
43
44 -h, -help show this help message
45 `
46
47 func Main() {
48 buffered := false
49 args := os.Args[1:]
50
51 if len(args) > 0 {
52 switch args[0] {
53 case `-b`, `--b`, `-buffered`, `--buffered`:
54 buffered = true
55 args = args[1:]
56
57 case `-h`, `--h`, `-help`, `--help`, `help`:
58 os.Stdout.WriteString(info[1:])
59 return
60 }
61 }
62
63 if len(args) > 0 && args[0] == `--` {
64 args = args[1:]
65 }
66
67 liveLines := !buffered
68 if !buffered {
69 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
70 liveLines = false
71 }
72 }
73
74 if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
75 os.Stderr.WriteString(`label: `)
76 os.Stderr.WriteString(err.Error())
77 os.Stderr.WriteString("\n")
78 os.Exit(1)
79 return
80 }
81 }
82
83 func run(w io.Writer, args []string, live bool) error {
84 const (
85 half = ` `
86 spaces = half + half
87 )
88
89 bw := bufio.NewWriterSize(w, 32*1024)
90 defer bw.Flush()
91
92 bw.WriteString("\x1b[7m")
93
94 left := len(spaces)
95
96 for i, s := range args {
97 if i > 0 {
98 if err := bw.WriteByte(' '); err != nil {
99 return io.EOF
100 }
101 left--
102 }
103 bw.WriteString(s)
104 left -= utf8.RuneCountInString(s)
105 }
106
107 if 0 < left && left < len(spaces) {
108 bw.WriteString(spaces[:left])
109 }
110
111 if _, err := bw.WriteString("\x1b[0m\n"); err != nil {
112 return io.EOF
113 }
114
115 if live && bw.Flush() != nil {
116 return io.EOF
117 }
118
119 return catl(bw, os.Stdin, live)
120 }
121
122 func catl(w *bufio.Writer, r io.Reader, live bool) error {
123 const gb = 1024 * 1024 * 1024
124 sc := bufio.NewScanner(r)
125 sc.Buffer(nil, 8*gb)
126
127 for i := 0; sc.Scan(); i++ {
128 s := sc.Bytes()
129 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
130 s = s[3:]
131 }
132
133 w.Write(s)
134 if w.WriteByte('\n') != nil {
135 return io.EOF
136 }
137
138 if live && w.Flush() != nil {
139 return io.EOF
140 }
141 }
142
143 return sc.Err()
144 }
File: ./last/last.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 /*
26 Note
27
28 A previous attempt was trying to act more like the standard `tail` app
29 for efficiency, by going backwards on the list of files, reading chunks
30 backwards to reach the starting position in the right file for (up to)
31 the last n lines.
32
33 That attempt had an intermittent bug, and was scrapped in favor of the
34 naive/slow approach used here, forward-scanning lines and keeping them
35 into a ring-buffer.
36 */
37
38 package last
39
40 import (
41 "bufio"
42 "io"
43 "os"
44 "strconv"
45 "strings"
46 )
47
48 const info = `
49 last [options...] [max lines...] [files...]
50
51 Keep at most the last n lines, or keep the last line by default. When not
52 given any filepaths, the standard input is used instead.
53
54 All (optional) leading options start with either single or double-dash:
55
56 -h, -help show this help message
57 `
58
59 type ringBuffer struct {
60 next int
61 max int
62 items []string
63 }
64
65 func (rb *ringBuffer) append(s string) {
66 if rb.next < len(rb.items) {
67 rb.items[rb.next] = s
68 } else if len(rb.items) < rb.max {
69 rb.items = append(rb.items, s)
70 } else if len(rb.items) > 0 {
71 rb.items[0] = s
72 }
73
74 rb.next++
75 if rb.next >= rb.max {
76 rb.next = 0
77 }
78 }
79
80 func Main() {
81 var latest ringBuffer
82 latest.max = 1
83 args := os.Args[1:]
84
85 if len(args) > 0 {
86 switch args[0] {
87 case `-h`, `--h`, `-help`, `--help`:
88 os.Stderr.WriteString(info[1:])
89 return
90 }
91 }
92
93 if len(args) > 0 {
94 s := strings.Replace(args[0], `_`, ``, -1)
95 n, err := strconv.ParseInt(s, 10, 64)
96 if err == nil {
97 latest.max = int(n)
98 args = args[1:]
99 }
100 }
101
102 if len(args) > 0 && args[0] == `--` {
103 args = args[1:]
104 }
105
106 if latest.max <= 0 {
107 return
108 }
109
110 if latest.max <= 1_000 {
111 latest.items = make([]string, 0, latest.max)
112 }
113
114 if err := run(args, &latest); err != nil {
115 os.Stderr.WriteString(`last: `)
116 os.Stderr.WriteString(err.Error())
117 os.Stderr.WriteString("\n")
118 os.Exit(1)
119 return
120 }
121
122 show(os.Stdout, latest)
123 }
124
125 func run(paths []string, rb *ringBuffer) error {
126 for _, path := range paths {
127 if err := handleFile(path, rb); err != nil {
128 return err
129 }
130 }
131
132 if len(paths) == 0 {
133 if err := visit(os.Stdin, rb); err != nil {
134 return err
135 }
136 }
137 return nil
138 }
139
140 func show(w io.Writer, rb ringBuffer) {
141 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
142 defer bw.Flush()
143
144 for i := rb.next; i < len(rb.items); i++ {
145 bw.WriteString(rb.items[i])
146 if err := bw.WriteByte('\n'); err != nil {
147 return
148 }
149 }
150
151 for i := 0; i < len(rb.items) && i < rb.next; i++ {
152 bw.WriteString(rb.items[i])
153 if err := bw.WriteByte('\n'); err != nil {
154 return
155 }
156 }
157 }
158
159 func handleFile(path string, rb *ringBuffer) error {
160 f, err := os.Open(path)
161 if err != nil {
162 return err
163 }
164 defer f.Close()
165 return visit(f, rb)
166 }
167
168 func visit(r io.Reader, rb *ringBuffer) error {
169 const gb = 1024 * 1024 * 1024
170 sc := bufio.NewScanner(r)
171 sc.Buffer(nil, 8*gb)
172
173 for sc.Scan() {
174 rb.append(sc.Text())
175 }
176 return sc.Err()
177 }
File: ./leak/leak.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package leak
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "os/exec"
33 )
34
35 const info = `
36 leak [options...] [command...] [command args...]
37
38
39 Emit copies of input lines both to stdout and to stderr, thus 'leaking'
40 the contents going through a pipe of commands. If a command is given, the
41 standard-error output is the standard output of that command, instead.
42
43 This tool's main use-case is to inspect/debug the intermediate stages of
44 a 'pipelined' shell command.
45
46 All (optional) leading options start with either single or double-dash:
47
48 -h, -help show this help message
49 `
50
51 type config struct {
52 // live is whether lines are flushed each time
53 live bool
54
55 inverted bool
56 }
57
58 func Main() {
59 var cfg config
60 cfg.live = true
61 cfg.inverted = false
62 args := os.Args[1:]
63
64 for len(args) > 0 {
65 switch args[0] {
66 case `-b`, `--b`, `-buffered`, `--buffered`:
67 cfg.live = false
68 args = args[1:]
69 continue
70
71 case `-h`, `--h`, `-help`, `--help`:
72 os.Stdout.WriteString(info[1:])
73 return
74
75 case `-i`, `--i`, `-inv`, `--inv`, `-hi`, `--hi`:
76 cfg.inverted = true
77 args = args[1:]
78 continue
79 }
80
81 break
82 }
83
84 if len(args) > 0 && args[0] == `--` {
85 args = args[1:]
86 }
87
88 if cfg.live {
89 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
90 cfg.live = false
91 }
92 }
93
94 err := run(os.Stdout, os.Stderr, os.Stdin, args, cfg)
95 if ee, ok := err.(*exec.ExitError); ok {
96 os.Exit(ee.ExitCode())
97 return
98 }
99 if err != nil && err != io.EOF {
100 os.Stderr.WriteString(`leak: `)
101 os.Stderr.WriteString(err.Error())
102 os.Stderr.WriteString("\n")
103 os.Exit(1)
104 return
105 }
106 }
107
108 func run(w1, w2 io.Writer, r io.Reader, args []string, cfg config) error {
109 bw1 := bufio.NewWriterSize(w1, 32*1024)
110 defer bw1.Flush()
111
112 bw2 := bufio.NewWriterSize(w2, 32*1024)
113 defer bw2.Flush()
114
115 if len(args) == 0 {
116 return tee(bw1, bw2, r, cfg)
117 }
118
119 cmd := exec.Command(args[0], args[1:]...)
120 if cfg.live {
121 cmd.Stdin = ioDuplicator{r, lineFlusher{bw1}}
122 cmd.Stdout = lineFlusher{bw2}
123 } else {
124 cmd.Stdin = ioDuplicator{r, bw1}
125 cmd.Stdout = bw2
126 }
127 cmd.Stderr = cmd.Stdout
128
129 if err := cmd.Start(); err != nil {
130 return err
131 }
132
133 return cmd.Wait()
134 }
135
136 func tee(w1 *bufio.Writer, w2 *bufio.Writer, r io.Reader, cfg config) error {
137 const gb = 1024 * 1024 * 1024
138 sc := bufio.NewScanner(r)
139 sc.Buffer(nil, 8*gb)
140
141 var buf []byte
142
143 for i := 0; sc.Scan(); i++ {
144 s := sc.Bytes()
145 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
146 s = s[3:]
147 }
148
149 w1.Write(s)
150 if w1.WriteByte('\n') != nil {
151 return io.EOF
152 }
153
154 // leak the (re)styled line to the second writer
155 if cfg.inverted {
156 buf = buf[:0]
157 buf = append(buf, "\x1b[7m"...)
158 // buf = appendPlain(buf, s)
159 buf = appendInvertedInner(buf, s)
160 buf = append(buf, "\x1b[0m"...)
161 w2.Write(buf)
162 } else {
163 w2.Write(s)
164 }
165
166 if w2.WriteByte('\n') != nil {
167 return io.EOF
168 }
169
170 if !cfg.live {
171 continue
172 }
173
174 // a write error may be the consequence of an output stream being
175 // closed, perhaps by another app along a pipe
176 if err := w1.Flush(); err != nil {
177 return io.EOF
178 }
179 if err := w2.Flush(); err != nil {
180 return io.EOF
181 }
182 }
183
184 return sc.Err()
185 }
186
187 func appendInvertedInner(dst []byte, src []byte) []byte {
188 for len(src) > 0 {
189 j := bytes.Index(src, []byte{0x1b, '[', '0', 'm'})
190 if j < 0 {
191 dst = append(dst, src...)
192 break
193 }
194
195 dst = append(dst, src[:j]...)
196 dst = append(dst, "\x1b[0m\x1b[7m"...)
197 j += len("\x1b[0m")
198 src = src[j:]
199 }
200
201 return dst
202 }
203
204 // appendPlain extends the slice given using the non-ANSI parts of a string
205 func appendPlain(dst []byte, src []byte) []byte {
206 for len(src) > 0 {
207 i, j := indexEscapeSequence(src)
208 if i < 0 {
209 dst = append(dst, src...)
210 break
211 }
212 if j < 0 {
213 j = len(src)
214 }
215
216 if i > 0 {
217 dst = append(dst, src[:i]...)
218 }
219
220 src = src[j:]
221 }
222
223 return dst
224 }
225
226 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
227 // the multi-byte sequences starting with ESC[; the result is a pair of slice
228 // indices which can be independently negative when either the start/end of
229 // a sequence isn't found; given their fairly-common use, even the hyperlink
230 // ESC]8 sequences are supported
231 func indexEscapeSequence(s []byte) (int, int) {
232 var prev byte
233
234 for i, b := range s {
235 if prev == '\x1b' && b == '[' {
236 j := indexLetter(s[i+1:])
237 if j < 0 {
238 return i, -1
239 }
240 return i - 1, i + 1 + j + 1
241 }
242
243 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
244 j := indexPair(s[i+1:], '\x1b', '\\')
245 if j < 0 {
246 return i, -1
247 }
248 return i - 1, i + 1 + j + 2
249 }
250
251 prev = b
252 }
253
254 return -1, -1
255 }
256
257 func indexLetter(s []byte) int {
258 for i, b := range s {
259 upper := b &^ 32
260 if 'A' <= upper && upper <= 'Z' {
261 return i
262 }
263 }
264
265 return -1
266 }
267
268 func indexPair(s []byte, x byte, y byte) int {
269 var prev byte
270
271 for i, b := range s {
272 if prev == x && b == y && i > 0 {
273 return i
274 }
275 prev = b
276 }
277
278 return -1
279 }
280
281 type ioDuplicator struct {
282 r io.Reader
283 w io.Writer
284 }
285
286 func (iod ioDuplicator) Read(p []byte) (n int, err error) {
287 n, err = iod.r.Read(p)
288 if n > 0 {
289 iod.w.Write(p[:n])
290 }
291 return n, err
292 }
293
294 type lineFlusher struct {
295 bw *bufio.Writer
296 }
297
298 func (lf lineFlusher) Write(p []byte) (n int, err error) {
299 n, err = lf.bw.Write(p)
300 if err != nil {
301 return n, err
302 }
303
304 if bytes.IndexByte(p, '\n') >= 0 {
305 err = lf.bw.Flush()
306 }
307
308 return n, err
309 }
File: ./limit/limit.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package limit
26
27 import (
28 "errors"
29 "io"
30 "os"
31 "strconv"
32 "strings"
33 )
34
35 const info = `
36 limit [options...] [max bytes...] [files...]
37
38 Limit output to only the first n given bytes, using the leading number given,
39 or the first 1024 bytes by default. This is just a more intuitive alternative
40 to using the basic command "head -c".
41
42 All (optional) leading options start with either single or double-dash:
43
44 -h, -help show this help message
45 `
46
47 var multipliers = []struct {
48 suffix string
49 factor int
50 }{
51 {`k`, 1024},
52 {`K`, 1024},
53 {`KB`, 1024},
54 {`KiB`, 1024},
55 {`m`, 1024 * 1024},
56 {`M`, 1024 * 1024},
57 {`MB`, 1024 * 1024},
58 {`MiB`, 1024 * 1024},
59 }
60
61 func Main() {
62 args := os.Args[1:]
63
64 if len(args) > 0 {
65 switch args[0] {
66 case `-h`, `--h`, `-help`, `--help`, `help`:
67 os.Stdout.WriteString(info[1:])
68 return
69 }
70 }
71
72 n := 1024
73 if len(args) > 0 {
74 mul := 1
75 s := strings.ReplaceAll(args[0], `_`, ``)
76 for _, m := range multipliers {
77 if strings.HasSuffix(s, m.suffix) {
78 mul = m.factor
79 break
80 }
81 }
82
83 if v, err := strconv.Atoi(s); err == nil {
84 n = mul * v
85 args = args[1:]
86 }
87 }
88
89 if n < 1 {
90 return
91 }
92
93 if len(args) > 0 && args[0] == `--` {
94 args = args[1:]
95 }
96
97 if err := run(args, &n); err != nil && err != io.EOF {
98 os.Stderr.WriteString(`limit: `)
99 os.Stderr.WriteString(err.Error())
100 os.Stderr.WriteString("\n")
101 os.Exit(1)
102 return
103 }
104 }
105
106 func run(args []string, n *int) error {
107 dashes := 0
108 for _, name := range args {
109 if name == `-` {
110 dashes++
111 }
112 if dashes > 1 {
113 return errors.New(`can't read stdin (dash) more than once`)
114 }
115 }
116
117 if len(args) == 0 {
118 if err := handleReader(os.Stdout, os.Stdin, n); err != nil {
119 return err
120 }
121 }
122
123 for _, name := range args {
124 if name == `-` {
125 if err := handleReader(os.Stdout, os.Stdin, n); err != nil {
126 return err
127 }
128 continue
129 }
130
131 if err := handleFile(os.Stdout, name, n); err != nil {
132 return err
133 }
134 }
135
136 return nil
137 }
138
139 func handleFile(w io.Writer, name string, n *int) error {
140 if name == `` || name == `-` {
141 return handleReader(w, os.Stdin, n)
142 }
143
144 f, err := os.Open(name)
145 if err != nil {
146 return errors.New(`can't read from file named "` + name + `"`)
147 }
148 defer f.Close()
149
150 return handleReader(w, f, n)
151 }
152
153 func handleReader(w io.Writer, r io.Reader, n *int) error {
154 var buf [32 * 1024]byte
155
156 for *n > 0 {
157 max := *n
158 if max > cap(buf) {
159 max = cap(buf)
160 }
161
162 got, err := os.Stdin.Read(buf[:max])
163 if got > 0 {
164 if _, err := os.Stdout.Write(buf[:got]); err != nil {
165 return io.EOF
166 }
167 }
168 *n -= got
169
170 if err != nil {
171 return io.EOF
172 }
173 }
174
175 return nil
176 }
File: ./lineup/lineup.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package lineup
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 "strconv"
34 )
35
36 const info = `
37 lineup [options...] [max-item-count...] [files...]
38
39 Put lines side-by-side up the item-count given per line, joining the items
40 using tabs. If not item-count is given, or the item-count is less than 1,
41 all lines will be tab-joined into a single output line.
42
43 All (optional) leading options start with either single or double-dash:
44
45 -h, -help show this help message
46 `
47
48 type config struct {
49 maxItems int
50 count int
51 remainder int
52 liveLines bool
53 }
54
55 func Main() {
56 var cfg config
57 cfg.maxItems = 0
58 cfg.liveLines = true
59 args := os.Args[1:]
60
61 for len(args) > 0 {
62 switch args[0] {
63 case `-b`, `--b`, `-buffered`, `--buffered`:
64 cfg.liveLines = false
65 args = args[1:]
66 continue
67
68 case `-h`, `--h`, `-help`, `--help`:
69 os.Stdout.WriteString(info[1:])
70 return
71 }
72
73 break
74 }
75
76 if len(args) > 0 {
77 if n, err := strconv.ParseInt(args[0], 10, 64); err == nil {
78 if n < 1 {
79 n = 0
80 }
81 cfg.maxItems = int(n)
82 args = args[1:]
83 }
84 }
85
86 if len(args) > 0 && args[0] == `--` {
87 args = args[1:]
88 }
89
90 if cfg.liveLines {
91 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
92 cfg.liveLines = false
93 }
94 }
95
96 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
97 os.Stderr.WriteString(`lineup: `)
98 os.Stderr.WriteString(err.Error())
99 os.Stderr.WriteString("\n")
100 os.Exit(1)
101 return
102 }
103 }
104
105 func run(w io.Writer, args []string, cfg config) error {
106 bw := bufio.NewWriterSize(w, 32*1024)
107 defer bw.Flush()
108
109 dashes := 0
110 for _, name := range args {
111 if name == `-` {
112 dashes++
113 }
114 if dashes > 1 {
115 return errors.New(`can't read stdin (dash) more than once`)
116 }
117 }
118
119 if len(args) == 0 {
120 if err := lineUp(bw, os.Stdin, &cfg); err != nil {
121 return err
122 }
123 }
124
125 for _, name := range args {
126 if name == `-` {
127 if err := lineUp(bw, os.Stdin, &cfg); err != nil {
128 return err
129 }
130 continue
131 }
132
133 if err := handleFile(bw, name, &cfg); err != nil {
134 return err
135 }
136 }
137
138 if cfg.count > 0 {
139 bw.WriteByte('\n')
140 }
141 return nil
142 }
143
144 func handleFile(w *bufio.Writer, name string, cfg *config) error {
145 if name == `` || name == `-` {
146 return lineUp(w, os.Stdin, cfg)
147 }
148
149 f, err := os.Open(name)
150 if err != nil {
151 return errors.New(`can't read from file named "` + name + `"`)
152 }
153 defer f.Close()
154
155 return lineUp(w, f, cfg)
156 }
157
158 func lineUp(w *bufio.Writer, r io.Reader, cfg *config) error {
159 const gb = 1024 * 1024 * 1024
160 sc := bufio.NewScanner(r)
161 sc.Buffer(nil, 8*gb)
162
163 for i := 0; sc.Scan(); i++ {
164 s := sc.Bytes()
165 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
166 s = s[3:]
167 }
168
169 if cfg.count > 0 {
170 var sep byte
171 if cfg.remainder == 0 {
172 sep = '\n'
173 } else {
174 sep = '\t'
175 }
176
177 if w.WriteByte(sep) != nil {
178 return io.EOF
179 }
180 }
181
182 w.Write(s)
183 cfg.count++
184
185 cfg.remainder++
186 if cfg.remainder >= cfg.maxItems && cfg.maxItems > 1 {
187 cfg.remainder = 0
188 }
189
190 if cfg.liveLines && w.Flush() != nil {
191 return io.EOF
192 }
193 }
194
195 return sc.Err()
196 }
File: ./ls/ls.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package ls
26
27 import (
28 "bufio"
29 "io"
30 "os"
31 "sort"
32 "strings"
33 )
34
35 const info = `
36 ls [options...] [folders...]
37
38 List top-level entries in the folder paths given: if no paths are given, the
39 current folder is listed by default.
40
41 Options
42
43 -a show all files, including those whose name starts with a dot
44 --help show this help message
45 `
46
47 type config struct {
48 all bool
49 long bool
50 }
51
52 func Main() {
53 args := os.Args[1:]
54
55 var cfg config
56 for len(args) > 0 {
57 switch args[0] {
58 case `-a`:
59 cfg.all = true
60 args = args[1:]
61 continue
62
63 case `--help`:
64 os.Stderr.WriteString(info[1:])
65 return
66
67 case `-l`:
68 cfg.long = true
69 args = args[1:]
70 continue
71 }
72
73 break
74 }
75
76 if len(args) > 0 && args[0] == `--` {
77 args = args[1:]
78 }
79
80 if err := run(args, cfg); err != nil && err != io.EOF {
81 os.Stderr.WriteString(`ls: `)
82 os.Stderr.WriteString(err.Error())
83 os.Stderr.WriteString("\n")
84 os.Exit(1)
85 return
86 }
87 }
88
89 func run(paths []string, cfg config) error {
90 w := bufio.NewWriterSize(os.Stdout, 32*1024)
91 defer w.Flush()
92
93 for i, path := range paths {
94 if len(paths) > 1 {
95 w.WriteString(path)
96 w.WriteString(":\n")
97 if i > 0 {
98 w.WriteString("\n")
99 }
100 }
101
102 if err := ls(w, path, cfg); err != nil {
103 return err
104 }
105 }
106
107 if len(paths) == 0 {
108 return ls(w, `.`, cfg)
109 }
110 return nil
111 }
112
113 func ls(w *bufio.Writer, path string, cfg config) error {
114 defer w.Flush()
115
116 entries, err := os.ReadDir(path)
117 if err != nil {
118 return err
119 }
120
121 sort.SliceStable(entries, func(i, j int) bool {
122 return compareNames(entries[i].Name(), entries[j].Name()) < 0
123 })
124
125 for _, e := range entries {
126 name := e.Name()
127
128 if !cfg.all && len(name) > 0 && name[0] == '.' {
129 continue
130 }
131
132 w.WriteString(name)
133 if _, err := w.WriteString("\n"); err != nil {
134 return io.EOF
135 }
136 }
137
138 return nil
139 }
140
141 func compareNames(x, y string) int {
142 if len(x) < len(y) && strings.HasPrefix(y, x) {
143 return -1
144 }
145 if len(y) < len(x) && strings.HasPrefix(x, y) {
146 return +1
147 }
148 return strings.Compare(x, y)
149 }
File: ./main.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 /*
26 To compile a smaller-sized command-line app, you can use the `go` command as
27 follows:
28
29 go build -ldflags "-s -w" -trimpath
30
31 If you have `tinygo`, you can do even better by instead using:
32
33 tinygo build -no-debug -opt=2 easybox
34 */
35
36 package main
37
38 import (
39 "fmt"
40 "io"
41 "os"
42 "path"
43 "sort"
44 "strings"
45 "unicode/utf8"
46
47 "./avoid"
48 "./backsort"
49 "./base64"
50 "./bitdump"
51 "./breakdown"
52 "./bytedump"
53 "./calc"
54 "./cat"
55 "./catl"
56 "./cbl"
57 "./chopleads"
58 "./clock"
59 "./coby"
60 "./compress"
61 "./countdown"
62 "./datauri"
63 "./dc"
64 "./dcol"
65 "./debase64"
66 "./decap"
67 "./decsv"
68 "./dedent"
69 "./dedup"
70 "./dejsonl"
71 "./delay"
72 "./dessv"
73 "./detab"
74 "./dtsv"
75 "./ecoli"
76 "./erase"
77 "./expand"
78 "./factor"
79 "./fail"
80 "./fh"
81 "./files"
82 "./filesizes"
83 "./finfo"
84 "./first"
85 "./fixlines"
86 "./folders"
87 "./gsub"
88 "./head"
89 "./hima"
90 "./htmlify"
91 "./id3pic"
92 "./ignore"
93 "./indent"
94 "./items"
95 "./json0"
96 "./json2"
97 "./jsonl"
98 "./jsons"
99 "./junk"
100 "./label"
101 "./last"
102 "./leak"
103 "./limit"
104 "./lineup"
105 "./ls"
106 "./match"
107 "./mimetype"
108 "./n"
109 "./ncol"
110 "./ngron"
111 "./nhex"
112 "./njson"
113 "./nl"
114 "./nn"
115 "./now"
116 "./nts"
117 "./null"
118 "./pac"
119 "./pad"
120 "./paramatch"
121 "./pbn"
122 "./pcol"
123 "./plain"
124 "./precho"
125 "./pretsv"
126 "./primes"
127 "./ptsv"
128 "./realign"
129 "./reprose"
130 "./restyle"
131 "./same"
132 "./sbs"
133 "./seq"
134 "./setint"
135 "./setsub"
136 "./shame512"
137 "./skip"
138 "./skiplast"
139 "./squeeze"
140 "./squomp"
141 "./stomp"
142 "./sym"
143 "./tacl"
144 "./tail"
145 "./tam"
146 "./tcatl"
147 "./teletype"
148 "./timer"
149 "./tolower"
150 "./transpose"
151 "./underline"
152 "./units"
153 "./utfate"
154 "./waveout"
155 "./zcat"
156 "./zj"
157
158 _ "embed"
159 )
160
161 //go:embed info.txt
162 var info string
163
164 // mains has some entries starting as nil to avoid circular-dependency errors
165 var mains = map[string]func(){
166 `args`: args,
167 `avoid`: avoid.Main,
168 `bitdump`: bitdump.Main,
169 `breakdown`: breakdown.Main,
170 `bytedump`: bytedump.Main,
171 `calc`: calc.Main,
172 `catl`: catl.Main,
173 `cbl`: cbl.Main,
174 `chopleads`: chopleads.Main,
175 `clock`: clock.Main,
176 `cls`: cls,
177 `coby`: coby.Main,
178 `compress`: compress.Main,
179 `countdown`: countdown.Main,
180 `datauri`: datauri.Main,
181 `dcol`: dcol.Main,
182 `debase64`: debase64.Main,
183 `decap`: decap.Main,
184 `decsv`: decsv.Main,
185 `dedent`: dedent.Main,
186 `dedup`: dedup.Main,
187 `dejsonl`: dejsonl.Main,
188 `delay`: delay.Main,
189 `dessv`: dessv.Main,
190 `detab`: detab.Main,
191 `dtsv`: dtsv.Main,
192 `echobar`: echobar,
193 `ecoli`: ecoli.Main,
194 `erase`: erase.Main,
195 `fail`: fail.Main,
196 `files`: files.Main,
197 `filesizes`: filesizes.Main,
198 `finfo`: finfo.Main,
199 `first`: first.Main,
200 `fixlines`: fixlines.Main,
201 `folders`: folders.Main,
202 `gsub`: gsub.Main,
203 `hecho`: hecho,
204 `hima`: hima.Main,
205 `htmlify`: htmlify.Main,
206 `id3pic`: id3pic.Main,
207 `ignore`: ignore.Main,
208 `indent`: indent.Main,
209 `items`: items.Main,
210 `json0`: json0.Main,
211 `json2`: json2.Main,
212 `jsonl`: jsonl.Main,
213 `jsons`: jsons.Main,
214 `junk`: junk.Main,
215 `label`: label.Main,
216 `last`: last.Main,
217 `leak`: leak.Main,
218 `limit`: limit.Main,
219 `lineup`: lineup.Main,
220 `match`: match.Main,
221 `mimetype`: mimetype.Main,
222 `n`: n.Main,
223 `ncol`: ncol.Main,
224 `ngron`: ngron.Main,
225 `nhex`: nhex.Main,
226 `njson`: njson.Main,
227 `nn`: nn.Main,
228 `now`: now.Main,
229 `nts`: nts.Main,
230 `pad`: pad.Main,
231 `paramatch`: paramatch.Main,
232 `pbn`: pbn.Main,
233 `pcol`: pcol.Main,
234 `plain`: plain.Main,
235 `precho`: precho.Main,
236 `pretsv`: pretsv.Main,
237 `primes`: primes.Main,
238 `ptsv`: ptsv.Main,
239 `realign`: realign.Main,
240 `reprose`: reprose.Main,
241 `restyle`: restyle.Main,
242 `ruler`: ruler,
243 `same`: same.Main,
244 `sbs`: sbs.Main,
245 `setint`: setint.Main,
246 `setsub`: setsub.Main,
247 `shame512`: shame512.Main,
248 `skip`: skip.Main,
249 `skiplast`: skiplast.Main,
250 `squeeze`: squeeze.Main,
251 `squomp`: squomp.Main,
252 `stomp`: stomp.Main,
253 `sym`: sym.Main,
254 `tacl`: tacl.Main,
255 `tam`: tam.Main,
256 `tcatl`: tcatl.Main,
257 `teletype`: teletype.Main,
258 `timer`: timer.Main,
259 `timezones`: timezones,
260 `tolower`: tolower.Main,
261 `transpose`: transpose.Main,
262 `underline`: underline.Main,
263 `units`: units.Main,
264 `utfate`: utfate.Main,
265 `waveout`: waveout.Main,
266 }
267
268 var experimental = map[string]func(){
269 `backsort`: backsort.Main,
270 `div`: div,
271 `fh`: fh.Main,
272 `mop`: mop,
273 `nil`: null.Main,
274 `pac`: pac.Main,
275 `nothing`: nothing,
276 `tinker`: tinker,
277 `ungron`: ungronMain,
278 `zj`: zj.Main,
279 }
280
281 var remakes = map[string]func(){
282 `base64`: base64.Main,
283 `cat`: cat.Main,
284 `clear`: clear,
285 `dc`: dc.Main,
286 `echo`: echo,
287 `expand`: expand.Main,
288 `factor`: factor.Main,
289 `fail`: fail.Main,
290 `false`: falseMain,
291 `head`: head.Main,
292 `ls`: ls.Main,
293 `nl`: nl.Main,
294 `seq`: seq.Main,
295 `sleep`: sleep,
296 `tail`: tail.Main,
297 `true`: trueMain,
298 `yes`: yes,
299 `zcat`: zcat.Main,
300 }
301
302 var aliases = map[string]string{
303 `a`: `avoid`,
304 `break`: `breakdown`,
305 `breaklines`: `breakdown`,
306 `ca`: `calc`,
307 `calculate`: `calc`,
308 `calculator`: `calc`,
309 `fc`: `calc`,
310 `frac`: `calc`,
311 `fraca`: `calc`,
312 `fracalc`: `calc`,
313 `bytes`: `cat`,
314 `c`: `cat`,
315 `catenate`: `cat`,
316 `concat`: `cat`,
317 `concatenate`: `cat`,
318 `load`: `cat`,
319 `lines`: `catl`,
320 `dcols`: `dcol`,
321 `dropcol`: `dcol`,
322 `dropcols`: `dcol`,
323 `dropcolumns`: `dcol`,
324 `unbase64`: `debase64`,
325 `uncsv`: `decsv`,
326 `deduplicate`: `dedup`,
327 `undup`: `dedup`,
328 `unique`: `dedup`,
329 `unjsonl`: `dejsonl`,
330 `slowlines`: `delay`,
331 `unssv`: `dessv`,
332 `untab`: `detab`,
333 `e`: `echo`,
334 `b`: `echobar`,
335 `bar`: `echobar`,
336 `fileinfo`: `finfo`,
337 `detrail`: `fixlines`,
338 `id3picture`: `id3pic`,
339 `id3thumb`: `id3pic`,
340 `id3thumbnail`: `id3pic`,
341 `aside`: `leak`,
342 `sideshow`: `leak`,
343 `automime`: `mimetype`,
344 `filemime`: `mimetype`,
345 `filetype`: `mimetype`,
346 `mime`: `mimetype`,
347 `mp3pic`: `id3pic`,
348 `mp3picture`: `id3pic`,
349 `mp3thumb`: `id3pic`,
350 `mp3thumbnail`: `id3pic`,
351 `idem`: `ignore`,
352 `iden`: `ignore`,
353 `identity`: `ignore`,
354 `j0`: `json0`,
355 `j2`: `json2`,
356 `jl`: `jsonl`,
357 `detsv`: `jsons`,
358 `prelabel`: `label`,
359 `prememo`: `label`,
360 `k`: `match`,
361 `keep`: `match`,
362 `m`: `match`,
363 `mops`: `mop`,
364 `ncols`: `ncol`,
365 `nicecol`: `ncol`,
366 `nicecols`: `ncol`,
367 `nicecolumns`: `ncol`,
368 `gron`: `ngron`,
369 `nicegron`: `ngron`,
370 `nh`: `nhex`,
371 `nicehex`: `nhex`,
372 `null`: `nil`,
373 `nicejson`: `njson`,
374 `nj`: `njson`,
375 `nicedigits`: `nn`,
376 `nicenum`: `nn`,
377 `nicenumbers`: `nn`,
378 `nicenums`: `nn`,
379 `ok`: `nothing`,
380 `rpn`: `pac`,
381 `parmatch`: `paramatch`,
382 `pmatch`: `paramatch`,
383 `mapa`: `paramatch`,
384 `mapara`: `paramatch`,
385 `matchpar`: `paramatch`,
386 `matchpara`: `paramatch`,
387 `pcols`: `pcol`,
388 `pickcol`: `pcol`,
389 `pickcols`: `pcol`,
390 `pickcolumns`: `pcol`,
391 `p`: `plain`,
392 `color`: `restyle`,
393 `recolor`: `restyle`,
394 `style`: `restyle`,
395 `symbol`: `sym`,
396 `symbols`: `sym`,
397 `syms`: `sym`,
398 `tty`: `teletype`,
399 `chrono`: `timer`,
400 `chronograph`: `timer`,
401 `timezone`: `timezones`,
402 `lower`: `tolower`,
403 `lowercase`: `tolower`,
404 `t`: `transpose`,
405 `tran`: `transpose`,
406 `trans`: `transpose`,
407 `transp`: `transpose`,
408 `u`: `underline`,
409 `uline`: `underline`,
410 `degron`: `ungron`,
411 `utf8`: `utfate`,
412 `zoomjson`: `zj`,
413 `decompress`: `zcat`,
414 `degzip`: `zcat`,
415 `uncompress`: `zcat`,
416 `ungzip`: `zcat`,
417 }
418
419 var blurbs = map[string]string{
420 `args`: `show all ARGumentS given after the tool name, one per line`,
421 `avoid`: `ignore lines matching any of the regexes given`,
422 `bitdump`: `show all bits for all input bytes`,
423 `breakdown`: `break input lines into multiple output lines by regexes`,
424 `bytedump`: `show bytes as hex values, with a wide ASCII panel`,
425 `calc`: `fractional calculator, with floating-point powers`,
426 `catl`: `conCATenate Lines, ensures text ends with a line-feed`,
427 `cbl`: `Count Bytes and Lines`,
428 `chopleads`: `ignore common leading starts of all non-empty lines`,
429 `clock`: `keep showing the current time, updating every new minute`,
430 `cls`: `CLear the Screen`,
431 `coby`: `COunt BYtes, and many other byte/text-related stats`,
432 `compress`: `gzip-compress all concatenated inputs given`,
433 `countdown`: `countdown the seconds/minutes/hours given`,
434 `datauri`: `turn bytes into data-URIs, auto-detecting MIME types`,
435 `dcol`: `Drop COLumns by name or by 1-based index`,
436 `decap`: `DECAPitate the first n lines from stdin, or 1 by default`,
437 `debase64`: `decode base64 text and data-URIs`,
438 `decompress`: `gzip-decompress concatenated bytes from all the inputs`,
439 `decsv`: `convert CSV tables into TSV tables, or into JSON`,
440 `dedent`: `ignore the common leading-space indentation`,
441 `dedup`: `deduplicate lines, emitting each unique line only once`,
442 `dejsonl`: `turn JSON Lines into proper JSON`,
443 `delay`: `wait the seconds given, before emitting each input line`,
444 `dessv`: `turn tables of space-separated values into TSV tables`,
445 `detab`: `expand tabs into runs of up to n spaces, or up to 4`,
446 `div`: `DIVide 2 numbers, or invert 1 number`,
447 `dtsv`: `Drop Tab-Separated Values, matching args to column names`,
448 `echobar`: `like 'echo', but right-pads and highlights its output`,
449 `ecoli`: `Expressions COloring LInes color-codes matching lines`,
450 `erase`: `ignore/erase all matching regexes away from each line`,
451 `fail`: `fail with the exit code given, or using 1 by default`,
452 `fh`: `Function Heatmapper draws 2-input (x, y) functions`,
453 `files`: `list all files in the folder(s) given`,
454 `filesizes`: `show sizes of files and block-counts (4K by default)`,
455 `finfo`: `show various file info, for plain-text and/or media files`,
456 `first`: `keep only the first n lines, or the first line by default`,
457 `fixlines`: `ignore carriage-returns, or even trailing spaces`,
458 `folders`: `list all folders in the folder(s) given`,
459 `gsub`: `Globally SUBstitute all regular expression matches`,
460 `hecho`: `Highlighted ECHO shows an ANSI-styled line`,
461 `help`: `show the help message for "easybox"`,
462 `hima`: `HIlight MAtches using the regexes given`,
463 `htmlify`: `turn plain-text lines into HTML documents`,
464 `id3pic`: `get the encoded picture out of audio files, if present`,
465 `ignore`: `ignore all arguments, copying stdin into stdout instead`,
466 `indent`: `indent lines, using 4 leading spaces by default`,
467 `items`: `emit words/items from input lines as single-item lines`,
468 `json0`: `minimize/fix JSON into the smallest-possible size`,
469 `json2`: `indent JSON into multiple lines, using 2 spaces per level`,
470 `jsonl`: `turn items from top-level JSON arrays into JSON Lines`,
471 `jsons`: `JSON Strings turns TSV into arrays of objects of strings`,
472 `junk`: `emit a given count of random bytes, or 1024 by default`,
473 `label`: `precede input lines with a styled line`,
474 `last`: `keep only the last n lines, or the last line by default`,
475 `leak`: `emit lines both to stdout and stderr; good to debug pipes`,
476 `limit`: `emit up to the first n bytes, or the first 1024 by default`,
477 `lineup`: `put input lines into (up to) n-items tab-separated lines`,
478 `match`: `only keep lines matching any of the regexes given`,
479 `mimetype`: `try to auto-detect the MIME-TYPE of all files given`,
480 `mop`: `Multiple OPerations, using the 2 numbers given`,
481 `n`: `Number lines, putting tabs between numbers and contents`,
482 `ncol`: `Nice COLumns realigns tables, color-coding their values`,
483 `ngron`: `Nice GRON mimics a subset of "gron", using better colors`,
484 `nhex`: `Nice HEXadecimal shows bytes as hex values and ASCII`,
485 `nil`: `emit nothing, also discarding all stdin bytes, if piped`,
486 `njson`: `Nice JSON indents and color-codes JSON data`,
487 `nn`: `Nice Numbers color-codes groups of digits for legibility`,
488 `now`: `show the current date and time, also for other timezones`,
489 `nts`: `Nice TimeStamp precedes each line with styled date/time`,
490 `pac`: `Postfix Array Calculator is an RPN-style calculator`,
491 `pad`: `right-pad lines, so they all have the same symbol-count`,
492 `pbn`: `Pick (lines) By Number, using 1-based or negative indices`,
493 `paramatch`: `PARAgraphs MATCHing any of the regexes given`,
494 `pcol`: `Pick COLumns by name or by 1-based index`,
495 `plain`: `ignore all ANSI-sequences, leaving unstyled text`,
496 `precho`: `PRecede ECHO, precedes input lines with an SSV line`,
497 `pretsv`: `PREcede TSV, precedes input lines with a TSV line`,
498 `primes`: `find prime numbers, up to the first million by default`,
499 `ptsv`: `Pick Tab-Separated Values, matching args to column names`,
500 `realign`: `realign items from the SSV/TSV tables given`,
501 `reprose`: `reflow/trim lines of text/prose to improve its legibility`,
502 `restyle`: `restyle lines using ANSI-codes for the style name given`,
503 `ruler`: `show a ruler-like pattern on a single line`,
504 `same`: `fail if the 2 files given have different contents`,
505 `sbs`: `Side By Side, wraps input lines in multiple columns`,
506 `seq`: `emit a sequence of numbers, one per line`,
507 `setint`: `SET INTersection of (unique) lines from the inputs given`,
508 `setsub`: `SET SUBtraction of (unique) lines from the first input`,
509 `shame512`: `group filepaths by common SHA-512 checksums`,
510 `skip`: `skip at most the first n lines, or the first by default`,
511 `skiplast`: `skip at most the last n lines, or the last by default`,
512 `squeeze`: `aggressively ignore spaces, especially runs of spaces`,
513 `squomp`: `squeeze non-empty lines and stomp empty(-ish) lines`,
514 `stomp`: `ignore leading/trailing empty lines, squeezing empty runs`,
515 `sym`: `lookup hard-to-type symbols by their common names/aliases`,
516 `tacl`: `TAC Lines emits text lines in backward-order`,
517 `tam`: `Time And Memory (and exit) runs/monitors commands`,
518 `tcatl`: `Titled conCATenate Lines, is like "catl" but with names`,
519 `teletype`: `mimic old-fashioned teletype devices, by delaying output`,
520 `timer`: `run a live chronograph, optionally timing a command`,
521 `timezones`: `lookup full timezone names from the city/place names given`,
522 `tolower`: `turn all uppercase letters into lowercase ones`,
523 `transpose`: `switch rows and columns from tables; output is always TSV`,
524 `tools`: `list all tools available`,
525 `underline`: `underline every nth line, or every 5th line by default`,
526 `ungron`: `turn GRON-style lines into valid JSON`,
527 `units`: `convert weird units into the international standard ones`,
528 `utfate`: `decode all other types of UTF text into UTF-8`,
529 `waveout`: `emit/calculate WAV-format sounds by formula`,
530 `yes`: `keep emitting a line with "yes", or the message given`,
531 `zj`: `Zoom Json, using the keys/indices given as arguments`,
532 }
533
534 func main() {
535 appName := os.Args[0]
536
537 // try to use the app's `name`, in case it's being called from a file-link
538 // named after one of the tools
539 if tool, ok := lookupTool(path.Base(appName)); ok {
540 tool()
541 return
542 }
543
544 if len(os.Args) < 2 {
545 showHelp(os.Stderr)
546 fmt.Fprintln(os.Stderr, ``)
547 fmt.Fprintln(os.Stderr, `easybox: no tool name given`)
548 os.Exit(1)
549 return
550 }
551
552 // try normal tool-lookup using the first command-line argument
553
554 toolName := os.Args[1]
555 os.Args = os.Args[1:]
556
557 if tool, ok := lookupTool(toolName); ok {
558 tool()
559 return
560 }
561
562 switch toolName {
563 case `-h`, `--h`, `-help`, `--help`, `help`:
564 showHelp(os.Stdout)
565
566 case `-l`, `--l`, `-list`, `--list`:
567 tools()
568
569 case `-links`, `--links`:
570 showLinksCommands(os.Stdout, appName, os.Args[1:])
571
572 case `-t`, `--t`, `-tools`, `--tools`, `tools`:
573 tools()
574
575 default:
576 const fs = "easybox: tool/alias named %q not found\n"
577 fmt.Fprintf(os.Stderr, fs, toolName)
578 os.Exit(1)
579 return
580 }
581 }
582
583 // dealias tries to lookup a string to the aliases given, returning the name
584 // given if the lookup fails
585 func dealias(aliases map[string]string, name string) string {
586 if s, ok := aliases[name]; ok {
587 return s
588 }
589 return name
590 }
591
592 func help() {
593 showHelp(os.Stdout)
594 }
595
596 func lookupTool(name string) (tool func(), ok bool) {
597 name = strings.ReplaceAll(name, `-`, ``)
598 name = strings.ReplaceAll(name, `_`, ``)
599 key := dealias(aliases, name)
600
601 if tool, ok := mains[key]; ok {
602 return tool, ok
603 }
604
605 if tool, ok := remakes[key]; ok {
606 return tool, ok
607 }
608
609 tool, ok = experimental[key]
610 return tool, ok
611 }
612
613 // showHelp has a parameter to write either to stdout or stderr
614 func showHelp(w io.Writer) {
615 fmt.Fprintln(w, info)
616
617 fmt.Fprintln(w, "\nTools Available")
618
619 maxlen := 0
620 names := make([]string, 0, max(len(mains), len(aliases)))
621 for k := range mains {
622 names = append(names, k)
623 maxlen = max(maxlen, utf8.RuneCountInString(k))
624 }
625 sort.Strings(names)
626
627 for _, s := range names {
628 fmt.Fprintf(w, " - %-*s %s\n", maxlen, s, blurbs[s])
629 }
630
631 fmt.Fprintln(w, "\nAliases Available")
632
633 maxlen = 0
634 names = names[:0]
635 for k := range aliases {
636 names = append(names, k)
637 maxlen = max(maxlen, utf8.RuneCountInString(k))
638 }
639 sort.Strings(names)
640
641 for _, k := range names {
642 fmt.Fprintf(w, " - %-*s -> %s\n", maxlen, k, aliases[k])
643 }
644
645 names = names[:0]
646 for k := range experimental {
647 names = append(names, k)
648 }
649 sort.Strings(names)
650
651 if len(names) == 0 {
652 return
653 }
654
655 fmt.Fprintln(w, "\nExperimental Tools Available")
656
657 for _, k := range names {
658 fmt.Fprintf(w, " - %s\n", k)
659 }
660 }
661
662 func showLinksCommands(w io.Writer, appName string, args []string) {
663 prefix := `.`
664 if len(args) > 0 {
665 prefix = args[0]
666 }
667 if len(prefix) > 0 && prefix[len(prefix)-1] == '/' {
668 prefix = prefix[:len(prefix)-1]
669 }
670 if len(prefix) == 0 {
671 prefix = `.`
672 }
673
674 names := make([]string, 0, len(mains))
675 for k := range mains {
676 names = append(names, k)
677 }
678
679 sort.Strings(names)
680
681 for _, s := range names {
682 const fs = "ln -s \"$(realpath \"$(which %s)\")\" %s/%s\n"
683 fmt.Fprintf(w, fs, appName, prefix, s)
684 }
685 }
686
687 // tinker is for little throw-away experiments
688 func tinker() {
689 }
File: ./main_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package main
26
27 import "testing"
28
29 func TestAliases(t *testing.T) {
30 for alias, name := range aliases {
31 if _, ok := mains[name]; ok {
32 continue
33 }
34 if _, ok := remakes[name]; ok {
35 continue
36 }
37 if _, ok := experimental[name]; ok {
38 continue
39 }
40
41 t.Errorf("alias %q leads nowhere", alias)
42 }
43 }
44
45 func TestBlurbs(t *testing.T) {
46 for name := range mains {
47 if blurbs[name] != `` {
48 continue
49 }
50 t.Errorf("no description/blurb for tool %q", name)
51 }
52
53 for name := range blurbs {
54 name = dealias(aliases, name)
55
56 if _, ok := mains[name]; ok {
57 continue
58 }
59 if _, ok := remakes[name]; ok {
60 continue
61 }
62 if _, ok := experimental[name]; ok {
63 continue
64 }
65 if name == `help` || name == `tools` {
66 continue
67 }
68
69 t.Errorf("description/blurb for name %q, but no tool", name)
70 }
71 }
File: ./match/match.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package match
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "regexp"
33 "strings"
34 )
35
36 const info = `
37 match [options...] [regular expressions...]
38
39 Only keep lines which match any of the extended-mode regular expressions
40 given. When not given any regex, match non-empty lines by default.
41
42 The options are, available both in single and double-dash versions
43
44 -h, -help show this help message
45 -i, -ins match regexes case-insensitively
46 -l, -links add a regex to match HTTP/HTTPS links case-insensitively
47 -q, -quiet never emit output: exit code still succeeds/fails
48 `
49
50 const linkRegexp = `(?i)https?://[a-z0-9+_.:%-]+(/[a-z0-9+_.%/,#?&=-]*)*`
51
52 func Main() {
53 nerr := 0
54 links := false
55 buffered := false
56 avoid := false
57 sensitive := true
58 quiet := false
59 args := os.Args[1:]
60
61 for len(args) > 0 {
62 switch args[0] {
63 case `-b`, `--b`, `-buffered`, `--buffered`:
64 buffered = true
65 args = args[1:]
66 continue
67
68 case `-h`, `--h`, `-help`, `--help`:
69 os.Stdout.WriteString(info[1:])
70 return
71
72 case `-i`, `--i`, `-ins`, `--ins`, `-insensitive`, `--insensitive`:
73 sensitive = false
74 args = args[1:]
75 continue
76
77 case `-l`, `--l`, `-links`, `--links`:
78 links = true
79 args = args[1:]
80 continue
81
82 case `-q`, `--q`, `-quiet`, `--quiet`:
83 quiet = true
84 args = args[1:]
85 continue
86
87 case `-v`, `--v`, `-inv`, `--inv`, `-neg`, `--neg`:
88 avoid = true
89 args = args[1:]
90 continue
91 }
92
93 if s := args[0]; s == `-` || s == `--` || !strings.HasPrefix(s, `-`) {
94 break
95 }
96
97 for s := args[0][1:]; len(s) > 0; s = s[1:] {
98 switch s[0] {
99 case 'a':
100 avoid = true
101 continue
102 case 'b':
103 buffered = true
104 continue
105 case 'h':
106 os.Stdout.WriteString(info[1:])
107 return
108 case 'i':
109 sensitive = false
110 continue
111 case 'l':
112 links = true
113 continue
114 case 'q':
115 quiet = true
116 continue
117 case 'v':
118 avoid = true
119 continue
120 default:
121 os.Stderr.WriteString(`match: unsupported option `)
122 os.Stderr.WriteString(args[0])
123 os.Stderr.WriteString("\n")
124 os.Exit(1)
125 }
126 }
127
128 args = args[1:]
129 }
130
131 if len(args) > 0 && args[0] == `--` {
132 args = args[1:]
133 }
134
135 liveLines := false
136 if !quiet {
137 liveLines = !buffered
138 if !buffered {
139 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
140 liveLines = false
141 }
142 }
143 }
144
145 if len(args) == 0 {
146 args = []string{`.`}
147 }
148
149 var exprs []*regexp.Regexp
150 if links {
151 exprs = make([]*regexp.Regexp, 0, len(args)+1)
152 exprs = append(exprs, regexp.MustCompile(linkRegexp))
153 } else {
154 exprs = make([]*regexp.Regexp, 0, len(args))
155 }
156
157 for _, src := range args {
158 var err error
159 var exp *regexp.Regexp
160 if !sensitive {
161 exp, err = regexp.Compile(`(?i)` + src)
162 } else {
163 exp, err = regexp.Compile(src)
164 }
165
166 if err != nil {
167 os.Stderr.WriteString(`match: `)
168 os.Stderr.WriteString(err.Error())
169 os.Stderr.WriteString("\n")
170 nerr++
171 }
172
173 exprs = append(exprs, exp)
174 }
175
176 if nerr > 0 {
177 os.Exit(1)
178 return
179 }
180
181 var buf []byte
182 sc := bufio.NewScanner(os.Stdin)
183 sc.Buffer(nil, 8*1024*1024*1024)
184 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
185
186 ok := false
187
188 for i := 0; sc.Scan(); i++ {
189 line := sc.Bytes()
190 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
191 line = line[3:]
192 }
193
194 s := line
195 if bytes.IndexByte(s, '\x1b') >= 0 {
196 buf = plain(buf[:0], s)
197 s = buf
198 }
199
200 if match(s, exprs) {
201 if avoid {
202 continue
203 }
204
205 ok = true
206 if quiet {
207 continue
208 }
209 if err := emit(bw, line, liveLines); err != nil {
210 break
211 }
212 }
213
214 if avoid {
215 ok = true
216 if quiet {
217 continue
218 }
219 if err := emit(bw, line, liveLines); err != nil {
220 break
221 }
222 }
223 }
224
225 if !ok {
226 os.Exit(1)
227 return
228 }
229 }
230
231 func emit(w *bufio.Writer, line []byte, live bool) error {
232 w.Write(line)
233 w.WriteByte('\n')
234
235 if live && w.Flush() != nil {
236 return io.EOF
237 }
238 return nil
239 }
240
241 func match(what []byte, with []*regexp.Regexp) bool {
242 for _, e := range with {
243 if e.Match(what) {
244 return true
245 }
246 }
247 return false
248 }
249
250 func plain(dst []byte, src []byte) []byte {
251 for len(src) > 0 {
252 i, j := indexEscapeSequence(src)
253 if i < 0 {
254 dst = append(dst, src...)
255 break
256 }
257 if j < 0 {
258 j = len(src)
259 }
260
261 if i > 0 {
262 dst = append(dst, src[:i]...)
263 }
264
265 src = src[j:]
266 }
267
268 return dst
269 }
270
271 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
272 // the multi-byte sequences starting with ESC[; the result is a pair of slice
273 // indices which can be independently negative when either the start/end of
274 // a sequence isn't found; given their fairly-common use, even the hyperlink
275 // ESC]8 sequences are supported
276 func indexEscapeSequence(s []byte) (int, int) {
277 var prev byte
278
279 for i, b := range s {
280 if prev == '\x1b' && b == '[' {
281 j := indexLetter(s[i+1:])
282 if j < 0 {
283 return i, -1
284 }
285 return i - 1, i + 1 + j + 1
286 }
287
288 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
289 j := indexPair(s[i+1:], '\x1b', '\\')
290 if j < 0 {
291 return i, -1
292 }
293 return i - 1, i + 1 + j + 2
294 }
295
296 prev = b
297 }
298
299 return -1, -1
300 }
301
302 func indexLetter(s []byte) int {
303 for i, b := range s {
304 upper := b &^ 32
305 if 'A' <= upper && upper <= 'Z' {
306 return i
307 }
308 }
309
310 return -1
311 }
312
313 func indexPair(s []byte, x byte, y byte) int {
314 var prev byte
315
316 for i, b := range s {
317 if prev == x && b == y && i > 0 {
318 return i
319 }
320 prev = b
321 }
322
323 return -1
324 }
File: ./mathplus/doc.go
1 /*
2 # mathplus
3
4 This is an add-on to the stdlib package `math`, with statistics-gathering
5 functionality and plenty more math-related functions.
6
7 This package also wraps types/methods from math/rand with nil-safe ones, which
8 act on the default value-generator when nil pointers are used.
9 */
10
11 package mathplus
File: ./mathplus/functions.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mathplus
26
27 import "math"
28
29 func Decade(x float64) float64 { return 10 * math.Floor(0.1*x) }
30 func Century(x float64) float64 { return 100 * math.Floor(0.01*x) }
31
32 func Round1(x float64) float64 { return 1e-1 * math.Round(1e+1*x) }
33 func Round2(x float64) float64 { return 1e-2 * math.Round(1e+2*x) }
34 func Round3(x float64) float64 { return 1e-3 * math.Round(1e+3*x) }
35 func Round4(x float64) float64 { return 1e-4 * math.Round(1e+4*x) }
36 func Round5(x float64) float64 { return 1e-5 * math.Round(1e+5*x) }
37 func Round6(x float64) float64 { return 1e-6 * math.Round(1e+6*x) }
38 func Round7(x float64) float64 { return 1e-7 * math.Round(1e+7*x) }
39 func Round8(x float64) float64 { return 1e-8 * math.Round(1e+8*x) }
40 func Round9(x float64) float64 { return 1e-9 * math.Round(1e+9*x) }
41
42 // Ln calculates the natural logarithm.
43 func Ln(x float64) float64 {
44 return math.Log(x)
45 }
46
47 // Log calculates logarithms using the base given.
48 // func Log(base float64, x float64) float64 {
49 // return math.Log(x) / math.Log(base)
50 // }
51
52 // Round rounds the number given to the number of decimal places given.
53 func Round(x float64, decimals int) float64 {
54 k := math.Pow(10, float64(decimals))
55 return math.Round(k*x) / k
56 }
57
58 // RoundBy rounds the number given to the unit-size given
59 // func RoundBy(x float64, by float64) float64 {
60 // return math.Round(by*x) / by
61 // }
62
63 // FloorBy quantizes a number downward by the unit-size given
64 // func FloorBy(x float64, by float64) float64 {
65 // mod := math.Mod(x, by)
66 // if x < 0 {
67 // if mod == 0 {
68 // return x - mod
69 // }
70 // return x - mod - by
71 // }
72 // return x - mod
73 // }
74
75 // Wrap linearly interpolates the number given to the range [0...1],
76 // according to its continuous domain, specified by the limits given
77 func Wrap(x float64, min, max float64) float64 {
78 return (x - min) / (max - min)
79 }
80
81 // AnchoredWrap works like Wrap, except when both domain boundaries are positive,
82 // the minimum becomes 0, and when both domain boundaries are negative, the
83 // maximum becomes 0.
84 func AnchoredWrap(x float64, min, max float64) float64 {
85 if min > 0 && max > 0 {
86 min = 0
87 } else if max < 0 && min < 0 {
88 max = 0
89 }
90 return (x - min) / (max - min)
91 }
92
93 // Unwrap undoes Wrap, by turning the [0...1] number given into its equivalent
94 // in the new domain given.
95 func Unwrap(x float64, min, max float64) float64 {
96 return (max-min)*x + min
97 }
98
99 // Clamp constrains the domain of the value given
100 func Clamp(x float64, min, max float64) float64 {
101 return math.Min(math.Max(x, min), max)
102 }
103
104 // Scale transforms a number according to its position in [xMin, xMax] into its
105 // correspondingly-positioned counterpart in [yMin, yMax]: if value isn't in its
106 // assumed domain, its result will be extrapolated accordingly
107 func Scale(x float64, xmin, xmax, ymin, ymax float64) float64 {
108 k := (x - xmin) / (xmax - xmin)
109 return (ymax-ymin)*k + ymin
110 }
111
112 // AnchoredScale works like Scale, except when both domain boundaries are positive,
113 // the minimum becomes 0, and when both domain boundaries are negative, the maximum
114 // becomes 0. This allows for proportionally-correct scaling of quantities, such
115 // as when showing data visually.
116 func AnchoredScale(x float64, xmin, xmax, ymin, ymax float64) float64 {
117 if xmin > 0 && xmax > 0 {
118 xmin = 0
119 } else if xmax < 0 && xmin < 0 {
120 xmax = 0
121 }
122 k := (x - xmin) / (xmax - xmin)
123 return (ymax-ymin)*k + ymin
124 }
125
126 // ForceRange is handy when trying to mold floating-point values into numbers
127 // valid for JSON, since NaN and Infinity are replaced by the values given;
128 // the infinity-replacement value is negated for negative infinity.
129 func ForceRange(x float64, nan, inf float64) float64 {
130 if math.IsNaN(x) {
131 return nan
132 }
133 if math.IsInf(x, -1) {
134 return -inf
135 }
136 if math.IsInf(x, +1) {
137 return inf
138 }
139 return x
140 }
141
142 // Sign returns the standardized sign of a value, either as -1, 0, or +1: NaN
143 // values stay as NaN, as is expected when using floating-point values.
144 func Sign(x float64) float64 {
145 if x > 0 {
146 return +1
147 }
148 if x < 0 {
149 return -1
150 }
151 if x == 0 {
152 return 0
153 }
154 return math.NaN()
155 }
156
157 // IsInteger checks if a floating-point value is already rounded to an integer
158 // value.
159 func IsInteger(x float64) bool {
160 _, frac := math.Modf(x)
161 return frac == 0
162 }
163
164 func Square(x float64) float64 {
165 return x * x
166 }
167
168 func Cube(x float64) float64 {
169 return x * x * x
170 }
171
172 func RoundTo(x float64, decimals float64) float64 {
173 k := math.Pow10(int(decimals))
174 return math.Round(k*x) / k
175 }
176
177 func RelDiff(x, y float64) float64 {
178 return (x - y) / y
179 }
180
181 // Bool booleanizes a number: 0 stays 0, and anything else becomes 1.
182 func Bool(x float64) float64 {
183 if x == 0 {
184 return 0
185 }
186 return 1
187 }
188
189 // DeNaN replaces NaN with the alternative value given: regular values are
190 // returned as given.
191 func DeNaN(x float64, instead float64) float64 {
192 if !math.IsNaN(x) {
193 return x
194 }
195 return instead
196 }
197
198 // DeInf replaces either infinity with the alternative value given: regular
199 // values are returned as given.
200 func DeInf(x float64, instead float64) float64 {
201 if !math.IsInf(x, 0) {
202 return x
203 }
204 return instead
205 }
206
207 // Revalue replaces NaN and either infinity with the alternative value given:
208 // regular values are returned as given.
209 func Revalue(x float64, instead float64) float64 {
210 if !math.IsNaN(x) && !math.IsInf(x, 0) {
211 return x
212 }
213 return instead
214 }
215
216 // Linear evaluates a linear polynomial, the first arg being the main input,
217 // followed by the polynomial coefficients in decreasing-power order.
218 func Linear(x float64, a, b float64) float64 {
219 return a*x + b
220 }
221
222 // Quadratic evaluates a 2nd-degree polynomial, the first arg being the main
223 // input, followed by the polynomial coefficients in decreasing-power order.
224 func Quadratic(x float64, a, b, c float64) float64 {
225 return (a*x+b)*x + c
226 }
227
228 // Cubic evaluates a cubic polynomial, the first arg being the main input,
229 // followed by the polynomial coefficients in decreasing-power order.
230 func Cubic(x float64, a, b, c, d float64) float64 {
231 return ((a*x+b)*x+c)*x + d
232 }
233
234 func LinearFMA(x float64, a, b float64) float64 {
235 return math.FMA(x, a, b)
236 }
237
238 func QuadraticFMA(x float64, a, b, c float64) float64 {
239 lin := math.FMA(x, a, b)
240 return math.FMA(lin, x, c)
241 }
242
243 func CubicFMA(x float64, a, b, c, d float64) float64 {
244 lin := math.FMA(x, a, b)
245 quad := math.FMA(lin, x, c)
246 return math.FMA(quad, x, d)
247 }
248
249 // Radians converts angular degrees into angular radians: 180 degrees are pi
250 // pi radians.
251 func Radians(deg float64) float64 {
252 const k = math.Pi / 180
253 return k * deg
254 }
255
256 // Degrees converts angular radians into angular degrees: pi radians are 180
257 // degrees.
258 func Degrees(rad float64) float64 {
259 const k = 180 / math.Pi
260 return k * rad
261 }
262
263 // Fract calculates the non-integer/fractional part of a number.
264 func Fract(x float64) float64 {
265 return x - math.Floor(x)
266 }
267
268 // Mix interpolates 2 numbers using a third number, used as an interpolation
269 // coefficient. This parameter naturally falls in the range [0, 1], but doesn't
270 // have to be: when given outside that range, the parameter can extrapolate in
271 // either direction instead.
272 func Mix(x, y float64, k float64) float64 {
273 return (1-k)*(y-x) + x
274 }
275
276 // Step implements a step function with a parametric threshold.
277 func Step(edge, x float64) float64 {
278 if x < edge {
279 return 0
280 }
281 return 1
282 }
283
284 // SmoothStep is like the `smoothstep` func found in GLSL, using a cubic
285 // interpolator in the transition region.
286 func SmoothStep(edge0, edge1, x float64) float64 {
287 if x <= edge0 {
288 return 0
289 }
290 if x >= edge1 {
291 return 1
292 }
293
294 // use the cubic hermite interpolator 3x^2 - 2x^3 in the transition band
295 return x * x * (3 - 2*x)
296 }
297
298 // Logistic approximates the math func of the same name.
299 func Logistic(x float64) float64 {
300 return 1 / (1 + math.Exp(-x))
301 }
302
303 // Sinc approximates the math func of the same name.
304 func Sinc(x float64) float64 {
305 if x != 0 {
306 return math.Sin(x) / x
307 }
308 return 1
309 }
310
311 // Sum adds all the numbers in an array.
312 func Sum(v ...float64) float64 {
313 s := 0.0
314 for _, f := range v {
315 s += f
316 }
317 return s
318 }
319
320 // Product multiplies all the numbers in an array.
321 func Product(v ...float64) float64 {
322 p := 1.0
323 for _, f := range v {
324 p *= f
325 }
326 return p
327 }
328
329 // Length calculates the Euclidean length of the vector given.
330 func Length(v ...float64) float64 {
331 ss := 0.0
332 for _, f := range v {
333 ss += f * f
334 }
335 return math.Sqrt(ss)
336 }
337
338 // Dot calculates the dot product of 2 vectors
339 func Dot(x []float64, y []float64) float64 {
340 l := len(x)
341 if len(y) < l {
342 l = len(y)
343 }
344
345 dot := 0.0
346 for i := 0; i < l; i++ {
347 dot += x[i] * y[i]
348 }
349 return dot
350 }
351
352 // Min finds the minimum value from the numbers given.
353 func Min(v ...float64) float64 {
354 min := +math.Inf(+1)
355 for _, f := range v {
356 min = math.Min(min, f)
357 }
358 return min
359 }
360
361 // Max finds the maximum value from the numbers given.
362 func Max(v ...float64) float64 {
363 max := +math.Inf(-1)
364 for _, f := range v {
365 max = math.Max(max, f)
366 }
367 return max
368 }
369
370 // Hypot calculates the Euclidean n-dimensional hypothenuse from the numbers
371 // given: all numbers can be lengths, or simply positional coordinates.
372 func Hypot(v ...float64) float64 {
373 sumsq := 0.0
374 for _, f := range v {
375 sumsq += f * f
376 }
377 return math.Sqrt(sumsq)
378 }
379
380 // Polyval evaluates a polynomial using Horner's algorithm. The array has all
381 // the coefficients in textbook order, from the highest power down to the final
382 // constant.
383 func Polyval(x float64, v ...float64) float64 {
384 if len(v) == 0 {
385 return 0
386 }
387
388 x0 := x
389 x = 1.0
390 y := 0.0
391 for i := len(v) - 1; i >= 0; i-- {
392 y += v[i] * x
393 x *= x0
394 }
395 return y
396 }
397
398 // LnGamma is a 1-input 1-output version of math.Lgamma from the stdlib.
399 func LnGamma(x float64) float64 {
400 y, s := math.Lgamma(x)
401 if s < 0 {
402 return math.NaN()
403 }
404 return y
405 }
406
407 // LnBeta calculates the natural-logarithm of the beta function.
408 func LnBeta(x float64, y float64) float64 {
409 return LnGamma(x) + LnGamma(y) - LnGamma(x+y)
410 }
411
412 // Beta calculates the beta function.
413 func Beta(x float64, y float64) float64 {
414 return math.Exp(LnBeta(x, y))
415 }
416
417 // Factorial calculates the product of all integers in [1, n]
418 func Factorial(n int) int64 {
419 return int64(math.Round(math.Gamma(float64(n + 1))))
420 }
421
422 // IsPrime checks whether an integer is bigger than 1 and can only be fully
423 // divided by 1 and itself, which is the definition of a prime number.
424 func IsPrime(n int64) bool {
425 // prime numbers start at 2
426 if n < 2 {
427 return false
428 }
429
430 // 2 is the only even prime
431 if n%2 == 0 {
432 return n == 2
433 }
434
435 // no divisor can be more than the square root of the target number:
436 // this limit makes the loop an O(sqrt(n)) one, instead of O(n); this
437 // is a major algorithmic speedup both in theory and in practice
438 max := int64(math.Floor(math.Sqrt(float64(n))))
439
440 // the only possible full-divisors are odd integers 3..sqrt(n),
441 // since reaching this point guarantees n is odd and n > 2
442 for d := int64(3); d <= max; d += 2 {
443 if n%d == 0 {
444 return false
445 }
446 }
447 return true
448 }
449
450 // LCM finds the least common-multiple of 2 positive integers; when one or
451 // both inputs aren't positive, this func returns 0.
452 func LCM(x, y int64) int64 {
453 if gcd := GCD(x, y); gcd > 0 {
454 return x * y / gcd
455 }
456 return 0
457 }
458
459 // GCD finds the greatest common-divisor of 2 positive integers; when one or
460 // both inputs aren't positive, this func returns 0.
461 func GCD(x, y int64) int64 {
462 if x < 1 || y < 1 {
463 return 0
464 }
465
466 // the loop below requires a >= b
467 a, b := x, y
468 if a < b {
469 a, b = y, x
470 }
471
472 for b > 0 {
473 a, b = b, a%b
474 }
475 return a
476 }
477
478 // Perm counts the number of all possible permutations from n objects when
479 // picking k times. When one or both inputs aren't positive, the result is 0.
480 func Perm(n, k int) int64 {
481 if n < k || n < 0 || k < 0 {
482 return 0
483 }
484
485 perm := int64(1)
486 for i := n - k + 1; i <= n; i++ {
487 perm *= int64(i)
488 }
489 return perm
490 }
491
492 // Choose counts the number of all possible combinations from n objects when
493 // picking k times. When one or both inputs aren't positive, the result is 0.
494 func Choose(n, k int) int64 {
495 if n < k || n < 0 || k < 0 {
496 return 0
497 }
498
499 // the log trick isn't always more accurate when there's no overflow:
500 // for those cases calculate using the textbook definition
501 f := math.Round(float64(Perm(n, k) / Factorial(k)))
502 if !math.IsInf(f, 0) {
503 return int64(f)
504 }
505
506 // calculate using the log-factorial of n, k, and n - k
507 a, _ := math.Lgamma(float64(n + 1))
508 b, _ := math.Lgamma(float64(k + 1))
509 c, _ := math.Lgamma(float64(n - k + 1))
510 return int64(math.Round(math.Exp(a - b - c)))
511 }
512
513 // BinomialMass calculates the probability mass of the binomial random process
514 // given. When the probability given isn't between 0 and 1, the result is NaN.
515 func BinomialMass(x, n int, p float64) float64 {
516 // invalid probability input
517 if p < 0 || p > 1 {
518 return math.NaN()
519 }
520 // events outside the support are impossible by definition
521 if x < 0 || x > n {
522 return 0
523 }
524
525 q := 1 - p
526 m := n - x
527 ncx := float64(Choose(n, x))
528 return ncx * math.Pow(p, float64(x)) * math.Pow(q, float64(m))
529 }
530
531 // CumulativeBinomialDensity calculates cumulative probabilities/masses up to
532 // the value given for the binomial random process given. When the probability
533 // given isn't between 0 and 1, the result is NaN.
534 func CumulativeBinomialDensity(x, n int, p float64) float64 {
535 // invalid probability input
536 if p < 0 || p > 1 {
537 return math.NaN()
538 }
539 if x < 0 {
540 return 0
541 }
542 if x >= n {
543 return 1
544 }
545
546 p0 := p
547 q0 := 1 - p0
548 q := math.Pow(q0, float64(n))
549
550 pbinom := 0.0
551 np1 := float64(n + 1)
552 for k := 0; k < x; k++ {
553 a, _ := math.Lgamma(np1)
554 b, _ := math.Lgamma(float64(k + 1))
555 c, _ := math.Lgamma(float64(n - k + 1))
556 // count all possible combinations for this event
557 ncomb := math.Round(math.Exp(a - b - c))
558 pbinom += ncomb * p * q
559 p *= p0
560 q /= q0
561 }
562 return pbinom
563 }
564
565 // NormalDensity calculates the density at a point along the normal distribution
566 // given.
567 func NormalDensity(x float64, mu, sigma float64) float64 {
568 z := (x - mu) / sigma
569 return math.Sqrt(0.5/sigma) * math.Exp(-(z*z)/sigma)
570 }
571
572 // CumulativeNormalDensity calculates the probability of a normal variate of
573 // being up to the value given.
574 func CumulativeNormalDensity(x float64, mu, sigma float64) float64 {
575 z := (x - mu) / sigma
576 return 0.5 + 0.5*math.Erf(z/math.Sqrt2)
577 }
578
579 // Epanechnikov is a commonly-used kernel function.
580 func Epanechnikov(x float64) float64 {
581 if math.Abs(x) > 1 {
582 // func is 0 ouside -1..+1
583 return 0
584 }
585 return 0.75 * (1 - x*x)
586 }
587
588 // Gauss is the commonly-used Gaussian kernel function.
589 func Gauss(x float64) float64 {
590 return math.Exp(-(x * x))
591 }
592
593 // Tricube is a commonly-used kernel function.
594 func Tricube(x float64) float64 {
595 a := math.Abs(x)
596 if a > 1 {
597 // func is 0 ouside -1..+1
598 return 0
599 }
600
601 b := a * a * a
602 c := 1 - b
603 return 70.0 / 81.0 * c * c * c
604 }
605
606 // SolveQuad finds the solutions of a 2nd-degree polynomial, using a formula
607 // which is more accurate than the textbook one.
608 func SolveQuad(a, b, c float64) (x1 float64, x2 float64) {
609 div := 2 * c
610 disc := math.Sqrt(b*b - 4*a*c)
611 x1 = div / (-b - disc)
612 x2 = div / (-b + disc)
613 return x1, x2
614 }
File: ./mathplus/functions_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mathplus
26
27 import (
28 "math"
29 "testing"
30 )
31
32 func TestRounding(t *testing.T) {
33 decadeTests := []struct {
34 Number float64
35 Expected float64
36 }{
37 {0, 0},
38 {-1, -10},
39 {-0.75, -10},
40 {-0.725, -10},
41 {123.532123143, 120},
42 {1932.532123143, 1930},
43 {2023.4, 2020},
44 }
45
46 for _, tc := range decadeTests {
47 y := Decade(tc.Number)
48 if y != tc.Expected {
49 const fs = `decade(%f): expected %f, got %f`
50 t.Fatalf(fs, tc.Number, tc.Expected, y)
51 }
52 }
53
54 centuryTests := []struct {
55 Number float64
56 Expected float64
57 }{
58 {0, 0},
59 {-1, -100},
60 {-0.75, -100},
61 {-0.725, -100},
62 {123.532123143, 100},
63 {1932.532123143, 1900},
64 {2023.4, 2000},
65 }
66
67 for _, tc := range centuryTests {
68 y := Century(tc.Number)
69 if y != tc.Expected {
70 const fs = `century(%f): expected %f, got %f`
71 t.Fatalf(fs, tc.Number, tc.Expected, y)
72 }
73 }
74
75 roundingTests := []struct {
76 Number float64
77 Expected [6]float64
78 }{
79 {0, [6]float64{0, 0, 0, 0, 0, 0}},
80 {-1, [6]float64{-1, -1, -1, -1, -1, -1}},
81 {-0.75, [6]float64{-0.8, -0.75, -0.75, -0.75, -0.75, -0.75}},
82 {-0.725, [6]float64{-0.7, -0.73, -0.725, -0.725, -0.725, -0.725}},
83 {2023.4, [6]float64{2023.4, 2023.4, 2023.4, 2023.4, 2023.4, 2023.4}},
84 {
85 123.532123143,
86 [6]float64{123.5, 123.53, 123.532, 123.5321, 123.53212, 123.532123},
87 },
88 {
89 1932.532123143,
90 [6]float64{1932.5, 1932.53, 1932.532, 1932.5321, 1932.53212, 1932.532123},
91 },
92 }
93
94 for _, tc := range roundingTests {
95 x := tc.Number
96 y := []float64{
97 Round1(x), Round2(x), Round3(x), Round4(x), Round5(x), Round6(x),
98 }
99
100 for i, f := range y {
101 exp := tc.Expected[i]
102 if math.Abs(exp-f) > 1e-12 {
103 const fs = `r%d(%f): expected %f, got %f`
104 t.Fatalf(fs, i+1, tc.Number, exp, f)
105 }
106 }
107 }
108 }
109
110 func TestScale(t *testing.T) {
111 scaleTests := []struct {
112 Input float64
113 InMin float64
114 InMax float64
115 OutMin float64
116 OutMax float64
117 Expected float64
118 }{
119 {-2, -5, 4, 0, 1, 1.0 / 3},
120 {0.1, 0, 0.5, -3, 5, -1.4},
121 }
122
123 for _, tc := range scaleTests {
124 in := tc.Input
125 exp := tc.Expected
126 got := Scale(in, tc.InMin, tc.InMax, tc.OutMin, tc.OutMax)
127 if got != exp {
128 const fs = `Scale(%f, %f, %f, %f, %f): expected %f, got %f`
129 t.Fatalf(fs, in, tc.InMin, tc.InMax, tc.OutMin, tc.OutMax, exp, got)
130 }
131 }
132 }
133
134 func TestIsPrime(t *testing.T) {
135 tests := []struct {
136 Input int64
137 Expected bool
138 }{
139 {-3, false},
140 {0, false},
141 {1, false},
142 {4, false},
143 {9, false},
144 {21, false},
145
146 {2, true},
147 {3, true},
148 {5, true},
149 {19, true},
150 // 15,485,863 is the millionth prime
151 {15_485_863, true},
152 }
153
154 for _, tc := range tests {
155 if v := IsPrime(tc.Input); v != tc.Expected {
156 const fs = `isprime(%d) wrongly returned %v`
157 t.Fatalf(fs, tc.Input, v)
158 }
159 }
160 }
161
162 func TestHorner(t *testing.T) {
163 tests := []struct {
164 X float64
165 C []float64
166 Expected float64
167 }{
168 {2, []float64{1, 2, 3}, 11},
169 {3, []float64{3, 5, -1}, 41},
170 }
171
172 for _, tc := range tests {
173 got := Polyval(tc.X, tc.C...)
174 if got != tc.Expected {
175 const fs = `horner(%f, %#v) gave %f, instead of %f`
176 t.Fatalf(fs, tc.X, tc.C, got, tc.Expected)
177 return
178 }
179 }
180 }
181
182 func TestGCD(t *testing.T) {
183 tests := []struct {
184 X int64
185 Y int64
186 Expected int64
187 }{
188 {0, 0, 0},
189 {-1, 10, 0},
190 {1, -10, 0},
191 {1, 1, 1},
192 {1, 7, 1},
193 {3 * 12, 12, 12},
194 {1280, 1920, 640},
195 }
196
197 for _, tc := range tests {
198 got := GCD(tc.X, tc.Y)
199 if got != tc.Expected {
200 const fs = `gcd(%d, %d) gave %d, instead of %d`
201 t.Fatalf(fs, tc.X, tc.Y, got, tc.Expected)
202 return
203 }
204 }
205 }
206
207 func TestPerm(t *testing.T) {
208 tests := []struct {
209 X int
210 Y int
211 Expected int64
212 }{
213 {10, 4, 5_040},
214 {5, 0, 1},
215 {5, 5, 120},
216 }
217
218 for _, tc := range tests {
219 got := Perm(tc.X, tc.Y)
220 if got != tc.Expected {
221 const fs = `perm(%d, %d) gave %d, instead of %d`
222 t.Fatalf(fs, tc.X, tc.Y, got, tc.Expected)
223 return
224 }
225 }
226 }
227
228 func TestChoose(t *testing.T) {
229 tests := []struct {
230 X int
231 Y int
232 Expected int64
233 }{
234 {10, 4, 210},
235 {10, 0, 1},
236 {10, 10, 1},
237 }
238
239 for _, tc := range tests {
240 got := Choose(tc.X, tc.Y)
241 if got != tc.Expected {
242 const fs = `comb(%d, %d) gave %d, instead of %d`
243 t.Fatalf(fs, tc.X, tc.Y, got, tc.Expected)
244 return
245 }
246 }
247 }
File: ./mathplus/integers.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mathplus
26
27 import "math"
28
29 const (
30 MinInt16 = -(1 << 15)
31 MinInt32 = -(1 << 31)
32 MinInt64 = -(1 << 63)
33
34 MaxInt16 = 1<<15 - 1
35 MaxInt32 = 1<<31 - 1
36 MaxInt64 = 1<<63 - 1
37
38 MaxUint16 = 1<<16 - 1
39 MaxUint32 = 1<<32 - 1
40 MaxUint64 = 1<<64 - 1
41 )
42
43 func CountIntegerDigits(n int64) int {
44 if n < 0 {
45 n = -n
46 }
47
48 // 0 doesn't have a log10
49 if n == 0 {
50 return 1
51 }
52 // add 1 to the floored logarithm, since digits are always full and
53 // with ceiling-like behavior, to use an analogy for this formula
54 return int(math.Floor(math.Log10(float64(n)))) + 1
55 }
56
57 func LoopThousandsGroups(n int64, fn func(i, n int)) {
58 // 0 doesn't have a log10
59 if n == 0 {
60 fn(0, 0)
61 return
62 }
63
64 sign := +1
65 if n < 0 {
66 n = -n
67 sign = -1
68 }
69
70 intLog1000 := int(math.Log10(float64(n)) / 3)
71 remBase := int64(math.Pow10(3 * intLog1000))
72
73 for i := 0; remBase > 0; i++ {
74 group := (1000 * n) / remBase / 1000
75 fn(i, sign*int(group))
76 // if original number was negative, ensure only first
77 // group gives a negative input to the callback
78 sign = +1
79
80 n %= remBase
81 remBase /= 1000
82 }
83 }
84
85 var pow10 = []int64{
86 1,
87 10,
88 100,
89 1000,
90 10000,
91 100000,
92 1000000,
93 10000000,
94 100000000,
95 1000000000, // last entry for int32
96 10000000000,
97 100000000000,
98 1000000000000,
99 10000000000000,
100 100000000000000,
101 1000000000000000,
102 10000000000000000,
103 100000000000000000,
104 1000000000000000000,
105 // 10000000000000000000,
106 }
107
108 var pow2 = []int64{
109 1,
110 2,
111 4,
112 8,
113 16,
114 32,
115 64,
116 128,
117 256,
118 512,
119 1024,
120 2048,
121 4096,
122 8192,
123 16384,
124 32768,
125 65536,
126 131072,
127 262144,
128 524288,
129 1048576,
130 2097152,
131 4194304,
132 8388608,
133 16777216,
134 33554432,
135 67108864,
136 134217728,
137 268435456,
138 536870912,
139 1073741824,
140 2147483648, // last entry for int32
141 4294967296,
142 8589934592,
143 17179869184,
144 34359738368,
145 68719476736,
146 137438953472,
147 274877906944,
148 549755813888,
149 1099511627776,
150 2199023255552,
151 4398046511104,
152 8796093022208,
153 17592186044416,
154 35184372088832,
155 70368744177664,
156 140737488355328,
157 281474976710656,
158 562949953421312,
159 1125899906842624,
160 2251799813685248,
161 4503599627370496,
162 9007199254740992,
163 18014398509481984,
164 36028797018963968,
165 72057594037927936,
166 144115188075855872,
167 288230376151711744,
168 576460752303423488,
169 1152921504606846976,
170 2305843009213693952,
171 4611686018427387904,
172 // 9223372036854775808,
173 }
174
175 // Log2Int gives you the floor of the base-2 logarithm, or negative results
176 // for invalid inputs. Values less than 1 aren't supported, since they don't
177 // have logarithms of any valid base, let alone base-2 logarithms.
178 func Log2Int(n int64) (log2 int, ok bool) {
179 if n < 1 {
180 return -1, false
181 }
182
183 v := uint64(n)
184 mask := uint64(1 << 63) // 2**63
185 for i := int64(63); i > 0; i-- {
186 if v&mask != 0 {
187 return int(i), true
188 }
189 mask >>= 1
190 }
191 return 0, true
192 }
193
194 // Log10Int gives you the floor of the base-10 logarithm, or negative results
195 // for invalid inputs. Values less than 1 aren't supported, since they don't
196 // have logarithms of any valid base, let alone base-10 logarithms.
197 func Log10Int(n int64) (log10 int, ok bool) {
198 if n < 1 {
199 return -1, false
200 }
201
202 for i := int64(0); i < 19; i++ {
203 n /= 10
204 if n == 0 {
205 return int(i), true
206 }
207 }
208 return 0, true
209 }
210
211 // Pow10Int gives you the integers powers of 10 as far as int64 allows: negative
212 // inputs aren't supported, since negative exponents don't give integer results.
213 func Pow10Int(n int) (power10 int64, ok bool) {
214 if 0 <= n && n < len(pow10) {
215 return pow10[n], true
216 }
217 return -1, false
218 }
219
220 // Pow2Int gives you the integers powers of 2 as far as int64 allows: negative
221 // inputs aren't supported, since negative exponents don't give integer results.
222 func Pow2Int(n int) (power2 int64, ok bool) {
223 if 0 <= n && n < len(pow2) {
224 return pow2[n], true
225 }
226 return -1, false
227 }
File: ./mathplus/integers_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mathplus
26
27 import "testing"
28
29 func TestCountIntegerDigits(t *testing.T) {
30 tests := map[int64]int{
31 0: 1,
32 -32: 2,
33 999: 3,
34 3_490: 4,
35 12_332: 5,
36 999_999: 6,
37 1_000_000: 7,
38 1_000_001: 7,
39 12_345_678: 8,
40 }
41
42 for input, expected := range tests {
43 if n := CountIntegerDigits(input); n != expected {
44 const fs = `integer digits in %d: got %d instead of %d`
45 t.Errorf(fs, input, n, expected)
46 }
47 }
48 }
49
50 func TestLoopThousandsGroups(t *testing.T) {
51 tests := map[int64][]int{
52 0: []int{0},
53 -32: []int{-32}, // negatives not supported yet
54 999: []int{999},
55 1_670: []int{1, 670},
56 3_490: []int{3, 490},
57 12_332: []int{12, 332},
58 999_999: []int{999, 999},
59 1_000_000: []int{1, 0, 0},
60 1_000_001: []int{1, 0, 1},
61 1_234_567: []int{1, 234, 567},
62 }
63
64 for input, expected := range tests {
65 count := 0
66 LoopThousandsGroups(input, func(i, n int) {
67 // t.Log(tc.Input, i, n)
68 if n != expected[i] {
69 const fs = `group %d in %d: got %d instead of %d`
70 t.Errorf(fs, i, input, n, expected[i])
71 }
72 count++
73 })
74
75 if count != len(expected) {
76 const fs = `thousands-groups from %d: got %d instead of %d`
77 t.Errorf(fs, input, count, len(expected))
78 }
79 }
80 }
81
82 func TestLog2Int(t *testing.T) {
83 tests := map[int64]int{
84 -3: -1,
85 1: 0,
86 2: 1,
87 3: 1,
88 4: 2,
89 1024: 10,
90 1_025: 10,
91 2*1024 - 1: 10,
92 }
93
94 for value, expected := range tests {
95 got, ok := Log2Int(value)
96 if got != expected || (ok && value < 1) {
97 const fs = `log2int(%d) = %d, but got %d instead`
98 t.Fatalf(fs, value, expected, got)
99 }
100 }
101 }
102
103 func TestLog10Int(t *testing.T) {
104 tests := map[int64]int{
105 -3: -1,
106 1: 0,
107 10: 1,
108 100: 2,
109 101: 2,
110 199: 2,
111 1_000_000: 6,
112 }
113
114 for value, expected := range tests {
115 got, ok := Log10Int(value)
116 if got != expected || (ok && value < 1) {
117 const fs = `log10int(%d) = %d, but got %d instead`
118 t.Fatalf(fs, value, expected, got)
119 }
120 }
121 }
File: ./mathplus/numbers.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mathplus
26
27 import (
28 "math"
29 )
30
31 const (
32 // the maximum integer a float64 can represent exactly; since float64
33 // uses a sign bit, instead of a 2-complement mantissa, -maxflint is
34 // the minimum integer
35 maxflint = 2 << 52
36 )
37
38 // StringWidth counts how many runes it takes to represent the value given as a
39 // string both as a plain no-commas string and as a string with digit-grouping
40 // commas, if needed. Each group is 3 digits. Showing numbers with commas makes
41 // long numbers easier to read.
42 func StringWidth(f float64, decimals int) (plain, nice int) {
43 if math.IsNaN(f) || math.IsInf(f, 0) {
44 return 0, 0
45 }
46
47 // avoid wrong results, since decimals will be added at the end
48 if decimals < 0 {
49 decimals = 0
50 }
51
52 extras := 0 // count non-digits, such as negatives and dots
53 if f < 0 {
54 f = -f // fix value for uintWidth
55 extras++ // count the leading negative sign
56 }
57 if decimals > 0 {
58 extras++ // count the decimal dot
59 }
60
61 // at this point, f >= 0 for sure
62 plain, nice = uintWidth(f)
63 plain += extras + decimals
64 nice += extras + decimals
65 return plain, nice
66 }
67
68 // uintWidth can't handle negatives correctly, as its name suggests
69 func uintWidth(f float64) (plain, nice int) {
70 // only 1 digit and 0 commas for 0 <= x < 10
71 if f < 10 {
72 return 1, 1
73 }
74
75 mag := math.Log10(math.Floor(f)) // order of magnitude
76 digits := int(mag) + 1 // integer digits
77 commas := int(mag) / 3 // commas separating digits in groups of 3
78 return digits, digits + commas
79 }
80
81 // Equals allows easily comparing numbers, including NaN values, which otherwise
82 // never equal anything they're compared to, including themselves.
83 func Equals(x, y float64) bool {
84 if x == y {
85 return true
86 }
87 return math.IsNaN(x) && math.IsNaN(y)
88 }
89
90 // ApproxEqual allows approximate comparisons, as well as checking if both values
91 // are NaN, which otherwise never equal themselves when compared directly. The
92 // last parameter must be positive for approx. comparisons, or zero for exact
93 // comparisons.
94 func ApproxEqual(x, y float64, maxdiff float64) bool {
95 if math.Abs(x-y) <= maxdiff {
96 return true
97 }
98 return math.IsNaN(x) && math.IsNaN(y)
99 }
100
101 // GuessDecimalsCount tries to guess the number of decimal digits of the number
102 // given.
103 func GuessDecimalsCount(x float64, max int) int {
104 if x < 0 {
105 x = -x
106 }
107
108 // only up to 16, because log10(2**-53) ~= -15.9546
109 if !(0 <= max && max <= 16) {
110 max = 16
111 }
112
113 const tol = 5e-13 // 1e-11, 1e-12, 5e-13
114 for digits := 0; digits <= max; digits++ {
115 _, frac := math.Modf(x)
116 frac = math.Abs(frac)
117 // when it's time to stop, the absolute value of the fraction part is
118 // either extremely close to 0 or extremely close to 1
119 if frac < tol || (1-frac) < tol {
120 return digits
121 }
122 x *= 10
123 }
124 return max
125 }
126
127 // Default returns the first non-NaN value among those given: failing that,
128 // the result will be NaN.
129 func Default(args ...float64) float64 {
130 for _, x := range args {
131 if !math.IsNaN(x) {
132 return x
133 }
134 }
135 return math.NaN()
136 }
137
138 // TrimSlice ignores all leading/trailing NaN values from the slice given: its
139 // main use-case is after sorting via sort.Float64s, since all NaN values are
140 // moved in place to the start/end of the now-sorted slice.
141 //
142 // # Example
143 //
144 // sort.Float64(values)
145 // res := mathplus.TrimSlice(values)
146 func TrimSlice(x []float64) []float64 {
147 // ignore all leading NaNs
148 for len(x) > 0 && math.IsNaN(x[0]) {
149 x = x[1:]
150 }
151 // ignore all trailing NaNs
152 for len(x) > 0 && math.IsNaN(x[len(x)-1]) {
153 x = x[:len(x)-1]
154 }
155 return x
156 }
157
158 // Linspace works like the Matlab function of the same name, except it takes a
159 // callback, generalizing its behavior.
160 func Linspace(a, incr, b float64, f func(x float64)) {
161 if incr <= 0 {
162 return
163 }
164
165 if a < b {
166 forwardLinspace(a, incr, b, f)
167 } else if a > b {
168 backwardLinspace(a, incr, b, f)
169 } else {
170 f(a)
171 }
172 }
173
174 func forwardLinspace(a, incr, b float64, f func(x float64)) {
175 for i := 0; true; i++ {
176 x := float64(i)*incr + a
177 if x <= b {
178 f(x)
179 } else {
180 return
181 }
182 }
183 }
184
185 func backwardLinspace(a, incr, b float64, f func(x float64)) {
186 for i := 0; true; i++ {
187 x := b - float64(i)*incr
188 if x >= a {
189 f(x)
190 } else {
191 return
192 }
193 }
194 }
195
196 // Seq is a special case of Linspace, where the increment is +/-1.
197 func Seq(a, b float64, f func(x float64)) {
198 Linspace(a, 1, b, f)
199 }
200
201 // Increment increments the number given by adding 1, when it's in the int-safe
202 // range of float64s; when outside that range, the smallest available delta is
203 // used instead.
204 func Increment(x float64) float64 {
205 if math.IsNaN(x) || math.IsInf(x, 0) {
206 return x
207 }
208 if incr := x + 1; incr != x {
209 return incr
210 }
211 return math.Float64frombits(math.Float64bits(x) + 1)
212 }
213
214 // Decrement decrements the number given by subtracting 1, when it's in the
215 // int-safe range of float64s: when outside that range, the smallest available
216 // delta is used instead.
217 func Decrement(x float64) float64 {
218 if math.IsNaN(x) || math.IsInf(x, 0) {
219 return x
220 }
221 if decr := x - 1; decr != x {
222 return decr
223 }
224 // subtracting 1 from the value's uint64 counterpart doesn't work for values
225 // less than the minimum exact-integer: uint64s use 2-complement arithmetic
226 return -math.Float64frombits(math.Float64bits(-x) + 1)
227 }
228
229 func Deapproximate(x float64) (min float64, max float64) {
230 if math.IsNaN(x) || math.IsInf(x, 0) {
231 return x, x
232 }
233
234 if x == 0 {
235 return -0.5, +0.5
236 }
237
238 if math.Remainder(x, 1) == 0 {
239 if math.Remainder(x, 10) != 0 {
240 return x - 0.5, x + 0.5
241 }
242
243 sign := Sign(x)
244 abs := math.Abs(x)
245 delta := float64(maxExactPow10(int(abs))) / 2
246 return sign*abs - delta, sign*abs + delta
247 }
248
249 // return surrounding integers when given non-integers
250 return math.Floor(x), math.Ceil(x)
251 }
252
253 func maxExactPow10(x int) int {
254 pow10 := 1
255 for {
256 if x%pow10 != 0 {
257 return pow10 / 10
258 }
259 pow10 *= 10
260 }
261 }
File: ./mathplus/numbers_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mathplus
26
27 import (
28 "math"
29 "strconv"
30 "testing"
31 )
32
33 func TestNumberWidth(t *testing.T) {
34 tests := []struct {
35 Input float64
36 Decimals int
37 PlainWidth int
38 PrettyWidth int
39 }{
40 {0, 0, len(`0`), len(`0`)},
41 {0.923123, 0, len(`0`), len(`0`)},
42 {0.923123, 2, len(`0.92`), len(`0.92`)},
43 {-3, 0, len(`-3`), len(`-3`)},
44 {-300, 0, len(`-300`), len(`-300`)},
45 {+2_300, 0, len(`2300`), len(`2,300`)},
46 {-1_000_000, 0, len(`-1000000`), len(`-1,000,000`)},
47 {+1_000_000, 0, len(`1000000`), len(`1,000,000`)},
48 {+1_610_058.23423, 4, len(`1610058.2342`), len(`1_610_058.2342`)},
49 {-317_289, 4, len(`-317289.0000`), len(`-317_289.0000`)},
50 {5_457_013.0000, 4, len(`5457013.0000`), len(`5_457_013.0000`)},
51 {-118.406800000000004, 15, 20, 20},
52 }
53
54 for _, tc := range tests {
55 pl, pr := StringWidth(tc.Input, tc.Decimals)
56 if pl != tc.PlainWidth {
57 const fs = `PlainWidth(%f, %d): expected %d, got %d`
58 t.Fatalf(fs, tc.Input, tc.Decimals, tc.PlainWidth, pl)
59 }
60 if pr != tc.PrettyWidth {
61 const fs = `PrettyWidth(%f, %d): expected %d, got %d`
62 t.Fatalf(fs, tc.Input, tc.Decimals, tc.PrettyWidth, pr)
63 }
64 }
65 }
66
67 func TestNumberWidthInternal(t *testing.T) {
68 tests := []struct {
69 Number float64
70 Decimals int
71 IntDigits int
72 Commas int
73 }{
74 {0, 0, 1, 0},
75 {-3, 0, 1, 0},
76 {-300, 0, 3, 0},
77 {+2_300, 0, 4, 1},
78 {-1_000_000, 0, 7, 2},
79 {+1_000_000, 0, 7, 2},
80 {+1_610_058.23423, 4, 7, 2},
81 {-317_289, 4, 6, 1},
82 {5_457_013.0000, 4, 7, 2},
83 }
84
85 for _, tc := range tests {
86 // remember that uintWidth can't handle negatives
87 digits, pretty := uintWidth(math.Abs(tc.Number))
88
89 commas := pretty - digits
90 if digits != tc.IntDigits || commas != tc.Commas {
91 const fs = `%f: expected %d and %d, but got %d and %d instead`
92 t.Fatalf(fs, tc.Number, tc.IntDigits, tc.Commas, digits, commas)
93 }
94 }
95 }
96
97 func TestGuessDecimalsCount(t *testing.T) {
98 tests := []struct {
99 Input float64
100 Expected int
101 }{
102 {-1, 0},
103 {-1.3, 1},
104 {-1.1, 1},
105 {10.103, 3},
106 {3.9500, 2},
107 {1.789000, 3},
108 {5.0409999000000000, 7},
109 }
110
111 for _, tc := range tests {
112 got := GuessDecimalsCount(tc.Input, -1)
113 if got != tc.Expected {
114 const fs = `guess(%f, %d) gave %d, but should have been %d`
115 t.Fatalf(fs, tc.Input, -1, got, tc.Expected)
116 }
117 }
118
119 var v = []float64{
120 1.773, 1.789, 1.773, 1.779, 1.817, 1.797, 1.780, 1.737,
121 1.723, 1.725, 1.733, 1.742, 1.746, 1.728, 1.726, 1.701,
122 1.690, 1.675, 1.680, 1.672, 1.685, 1.679, 1.674, 1.653,
123 1.668, 1.669, 1.680, 1.693, 1.713, 1.712, 1.733, 1.744,
124 1.739, 1.708, 1.695, 1.691, 1.715, 1.733, 1.730, 1.722,
125 1.737, 1.749, 1.774, 1.773, 1.789, 1.804, 1.795, 1.752,
126 1.770, 1.736, 1.717, 1.690, 1.696, 1.703, 1.679, 1.673,
127 1.694, 1.710, 1.711, 1.739, 1.725, 1.748, 1.764, 1.781,
128 1.766, 1.741, 1.739, 1.750, 1.752, 1.731, 1.717, 1.699,
129 1.689, 1.701, 1.713, 1.715, 1.713, 1.780, 1.795, 1.822,
130 1.832, 1.835, 1.846, 1.857, 1.869, 1.847, 1.831, 1.874,
131 1.928, 1.900, 1.925, 1.888, 1.842, 1.836, 1.825, 1.804,
132 1.745, 1.753, 1.779, 1.797, 1.812, 1.820, 1.829, 1.818,
133 1.799, 1.795, 1.795, 1.792, 1.792, 1.760, 1.741, 1.721,
134 1.718, 1.734, 1.751, 1.776, 1.768, 1.774, 1.790, 1.813,
135 1.840, 1.861, 1.853, 1.834, 1.848, 1.859, 1.856, 1.839,
136 1.826, 1.833, 1.850, 1.861, 1.874, 1.905, 1.931, 1.951,
137 1.971, 1.985, 1.997, 2.023, 2.047, 2.080, 2.103, 2.139,
138 2.155, 2.151, 2.142, 2.147, 2.147, 2.165, 2.171, 2.176,
139 2.174, 2.190, 2.192, 2.184, 2.170, 2.155, 2.155, 2.140,
140 2.129, 2.124, 2.128, 2.146, 2.135, 2.151, 2.141, 2.134,
141 2.086, 2.059, 2.014, 1.980, 1.969, 1.972, 1.953, 1.927,
142 1.915, 1.901, 1.905, 1.941, 1.951, 1.940, 1.949, 1.959,
143 1.985, 1.995, 2.001, 2.002, 2.017, 2.018, 2.006, 1.997,
144 1.994, 1.987, 1.978, 1.977, 1.940, 1.928, 1.909, 1.813,
145 1.736, 1.710, 1.712, 1.726, 1.740, 1.747, 1.748, 1.745,
146 1.728, 1.714, 1.656, 1.649, 1.642, 1.630, 1.612, 1.588,
147 1.583, 1.570, 1.568, 1.561, 1.537, 1.542, 1.548, 1.538,
148 1.520, 1.521, 1.513, 1.494, 1.469, 1.458, 1.449, 1.441,
149 1.434, 1.428, 1.431, 1.440, 1.447, 1.457, 1.456, 1.457,
150 1.450, 1.451, 1.439, 1.423, 1.374, 1.100, 1.147, 1.134,
151 1.121, 1.119, 1.115,
152 }
153
154 for i, x := range v {
155 guess := gdc(t, x)
156 // guess := GuessDecimalsCount(v, -1)
157 if guess > 3 {
158 _, diff := math.Modf(1000 * x)
159 t.Logf("len: %d\n", len(v))
160 const fs = `(item %2d) %f doesn't have %d decimals; diff: %.20f`
161 t.Fatalf(fs, i+1, x, guess, diff)
162 }
163 }
164
165 v = []float64{
166 6000.00, 2300.00, 2000.00, 1700.00, 1500.00, 1500.00, 1400.00, 1300.00,
167 900.00, 800.00, 800.00, 700.00, 600.00, 550.00, 550.00, 400.00,
168 320.39, 300.00, 200.00, 198.56, 155.94, 155.73, 98.15, 80.00,
169 80.00, 74.50, 70.00, 70.00, 70.00, 70.00, 70.00, 65.00,
170 60.00, 55.45, 49.00, 35.00, 35.00, 25.00, 25.00, 25.00,
171 20.00, 15.00, 15.00, 12.00, 10.00, 10.00, 10.00, 10.00,
172 7.00, 6.00, 6.00, 5.00, 3.00, 2.00, 1.00,
173 }
174 for i, x := range v {
175 guess := gdc(t, x)
176 // guess := GuessDecimalsCount(v, -1)
177 if guess > 2 {
178 _, diff := math.Modf(100 * x)
179 t.Logf("len: %d\n", len(v))
180 const fs = `(item %2d) %f doesn't have %d decimals; diff: %.20f`
181 t.Fatalf(fs, i+1, x, guess, diff)
182 }
183 }
184 }
185
186 // gdc logs each step of the loop inside GuessDecimalsCount
187 func gdc(t *testing.T, x float64) int {
188 const max = 16
189 const tol = 5e-11 // 5e-13
190 for digits := 0; digits <= max; digits++ {
191 _, frac := math.Modf(x)
192 const fs = "x: %f\tdigits: %d\tdiff: %.20f\tcdiff: %.20f\n"
193 t.Logf(fs, x, digits, frac, 1-frac)
194 ar := math.Abs(frac)
195 if ar < tol || (1-ar) < tol {
196 return digits
197 }
198 x *= 10
199 }
200 return max
201 }
202
203 func TestIncrement(t *testing.T) {
204 tests := []struct {
205 Input float64
206 Expected float64
207 }{
208 {math.NaN(), math.NaN()},
209 {math.Inf(-1), math.Inf(-1)},
210 {math.Inf(+1), math.Inf(+1)},
211
212 {-maxflint - 2, -maxflint},
213 {-maxflint, -maxflint + 1},
214 {-1, 0},
215 {0, 1},
216 {10.25, 11.25},
217 {maxflint - 1, maxflint},
218 {maxflint, maxflint + 2},
219 }
220
221 for _, tc := range tests {
222 name := strconv.FormatFloat(tc.Input, 'f', 2, 64)
223 t.Run(name, func(t *testing.T) {
224 got := Increment(tc.Input)
225 if !Equals(got, tc.Expected) {
226 const fs = `got %v, instead of %v`
227 t.Fatalf(fs, got, tc.Expected)
228 }
229 })
230 }
231 }
232
233 func TestDecrement(t *testing.T) {
234 tests := []struct {
235 Input float64
236 Expected float64
237 }{
238 {math.NaN(), math.NaN()},
239 {math.Inf(-1), math.Inf(-1)},
240 {math.Inf(+1), math.Inf(+1)},
241
242 {-maxflint, -maxflint - 2},
243 {-maxflint + 1, -maxflint},
244 {-1, -2},
245 {0, -1},
246 {11.25, 10.25},
247 {maxflint, maxflint - 1},
248 {maxflint + 2, maxflint},
249 }
250
251 for _, tc := range tests {
252 name := strconv.FormatFloat(tc.Input, 'f', 2, 64)
253 t.Run(name, func(t *testing.T) {
254 got := Decrement(tc.Input)
255 if !Equals(got, tc.Expected) {
256 const fs = `got %v, instead of %v`
257 t.Fatalf(fs, got, tc.Expected)
258 }
259 })
260 }
261 }
262
263 func TestDeapproximate(t *testing.T) {
264 tests := []struct {
265 Input float64
266 ExpectedMin float64
267 ExpectedMax float64
268 }{
269 {0, -0.5, +0.5},
270 {1, 0.5, 1.5},
271 {10, 5, 15},
272 {100, 50, 150},
273 {101, 100.5, 101.5},
274 {102, 101.5, 102.5},
275 {120, 115, 125},
276 }
277
278 for _, tc := range tests {
279 name := strconv.FormatFloat(tc.Input, 'f', 6, 64)
280 t.Run(name, func(t *testing.T) {
281 min, max := Deapproximate(tc.Input)
282 if !Equals(min, tc.ExpectedMin) || !Equals(max, tc.ExpectedMax) {
283 const fs = `got %f and %f, instead of %f and %f`
284 t.Fatalf(fs, min, max, tc.ExpectedMin, tc.ExpectedMax)
285 }
286 })
287 }
288 }
289
290 func TestMaxExactPow10(t *testing.T) {
291 tests := []struct {
292 Input int
293 Expected int
294 }{
295 // {0, 1},
296 {1, 1},
297 {4, 1},
298 {10, 10},
299 {100, 100},
300 {101, 1},
301 {102, 1},
302 {120, 10},
303 }
304
305 for _, tc := range tests {
306 name := strconv.Itoa(tc.Input)
307 t.Run(name, func(t *testing.T) {
308 got := maxExactPow10(int(tc.Input))
309 if got != int(tc.Expected) {
310 const fs = `got %d, instead of %d`
311 t.Fatalf(fs, got, tc.Expected)
312 }
313 })
314 }
315 }
File: ./mathplus/statistics.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mathplus
26
27 import (
28 "math"
29 )
30
31 // Quantile calculates a sorted array's quantile, parameterized by a number in
32 // [0, 1]; the sorted array must be in increasing order and can't contain any
33 // NaN value.
34 func Quantile(x []float64, q float64) float64 {
35 l := len(x)
36 // quantiles aren't defined for empty arrays/samples
37 if l == 0 {
38 return math.NaN()
39 }
40
41 // calculate indices of surrounding values, which match when index isn't
42 // fractional
43 mid := float64(l-1) * q
44 low := math.Floor(mid)
45 high := math.Ceil(mid)
46
47 // for fractional indices, interpolate their 2 surrounding values
48 a := x[int(low)]
49 b := x[int(high)]
50 return a + (mid-low)*(b-a)
51 }
52
53 // welford has almost everything needed to implement Welford's running-stats
54 // algorithm for the arithmetic mean and the standard deviation: the only
55 // thing missing is the count of values so far, which you must provide every
56 // time you call its methods.
57 type welford struct {
58 Mean float64
59 meanOfSquares float64
60 }
61
62 // Update advances Welford's algorithm, using the value and item-count given.
63 func (w *welford) Update(x float64, n int) {
64 d1 := x - w.Mean
65 w.Mean += d1 / float64(n)
66 d2 := x - w.Mean
67 w.meanOfSquares += d1 * d2
68 }
69
70 // SD calculates the current standard-deviation. The first parameter is the
71 // bias-correction to use: 0 gives you the `population` SD, 1 gives you the
72 // `sample` SD; 1.5 is very-rarely used and only in special circumstances.
73 func (w welford) SD(bias float64, n int) float64 {
74 denom := float64(n) - bias
75 return math.Sqrt(w.meanOfSquares / denom)
76 }
77
78 // RMS calculates the current root-mean-square statistic.
79 func (w welford) RMS() float64 {
80 return math.Sqrt(w.meanOfSquares)
81 }
82
83 // NumberSummary has/updates numeric constant-space running stats.
84 type NumberSummary struct {
85 // struct welford has the Mean public field
86 welford
87
88 // Count is how many values there are so far, including NaNs
89 Count int
90
91 // NaN counts NaN values so far
92 NaN int
93
94 // Integers counts all integers so far
95 Integers int
96
97 // Negatives counts all negative number so far
98 Negatives int
99
100 // Zeros counts all zeros so far
101 Zeros int
102
103 // Positives counts all positive numbers so far
104 Positives int
105
106 // Min is the least number so far, ignoring NaNs
107 Min float64
108
109 // Max is the highest number so far, ignoring NaNs
110 Max float64
111
112 // Sum is the sum of all numbers so far, ignoring NaNs
113 Sum float64
114
115 // sumOfLogs is used to calculate the geometric mean
116 sumOfLogs float64
117 }
118
119 // Update does exactly what it says.
120 func (ns *NumberSummary) Update(f float64) {
121 if ns.Count == 0 {
122 ns.Min = math.Inf(+1)
123 ns.Max = math.Inf(-1)
124 }
125
126 ns.Count++
127 if math.IsNaN(f) {
128 ns.NaN++
129 return
130 }
131
132 if _, r := math.Modf(f); r == 0 {
133 ns.Integers++
134 }
135
136 if f > 0 {
137 ns.Positives++
138 } else if f == 0 {
139 ns.Zeros++
140 } else if f < 0 {
141 ns.Negatives++
142 }
143
144 ns.Sum += f
145 ns.sumOfLogs += math.Log(f)
146 ns.Min = math.Min(ns.Min, f)
147 ns.Max = math.Max(ns.Max, f)
148 ns.welford.Update(f, ns.Valid())
149 }
150
151 // Valid finds how many numbers are valid so far
152 func (ns NumberSummary) Valid() int {
153 return ns.Count - ns.NaN
154 }
155
156 // Invalid finds how many numbers are invalid so far
157 func (ns NumberSummary) Invalid() int {
158 return ns.NaN
159 }
160
161 // Geomean calculates the current geometric mean
162 func (ns NumberSummary) Geomean() float64 {
163 if ns.Negatives > 0 || ns.Zeros > 0 {
164 return math.NaN()
165 }
166 return math.Exp(ns.sumOfLogs / float64(ns.Valid()))
167 }
168
169 // SD calculates the current standard-deviation. The only parameter is the
170 // bias-correction to use:
171 //
172 // 0 means calculate the current population standard-deviation
173 // 1 means calculate the current sample standard-deviation
174 // 1.5 is very-rarely used and only in special circumstances
175 func (ns NumberSummary) SD(bias float64) float64 {
176 return ns.welford.SD(bias, ns.Valid())
177 }
178
179 // CommonQuantiles groups all the most commonly-used quantiles in practice.
180 //
181 // Funcs AppendAllDeciles and AppendAllPercentiles give you other sets of
182 // commonly-used ranking stats.
183 type CommonQuantiles struct {
184 Min float64
185 P01 float64 // 1st percentile
186 P05 float64 // 5th percentile
187 P10 float64 // 10th percentile, also the 1st decile
188 P25 float64 // 1st quartile, also the 25th percentile
189 P50 float64 // 2nd quartile, also the 50th percentile
190 P75 float64 // 3rd quartile, also the 75th percentile
191 P90 float64
192 P95 float64
193 P99 float64
194 Max float64
195 }
196
197 // NewCommonQuantiles is a convenience constructor for struct CommonQuantiles.
198 func NewCommonQuantiles(x []float64) CommonQuantiles {
199 return CommonQuantiles{
200 Min: Quantile(x, 0.00),
201 P01: Quantile(x, 0.01),
202 P05: Quantile(x, 0.05),
203 P10: Quantile(x, 0.10),
204 P25: Quantile(x, 0.25),
205 P50: Quantile(x, 0.50),
206 P75: Quantile(x, 0.75),
207 P90: Quantile(x, 0.90),
208 P95: Quantile(x, 0.95),
209 P99: Quantile(x, 0.99),
210 Max: Quantile(x, 1.00),
211 }
212 }
213
214 // AppendAllDeciles appends 11 items to the slice given, which ultimately
215 // lets you reuse slices for multiple such calculations, thus avoiding extra
216 // allocations.
217 //
218 // The 1st item returned is the minimum, and the last one is the maximum.
219 func AppendAllDeciles(dest []float64, x []float64) []float64 {
220 for i := 0; i <= 10; i++ {
221 dest = append(dest, Quantile(x, float64(i)/10))
222 }
223 return dest
224 }
225
226 // AppendAllPercentiles appends 101 items to the slice given, which ultimately
227 // lets you reuse slices for multiple such calculations, thus avoiding extra
228 // allocations.
229 //
230 // The 1st item returned is the minimum, while the last one is the maximum.
231 func AppendAllPercentiles(dest []float64, x []float64) []float64 {
232 for i := 0; i <= 100; i++ {
233 dest = append(dest, Quantile(x, float64(i)/100))
234 }
235 return dest
236 }
File: ./mathplus/statistics_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mathplus
26
27 import (
28 "math"
29 "testing"
30 )
31
32 type NumericTestResult struct {
33 Count int
34 Valid int
35 Integers int
36 Negatives int
37 Zeros int
38 Positives int
39
40 Min float64
41 Max float64
42 Sum float64
43 Mean float64
44 Geomean float64
45 SD float64
46 RMS float64
47 }
48
49 func (tr NumericTestResult) Match(ns NumberSummary) bool {
50 return true &&
51 tr.Count == ns.Count &&
52 tr.Valid == ns.Valid() &&
53 tr.Integers == ns.Integers &&
54 tr.Negatives == ns.Negatives &&
55 tr.Zeros == ns.Zeros &&
56 tr.Positives == ns.Positives &&
57 equals(tr.Min, ns.Min) &&
58 equals(tr.Max, ns.Max) &&
59 equals(tr.Sum, ns.Sum) &&
60 equals(tr.Mean, ns.Mean) &&
61 equals(tr.Geomean, ns.Geomean()) &&
62 equals(tr.SD, ns.SD(0)) &&
63 equals(tr.RMS, ns.RMS()) &&
64 true
65 }
66
67 func (tr NumericTestResult) Test(t *testing.T, ns NumberSummary) {
68 var checks = []struct {
69 X float64
70 Y float64
71 Message string
72 }{
73 {float64(tr.Count), float64(ns.Count), `count`},
74 {float64(tr.Valid), float64(ns.Valid()), `valid`},
75 {float64(tr.Integers), float64(ns.Integers), `integers`},
76 {float64(tr.Negatives), float64(ns.Negatives), `negatives`},
77 {float64(tr.Zeros), float64(ns.Zeros), `zeros`},
78 {float64(tr.Positives), float64(ns.Positives), `positives`},
79 {float64(tr.Min), float64(ns.Min), `min`},
80 {float64(tr.Max), float64(ns.Max), `max`},
81 {float64(tr.Sum), float64(ns.Sum), `sum`},
82 {float64(tr.Mean), float64(ns.Mean), `mean`},
83 {float64(tr.Geomean), float64(ns.Geomean()), `geomean`},
84 {float64(tr.SD), float64(ns.SD(0)), `sd`},
85 {float64(tr.RMS), float64(ns.RMS()), `rms`},
86 }
87
88 const fs = "field %q failed: %f and %f differ\nexpected %#v\ngot %#v"
89 for _, tc := range checks {
90 if !equals(tc.X, tc.Y) {
91 t.Fatalf(fs, tc.Message, tc.X, tc.Y, tr, ns)
92 return
93 }
94 }
95 }
96
97 var numericTests = []struct {
98 Description string
99 Input []float64
100 Expected NumericTestResult
101 }{
102 {
103 Description: `no values`,
104 Input: []float64{},
105 Expected: NumericTestResult{
106 Count: 0,
107 Valid: 0,
108 Integers: 0,
109 // Min: math.Inf(+1),
110 // Max: math.Inf(-1),
111 Min: 0.0,
112 Max: 0.0,
113 Sum: 0.0,
114 Mean: 0.0,
115 Geomean: math.NaN(),
116 SD: math.NaN(),
117 RMS: 0.0,
118 },
119 },
120 {
121 Description: `just a value`,
122 Input: []float64{-3.5},
123 Expected: NumericTestResult{
124 Count: 1,
125 Valid: 1,
126 Integers: 0,
127 Negatives: 1,
128 Zeros: 0,
129 Positives: 0,
130 Min: -3.5,
131 Max: -3.5,
132 Sum: -3.5,
133 Mean: -3.5,
134 Geomean: math.NaN(),
135 SD: 0.0,
136 RMS: 0.0,
137 },
138 },
139 {
140 Description: `integers 1..10`,
141 Input: []float64{math.NaN(), 1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
142 Expected: NumericTestResult{
143 Count: 11,
144 Valid: 10,
145 Integers: 10,
146 Negatives: 0,
147 Zeros: 0,
148 Positives: 10,
149 Min: 1,
150 Max: 10,
151 Sum: 55,
152 Mean: 5.5,
153 Geomean: 4.528728688116766,
154 RMS: 9.082951062292475,
155 SD: 2.8722813232690143,
156 },
157 },
158 {
159 Description: `nothing valid`,
160 Input: []float64{
161 math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN(),
162 },
163 Expected: NumericTestResult{
164 Count: 5,
165 Valid: 0,
166 Integers: 0,
167 Min: math.Inf(+1),
168 Max: math.Inf(-1),
169 Sum: 0.0,
170 Mean: 0.0,
171 Geomean: math.NaN(),
172 SD: math.NaN(),
173 RMS: 0.0,
174 },
175 },
176 }
177
178 func TestNumberSummary(t *testing.T) {
179 for _, tc := range numericTests {
180 var s NumberSummary
181 for _, x := range tc.Input {
182 s.Update(x)
183 }
184 tc.Expected.Test(t, s)
185 }
186 }
187
188 func equals(x, y float64) bool {
189 if x == y {
190 return true
191 }
192 return math.IsNaN(x) && math.IsNaN(y)
193 }
File: ./mediainfo/aiff.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "errors"
29 "io"
30 "math"
31 )
32
33 // http://paulbourke.net/dataformats/audio/
34
35 var errTruncatedAIFF = errors.New("unexpected end of AIFF data")
36
37 func aiffDuration(r io.Reader) (seconds float64, err error) {
38 data, err := io.ReadAll(r)
39 if err != nil {
40 return 0, err
41 }
42
43 // all these are read when in the COMM block
44 numChan := 0
45 sampleSize := 0
46 sampleRate := 0
47
48 for size := 8; len(data) >= size; data = data[size:] {
49 if len(data) < 8 {
50 return seconds, errTruncatedAIFF
51 }
52 size = int(bytes2uint(data[4:8])) + 8
53 if len(data) < size {
54 return seconds, errTruncatedAIFF
55 }
56
57 switch id := string(data[:4]); id {
58 case "FORM":
59 if len(data) < 12 || !match4(data[8:12], 'A', 'I', 'F', 'F') {
60 return math.NaN(), errTruncatedAIFF
61 }
62 size = 12
63
64 case "COMM":
65 if len(data) < 25 {
66 return math.NaN(), errTruncatedAIFF
67 }
68 numChan = int(bytes2uint(data[8:10]))
69 sampleSize = int(bytes2uint(data[14:16])) / 8
70 sampleRate = int(float80(data[16:26]))
71
72 case "SSND":
73 if len(data) < 12 {
74 return math.NaN(), errTruncatedAIFF
75 }
76 offset := int(bytes2uint(data[8:12]))
77 n := (size - offset - 4) / (sampleSize * numChan)
78 seconds += float64(n) / float64(sampleRate)
79 }
80 }
81
82 return seconds, nil
83 }
File: ./mediainfo/au.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 "math"
32 )
33
34 var (
35 errInvalidAuData = errors.New("invalid AU data")
36 errUnsupportedAuEncoding = errors.New("unsupported AU data encoding")
37 )
38
39 type auHeader struct {
40 Magic uint32 // ".snd" if data are valid
41 Offset uint32
42 Size uint32
43 Encoding uint32
44 SampleRate uint32
45 Channels uint32
46 }
47
48 func auDuration(r io.Reader, n int) (seconds float64, err error) {
49 var header auHeader
50 err = binary.Read(r, binary.BigEndian, &header)
51 if err != nil {
52 return 0, err
53 }
54
55 // check if first 4 bytes are ".snd" in ascii
56 if header.Magic != 0x2e736e64 {
57 return math.NaN(), errInvalidAuData
58 }
59
60 // find how many bytes each sample takes
61 itemSize := 0
62 switch header.Encoding {
63 case 2:
64 itemSize = 1
65 case 3:
66 itemSize = 2
67 case 4:
68 itemSize = 3
69 case 5, 6:
70 itemSize = 4
71 case 7:
72 itemSize = 8
73 default:
74 return math.NaN(), errUnsupportedAuEncoding
75 }
76
77 rate := header.SampleRate * header.Channels * uint32(itemSize)
78 // if header has an unknown data size, calculate it from the file size
79 if header.Size == 0xffffffff {
80 // the au file header is 24 bytes
81 return float64(n-int(header.Offset)-24) / float64(rate), nil
82 }
83 return float64(header.Size) / float64(rate), nil
84 }
File: ./mediainfo/avi.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "bytes"
29 "encoding/binary"
30 "io"
31 "math"
32 )
33
34 // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/api/Aviriff/ns-aviriff-avimainheader
35 type aviMainHeader struct {
36 Type [4]byte // "avih"
37 Size uint32 // structure size minus 8
38 MicroSecPerFrame uint32
39 MaxBytesPerSec uint32
40 PaddingGranularity uint32
41 Flags uint32
42 TotalFrames uint32
43 InitialFrames uint32
44 Streams uint32
45 SuggestedBufferSize uint32
46 Width uint32
47 Height uint32
48 Reserved [4]uint32
49 }
50
51 // The stream header chunk ('strh') consists of an AVISTREAMHEADER structure
52 // Note: there seem to be 4 more bytes in between "strh" and the start of the struct
53 // https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference
54 // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/api/avifmt/ns-avifmt-avistreamheader
55 type aviStreamHeader struct {
56 Type [4]byte // either "vids" or "auds"
57 Handler [4]byte
58 Flags uint32
59 Priority uint16
60 Language uint16
61 InitialFrames uint32
62
63 Scale uint32
64 Rate uint32
65 Start uint32
66 Length uint32
67
68 SugBufferSize uint32 // suggested buffer size
69 Quality uint32
70 SampleSize uint32
71
72 // `frame info` data are supposed to follow, whatever those are
73 }
74
75 func aviDuration(r io.Reader) (seconds float64, err error) {
76 buf := make([]byte, 2048)
77 n, err := r.Read(buf)
78 if err != io.EOF && err != nil {
79 return math.NaN(), err
80 }
81
82 sec := 0.0
83 buf = buf[:n]
84 for {
85 i := bytes.Index(buf, []byte{'s', 't', 'r', 'h'})
86 if i < 0 {
87 break
88 }
89 i += 8
90 buf = buf[i:]
91
92 dur, err := aviStreamDuration(buf)
93 if err != nil {
94 break
95 }
96 if math.IsNaN(dur) {
97 continue
98 }
99 sec = math.Max(sec, dur)
100 }
101
102 if sec == 0 && n > 0 {
103 return math.NaN(), nil
104 }
105 return sec, nil
106 }
107
108 func aviStreamDuration(data []byte) (seconds float64, err error) {
109 var sh aviStreamHeader
110 r := bytes.NewReader(data)
111 err = binary.Read(r, binary.LittleEndian, &sh)
112 if err != nil {
113 return math.NaN(), err
114 }
115 return float64(sh.Length) * float64(sh.Scale) / float64(sh.Rate), nil
116 }
117
118 func aviResolution(r io.Reader) (int, int, int, error) {
119 buf := make([]byte, 2048)
120 n, err := r.Read(buf)
121 if err != io.EOF && err != nil {
122 return -1, -1, -1, err
123 }
124
125 buf = buf[:n]
126 i := bytes.Index(buf, []byte{'a', 'v', 'i', 'h'})
127 if i < 0 {
128 return -1, -1, -1, nil
129 }
130
131 var mh aviMainHeader
132 r = bytes.NewReader(buf[i:])
133 err = binary.Read(r, binary.LittleEndian, &mh)
134 if err != nil {
135 return -1, -1, -1, err
136 }
137
138 return int(mh.Width), int(mh.Height), -1, nil
139 }
File: ./mediainfo/bmp.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 )
32
33 var errUnsupportedBMPFormat = errors.New("unsupported BMP format")
34
35 type bmpHeader struct {
36 Type [2]byte
37 Size uint32
38 Reserved uint32
39 PixelArrayOffset uint32
40 InfoHeaderSize uint32
41 Width int32
42 Height int32
43 ColorPlanes uint16
44 BitsPerPixel uint16
45 }
46
47 func bmpResolution(r io.Reader) (int, int, int, error) {
48 var header bmpHeader
49 err := binary.Read(r, binary.LittleEndian, &header)
50 if err != nil {
51 return 0, 0, 0, err
52 }
53 // only windows bitmaps are supported
54 if header.Type[0] != 'B' || header.Type[1] != 'M' {
55 return 0, 0, 0, errUnsupportedBMPFormat
56 }
57 return int(header.Width), int(header.Height), int(header.BitsPerPixel), nil
58 }
File: ./mediainfo/doc.go
1 /*
2 # mediainfo
3
4 Package to extract all sorts of information from media (pics, audio, video)
5 files/data.
6
7 Right now it can find picture resolution for
8 - GIF
9 - PNG
10 - WEBP (only some of its variants)
11
12 and the play length in seconds for many common audio/video files, such as
13 - AAC
14 - AIFF
15 - AVI
16 - FLAC
17 - MP3
18 - MP4
19 - WAVE
20
21 Notably missing in the list of supported formats is JPEG.
22 */
23
24 package mediainfo
File: ./mediainfo/flac.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 "math"
32 )
33
34 // https://gist.github.com/lukasklein/8c474782ed66c7115e10904fecbed86a
35
36 var (
37 errShortFlacInfoStream = errors.New("not enough bytes in the stream-info block")
38 errInvalidFlacSampleRate = errors.New("invalid sample rate")
39 errNoFlacMarker = errors.New("data not marked with \"fLaC\"")
40 )
41
42 func flacDuration(r io.Reader) (seconds float64, err error) {
43 // check if file starts with a flac marker
44 var flac [4]byte
45 err = binary.Read(r, binary.BigEndian, &flac)
46 if err == io.EOF {
47 return 0, nil
48 }
49 if err != nil {
50 return 0, err
51 }
52 if flac[0] != 'f' || flac[1] != 'L' || flac[2] != 'a' || flac[3] != 'C' {
53 return 0, errNoFlacMarker
54 }
55
56 for {
57 // read block-marker and block-size packed in 4 bytes
58 var meta [4]byte
59 err := binary.Read(r, binary.BigEndian, &meta)
60 if err == io.EOF {
61 return 0, nil
62 }
63 if err != nil {
64 return 0, err
65 }
66
67 blockType := meta[0] & 0x7f
68 size := bytes2uint(meta[1:4])
69 // block-type 0 means it's a stream-info block
70 if blockType == 0 {
71 info := make([]byte, size)
72 err := binary.Read(r, binary.BigEndian, &info)
73 if err == io.EOF {
74 return 0, nil
75 }
76 if err != nil {
77 return 0, err
78 }
79
80 // https://xiph.org/flac/format.html#metadata_block_streaminfo
81 if len(info) < 18 {
82 return math.NaN(), errShortFlacInfoStream
83 }
84 sr := bytes2uint(info[10:13]) >> 4 // lowest bits are metadata unrelated to sample rate
85 n := bytes2uint([]byte{info[13] & 0x0f, info[14], info[15], info[16], info[17]})
86 if sr == 0 {
87 return math.NaN(), errInvalidFlacSampleRate
88 }
89 return float64(n) / float64(sr), nil
90 }
91 }
92 }
File: ./mediainfo/gif.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 )
32
33 var errInvalidGIFSignature = errors.New("invalid GIF signature")
34
35 // http://www33146ue.sakura.ne.jp/staff/iz/formats/gif.html
36 // global color table length is determined by the bits of its related info field
37 // when top bit is 0 it's 0, else it's 2 to the (lowest 3 bits + 1)
38 type gifHeader struct {
39 Signature [6]byte // "GIF87a" or "GIF89a"
40 LogicalScreenWidth uint16
41 LogicalScreenHeight uint16
42 GlobalColorTableInfo byte
43 BackgroundColorIndex byte
44 PixelAspectRatio byte
45 // GlobalColorTable: variable-length RGB array
46 }
47
48 func gifResolution(r io.ReadSeeker) (int, int, int, error) {
49 var header gifHeader
50 err := binary.Read(r, binary.LittleEndian, &header)
51 if err != nil {
52 return 0, 0, 0, err
53 }
54
55 if !gifSignatureIsValid(header.Signature) {
56 return 0, 0, 0, errInvalidGIFSignature
57 }
58 return int(header.LogicalScreenWidth), int(header.LogicalScreenHeight), 8, nil
59 }
60
61 func gifSignatureIsValid(s [6]byte) bool {
62 // valid GIF data must start either with "GIF87a" or "GIF89a"
63 if s[0] != 'G' || s[1] != 'I' || s[2] != 'F' {
64 return false
65 }
66 if s[3] != '8' || (s[4] != '7' && s[4] != '9') || s[5] != 'a' {
67 return false
68 }
69 return true
70 }
File: ./mediainfo/heic.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "bytes"
29 "io"
30 )
31
32 func heicResolution(r io.Reader) (int, int, int, error) {
33 var buf [4 * 1024]byte
34 n, err := r.Read(buf[:])
35 data := buf[:n]
36
37 // seek the 1st `ispe` marker
38 i := bytes.Index(data, []byte("ispe"))
39 if i < 0 {
40 return -1, -1, -1, ErrResolutionNotFound
41 }
42
43 // seek the 2nd `ispe` marker
44 data = data[i+len("ispe"):]
45 i = bytes.Index(data, []byte("ispe"))
46 if i < 0 {
47 return -1, -1, -1, ErrResolutionNotFound
48 }
49
50 data = data[i:]
51 if len(data) < 16 {
52 return -1, -1, -1, ErrResolutionNotFound
53 }
54
55 // width starts 8 bytes after the 2nd `ispe` marker and is big-endian
56 data = data[8:]
57 width := 16_777_216 * int(data[0])
58 width += 65_536 * int(data[1])
59 width += 256 * int(data[2])
60 width += int(data[3])
61
62 // height starts 4 bytes after the width and is big-endian
63 data = data[4:]
64 height := 16_777_216 * int(data[0])
65 height += 65_536 * int(data[1])
66 height += 256 * int(data[2])
67 height += int(data[3])
68
69 // bits-per-pixel are unknown, unless `pixi` metadata are found next
70 bpp := -1
71
72 // seek the `pixi` marker after the `ispe` markers
73 if i := bytes.Index(data, []byte("pixi")); i >= 0 {
74 if data := data[i+len("pixi"):]; len(data) >= 8 {
75 // color-depth info starts 4 bytes after the `pixi` marker
76 data = data[4:]
77
78 // get the number of color channels/components
79 n := data[0]
80 data = data[1:]
81
82 // add the bits-per-pixel count for each color channel/component
83 bpp = 0
84 for i := 0; i < int(n) && len(data) > 0; i++ {
85 bpp += int(data[0])
86 data = data[1:]
87 }
88 }
89 }
90
91 if err == io.EOF {
92 err = nil
93 }
94 return width, height, bpp, err
95 }
File: ./mediainfo/ico.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "encoding/binary"
29 "io"
30 )
31
32 // https://en.wikipedia.org/wiki/ICO_(file_format)
33
34 const (
35 IconDirTypeIcon = 1
36 IconDirTypeCursor = 2
37 )
38
39 type IconDir struct {
40 Reserved uint16 // must be 0
41 Type uint16 // 1 means icon, 2 means cursor
42 NumPics uint16 // how many differently-resoluted pics are available
43 }
44
45 type IconDirEntry struct {
46 Width uint8 // image width in pixels: 0 means 256
47 Height uint8 // image height in pixels: 0 means 256
48
49 ColorCount uint8
50 Reserved uint8
51
52 Extra1 uint16 // color palettes for icons, horizontal hotspot position for cursors
53 Extra2 uint16 // bits-per-pixel for icons, vertical hotspot position for cursors
54
55 Size uint32 // how many bytes image data use
56 Offset uint32 // where image bytes start from the beginning of the stream
57 }
58
59 func icoResolution(r io.ReadSeeker) (int, int, int, error) {
60 var header IconDir
61 err := binary.Read(r, binary.LittleEndian, &header)
62 if err != nil {
63 return 0, 0, 0, err
64 }
65
66 width := 0
67 height := 0
68 maxbpp := 8
69 for i := 0; i < int(header.NumPics); i++ {
70 var e IconDirEntry
71 err = binary.Read(r, binary.LittleEndian, &e)
72 if err == io.EOF {
73 return width, height, maxbpp, nil
74 }
75 if err != nil {
76 return 0, 0, 0, err
77 }
78
79 w := int(e.Width)
80 // 0 width means 256
81 if w == 0 {
82 w = 256
83 }
84 h := int(e.Height)
85 // 0 height means 256
86 if h == 0 {
87 h = 256
88 }
89 bpp := 8
90 if e.ColorCount == 0 {
91 bpp = int(e.Extra2)
92 }
93
94 // keep track of max width, height, and bpp
95 if w > width {
96 width = w
97 }
98 if h > height {
99 height = h
100 }
101 if bpp > maxbpp {
102 maxbpp = bpp
103 }
104 }
105
106 return width, height, maxbpp, nil
107 }
File: ./mediainfo/id3v2.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "bytes"
29 "errors"
30 "io"
31 )
32
33 // calcSizeID3v2 finds the ID3v2 size in bytes from the ID3v2 header given; if
34 // the slice given isn't a valid/complete ID3v2 header, the result is 0
35 func calcSizeID3v2(b []byte) int {
36 if len(b) >= 10 && bytes.HasPrefix(b, []byte{'I', 'D', '3'}) {
37 n := 0
38 // each byte has top bit 0, and the other 7 bits as payload: the 4
39 // bytes thus result in a 28-bit value
40 n += int(b[6]) * 128 * 128 * 128
41 n += int(b[7]) * 128 * 128
42 n += int(b[8]) * 128
43 n += int(b[9])
44 return n
45 }
46 return 0
47 }
48
49 func CopyThumbnailMP3(w io.Writer, r io.Reader) (mimetype string, err error) {
50 const bufsize = 128 * 1024
51 var buf [bufsize]byte
52
53 for {
54 n, err := r.Read(buf[:])
55 data := buf[:n]
56
57 if i := bytes.Index(data, []byte{'A', 'P', 'I', 'C'}); i >= 0 {
58 return handleAPIC(w, r, data[i+len("APIC"):])
59 }
60
61 if err == io.EOF {
62 return mimetype, errors.New("no thumbnail found")
63 }
64 if err != nil {
65 return mimetype, err
66 }
67 }
68 }
69
70 func handleAPIC(w io.Writer, r io.Reader, data []byte) (mimetype string, err error) {
71 const bufsize = 128 * 1024
72 var buf [bufsize]byte
73
74 if len(data) < 4 {
75 const msg = "failed to detect thumbnail-payload size"
76 return "", errors.New(msg)
77 }
78
79 size := 0
80 // section-size seems stored as 4 little-endian bytes
81 size += int(data[3]) * 128 * 128 * 128
82 size += int(data[2]) * 128 * 128
83 size += int(data[1]) * 128
84 size += int(data[0])
85
86 i, j := findThumbnailMIME(data)
87 if i < 0 {
88 const msg = "failed to sync to start of thumbnail data"
89 return mimetype, errors.New(msg)
90 }
91
92 mimetype = string(data[i:j])
93 data = data[j:]
94 size -= j
95 if len(data) < 2 {
96 n, _ := r.Read(buf[:])
97 data = buf[:n]
98 }
99 data = data[2:]
100 size -= 2
101 if i := bytes.IndexByte(data, 0); i >= 0 {
102 data = data[i+1:]
103 size -= i + 1
104 } else {
105 const msg = "failed to sync to comment before thumbnail"
106 return mimetype, errors.New(msg)
107 }
108
109 start := bytes.NewReader(data)
110 rest := io.LimitReader(r, int64(size-len(data)))
111 _, err = io.Copy(w, io.MultiReader(start, rest))
112 return mimetype, err
113 }
114
115 func findThumbnailMIME(data []byte) (start int, stop int) {
116 if i := bytes.Index(data, []byte("image/")); i >= 0 {
117 if j := bytes.IndexByte(data[i:], 0); j >= 0 {
118 return i, i + j
119 }
120 }
121 return -1, -1
122 }
File: ./mediainfo/jpeg.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "bytes"
29 "encoding/binary"
30 "io"
31 )
32
33 // https://www.media.mit.edu/pia/Research/deepview/exif.html
34
35 // https://en.wikipedia.org/wiki/JPEG#Syntax_and_structure
36
37 func jpegResolution(r io.Reader) (int, int, int, error) {
38 var data [1024 * 8]byte
39 n, err := r.Read(data[:])
40 if err != nil {
41 return 0, 0, 0, err
42 }
43 buf := data[:n]
44
45 if bytes.Contains(buf, []byte{'J', 'F', 'I', 'F'}) {
46 return jpegResolutionJFIF(buf, binary.BigEndian)
47 }
48 if bytes.Contains(buf, []byte{'E', 'x', 'i', 'f', 0, 0, 'I', 'I'}) {
49 return jpegResolutionEXIF(buf, binary.LittleEndian)
50 }
51 if bytes.Contains(buf, []byte{'E', 'x', 'i', 'f', 0, 0, 'M', 'M'}) {
52 return jpegResolutionEXIF(buf, binary.BigEndian)
53 }
54 return jpegResolutionJFIF(buf, binary.BigEndian)
55 }
56
57 func jpegResolutionEXIF(data []byte, order binary.ByteOrder) (int, int, int, error) {
58 var imageWidth [2]byte = [2]byte{0xa0, 0x02}
59 var imageHeight [2]byte = [2]byte{0xa0, 0x03}
60 if order == binary.LittleEndian {
61 imageWidth[0], imageWidth[1] = imageWidth[1], imageWidth[0]
62 imageHeight[0], imageHeight[1] = imageHeight[1], imageHeight[0]
63 }
64
65 var w, h int
66 for sub := data; len(sub) > 0; {
67 wt, ht, rest := jpegWidthHeightEXIF(sub, order, imageWidth, imageHeight)
68 if wt > w {
69 w = wt
70 }
71 if ht > h {
72 h = ht
73 }
74 sub = rest
75 }
76
77 // if resolution not found in EXIF metadata, try as a JFIF: sometimes it works
78 if w == 0 && h == 0 {
79 // return jpegResolutionJFIF(buf, order)
80 return jpegResolutionJFIF(data, binary.BigEndian)
81 }
82 return int(w), int(h), -1, nil
83 }
84
85 func jpegWidthHeightEXIF(data []byte, order binary.ByteOrder, imageWidth, imageHeight [2]byte) (int, int, []byte) {
86 var w, h uint16
87 const markerLen = len(imageWidth)
88
89 if i := bytes.Index(data, imageWidth[:]); i >= 0 && i+markerLen+2 < len(data) {
90 data = data[i+markerLen:]
91 offset := order.Uint16(data)
92 i = 2 + int(offset)
93 if i+2 < len(data) {
94 data = data[i:]
95 w = order.Uint16(data)
96 }
97 } else {
98 return -1, -1, nil
99 }
100
101 if i := bytes.Index(data, imageHeight[:]); i >= 0 && i+markerLen+2 < len(data) {
102 data = data[i+markerLen:]
103 offset := order.Uint16(data)
104 i = 2 + int(offset)
105 if i+2 < len(data) {
106 data = data[i:]
107 h = order.Uint16(data)
108 }
109 } else {
110 return int(w), -1, nil
111 }
112
113 return int(w), int(h), data
114 }
115
116 func jpegResolutionJFIF(buf []byte, order binary.ByteOrder) (int, int, int, error) {
117 var i int
118 // start of frame baseline-DCT
119 i = bytes.Index(buf, []byte{0xff, 0xc0, 0x00, 0x11, 0x08})
120 if i >= 0 && i+5+2*2 < len(buf) {
121 h := order.Uint16(buf[i+5:])
122 w := order.Uint16(buf[i+7:])
123 return int(w), int(h), -1, nil
124 }
125 // start of frame progressive-DCT
126 i = bytes.Index(buf, []byte{0xff, 0xc2, 0x00, 0x11, 0x08})
127 if i >= 0 && i+5+2*2 < len(buf) {
128 h := order.Uint16(buf[i+5:])
129 w := order.Uint16(buf[i+7:])
130 return int(w), int(h), -1, nil
131 }
132 return 0, 0, 0, ErrResolutionNotFound
133 }
File: ./mediainfo/mediainfo.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "errors"
29 "io"
30 "math"
31 "os"
32 "path/filepath"
33 "strings"
34 )
35
36 var (
37 ErrUnsupportedFormat = errors.New("not a supported media format")
38 ErrNotPlayableMedia = errors.New("not a playable media format")
39 ErrNotPicture = errors.New("not a supported picture format")
40
41 ErrDurationNotFound = errors.New("duration not found")
42 ErrResolutionNotFound = errors.New("resolution not found")
43
44 errNotSeeker = errors.New("a reader which could also seek was needed")
45 )
46
47 // Duration returns the time duration of the stream given, assuming if it's audio/video
48 func Duration(r io.Reader, n int, typeHint string) (seconds float64, err error) {
49 switch normalizeTypeHint(typeHint) {
50 case "aiff", "aif", "snd", "AIFF", "AIF", "SND":
51 return aiffDuration(r)
52 case "au", "AU":
53 return auDuration(r, n)
54 case "flac", "x-flac", "FLAC", "X-FLAC": // the flac MIME-type is audio/x-flac
55 return flacDuration(r)
56 case "mp3", "MP3", "mpeg", "MPEG": // the mp3 MIME-type is audio/mpeg
57 rs, ok := r.(io.ReadSeeker)
58 if !ok {
59 return math.NaN(), errNotSeeker
60 }
61 return mp3Duration(rs)
62 case
63 "aac", "m4a", "m4b", "mp4", "m4v", "mov", "3gp",
64 "AAC", "M4A", "M4B", "MP4", "M4V", "MOV", "3GP":
65 rs, ok := r.(io.ReadSeeker)
66 if !ok {
67 return math.NaN(), errNotSeeker
68 }
69 return mpeg4Duration(rs)
70 case "wav", "x-wav", "WAV", "X-WAV": // the wav MIME-type is audio/x-wav
71 return waveDuration(r)
72 case "avi", "AVI":
73 return aviDuration(r)
74 case "webm", "WEBM":
75 return webmDuration(r)
76 case "aifc", "wma", "AIFC", "WMA":
77 return math.NaN(), ErrUnsupportedFormat
78 case "mkv", "MKV":
79 // only works if it's a WEBM from youtube
80 return webmDuration(r)
81 case "wmv", "divx", "mpg", "ogg", "opus":
82 return math.NaN(), ErrUnsupportedFormat
83 case "WMV", "DIVX", "MPG", "OGG", "OPUS":
84 return math.NaN(), ErrUnsupportedFormat
85 default:
86 return math.NaN(), ErrNotPlayableMedia
87 }
88 }
89
90 // FileDuration returns the time duration of the file given, assuming if it's audio/video,
91 // otherwise it's NaN: the reading position must be at the beginning before calling this function
92 func FileDuration(f *os.File) (seconds float64, err error) {
93 n := 0
94 info, err := f.Stat()
95 if err == nil {
96 n = int(info.Size())
97 }
98 return Duration(f, n, f.Name())
99 }
100
101 // Resolution returns the size of the file given, assuming it's a picture
102 func Resolution(r io.ReadSeeker, typeHint string) (w int, h int, bitDepth int, err error) {
103 switch normalizeTypeHint(typeHint) {
104 case "mp4", "MP4":
105 return mpeg4Resolution(r)
106 case "avi", "AVI":
107 return aviResolution(r)
108 case "png", "PNG":
109 return pngResolution(r)
110 case "jpeg", "jpg", "JPEG", "JPG":
111 return jpegResolution(r)
112 case "heic", "HEIC":
113 return heicResolution(r)
114 case "gif", "GIF":
115 return gifResolution(r)
116 case "bmp", "BMP":
117 return bmpResolution(r)
118 case "webp", "WEBP":
119 return webpResolution(r)
120 case "svg", "SVG":
121 return svgResolution(r)
122 case "psd", "PSD":
123 return psdResolution(r)
124 case "tiff", "TIFF", "tif", "TIF":
125 return tiffResolution(r)
126 case "ico", "cur", "ICO", "CUR":
127 return icoResolution(r)
128 case "tga", "jp2", "TGA", "JP2":
129 return 0, 0, 0, ErrUnsupportedFormat
130 default:
131 return 0, 0, 0, ErrNotPicture
132 }
133 }
134
135 func normalizeTypeHint(s string) string {
136 // use the file extension if given a filename, ignoring leading dots
137 if ext := filepath.Ext(s); ext != "" {
138 return strings.TrimPrefix(ext, ".")
139 }
140
141 // trim major MIME types
142 if i := strings.LastIndex(s, "/"); i >= 0 {
143 s = s[i+1:]
144 }
145 // trim charset type, which is preceded by a semicolon in MIME types
146 if i := strings.LastIndex(s, ";"); i >= 0 {
147 s = s[:i]
148 }
149 return s
150 }
File: ./mediainfo/mp3.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 /*
28 The MIT License (MIT)
29
30 Copyright (c) 2026 pacman64
31
32 Permission is hereby granted, free of charge, to any person obtaining a copy of
33 this software and associated documentation files (the "Software"), to deal
34 in the Software without restriction, including without limitation the rights to
35 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
36 of the Software, and to permit persons to whom the Software is furnished to do
37 so, subject to the following conditions:
38
39 The above copyright notice and this permission notice shall be included in all
40 copies or substantial portions of the Software.
41
42 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
43 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
44 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
45 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
46 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
47 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
48 SOFTWARE.
49 */
50
51 import (
52 "errors"
53 "io"
54 "math"
55 )
56
57 // http://www.mp3-tech.org/programmer/frame_header.html
58
59 /*
60 aaaaaaaaaaa bb cc d eeee ff g h ii jj k l mm
61 11111111111 00 00 0 0000 00 0 0 00 00 0 0 00 frame-sync mask
62 00000000000 11 00 0 0000 00 0 0 00 00 0 0 00 audio version mask
63 00000000000 00 11 0 0000 00 0 0 00 00 0 0 00 audio layer mask
64 00000000000 00 00 1 0000 00 0 0 00 00 0 0 00 CRC check mask
65 00000000000 00 00 0 1111 00 0 0 00 00 0 0 00 bit-rate index mask
66 00000000000 00 00 0 0000 11 0 0 00 00 0 0 00 sample-rate index mask
67 00000000000 00 00 0 0000 00 1 0 00 00 0 0 00 frame-padding check mask
68
69 aaaaaaaa aaabbccd eeeeffgh iijjklmm
70 */
71
72 // these errors are for the rarely-used reserved options in frame headers
73 var (
74 ErrMP3ReservedLayer = errors.New("MP3 data use reserved format layer")
75 ErrMP3ReservedVersion = errors.New("MP3 data use reserved format versions")
76 )
77
78 var mp3BitRates = []int{
79 0, 0, 0, 0, 0, 0, // free bit-rates
80 32000, 32000, 32000, 32000, 8000, 8000,
81 64000, 48000, 40000, 48000, 16000, 16000,
82 96000, 56000, 48000, 56000, 24000, 24000,
83 128000, 64000, 56000, 64000, 32000, 32000,
84 160000, 80000, 64000, 80000, 40000, 40000,
85 192000, 96000, 80000, 96000, 48000, 48000,
86 224000, 112000, 96000, 112000, 56000, 56000,
87 256000, 128000, 112000, 128000, 64000, 64000,
88 288000, 160000, 128000, 144000, 80000, 80000,
89 320000, 192000, 160000, 160000, 96000, 96000,
90 352000, 224000, 192000, 176000, 112000, 112000,
91 384000, 256000, 224000, 192000, 128000, 128000,
92 416000, 320000, 256000, 224000, 144000, 144000,
93 448000, 384000, 320000, 256000, 160000, 160000,
94 0, 0, 0, 0, 0, 0, // reserved (invalid) space for bit-rates
95 }
96
97 var mp3SampleRates = []int{
98 44100, 22050, 11025,
99 48000, 24000, 12000,
100 32000, 16000, 8000,
101 0, 0, 0,
102 }
103
104 // https://stackoverflow.com/questions/6220660/calculating-the-length-of-mp3-frames-in-milliseconds
105 var mp3SamplesPerFrame = []int{
106 384, 1152, 1152, // MPEG 1
107 384, 1152, 576, // MPEG 2
108 384, 1152, 576, // MPEG 2.5
109 }
110
111 // mp3Duration tries to find the duration in seconds of the MP3 stream given
112 func mp3Duration(r io.ReadSeeker) (seconds float64, err error) {
113 // buffers larger than 32kb don't seem to speed things up further
114 var b [32 * 1024]byte
115
116 // how many leading bytes to skip/ignore from current chunk
117 skip := 0
118
119 for i := 0; true; i++ {
120 n, err := r.Read(b[:])
121 if n <= 0 {
122 return seconds, nil
123 }
124 if err != nil && err != io.EOF {
125 return seconds, err
126 }
127
128 // only check for ID3v2 metadata on the first chunk read
129 if i == 0 && n >= 10 {
130 skip = calcSizeID3v2(b[:n])
131 }
132
133 // done, if there aren't enough data for a frame intro
134 if n < 3 {
135 return seconds, nil
136 }
137
138 // check whether whole chunk needs skipping, as unlikely as that is
139 if n < skip {
140 skip -= n
141 continue
142 }
143
144 dt, skipnext, err := mp3SliceDuration(b[skip:n])
145 if err != nil {
146 return seconds, err
147 }
148
149 skip = skipnext
150 seconds += dt
151 }
152
153 return seconds, nil
154 }
155
156 // mp3SliceDuration handles the slice logic for func mp3Duration
157 func mp3SliceDuration(b []byte) (sec float64, skip int, err error) {
158 // when there aren't enough data for a frame, the duration is 0 seconds
159 if len(b) < 3 {
160 return 0, 0, nil
161 }
162
163 // upper-limit for index is 2 less than length, since there's a 2-byte
164 // look-ahead in loop
165 for i := 0; i < len(b)-2; i++ {
166 // frames start with their first 11 bits all on
167 syn := b[i]
168 if syn != 255 {
169 // not all the first 8 bits are on: not a frame-sync
170 continue
171 }
172 h1 := b[i+1]
173 if h1 < 224 {
174 // not all the 3 extra bits are on: not a frame-sync
175 continue
176 }
177 h2 := b[i+2]
178
179 // check the audio layer number using the 3rd-last and 2nd-last bits
180 layer := 0
181 switch h1 & 0b00000110 {
182 case 0:
183 // return t, ErrMP3ReservedLayer
184 continue
185 case 2:
186 layer = 3
187 case 4:
188 layer = 2
189 case 6:
190 layer = 1
191 }
192
193 // check the MPEG version using the 5th-last and 4th-last bits:
194 // version 3 means MPEG 2.5
195 version := 0
196 switch h1 & 0b00011000 {
197 case 0: // MPEG 2.5
198 version = 3
199 case 8: // reserved (invalid) // 0b00001000
200 // return t, ErrMP3ReservedVersion
201 // ignore frames with a reserved-value version, instead of
202 // giving an error
203 continue
204 case 16: // MPEG 2 // 0b00010000
205 version = 2
206 case 24: // MPEG 1 // 0b00011000
207 version = 1
208 }
209
210 // check for frame padding using the 2nd-last bit
211 padding := 0
212 if h2&0b00000010 != 0 {
213 padding = 1
214 }
215
216 bitRateRow := int(h2 >> 4)
217 if bitRateRow == 0 || bitRateRow == 15 {
218 continue
219 }
220
221 sampleRateRow := int(h2 & 0b00001100 >> 2)
222 if sampleRateRow == 3 {
223 continue
224 }
225
226 bitRate := 0
227 switch version {
228 case 1:
229 bitRate = mp3BitRates[6*bitRateRow+layer-1]
230 case 2, 3:
231 bitRate = mp3BitRates[6*bitRateRow+2*layer-1]
232 }
233 sampleRate := mp3SampleRates[3*sampleRateRow+version-1]
234
235 // update time duration value
236 spf := mp3SamplesPerFrame[3*(version-1)+layer-1]
237 sec += float64(spf) / float64(sampleRate)
238
239 // calculate how many bytes to jump forward
240 //
241 // http://www.mp3-converter.com/mp3codec/frames.htm
242 // the formula suggested there seems wrong
243 // frame_size = 144 * bit_rate / (sample_rate + padding)
244 // and should instead be
245 // frame_size = floor(144 * bit_rate / sample_rate) + padding
246 n := int(math.Floor(float64(144*bitRate)/float64(sampleRate))) + padding
247
248 // handle skipping inside current slice
249 if i+n < len(b)-3 {
250 // jump ahead by n - 1 instead of n, since the loop already adds 1
251 i += n - 1
252 continue
253 }
254
255 // handle skipping beyond current slice
256 skip = n - (len(b) - i)
257 if skip < 0 {
258 skip = 0
259 }
260 return sec, skip, nil
261 }
262
263 return sec, 0, nil
264 }
File: ./mediainfo/mp3_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "io"
29 "math"
30 "os"
31 "testing"
32 )
33
34 func TestDurationMP3(t *testing.T) {
35 fname := `testdata/test.mp3`
36 f, err := os.Open(fname)
37 if os.IsNotExist(err) {
38 t.Skipf(`file %s not available: skipping test`, fname)
39 return
40 }
41 if err != nil {
42 t.Error(err.Error())
43 return
44 }
45 defer f.Close()
46
47 d, err := Duration(f, 0, ".mp3")
48 if err != nil {
49 t.Error(err.Error())
50 return
51 }
52
53 const exp = 10.187755
54 if math.Abs(d-exp) > 1e-6 {
55 const fs = "expected duration of %f seconds, but got %f instead"
56 t.Fatalf(fs, exp, d)
57 }
58 }
59
60 // BenchmarkDurationMP3 mainly tests how the buffer-size used in func
61 // mp3Duration affects performance
62 func BenchmarkDurationMP3(b *testing.B) {
63 fname := `testdata/test.mp3`
64 f, err := os.Open(fname)
65 if os.IsNotExist(err) {
66 b.Skipf(`file %s not available: skipping benchmark`, fname)
67 return
68 }
69 if err != nil {
70 b.Error(err.Error())
71 return
72 }
73 defer f.Close()
74
75 b.Run("mp3-duration", func(b *testing.B) {
76 f.Seek(0, io.SeekStart)
77 b.ResetTimer()
78 mp3Duration(f)
79 })
80 }
File: ./mediainfo/mpeg4.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "bytes"
29 "encoding/binary"
30 "io"
31 "math"
32 )
33
34 // For a general description of the mpeg4 container format see
35 // http://www.cimarronsystems.com/wp-content/uploads/2017/04/Elements-of-the-H.264-VideoAAC-Audio-MP4-Movie-v2_0.pdf
36 // especially pages 4 and 5 describing the "moov" chunk
37
38 // Details of the 9-item matrix are in section "Matrices" (page 199) from the official Quicktime Format spec
39 // https://developer.apple.com/standards/qtff-2001.pdf
40
41 type mpeg4ChunkHeader struct {
42 Size uint32
43 Type [4]byte
44 }
45
46 type mpeg4MOOVChunkInfo struct {
47 // what follows is the info section for the first track
48 mpeg4ChunkHeader
49
50 Version byte
51 Flags [3]byte
52
53 CreationTime uint32 // seconds since the start of 1904
54 ModificationTime uint32 // seconds since the start of 1904
55 TimeScale uint32 // number of time units per seconds
56 Duration uint32 // total play length in time units
57
58 PreferredRate uint32
59 PreferredVolume uint16
60 Reserved [10]byte // should all be 0s
61
62 // more fields which I don't care about
63 }
64
65 // this one comes right after a chunk header of type "trak"
66 type mpeg4TrackHeaderInfo struct {
67 mpeg4ChunkHeader // type is "tkhd"
68
69 Version byte
70 Flags [3]byte
71
72 CreationTime uint32
73 ModificationTime uint32
74 TrackID uint32
75 Reserved uint32
76 Duration uint32
77
78 ReservedZeros [8]byte // should all be 0s
79 Layer uint16
80 AlternativeGroup uint16
81 Matrix [9]uint32
82
83 TrackWidth uint32 // divide by 65,536 for video width
84 TrackHeight uint32 // divide by 65,536 for video height
85 }
86
87 func mpeg4Duration(r io.ReadSeeker) (float64, error) {
88 var h mpeg4ChunkHeader
89 for {
90 err := binary.Read(r, binary.BigEndian, &h)
91 if err == io.EOF {
92 return math.NaN(), nil
93 }
94 if err != nil {
95 return math.NaN(), err
96 }
97
98 if h.Type[0] == 'm' && h.Type[1] == 'o' && h.Type[2] == 'o' && h.Type[3] == 'v' {
99 var info mpeg4MOOVChunkInfo
100 err := binary.Read(r, binary.BigEndian, &info)
101 return float64(info.Duration) / float64(info.TimeScale), err
102 }
103 r.Seek(int64(h.Size)-8, io.SeekCurrent)
104 }
105 }
106
107 func mpeg4Resolution(r io.ReadSeeker) (int, int, int, error) {
108 buf := make([]byte, 2048)
109 n, err := r.Read(buf)
110 if err != nil {
111 return -1, -1, -1, err
112 }
113
114 buf = buf[:n]
115 i := bytes.Index(buf, []byte{'t', 'r', 'a', 'k'})
116 if i < 0 {
117 return -1, -1, -1, nil
118 }
119
120 i += 8 // skip over the "trak" chunk header
121 r = bytes.NewReader(buf[i:])
122 var info mpeg4TrackHeaderInfo
123 err = binary.Read(r, binary.BigEndian, &info)
124 return int(info.TrackWidth / 65_536), int(info.TrackHeight / 65_536), -1, err
125 }
File: ./mediainfo/png.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 )
32
33 var (
34 errInvalidPNGSignature = errors.New("invalid PNG signature")
35 errInvalidPNGColorType = errors.New("invalid PNG color type")
36 )
37
38 type pngHeader struct {
39 Signature uint64
40 Image struct {
41 ChunkLength uint32
42 ChunkType [4]byte
43 Width int32
44 Height int32
45 BitDepth byte
46 ColorType byte
47 CompressionMethod byte
48 FilterMethod byte
49 InterlaceMethod byte
50 }
51 }
52
53 func pngResolution(r io.Reader) (int, int, int, error) {
54 var header pngHeader
55 err := binary.Read(r, binary.BigEndian, &header)
56 if err != nil {
57 return 0, 0, 0, err
58 }
59 if header.Signature != 9894494448401390090 {
60 return 0, 0, 0, errInvalidPNGSignature
61 }
62
63 var bpp int
64 switch header.Image.ColorType {
65 case 0: // grayscale
66 bpp = int(1 * header.Image.BitDepth)
67 case 2: // truecolor
68 bpp = int(3 * header.Image.BitDepth)
69 case 3: // indexed
70 bpp = int(1 * header.Image.BitDepth)
71 case 4: // grayscale + alpha
72 bpp = int(2 * header.Image.BitDepth)
73 case 6: // truecolor + alpha
74 bpp = int(4 * header.Image.BitDepth)
75 default:
76 return 0, 0, 0, errInvalidPNGColorType
77 }
78 return int(header.Image.Width), int(header.Image.Height), bpp, nil
79 }
File: ./mediainfo/psd.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 )
32
33 // https://docs.fileformat.com/image/psd/
34
35 type psdHeader struct {
36 Signature [4]byte // "8BPS"
37 Version uint16 // always 1
38 Reserved [6]byte // all zero bits
39 NumChannels uint16 // range allowed is 1..56
40 Height int32 // range allowed is 1..30000
41 Width int32 // range allowed is 1..30000
42 Depth uint16 // bits per channel
43 ColorMode uint16
44 }
45
46 var errUnsupportedPSDFormat = errors.New("data doesn't start with PSD file signature")
47
48 func psdResolution(r io.Reader) (int, int, int, error) {
49 var h psdHeader
50 err := binary.Read(r, binary.BigEndian, &h)
51 if err != nil {
52 return 0, 0, 0, err
53 }
54 s := h.Signature
55 if s[0] != '8' || s[1] != 'B' || s[2] != 'P' || s[3] != 'S' {
56 return 0, 0, 0, errUnsupportedPSDFormat
57 }
58 return int(h.Width), int(h.Height), int(h.NumChannels * h.Depth), nil
59 }
File: ./mediainfo/read.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "math"
29 )
30
31 func bytes2uint(s []byte) uint {
32 total := uint(0)
33 for _, b := range s {
34 total <<= 8
35 total += uint(b)
36 }
37 return total
38 }
39
40 // https://www.onicos.com/staff/iz/formats/ieee.c
41
42 func float80(s []byte) float64 {
43 f := 0.0
44 expon := (int(s[0]&0x7F) << 8) | int(s[1]&0xFF)
45 hiMant := (int(s[2]&0xFF) << 24) | (int(s[3]&0xFF) << 16) | (int(s[4]&0xFF) << 8) | (int(s[5] & 0xFF))
46 loMant := (int(s[6]&0xFF) << 24) | (int(s[7]&0xFF) << 16) | (int(s[8]&0xFF) << 8) | (int(s[9] & 0xFF))
47 sign := 1.0
48 if s[0]&0x80 != 0 {
49 sign = -1.0
50 }
51
52 if expon == 0 && hiMant == 0 && loMant == 0 {
53 // floating-point can have value -0
54 return sign * 0
55 }
56
57 // detect Infinity or NaN
58 if expon == 0x7FFF {
59 f = math.Inf(1)
60 } else {
61 expon -= 16383
62 f = math.Ldexp(float64(hiMant), expon-31) + math.Ldexp(float64(loMant), expon-31-32)
63 }
64
65 return sign * f
66 }
67
68 func match4(buf []byte, a, b, c, d byte) bool {
69 return len(buf) >= 4 && buf[0] == a && buf[1] == b && buf[2] == c && buf[3] == d
70 }
File: ./mediainfo/svg.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "bytes"
29 "io"
30 "strconv"
31 )
32
33 func svgResolution(r io.Reader) (int, int, int, error) {
34 var data [1024]byte
35 n, err := r.Read(data[:])
36 if err != nil {
37 return -1, -1, -1, err
38 }
39
40 w := -1
41 h := -1
42 buf := data[:n]
43
44 if i := bytes.Index(buf, []byte("width=\"")); i >= 0 {
45 sub := buf[i+len("width=\""):]
46 if i = bytes.IndexRune(sub, '"'); i >= 0 {
47 if n, err := strconv.Atoi(string(sub[:i])); err == nil {
48 w = n
49 }
50 }
51 }
52
53 if i := bytes.Index(buf, []byte("height=\"")); i >= 0 {
54 sub := buf[i+len("height=\""):]
55 if i = bytes.IndexRune(sub, '"'); i >= 0 {
56 if n, err := strconv.Atoi(string(sub[:i])); err == nil {
57 h = n
58 }
59 }
60 }
61
62 return w, h, -1, nil
63 }
File: ./mediainfo/tiff.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "io"
29 )
30
31 func tiffResolution(r io.Reader) (int, int, int, error) {
32 return jpegResolution(r)
33 }
File: ./mediainfo/wav.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 "math"
32 "os"
33 )
34
35 var errFileNeeded = errors.New(
36 "a file was needed, since declared audio-data size exceeds 4GB",
37 )
38
39 type riffHeader struct {
40 Format [4]byte
41 Size uint32
42 Type [4]byte // "WAVE"
43 Chunk [4]byte // "fmt "
44 }
45
46 // http://www.topherlee.com/software/pcm-tut-wavformat.html
47 type riffWave32Info struct {
48 FormatLength uint32
49 Format uint16
50 Channels uint16
51 SampleRate uint32
52 BytesPerSecond uint32
53
54 HardToName uint16
55 BitsPerSample uint16
56
57 Data [4]byte // "data"
58 DataLength uint32
59 }
60
61 // https://tech.ebu.ch/docs/tech/tech3306v1_1.pdf pages 8 and 9
62 // type rf64WaveInfo struct {
63 // Neg1 uint32 // the 32-bite size field should be -1 in rf64 format
64 // Wave [4]byte // "WAVE"
65 // DS64 [4]byte // "ds64"
66
67 // _size uint32
68 // RIFFSize uint64
69 // DataSize uint64
70 // SampleCount uint64
71 // TableLength uint32
72 // Table uint32
73
74 // // variable-length area should follow here
75
76 // // riffWave32Info
77 // }
78
79 type waveFormat int
80
81 const (
82 riffWaveFormat = waveFormat(1)
83 rf64WaveFormat = waveFormat(2)
84 unknownWaveFormat = waveFormat(3)
85 )
86
87 func detectWaveType(h riffHeader) waveFormat {
88 t := h.Type
89 if t[0] != 'W' || t[1] != 'A' || t[2] != 'V' || t[3] != 'E' {
90 return unknownWaveFormat
91 }
92
93 c := h.Chunk
94 if c[0] != 'f' || c[1] != 'm' || c[2] != 't' || c[3] != ' ' {
95 return unknownWaveFormat
96 }
97
98 f := h.Format
99 if f[0] == 'R' && f[1] == 'I' && f[2] == 'F' && f[3] == 'F' {
100 return riffWaveFormat
101 }
102 if f[0] == 'B' && f[1] == 'W' && f[2] == '6' && f[3] == '4' {
103 // return rf64WaveFormat
104 return riffWaveFormat
105 }
106 return unknownWaveFormat
107 }
108
109 var errInvalidWave = errors.New("invalid wave audio format")
110
111 func waveDuration(r io.Reader) (seconds float64, err error) {
112 var h riffHeader
113 err = binary.Read(r, binary.LittleEndian, &h)
114 if err != nil {
115 return math.NaN(), err
116 }
117
118 switch detectWaveType(h) {
119 case riffWaveFormat:
120 return riffWave32Duration(r, h.Size)
121 case rf64WaveFormat:
122 return rf64WaveDuration(r)
123 default:
124 return math.NaN(), errInvalidWave
125 }
126 }
127
128 func riffWave32Duration(r io.Reader, size uint32) (seconds float64, err error) {
129 if size != ^uint32(0) {
130 return _riffWave32Duration(r, uint64(size))
131 }
132
133 // handle case where size is >= 4GB, using a file
134 f, ok := r.(*os.File)
135 if !ok {
136 return math.NaN(), errFileNeeded
137 }
138 st, err := f.Stat()
139 if err != nil {
140 return math.NaN(), err
141 }
142 return _riffWave32Duration(r, uint64(st.Size()))
143 }
144
145 func _riffWave32Duration(r io.Reader, size uint64) (seconds float64, err error) {
146 var h riffWave32Info
147 err = binary.Read(r, binary.LittleEndian, &h)
148 if err != nil {
149 return math.NaN(), err
150 }
151 dlen := uint64(h.DataLength)
152 n := math.Max(float64(size-dlen), float64(h.DataLength))
153 seconds = n / float64(h.BytesPerSecond)
154 return seconds, nil
155 }
156
157 func rf64WaveDuration(r io.Reader) (seconds float64, err error) {
158 _ = r
159 return math.NaN(), errInvalidWave
160 }
File: ./mediainfo/webm.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "bytes"
29 "io"
30 "math"
31 "strconv"
32 "strings"
33 )
34
35 // youtube, which is the dominant source/standard for webm files, gives string-format durations
36 func webmDuration(r io.Reader) (seconds float64, err error) {
37 buf := make([]byte, 2048) // 2 kb is more than enough to get all DURATIOND fields
38 n, err := r.Read(buf)
39 if err != io.EOF && err != nil {
40 return math.NaN(), err
41 }
42
43 sec := 0.0
44 buf = buf[:n]
45 // since there are 2 DURATIOND fields near the start of youtube webm files,
46 // keep the highest of these to get a more accurate result
47 for {
48 i := bytes.Index(buf, []byte{'D', 'U', 'R', 'A', 'T', 'I', 'O', 'N', 'D'})
49 if i < 0 {
50 break
51 }
52
53 start := i + len("DURATIOND")
54 buf = buf[start:]
55 dur := parseWEBMDurationJunk(buf)
56 sec = math.Max(sec, dur)
57 }
58
59 if sec == 0 && n > 0 {
60 return math.NaN(), nil
61 }
62 return sec, nil
63 }
64
65 func parseWEBMDurationJunk(data []byte) (seconds float64) {
66 // get starting index of duration string
67 start := 0
68 for i, b := range data {
69 if '0' <= b && b <= '9' {
70 start = i
71 break
72 }
73 }
74
75 numpieces := 0
76 dotsAvailable := 1
77 // get limit index of duration string
78 stop := 0
79 for i, b := range data[start:] {
80 if b == ':' {
81 numpieces++
82 continue
83 }
84 if '0' <= b && b <= '9' {
85 continue
86 }
87 if b == '.' && dotsAvailable > 0 {
88 dotsAvailable--
89 continue
90 }
91 stop = start + i
92 break
93 }
94
95 // don't even bother splitting if there aren't any `:`s to separate time fields
96 if numpieces == 0 {
97 return math.NaN()
98 }
99
100 sec := 0.0
101 value := 1.0
102 pieces := strings.Split(string(data[start:stop]), ":")
103 // no need to worry about fields beyond hours, since youtube limits duration to 12 hours
104 // https://support.google.com/youtube/answer/71673?co=GENIE.Platform%3DDesktop&hl=en
105 for i := len(pieces) - 1; i >= 0; i-- {
106 f, err := strconv.ParseFloat(pieces[i], 64)
107 if err != nil {
108 return math.NaN()
109 }
110 sec += value * f
111 value *= 60
112 }
113 return sec
114 }
File: ./mediainfo/webp.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 )
32
33 // https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification
34 type webpHeader struct {
35 Signature [4]byte // "RIFF"
36 BlockLength uint32
37 ContainerName [4]byte // "WEBP"
38 ChunkTag [4]byte // "VP8L", or "VP8X", or "VP8 "
39 Extra [8]byte // dunno what to call this field
40 Data [20]byte
41
42 // Info [4]byte // StreamLength uint32
43 // HeaderEnd byte // 0x2f
44 }
45
46 var errInvalidWEBPFormat = errors.New("invalid WEBP format")
47
48 func webpResolution(r io.Reader) (int, int, int, error) {
49 var header webpHeader
50 err := binary.Read(r, binary.LittleEndian, &header)
51 if err != nil {
52 return 0, 0, 0, err
53 }
54 if !webpHeaderIsValid(header) {
55 return 0, 0, 0, errInvalidWEBPFormat
56 }
57
58 // https://github.com/golang/image/blob/master/webp/decode.go
59 switch header.ChunkTag[3] {
60 case 'L':
61 return -1, -1, -1, ErrUnsupportedFormat
62
63 case 'X':
64 b := header.Data
65 width := int(uint32(b[0])|uint32(b[1])<<8|uint32(b[2])<<16) + 1
66 height := int(uint32(b[3])|uint32(b[4])<<8|uint32(b[5])<<16) + 1
67 bpp := -1
68 return width, height, bpp, nil
69
70 case ' ':
71 return -1, -1, -1, ErrUnsupportedFormat
72
73 default:
74 // webpHeaderIsValid should have prevented reaching this point
75 return -1, -1, -1, errInvalidWEBPFormat
76 }
77 }
78
79 func webpHeaderIsValid(h webpHeader) bool {
80 s := h.Signature
81 if s[0] != 'R' || s[1] != 'I' || s[2] != 'F' || s[3] != 'F' {
82 return false
83 }
84 n := h.ContainerName
85 if n[0] != 'W' || n[1] != 'E' || n[2] != 'B' || n[3] != 'P' {
86 return false
87 }
88 t := h.ChunkTag
89 if t[0] != 'V' || t[1] != 'P' || t[2] != '8' || (t[3] != 'L' && t[3] != 'X' && t[3] != ' ') {
90 return false
91 }
92 return true
93 }
File: ./mimetype/mimetype.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package mimetype
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "io/fs"
32 "os"
33 "path/filepath"
34
35 "../filetypes"
36 )
37
38 const info = `
39 mimetype [options...] [filenames...]
40
41 Try to auto-detect MIME-TYPEs for all the files given. If no files are given,
42 the standard input is used by default.
43
44 The options are, available both in single and double-dash versions
45
46 -h, -help show this help message
47 `
48
49 func Main() {
50 liveLines := true
51 args := os.Args[1:]
52
53 for len(args) > 0 {
54 switch args[0] {
55 case `-b`, `--b`, `-buffered`, `--buffered`:
56 liveLines = false
57 args = args[1:]
58 continue
59
60 case `-h`, `--h`, `-help`, `--help`:
61 os.Stdout.WriteString(info[1:])
62 return
63 }
64
65 break
66 }
67
68 if len(args) > 0 && args[0] == `--` {
69 args = args[1:]
70 }
71
72 if liveLines {
73 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
74 liveLines = false
75 }
76 }
77
78 paths, ok := findAllFiles(args)
79 if !ok {
80 os.Exit(1)
81 return
82 }
83
84 if err := run(paths, liveLines); err != nil && err != io.EOF {
85 showError(err)
86 os.Exit(1)
87 return
88 }
89 }
90
91 func showError(err error) {
92 os.Stderr.WriteString(`mimetype: `)
93 os.Stderr.WriteString(err.Error())
94 os.Stderr.WriteString("\n")
95 }
96
97 // findAllFiles can be given a mix of file/folder paths, finding all files
98 // recursively in folders, avoiding duplicates
99 func findAllFiles(paths []string) (files []string, success bool) {
100 walk := filepath.WalkDir
101 got := make(map[string]struct{})
102 success = true
103
104 for _, path := range paths {
105 if _, ok := got[path]; ok {
106 continue
107 }
108 got[path] = struct{}{}
109
110 // a dash means standard input
111 if path == `-` {
112 files = append(files, path)
113 continue
114 }
115
116 info, err := os.Stat(path)
117 if os.IsNotExist(err) {
118 // on windows, file-not-found messages may mention `CreateFile`,
119 // even when trying to open files in read-only mode
120 err = errors.New(`can't find file/folder named ` + path)
121 }
122
123 if err != nil {
124 showError(err)
125 success = false
126 continue
127 }
128
129 if !info.IsDir() {
130 files = append(files, path)
131 continue
132 }
133
134 err = walk(path, func(path string, info fs.DirEntry, err error) error {
135 path, err = filepath.Abs(path)
136 if err != nil {
137 showError(err)
138 success = false
139 return err
140 }
141
142 if _, ok := got[path]; ok {
143 if info.IsDir() {
144 return fs.SkipDir
145 }
146 return nil
147 }
148 got[path] = struct{}{}
149
150 if err != nil {
151 showError(err)
152 success = false
153 return err
154 }
155
156 if info.IsDir() {
157 return nil
158 }
159
160 files = append(files, path)
161 return nil
162 })
163
164 if err != nil {
165 showError(err)
166 success = false
167 }
168 }
169
170 return files, success
171 }
172
173 func run(paths []string, liveLines bool) error {
174 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
175 defer bw.Flush()
176
177 if _, err := bw.WriteString("name\tmime\n"); err != nil {
178 return io.EOF
179 }
180 if liveLines && bw.Flush() != nil {
181 return io.EOF
182 }
183
184 for _, path := range paths {
185 if err := handleFile(bw, path, liveLines); err != nil {
186 return err
187 }
188 }
189 if len(paths) == 0 {
190 return mimeType(bw, os.Stdin, `-`, liveLines)
191 }
192 return nil
193 }
194
195 func handleFile(w *bufio.Writer, path string, liveLines bool) error {
196 f, err := os.Open(path)
197 if err != nil {
198 return err
199 }
200 defer f.Close()
201 return mimeType(w, f, path, liveLines)
202 }
203
204 func mimeType(w *bufio.Writer, r io.Reader, path string, live bool) error {
205 var buf [64]byte
206 n, err := r.Read(buf[:])
207 if n < 1 && err != nil {
208 return err
209 }
210
211 if n == 0 {
212 const pre = `can't autodetect MIME-type for empty file named `
213 return errors.New(pre + path)
214 }
215
216 mime, ok := filetypes.DetectMIME(buf[:n])
217 if !ok {
218 const pre = `can't autodetect MIME-type for file named `
219 return errors.New(pre + path)
220 }
221
222 w.WriteString(path)
223 w.WriteByte('\t')
224 w.WriteString(mime)
225 if w.WriteByte('\n') != nil {
226 return io.EOF
227 }
228
229 if live && w.Flush() != nil {
230 return io.EOF
231 }
232 return nil
233 }
File: ./mit-license.txt
1 The MIT License (MIT)
2
3 Copyright (c) 2026 pacman64
4
5 Permission is hereby granted, free of charge, to any person obtaining a copy of
6 this software and associated documentation files (the "Software"), to deal
7 in the Software without restriction, including without limitation the rights to
8 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9 of the Software, and to permit persons to whom the Software is furnished to do
10 so, subject to the following conditions:
11
12 The above copyright notice and this permission notice shall be included in all
13 copies or substantial portions of the Software.
14
15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 SOFTWARE.
File: ./n/n.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package n
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "os"
32 "strconv"
33 "strings"
34 )
35
36 const info = `
37 n [options...] [start...] [filenames...]
38
39 Number lines starting from the (optional) line-count given, or starting to
40 count from 1 by default. Line counts and line contents are separated by a
41 tab.
42
43 The options are, available both in single and double-dash versions
44
45 -h, -help show this help message
46 `
47
48 type config struct {
49 n int
50 liveLines bool
51 }
52
53 func Main() {
54 var cfg config
55 cfg.n = 1
56 cfg.liveLines = true
57 args := os.Args[1:]
58
59 for len(args) > 0 {
60 switch args[0] {
61 case `-b`, `--b`, `-buffered`, `--buffered`:
62 cfg.liveLines = false
63 args = args[1:]
64 continue
65
66 case `-h`, `--h`, `-help`, `--help`:
67 os.Stdout.WriteString(info[1:])
68 return
69 }
70
71 break
72 }
73
74 if len(args) > 0 {
75 s := strings.Replace(args[0], `_`, ``, -1)
76 if v, err := strconv.ParseInt(s, 10, 64); err == nil {
77 cfg.n = int(v)
78 args = args[1:]
79 }
80 }
81
82 if len(args) > 0 && args[0] == `--` {
83 args = args[1:]
84 }
85
86 if cfg.liveLines {
87 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
88 cfg.liveLines = false
89 }
90 }
91
92 if err := run(args, &cfg); err != nil && err != io.EOF {
93 os.Stderr.WriteString(`n: `)
94 os.Stderr.WriteString(err.Error())
95 os.Stderr.WriteString("\n")
96 os.Exit(1)
97 return
98 }
99 }
100
101 func run(paths []string, cfg *config) error {
102 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
103 defer bw.Flush()
104
105 for _, p := range paths {
106 if err := handleFile(bw, p, cfg); err != nil {
107 return err
108 }
109 }
110
111 if len(paths) == 0 {
112 return handleReader(bw, os.Stdin, cfg)
113 }
114
115 return nil
116 }
117
118 func handleFile(w *bufio.Writer, path string, cfg *config) error {
119 f, err := os.Open(path)
120 if err != nil {
121 // on windows, file-not-found error messages may mention `CreateFile`,
122 // even when trying to open files in read-only mode
123 return errors.New(`can't open file named ` + path)
124 }
125 defer f.Close()
126 return handleReader(w, f, cfg)
127 }
128
129 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
130 var buf [24]byte
131 const gb = 1024 * 1024 * 1024
132 sc := bufio.NewScanner(r)
133 sc.Buffer(nil, 8*gb)
134
135 for i := 0; sc.Scan(); i++ {
136 s := sc.Text()
137 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
138 s = s[3:]
139 }
140
141 w.Write(strconv.AppendInt(buf[:0], int64(cfg.n), 10))
142 w.WriteByte('\t')
143 w.WriteString(s)
144 cfg.n++
145
146 if w.WriteByte('\n') != nil {
147 return io.EOF
148 }
149
150 if cfg.liveLines && w.Flush() != nil {
151 return io.EOF
152 }
153 }
154
155 return sc.Err()
156 }
File: ./ncol/ncol.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package ncol
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "os"
32 "strconv"
33 "strings"
34 "unicode/utf8"
35 )
36
37 const info = `
38 ncol [options...] [filenames...]
39
40
41 Nice COLumns realigns and styles data tables using ANSI color sequences. In
42 particular, all auto-detected numbers are styled so they're easier to read
43 at a glance. Input tables can be either lines of space-separated values or
44 tab-separated values, and are auto-detected using the first non-empty line.
45
46 When not given filepaths to read data from, this tool reads from standard
47 input by default.
48
49 The options are, available both in single and double-dash versions
50
51 -h, -help show this help message
52 -m, -max-columns use the row with the most items for the item-count
53 -no-sums, -unsummed avoid showing a final row with column sums
54 -no-tiles, -untiled avoid showing color-coded tiles
55 -s, -simple avoid showing color-coded tiles and sums
56 `
57
58 const columnGap = 2
59
60 // altDigitStyle is used to make 4+ digit-runs easier to read
61 const altDigitStyle = "\x1b[38;2;168;168;168m"
62
63 func Main() {
64 sums := true
65 tiles := true
66 maxCols := false
67 args := os.Args[1:]
68
69 for len(args) > 0 {
70 switch args[0] {
71 case `-h`, `--h`, `-help`, `--help`:
72 os.Stdout.WriteString(info[1:])
73 return
74
75 case
76 `-m`, `--m`,
77 `-maxcols`, `--maxcols`,
78 `-max-columns`, `--max-columns`:
79 maxCols = true
80 args = args[1:]
81 continue
82
83 case
84 `-no-sums`, `--no-sums`, `-no-totals`, `--no-totals`,
85 `-unsummed`, `--unsummed`, `-untotaled`, `--untotaled`,
86 `-untotalled`, `--untotalled`:
87 sums = false
88 args = args[1:]
89 continue
90
91 case `-no-tiles`, `--no-tiles`, `-untiled`, `--untiled`:
92 tiles = false
93 args = args[1:]
94 continue
95
96 case `-s`, `--s`, `-simple`, `--simple`:
97 sums = false
98 tiles = false
99 args = args[1:]
100 continue
101 }
102
103 break
104 }
105
106 if len(args) > 0 && args[0] == `--` {
107 args = args[1:]
108 }
109
110 var res table
111 res.MaxColumns = maxCols
112 res.ShowTiles = tiles
113 res.ShowSums = sums
114
115 if err := run(args, &res); err != nil {
116 os.Stderr.WriteString(`ncol: `)
117 os.Stderr.WriteString(err.Error())
118 os.Stderr.WriteString("\n")
119 os.Exit(1)
120 return
121 }
122 }
123
124 // table has all summary info gathered from the data, along with the row
125 // themselves, stored as lines/strings
126 type table struct {
127 Columns int
128
129 Rows []string
130
131 MaxWidth []int
132
133 MaxDotDecimals []int
134
135 Numeric []int
136
137 Sums []float64
138
139 LoopItems func(line string, items int, t *table, f itemFunc) int
140
141 sb strings.Builder
142
143 MaxColumns bool
144
145 ShowTiles bool
146
147 ShowSums bool
148 }
149
150 type itemFunc func(i int, s string, t *table)
151
152 func run(paths []string, res *table) error {
153 for _, p := range paths {
154 if err := handleFile(res, p); err != nil {
155 return err
156 }
157 }
158
159 if len(paths) == 0 {
160 if err := handleReader(res, os.Stdin); err != nil {
161 return err
162 }
163 }
164
165 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
166 defer bw.Flush()
167 realign(bw, res)
168 return nil
169 }
170
171 func handleFile(res *table, path string) error {
172 f, err := os.Open(path)
173 if err != nil {
174 // on windows, file-not-found error messages may mention `CreateFile`,
175 // even when trying to open files in read-only mode
176 return errors.New(`can't open file named ` + path)
177 }
178 defer f.Close()
179 return handleReader(res, f)
180 }
181
182 func handleReader(t *table, r io.Reader) error {
183 const gb = 1024 * 1024 * 1024
184 sc := bufio.NewScanner(r)
185 sc.Buffer(nil, 8*gb)
186
187 const maxInt = int(^uint(0) >> 1)
188 maxCols := maxInt
189
190 for i := 0; sc.Scan(); i++ {
191 s := sc.Text()
192 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
193 s = s[3:]
194 }
195
196 if len(s) == 0 {
197 continue
198 }
199
200 t.Rows = append(t.Rows, s)
201
202 if t.Columns == 0 {
203 if t.LoopItems == nil {
204 if strings.IndexByte(s, '\t') >= 0 {
205 t.LoopItems = loopItemsTSV
206 } else {
207 t.LoopItems = loopItemsSSV
208 }
209 }
210
211 if !t.MaxColumns {
212 t.Columns = t.LoopItems(s, maxCols, t, doNothing)
213 maxCols = t.Columns
214 }
215 }
216
217 t.LoopItems(s, maxCols, t, updateItem)
218 }
219
220 return sc.Err()
221 }
222
223 // doNothing is given to LoopItems to count items, while doing nothing else
224 func doNothing(i int, s string, t *table) {}
225
226 func updateItem(i int, s string, t *table) {
227 // ensure column-info-slices have enough room
228 if i >= len(t.MaxWidth) {
229 // update column-count if in max-columns mode
230 if t.MaxColumns {
231 t.Columns = i + 1
232 }
233 t.MaxWidth = append(t.MaxWidth, 0)
234 t.MaxDotDecimals = append(t.MaxDotDecimals, 0)
235 t.Numeric = append(t.Numeric, 0)
236 t.Sums = append(t.Sums, 0)
237 }
238
239 // keep track of widest rune-counts for each column
240 w := countWidth(s)
241 if t.MaxWidth[i] < w {
242 t.MaxWidth[i] = w
243 }
244
245 // update stats for numeric items
246 if isNumeric(s, &(t.sb)) {
247 dd := countDotDecimals(s)
248 if t.MaxDotDecimals[i] < dd {
249 t.MaxDotDecimals[i] = dd
250 }
251
252 t.Numeric[i]++
253 f, _ := strconv.ParseFloat(t.sb.String(), 64)
254 t.Sums[i] += f
255 }
256 }
257
258 // loopItemsSSV loops over a line's items, allocation-free style; when given
259 // empty strings, the callback func is never called
260 func loopItemsSSV(s string, max int, t *table, f itemFunc) int {
261 i := 0
262 s = trimTrailingSpaces(s)
263
264 for {
265 s = trimLeadingSpaces(s)
266 if len(s) == 0 {
267 return i
268 }
269
270 if i+1 == max {
271 f(i, s, t)
272 return i + 1
273 }
274
275 j := strings.IndexByte(s, ' ')
276 if j < 0 {
277 f(i, s, t)
278 return i + 1
279 }
280
281 f(i, s[:j], t)
282 s = s[j+1:]
283 i++
284 }
285 }
286
287 func trimLeadingSpaces(s string) string {
288 for len(s) > 0 && s[0] == ' ' {
289 s = s[1:]
290 }
291 return s
292 }
293
294 func trimTrailingSpaces(s string) string {
295 for len(s) > 0 && s[len(s)-1] == ' ' {
296 s = s[:len(s)-1]
297 }
298 return s
299 }
300
301 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
302 // when given empty strings, the callback func is never called
303 func loopItemsTSV(s string, max int, t *table, f itemFunc) int {
304 if len(s) == 0 {
305 return 0
306 }
307
308 i := 0
309
310 for {
311 if i+1 == max {
312 f(i, s, t)
313 return i + 1
314 }
315
316 j := strings.IndexByte(s, '\t')
317 if j < 0 {
318 f(i, s, t)
319 return i + 1
320 }
321
322 f(i, s[:j], t)
323 s = s[j+1:]
324 i++
325 }
326 }
327
328 func skipLeadingEscapeSequences(s string) string {
329 for len(s) >= 2 {
330 if s[0] != '\x1b' {
331 return s
332 }
333
334 switch s[1] {
335 case '[':
336 s = skipSingleLeadingANSI(s[2:])
337
338 case ']':
339 if len(s) < 3 || s[2] != '8' {
340 return s
341 }
342 s = skipSingleLeadingOSC(s[3:])
343
344 default:
345 return s
346 }
347 }
348
349 return s
350 }
351
352 func skipSingleLeadingANSI(s string) string {
353 for len(s) > 0 {
354 upper := s[0] &^ 32
355 s = s[1:]
356 if 'A' <= upper && upper <= 'Z' {
357 break
358 }
359 }
360
361 return s
362 }
363
364 func skipSingleLeadingOSC(s string) string {
365 var prev byte
366
367 for len(s) > 0 {
368 b := s[0]
369 s = s[1:]
370 if prev == '\x1b' && b == '\\' {
371 break
372 }
373 prev = b
374 }
375
376 return s
377 }
378
379 // isNumeric checks if a string is valid/useable as a number
380 func isNumeric(s string, sb *strings.Builder) bool {
381 if len(s) == 0 {
382 return false
383 }
384
385 sb.Reset()
386
387 s = skipLeadingEscapeSequences(s)
388 if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
389 sb.WriteByte(s[0])
390 s = s[1:]
391 }
392
393 s = skipLeadingEscapeSequences(s)
394 if len(s) == 0 {
395 return false
396 }
397 if b := s[0]; b == '.' {
398 sb.WriteByte(b)
399 return isDigits(s[1:], sb)
400 }
401
402 digits := 0
403
404 for {
405 s = skipLeadingEscapeSequences(s)
406 if len(s) == 0 {
407 break
408 }
409
410 b := s[0]
411 sb.WriteByte(b)
412
413 if b == '.' {
414 return isDigits(s[1:], sb)
415 }
416
417 if !('0' <= b && b <= '9') {
418 return false
419 }
420
421 digits++
422 s = s[1:]
423 }
424
425 s = skipLeadingEscapeSequences(s)
426 return len(s) == 0 && digits > 0
427 }
428
429 func isDigits(s string, sb *strings.Builder) bool {
430 if len(s) == 0 {
431 return false
432 }
433
434 digits := 0
435
436 for {
437 s = skipLeadingEscapeSequences(s)
438 if len(s) == 0 {
439 break
440 }
441
442 if b := s[0]; '0' <= b && b <= '9' {
443 sb.WriteByte(b)
444 s = s[1:]
445 digits++
446 } else {
447 return false
448 }
449 }
450
451 s = skipLeadingEscapeSequences(s)
452 return len(s) == 0 && digits > 0
453 }
454
455 // countDecimals counts decimal digits from the string given, assuming it
456 // represents a valid/useable float64, when parsed
457 func countDecimals(s string) int {
458 dot := strings.IndexByte(s, '.')
459 if dot < 0 {
460 return 0
461 }
462
463 decs := 0
464 s = s[dot+1:]
465
466 for len(s) > 0 {
467 s = skipLeadingEscapeSequences(s)
468 if len(s) == 0 {
469 break
470 }
471 if '0' <= s[0] && s[0] <= '9' {
472 decs++
473 }
474 s = s[1:]
475 }
476
477 return decs
478 }
479
480 // countDotDecimals is like func countDecimals, but this one also includes
481 // the dot, when any decimals are present, else the count stays at 0
482 func countDotDecimals(s string) int {
483 decs := countDecimals(s)
484 if decs > 0 {
485 return decs + 1
486 }
487 return decs
488 }
489
490 func countWidth(s string) int {
491 width := 0
492
493 for len(s) > 0 {
494 i, j := indexEscapeSequence(s)
495 if i < 0 {
496 break
497 }
498 if j < 0 {
499 j = len(s)
500 }
501
502 width += utf8.RuneCountInString(s[:i])
503 s = s[j:]
504 }
505
506 // count trailing/all runes in strings which don't end with ANSI-sequences
507 width += utf8.RuneCountInString(s)
508 return width
509 }
510
511 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
512 // the multi-byte sequences starting with ESC[; the result is a pair of slice
513 // indices which can be independently negative when either the start/end of
514 // a sequence isn't found; given their fairly-common use, even the hyperlink
515 // ESC]8 sequences are supported
516 func indexEscapeSequence(s string) (int, int) {
517 var prev byte
518
519 for i := range s {
520 b := s[i]
521
522 if prev == '\x1b' && b == '[' {
523 j := indexLetter(s[i+1:])
524 if j < 0 {
525 return i, -1
526 }
527 return i - 1, i + 1 + j + 1
528 }
529
530 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
531 j := indexPair(s[i+1:], '\x1b', '\\')
532 if j < 0 {
533 return i, -1
534 }
535 return i - 1, i + 1 + j + 2
536 }
537
538 prev = b
539 }
540
541 return -1, -1
542 }
543
544 func indexLetter(s string) int {
545 for i, b := range s {
546 upper := b &^ 32
547 if 'A' <= upper && upper <= 'Z' {
548 return i
549 }
550 }
551
552 return -1
553 }
554
555 func indexPair(s string, x byte, y byte) int {
556 var prev byte
557
558 for i := range s {
559 b := s[i]
560 if prev == x && b == y && i > 0 {
561 return i
562 }
563 prev = b
564 }
565
566 return -1
567 }
568
569 func realign(w *bufio.Writer, t *table) {
570 // make sums row first, as final alignments are usually affected by these
571 var sums []string
572 if t.ShowSums {
573 sums = make([]string, 0, t.Columns)
574
575 for i := 0; i < t.Columns; i++ {
576 if t.Numeric[i] == 0 {
577 sums = append(sums, `-`)
578 if t.MaxWidth[i] < 1 {
579 t.MaxWidth[i] = 1
580 }
581 continue
582 }
583
584 decs := t.MaxDotDecimals[i]
585 if decs > 0 {
586 decs--
587 }
588
589 var buf [64]byte
590 s := strconv.AppendFloat(buf[:0], t.Sums[i], 'f', decs, 64)
591 sums = append(sums, string(s))
592 if t.MaxWidth[i] < len(s) {
593 t.MaxWidth[i] = len(s)
594 }
595 }
596 }
597
598 // due keeps track of how many spaces are due, when separating realigned
599 // items from their immediate predecessor on the same row; this counter
600 // is also used to right-pad numbers with decimals, as such items can be
601 // padded with spaces from either side
602 due := 0
603
604 showItem := func(i int, s string, t *table) {
605 if i > 0 {
606 due += columnGap
607 }
608
609 if isNumeric(s, &(t.sb)) {
610 dd := countDotDecimals(s)
611 rpad := t.MaxDotDecimals[i] - dd
612 width := countWidth(s)
613 lpad := t.MaxWidth[i] - (width + rpad) + due
614 writeSpaces(w, lpad)
615 f, _ := strconv.ParseFloat(t.sb.String(), 64)
616 writeNumericItem(w, s, numericStyle(f))
617 due = rpad
618 return
619 }
620
621 writeSpaces(w, due)
622 w.WriteString(s)
623 due = t.MaxWidth[i] - countWidth(s)
624 }
625
626 writeTile := func(i int, s string, t *table) {
627 // make empty items stand out
628 if len(s) == 0 {
629 w.WriteString("\x1b[0m○")
630 return
631 }
632
633 if isNumeric(s, &(t.sb)) {
634 f, _ := strconv.ParseFloat(t.sb.String(), 64)
635 w.WriteString(numericStyle(f))
636 w.WriteString("■")
637 return
638 }
639
640 // make padded items stand out: these items have spaces at either end
641 if s[0] == ' ' || s[len(s)-1] == ' ' {
642 w.WriteString("\x1b[38;2;196;160;0m■")
643 return
644 }
645
646 w.WriteString("\x1b[38;2;128;128;128m■")
647 }
648
649 // show realigned rows
650
651 for _, line := range t.Rows {
652 due = 0
653
654 if t.ShowTiles {
655 end := t.LoopItems(line, t.Columns, t, writeTile)
656 if end < len(t.MaxWidth)-1 {
657 w.WriteString("\x1b[0m")
658 }
659 // make rows with missing trailing items stand out
660 for i := end; i < len(t.MaxWidth); i++ {
661 w.WriteString("×")
662 }
663 w.WriteString("\x1b[0m")
664 due += columnGap
665 }
666
667 t.LoopItems(line, t.Columns, t, showItem)
668 if w.WriteByte('\n') != nil {
669 return
670 }
671 }
672
673 if t.Columns > 0 && t.ShowSums {
674 realignSums(w, t, sums)
675 }
676 }
677
678 func realignSums(w *bufio.Writer, t *table, sums []string) {
679 due := 0
680 if t.ShowTiles {
681 due += t.Columns + columnGap
682 }
683
684 for i, s := range sums {
685 if i > 0 {
686 due += columnGap
687 }
688
689 if t.Numeric[i] == 0 {
690 writeSpaces(w, due)
691 w.WriteString(s)
692 due = t.MaxWidth[i] - countWidth(s)
693 continue
694 }
695
696 lpad := t.MaxWidth[i] - len(s) + due
697 writeSpaces(w, lpad)
698 writeNumericItem(w, s, numericStyle(t.Sums[i]))
699 due = 0
700 }
701
702 w.WriteByte('\n')
703 }
704
705 // writeSpaces does what it says, minimizing calls to write-like funcs
706 func writeSpaces(w *bufio.Writer, n int) {
707 const spaces = ` `
708 if n < 1 {
709 return
710 }
711
712 for n >= len(spaces) {
713 w.WriteString(spaces)
714 n -= len(spaces)
715 }
716 w.WriteString(spaces[:n])
717 }
718
719 func writeRowTiles(w *bufio.Writer, s string, t *table, writeTile itemFunc) {
720 end := t.LoopItems(s, t.Columns, t, writeTile)
721
722 if end < len(t.MaxWidth)-1 {
723 w.WriteString("\x1b[0m")
724 }
725 for i := end + 1; i < len(t.MaxWidth); i++ {
726 w.WriteString("×")
727 }
728 w.WriteString("\x1b[0m")
729 }
730
731 func numericStyle(f float64) string {
732 if f > 0 {
733 if float64(int64(f)) == f {
734 return "\x1b[38;2;0;135;0m"
735 }
736 return "\x1b[38;2;0;155;95m"
737 }
738 if f < 0 {
739 if float64(int64(f)) == f {
740 return "\x1b[38;2;204;0;0m"
741 }
742 return "\x1b[38;2;215;95;95m"
743 }
744 if f == 0 {
745 return "\x1b[38;2;0;95;215m"
746 }
747 return "\x1b[38;2;128;128;128m"
748 }
749
750 func writeNumericItem(w *bufio.Writer, s string, startStyle string) {
751 w.WriteString(startStyle)
752 if len(s) > 0 && (s[0] == '-' || s[0] == '+') {
753 w.WriteByte(s[0])
754 s = s[1:]
755 }
756
757 dot := strings.IndexByte(s, '.')
758 if dot < 0 {
759 restyleDigits(w, s, altDigitStyle)
760 w.WriteString("\x1b[0m")
761 return
762 }
763
764 if len(s[:dot]) > 3 {
765 restyleDigits(w, s[:dot], altDigitStyle)
766 w.WriteString("\x1b[0m")
767 w.WriteString(startStyle)
768 w.WriteByte('.')
769 } else {
770 w.WriteString(s[:dot])
771 w.WriteByte('.')
772 }
773
774 rest := s[dot+1:]
775 restyleDigits(w, rest, altDigitStyle)
776 if len(rest) < 4 {
777 w.WriteString("\x1b[0m")
778 }
779 }
780
781 // restyleDigits renders a run of digits as alternating styled/unstyled runs
782 // of 3 digits, which greatly improves readability, and is the only purpose
783 // of this app; string is assumed to be all decimal digits
784 func restyleDigits(w *bufio.Writer, digits string, altStyle string) {
785 if len(digits) < 4 {
786 // digit sequence is short, so emit it as is
787 w.WriteString(digits)
788 return
789 }
790
791 // separate leading 0..2 digits which don't align with the 3-digit groups
792 i := len(digits) % 3
793 // emit leading digits unstyled, if there are any
794 w.WriteString(digits[:i])
795 // the rest is guaranteed to have a length which is a multiple of 3
796 digits = digits[i:]
797
798 // start by styling, unless there were no leading digits
799 style := i != 0
800
801 for len(digits) > 0 {
802 if style {
803 w.WriteString(altStyle)
804 w.WriteString(digits[:3])
805 w.WriteString("\x1b[0m")
806 } else {
807 w.WriteString(digits[:3])
808 }
809
810 // advance to the next triple: the start of this func is supposed
811 // to guarantee this step always works
812 digits = digits[3:]
813
814 // alternate between styled and unstyled 3-digit groups
815 style = !style
816 }
817 }
File: ./ncol/ncol_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package ncol
26
27 import "testing"
28
29 func TestCountWidth(t *testing.T) {
30 tests := map[string]struct {
31 input string
32 expected int
33 }{
34 `empty`: {``, 0},
35 `empty ANSI`: {"\x1b[38;5;0;0;0m\x1b[0m", 0},
36 `simple plain`: {`abc def`, 7},
37 `unicode plain`: {`abc●def`, 7},
38 `simple ANSI`: {"abc \x1b[7mde\x1b[0mf", 7},
39 `unicode ANSI`: {"abc●\x1b[7mde\x1b[0mf", 7},
40 }
41
42 for name, tc := range tests {
43 t.Run(name, func(t *testing.T) {
44 got := countWidth(tc.input)
45 if got != tc.expected {
46 const fs = "expected width %d, got %d instead"
47 t.Errorf(fs, tc.expected, got)
48 }
49 })
50 }
51 }
File: ./ngron/data.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package ngron
26
27 // escapedStringBytes helps funcs like handleString and writeString treat all
28 // string bytes quickly and correctly, using their officially-supported JSON
29 // escape sequences
30 //
31 // https://www.rfc-editor.org/rfc/rfc8259#section-7
32 var escapedStringBytes = [128][]byte{
33 {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
34 {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
35 {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
36 {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
37 {'\\', 'b'}, {'\\', 't'},
38 {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
39 {'\\', 'f'}, {'\\', 'r'},
40 {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
41 {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
42 {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
43 {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
44 {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
45 {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
46 {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
47 {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
48 {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
49 {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
50 {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
51 {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
52 {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
53 {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
54 {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
55 {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
56 {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
57 {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
58 {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
59 {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
60 {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
61 }
File: ./ngron/json2.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package ngron
26
27 import (
28 "bufio"
29 "encoding/json"
30 "errors"
31 "io"
32 "strconv"
33 )
34
35 // writeSpaces does what it says, minimizing calls to write-like funcs
36 func writeSpaces(w *bufio.Writer, n int) {
37 const spaces = ` `
38 if n < 1 {
39 return
40 }
41
42 for n >= len(spaces) {
43 w.WriteString(spaces)
44 n -= len(spaces)
45 }
46 w.WriteString(spaces[:n])
47 }
48
49 // json2 does it all, given a reader and a writer
50 func json2(w *bufio.Writer, data any) error {
51 if err := writeValue(w, data, 0, 0); err != nil {
52 return err
53 }
54 // don't forget ending the last line for the last value
55 w.WriteByte('\n')
56
57 return nil
58 }
59
60 // writeValue handles recursion for func json2
61 func writeValue(w *bufio.Writer, data any, pre, level int) error {
62 switch data := data.(type) {
63 case nil:
64 writeSpaces(w, 2*pre)
65 w.WriteString(`null`)
66 return nil
67
68 case bool:
69 writeSpaces(w, 2*pre)
70 if data {
71 w.WriteString(`true`)
72 } else {
73 w.WriteString(`false`)
74 }
75 return nil
76
77 case int:
78 writeSpaces(w, 2*pre)
79 writeInt(w, data)
80 return nil
81
82 case float64:
83 writeSpaces(w, 2*pre)
84 writeFloat(w, data)
85 return nil
86
87 case json.Number:
88 writeSpaces(w, 2*pre)
89 w.WriteString(data.String())
90 return nil
91
92 case string:
93 return writeString(w, data, pre)
94
95 case []any:
96 return writeArray(w, data, pre, level)
97
98 case object:
99 return writeObject(w, data, pre, level)
100
101 default:
102 // return fmt.Errorf(`unsupported type %T`, t)
103 return errors.New(`unsupported value-type for JSON`)
104 }
105 }
106
107 // writeArray handles arrays for func writeValue
108 func writeArray(w *bufio.Writer, data []any, pre, level int) error {
109 for i, v := range data {
110 if i == 0 {
111 writeSpaces(w, 2*pre)
112 w.WriteByte('[')
113 w.WriteByte('\n')
114 } else {
115 w.WriteByte(',')
116 w.WriteByte('\n')
117 if w.Flush() != nil {
118 // a write error may be the consequence of stdout being closed,
119 // perhaps by another app along a pipe
120 return io.EOF
121 }
122 }
123
124 if err := writeValue(w, v, level+1, level+1); err != nil {
125 return err
126 }
127 }
128
129 if len(data) == 0 {
130 writeSpaces(w, 2*pre)
131 w.WriteByte('[')
132 w.WriteByte(']')
133 } else {
134 w.WriteByte('\n')
135 writeSpaces(w, 2*level)
136 w.WriteByte(']')
137 }
138 return nil
139 }
140
141 func writeFloat(w *bufio.Writer, data float64) error {
142 var buf [32]byte
143 w.Write(strconv.AppendFloat(buf[:0], data, 'f', -1, 64))
144 return nil
145 }
146
147 func writeInt(w *bufio.Writer, data int) error {
148 var buf [32]byte
149 w.Write(strconv.AppendInt(buf[:0], int64(data), 64))
150 return nil
151 }
152
153 // writeObject handles objects for func writeValue
154 func writeObject(w *bufio.Writer, data object, pre, level int) error {
155 for i, k := range data.Keys {
156 if i == 0 {
157 writeSpaces(w, 2*pre)
158 w.WriteByte('{')
159 w.WriteByte('\n')
160 } else {
161 w.WriteByte(',')
162 w.WriteByte('\n')
163 }
164
165 if err := writeString(w, k, level+1); err != nil {
166 return err
167 }
168
169 w.WriteString(": ")
170
171 if err := writeValue(w, data.Map[k], 0, level+1); err != nil {
172 return err
173 }
174 }
175
176 if len(data.Keys) == 0 {
177 writeSpaces(w, 2*pre)
178 w.WriteByte('{')
179 w.WriteByte('}')
180 } else {
181 w.WriteByte('\n')
182 writeSpaces(w, 2*level)
183 w.WriteByte('}')
184 }
185 return nil
186 }
187
188 // writeString handles strings for func writeValue, and keys for func
189 // writeObject
190 func writeString(w *bufio.Writer, s string, level int) error {
191 writeSpaces(w, 2*level)
192
193 for _, r := range s {
194 if r < ' ' || r == '"' || r == '\\' {
195 w.WriteByte('"')
196 for _, r := range s {
197 if int(r) < len(escapedStringBytes) {
198 w.Write(escapedStringBytes[r])
199 } else {
200 w.WriteRune(r)
201 }
202 }
203 w.WriteByte('"')
204 return nil
205 }
206 }
207
208 w.WriteByte('"')
209 w.WriteString(s)
210 w.WriteByte('"')
211 return nil
212 }
File: ./ngron/ngron.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package ngron
26
27 import (
28 "bufio"
29 "encoding/json"
30 "errors"
31 "io"
32 "os"
33 "strconv"
34 )
35
36 const info = `
37 ngron [options...] [filepath...]
38
39
40 Nice GRON converts JSON data into 'grep'-friendly lines, similar to what
41 tool 'gron' (GRep jsON; https://github.com/tomnomnom/gron) does. This tool
42 uses nicer ANSI styles than the original, hence its name.
43
44 Unlike the original 'gron', there's no sort-mode: object keys are always
45 remembered in the original input order. The original 'gron' also supports
46 reading from URIs, but this tool doesn't.
47
48 When not given a named source (filepath) to read from, JSON data are read
49 from the standard input.
50
51 Options, where leading double-dashes are also allowed:
52
53 -c, -color color, enables ANSI-styled output-mode
54 -m, -monochrome monochrome (default), enables unstyled output-mode
55 -h, -help show this help message
56 -u, -ungron turn GRON-style lines into valid JSON
57 `
58
59 type emitConfig struct {
60 path func(w *bufio.Writer, path []any) error
61 null func(w *bufio.Writer) error
62 boolean func(w *bufio.Writer, b bool) error
63 number func(w *bufio.Writer, n json.Number) error
64 key func(w *bufio.Writer, k string) error
65 text func(w *bufio.Writer, s string) error
66
67 arrayDecl string
68 objectDecl string
69 }
70
71 var monochrome = emitConfig{
72 path: monoPath,
73 null: monoNull,
74 boolean: monoBool,
75 number: monoNumber,
76 key: monoString,
77 text: monoString,
78
79 arrayDecl: `[]`,
80 objectDecl: `{}`,
81 }
82
83 var styled = emitConfig{
84 path: styledPath,
85 null: styledNull,
86 boolean: styledBool,
87 number: styledNumber,
88 key: styledString,
89 text: styledString,
90
91 arrayDecl: "\x1b[38;2;168;168;168m[]\x1b[0m",
92 objectDecl: "\x1b[38;2;168;168;168m{}\x1b[0m",
93 }
94
95 var config = monochrome
96
97 func Main() {
98 args := os.Args[1:]
99 run := gron
100
101 for len(args) > 0 {
102 switch args[0] {
103 case `-c`, `--c`, `-color`, `--color`, `-colorize`, `--colorize`:
104 config = styled
105 args = args[1:]
106 continue
107
108 case `-h`, `--h`, `-help`, `--help`:
109 os.Stdout.WriteString(info[1:])
110 return
111
112 case `-m`, `--m`, `-monochrome`, `--monochrome`:
113 config = monochrome
114 args = args[1:]
115 continue
116
117 case `-u`, `--u`, `-ungron`, `--ungron`:
118 run = ungron
119 args = args[1:]
120 continue
121 }
122
123 break
124 }
125
126 if len(args) > 0 && args[0] == `--` {
127 args = args[1:]
128 }
129
130 if len(args) > 2 {
131 os.Stderr.WriteString("multiple inputs not allowed\n")
132 os.Exit(1)
133 return
134 }
135
136 // figure out whether input should come from a named file or from stdin
137 path := `-`
138 if len(args) > 0 {
139 path = args[0]
140 }
141
142 err := handleInput(os.Stdout, path, run)
143 if err != nil && err != io.EOF {
144 os.Stderr.WriteString(`ngron: `)
145 os.Stderr.WriteString(err.Error())
146 os.Stderr.WriteString("\n")
147 os.Exit(1)
148 return
149 }
150 }
151
152 type handlerFunc func(*bufio.Writer, *json.Decoder, json.Token, []any) error
153 type runnerFunc func(w *bufio.Writer, r io.Reader) error
154
155 // handleInput simplifies control-flow for func main
156 func handleInput(w io.Writer, path string, run runnerFunc) error {
157 if path == `-` {
158 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
159 defer bw.Flush()
160 return run(bw, os.Stdin)
161 }
162
163 f, err := os.Open(path)
164 if err != nil {
165 // on windows, file-not-found error messages may mention `CreateFile`,
166 // even when trying to open files in read-only mode
167 return errors.New(`can't open file named ` + path)
168 }
169 defer f.Close()
170
171 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
172 defer bw.Flush()
173 return run(bw, f)
174 }
175
176 // gron does it all, given a reader and a writer
177 func gron(w *bufio.Writer, r io.Reader) error {
178 dec := json.NewDecoder(r)
179 // avoid parsing numbers, so unusually-long numbers are kept verbatim,
180 // even if JSON parsers aren't required to guarantee such input-fidelity
181 // for numbers
182 dec.UseNumber()
183
184 t, err := dec.Token()
185 if err == io.EOF {
186 return errors.New(`input has no JSON values`)
187 }
188
189 if err = handleToken(w, dec, t, make([]any, 0, 50)); err != nil {
190 return err
191 }
192
193 _, err = dec.Token()
194 if err == io.EOF {
195 // input is over, so it's a success
196 return nil
197 }
198
199 if err == nil {
200 // a successful `read` is a failure, as it means there are
201 // trailing JSON tokens
202 return errors.New(`unexpected trailing data`)
203 }
204
205 // any other error, perhaps some invalid-JSON-syntax-type error
206 return err
207 }
208
209 // handleToken handles recursion for func run
210 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, path []any) error {
211 switch t := t.(type) {
212 case json.Delim:
213 switch t {
214 case json.Delim('['):
215 return handleArray(w, dec, path)
216 case json.Delim('{'):
217 return handleObject(w, dec, path)
218 default:
219 return errors.New(`unsupported JSON syntax ` + string(t))
220 }
221
222 case nil:
223 config.path(w, path)
224 config.null(w)
225 return endLine(w)
226
227 case bool:
228 config.path(w, path)
229 config.boolean(w, t)
230 return endLine(w)
231
232 case json.Number:
233 config.path(w, path)
234 config.number(w, t)
235 return endLine(w)
236
237 case string:
238 config.path(w, path)
239 config.text(w, t)
240 return endLine(w)
241
242 default:
243 // return fmt.Errorf(`unsupported token type %T`, t)
244 return errors.New(`invalid JSON token`)
245 }
246 }
247
248 // handleArray handles arrays for func handleToken
249 func handleArray(w *bufio.Writer, dec *json.Decoder, path []any) error {
250 config.path(w, path)
251 w.WriteString(config.arrayDecl)
252 if err := endLine(w); err != nil {
253 return err
254 }
255
256 path = append(path, 0)
257 last := len(path) - 1
258
259 for i := 0; true; i++ {
260 path[last] = i
261
262 t, err := dec.Token()
263 if err == io.EOF {
264 return errors.New(`end of JSON before array was closed`)
265 }
266 if err != nil {
267 return err
268 }
269
270 if t == json.Delim(']') {
271 return nil
272 }
273
274 err = handleToken(w, dec, t, path)
275 if err != nil {
276 return err
277 }
278 }
279
280 // make the compiler happy
281 return nil
282 }
283
284 // handleObject handles objects for func handleToken
285 func handleObject(w *bufio.Writer, dec *json.Decoder, path []any) error {
286 config.path(w, path)
287 w.WriteString(config.objectDecl)
288 if err := endLine(w); err != nil {
289 return err
290 }
291
292 path = append(path, ``)
293 last := len(path) - 1
294
295 for i := 0; true; i++ {
296 t, err := dec.Token()
297 if err == io.EOF {
298 return errors.New(`end of JSON before object was closed`)
299 }
300 if err != nil {
301 return err
302 }
303
304 if t == json.Delim('}') {
305 return nil
306 }
307
308 k, ok := t.(string)
309 if !ok {
310 return errors.New(`expected a string for a key-value pair`)
311 }
312
313 path[last] = k
314 if err != nil {
315 return err
316 }
317
318 t, err = dec.Token()
319 if err == io.EOF {
320 return errors.New(`expected a value for a key-value pair`)
321 }
322
323 err = handleToken(w, dec, t, path)
324 if err != nil {
325 return err
326 }
327 }
328
329 // make the compiler happy
330 return nil
331 }
332
333 func monoPath(w *bufio.Writer, path []any) error {
334 var buf [24]byte
335
336 w.WriteString(`json`)
337
338 for _, v := range path {
339 switch v := v.(type) {
340 case int:
341 w.WriteByte('[')
342 w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
343 w.WriteByte(']')
344
345 case string:
346 if !needsEscaping(v) {
347 w.WriteByte('.')
348 w.WriteString(v)
349 continue
350 }
351 w.WriteByte('[')
352 monoString(w, v)
353 w.WriteByte(']')
354 }
355 }
356
357 w.WriteString(` = `)
358 return nil
359 }
360
361 func monoNull(w *bufio.Writer) error {
362 w.WriteString(`null`)
363 return nil
364 }
365
366 func monoBool(w *bufio.Writer, b bool) error {
367 if b {
368 w.WriteString(`true`)
369 } else {
370 w.WriteString(`false`)
371 }
372 return nil
373 }
374
375 func monoNumber(w *bufio.Writer, n json.Number) error {
376 w.WriteString(n.String())
377 return nil
378 }
379
380 func monoString(w *bufio.Writer, s string) error {
381 for _, r := range s {
382 if r > 127 {
383 b, err := json.Marshal(s)
384 if err != nil {
385 return err
386 }
387 w.Write(b)
388 return nil
389 }
390
391 if r < ' ' || r == '"' || r == '\\' {
392 w.WriteByte('"')
393 for i := range s {
394 w.Write(escapedStringBytes[s[i]])
395 }
396 w.WriteByte('"')
397 return nil
398 }
399 }
400
401 w.WriteByte('"')
402 w.WriteString(s)
403 w.WriteByte('"')
404 return nil
405 }
406
407 func styledPath(w *bufio.Writer, path []any) error {
408 var buf [24]byte
409
410 w.WriteString("\x1b[38;2;135;95;255mjson\x1b[0m")
411
412 for _, v := range path {
413 switch v := v.(type) {
414 case int:
415 w.WriteString("\x1b[38;2;168;168;168m[")
416 w.WriteString("\x1b[38;2;0;135;95m")
417 w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
418 w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
419
420 case string:
421 if !needsEscaping(v) {
422 w.WriteString("\x1b[38;2;168;168;168m.")
423 w.WriteString("\x1b[38;2;135;95;255m")
424 w.WriteString(v)
425 w.WriteString("\x1b[0m")
426 continue
427 }
428
429 w.WriteString("\x1b[38;2;168;168;168m[")
430 styledString(w, v)
431 w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
432 }
433 }
434
435 w.WriteString(" \x1b[38;2;168;168;168m=\x1b[0m ")
436 return nil
437 }
438
439 func styledNull(w *bufio.Writer) error {
440 w.WriteString("\x1b[38;2;168;168;168m")
441 w.WriteString(`null`)
442 w.WriteString("\x1b[0m")
443 return nil
444 }
445
446 func styledBool(w *bufio.Writer, b bool) error {
447 if b {
448 w.WriteString("\x1b[38;2;95;175;215mtrue\x1b[0m")
449 } else {
450 w.WriteString("\x1b[38;2;95;175;215mfalse\x1b[0m")
451 }
452 return nil
453 }
454
455 func styledNumber(w *bufio.Writer, n json.Number) error {
456 w.WriteString("\x1b[38;2;0;135;95m")
457 w.WriteString(n.String())
458 w.WriteString("\x1b[0m")
459 return nil
460 }
461
462 func styledString(w *bufio.Writer, s string) error {
463 for _, r := range s {
464 if r > 127 {
465 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
466 b, err := json.Marshal(s)
467 if err != nil {
468 return err
469 }
470 w.Write(b[1 : len(b)-1])
471 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
472 return nil
473 }
474
475 if r < ' ' || r == '"' || r == '\\' {
476 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
477 for i := range s {
478 w.Write(escapedStringBytes[s[i]])
479 }
480 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
481 return nil
482 }
483 }
484
485 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
486 w.WriteString(s)
487 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
488 return nil
489 }
490
491 func needsEscaping(s string) bool {
492 if len(s) == 0 {
493 return true
494 }
495
496 for _, r := range s {
497 if r < ' ' || r > '~' {
498 return true
499 }
500
501 switch r {
502 case '"', '\'', '\\':
503 return true
504 }
505 }
506
507 return false
508 }
509
510 func endLine(w *bufio.Writer) error {
511 w.WriteByte(';')
512 if err := w.WriteByte('\n'); err == nil {
513 return nil
514 }
515 return io.EOF
516 }
File: ./ngron/ungron.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package ngron
26
27 import (
28 "bufio"
29 "encoding/json"
30 "errors"
31 "fmt"
32 "io"
33 "math"
34 "strconv"
35 "strings"
36 )
37
38 var errBadFloatJSON = errors.New(`JSON numbers can't be NaNs or Infinities`)
39
40 // object is a map which also remembers the order of its keys
41 type object struct {
42 Keys []string
43 Map map[string]any
44 }
45
46 func ungron(w *bufio.Writer, r io.Reader) error {
47 data, err := parseGRON(r)
48 if err != nil {
49 return err
50 }
51 return json2(w, data)
52 }
53
54 func parseFloatJSON(s string) (f float64, err error) {
55 f, err = strconv.ParseFloat(s, 64)
56 if math.IsInf(f, 0) || math.IsNaN(f) {
57 return math.NaN(), errBadFloatJSON
58 }
59 return f, err
60 }
61
62 func parseGRON(r io.Reader) (data any, err error) {
63 const gb = 1024 * 1024 * 1024
64 sc := bufio.NewScanner(r)
65 sc.Buffer(nil, 8*gb)
66
67 for sc.Scan() {
68 if err := ungronUpdate(&data, sc.Text()); err != nil {
69 err = fmt.Errorf(`%s: %w`, sc.Text(), err)
70 return data, err
71 }
72 }
73 return data, sc.Err()
74 }
75
76 func ungronUpdate(data *any, line string) error {
77 // ensure lines starts with `json`
78 rest, ok := strings.CutPrefix(line, `json`)
79 if !ok {
80 return fmt.Errorf(`valid gron lines always start with 'json'`)
81 }
82
83 line = rest
84 line = trimLeadingSpaces(line)
85 line = trimTrailingSpaces(line)
86
87 // ignore trailing semicolon
88 if len(line) > 0 && line[len(line)-1] == ';' {
89 line = line[:len(line)-1]
90 }
91
92 // ignore empty(ish) lines
93 if len(line) == 0 {
94 return nil
95 }
96
97 return ungronUpdateRec(data, line)
98 }
99
100 func ungronUpdateRec(data *any, line string) error {
101 line = trimLeadingSpaces(line)
102 if len(line) == 0 {
103 return errors.New(`invalid gron line is missing value-assignment`)
104 }
105
106 switch line[0] {
107 case '=':
108 return ungronAssign(data, line[1:])
109
110 case '.':
111 return ungronIndexIdent(data, line[1:])
112
113 case '[':
114 return ungronIndex(data, line[1:])
115
116 default:
117 return fmt.Errorf(`unexpected syntax %c`, line[0])
118 }
119 }
120
121 func ungronAssign(data *any, line string) error {
122 line = trimLeadingSpaces(line)
123
124 if line == `[]` {
125 *data = []any{}
126 return nil
127 }
128 if line == `{}` {
129 *data = object{Map: make(map[string]any)}
130 return nil
131 }
132
133 if _, err := parseFloatJSON(line); err == nil {
134 *data = json.Number(line)
135 return nil
136 }
137
138 var res any
139 if err := json.Unmarshal([]byte(line), &res); err != nil {
140 return err
141 }
142 *data = res
143 return nil
144 }
145
146 func ungronIndexIdent(data *any, line string) error {
147 line = trimLeadingSpaces(line)
148
149 if k, ok, rest := scanIdentifier(line); ok {
150 line = trimLeadingSpaces(rest)
151 kv, ok := (*data).(object)
152 if !ok {
153 return errors.New(`expected an object`)
154 }
155
156 v, ok := kv.Map[k]
157 if !ok {
158 kv.Keys = append(kv.Keys, k)
159 v = nil
160 }
161
162 var res any = v
163 if err := ungronUpdateRec(&res, line); err != nil {
164 return err
165 }
166 kv.Map[k] = res
167 *data = kv
168 return nil
169 }
170
171 return errors.New(`missing identifier`)
172 }
173
174 func ungronIndex(data *any, line string) error {
175 line = trimLeadingSpaces(line)
176
177 if i, ok, rest := scanArrayIndex(line); ok {
178 if i < 0 {
179 return errors.New(`negative array indices not supported`)
180 }
181
182 line = trimLeadingSpaces(rest)
183 if len(line) == 0 || line[0] != ']' {
184 return errors.New(`missing ']' syntax`)
185 }
186 line = line[1:]
187 line = trimLeadingSpaces(line)
188
189 v, ok := (*data).([]any)
190 if !ok {
191 const msg = `expected an array given the numeric index`
192 return errors.New(msg)
193 }
194
195 for len(v) <= i {
196 v = append(v, nil)
197 }
198 *data = v
199
200 return ungronUpdateRec(&v[i], line)
201 }
202
203 if k, ok, rest := scanQuotedKey(line); ok {
204 var key string
205 // json.Unmarshal seems faster than strings.IndexByte and slicing
206 // the quotes out, when no backslashes are found in the quoted key
207 if err := json.Unmarshal([]byte(k), &key); err != nil {
208 return err
209 }
210
211 line = trimLeadingSpaces(rest)
212 if len(line) == 0 || line[0] != ']' {
213 return errors.New(`missing ']' syntax`)
214 }
215 line = line[1:]
216 line = trimLeadingSpaces(line)
217
218 kv, ok := (*data).(object)
219 if !ok {
220 return errors.New(`expected an object`)
221 }
222
223 if v, ok := kv.Map[key]; ok {
224 return ungronUpdateRec(&v, line)
225 }
226
227 var v any
228 if err := ungronUpdateRec(&v, line); err != nil {
229 return err
230 }
231 kv.Map[key] = v
232 return nil
233 }
234
235 return errors.New(`missing key/index`)
236 }
237
238 func scanArrayIndex(s string) (n int, ok bool, rest string) {
239 end := -1
240 for i, b := range s {
241 if '0' <= b && b <= '9' {
242 continue
243 }
244 end = i
245 break
246 }
247
248 if end < 1 {
249 return 0, false, s
250 }
251
252 if n, err := strconv.Atoi(s[:end]); err == nil {
253 return n, true, s[end:]
254 }
255 return 0, false, s
256 }
257
258 func scanIdentifier(s string) (id string, ok bool, rest string) {
259 end := -1
260 for i, b := range s {
261 if i > 0 && '0' <= b && b <= '9' {
262 continue
263 }
264 if b == '_' {
265 continue
266 }
267 if 'A' <= b && b <= 'Z' {
268 continue
269 }
270 if 'a' <= b && b <= 'z' {
271 continue
272 }
273
274 end = i
275 break
276 }
277
278 if end < 1 {
279 return ``, false, s
280 }
281 return s[:end], true, s[end:]
282 }
283
284 func scanQuotedKey(s string) (k string, ok bool, rest string) {
285 end := -1
286 esc := false
287
288 for i, b := range s {
289 if esc {
290 esc = false
291 continue
292 }
293 if b == '\\' {
294 esc = true
295 }
296 if b != '"' || i == 0 {
297 continue
298 }
299 end = i + 1
300 break
301 }
302
303 if end < 0 {
304 return ``, false, s
305 }
306 return s[:end], true, s[end:]
307 }
308
309 func trimLeadingSpaces(s string) string {
310 for len(s) > 0 && s[0] == ' ' {
311 s = s[1:]
312 }
313 return s
314 }
315
316 func trimTrailingSpaces(s string) string {
317 for len(s) > 0 && s[len(s)-1] == ' ' {
318 s = s[:len(s)-1]
319 }
320 return s
321 }
File: ./ngron/ungron_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package ngron
26
27 import (
28 "encoding/json"
29 "reflect"
30 "strconv"
31 "strings"
32 "testing"
33 )
34
35 func TestLines(t *testing.T) {
36 var tests = []struct {
37 lines []string
38 expected any
39 }{
40 {[]string{`json = null;`}, nil},
41 {[]string{`json = false;`}, false},
42 {[]string{`json = true;`}, true},
43 {[]string{`json = 123;`}, 123},
44 {[]string{`json = 123.25;`}, 123.25},
45 {[]string{`json = "abc";`}, `abc`},
46 {[]string{`json = [];`}, []any{}},
47 {[]string{`json = {};`}, object{}},
48
49 {
50 []string{
51 `json = [];`,
52 `json[0] = 0;`,
53 `json[1] = 1;`,
54 `json[2] = 2;`,
55 `json[3] = 3;`,
56 `json[4] = 4;`,
57 },
58 []any{0, 1, 2, 3, 4},
59 },
60
61 {
62 []string{
63 `json = {};`,
64 `json.abc = 123;`,
65 `json.def = 456;`,
66 },
67 object{
68 Keys: []string{`abc`, `def`},
69 Map: map[string]any{
70 `abc`: 123,
71 `def`: 456,
72 },
73 },
74 },
75 }
76
77 for _, tc := range tests {
78 src := strings.Join(tc.lines, "\n")
79
80 t.Run(src, func(t *testing.T) {
81 data, err := parseGRON(strings.NewReader(src))
82 if err != nil {
83 t.Fatal(err.Error())
84 return
85 }
86
87 if !same(data, tc.expected) {
88 exp, _ := json.Marshal(tc.expected)
89 got, _ := json.Marshal(data)
90 const f = `expected %s, but got %s instead`
91 t.Fatalf(f, string(exp), string(got))
92 return
93 }
94 })
95 }
96 }
97
98 func same(x, y any) bool {
99 if x == nil || y == nil {
100 return x == nil && y == nil
101 }
102
103 if _, ok := x.(json.Number); ok {
104 if n, ok := y.(int); ok {
105 y = json.Number(strconv.FormatInt(int64(n), 10))
106 }
107 if n, ok := y.(float64); ok {
108 y = json.Number(strconv.FormatFloat(n, 'f', -1, 64))
109 }
110 }
111
112 if n, ok := x.(int); ok {
113 x = float64(n)
114 }
115 if n, ok := y.(int); ok {
116 y = float64(n)
117 }
118
119 if reflect.TypeOf(x).String() != reflect.TypeOf(y).String() {
120 return false
121 }
122
123 switch x := x.(type) {
124 case nil:
125 return true
126
127 case bool:
128 y := y.(bool)
129 return x == y
130
131 case int:
132 y := y.(int)
133 return x == y
134
135 case float64:
136 y := y.(float64)
137 return x == y
138
139 case json.Number:
140 y := y.(json.Number)
141 return x == y
142
143 case string:
144 y := y.(string)
145 return x == y
146
147 case []any:
148 y := y.([]any)
149 if len(x) != len(y) {
150 return false
151 }
152 for i := range x {
153 if !same(x[i], y[i]) {
154 return false
155 }
156 }
157 return true
158
159 case object:
160 y := y.(object)
161 if len(x.Keys) != len(y.Keys) {
162 return false
163 }
164 for i := range x.Keys {
165 if x.Keys[i] != y.Keys[i] {
166 return false
167 }
168 if !same(x.Map[x.Keys[i]], y.Map[y.Keys[i]]) {
169 return false
170 }
171 }
172 return true
173
174 default:
175 return false
176 }
177 }
File: ./nhex/ansi.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package nhex
26
27 import (
28 "bufio"
29 "fmt"
30 "strconv"
31 )
32
33 // styledHexBytes is a super-fast direct byte-to-result lookup table, and was
34 // autogenerated by running the command
35 //
36 // seq 0 255 | ./hex-styles.awk
37 var styledHexBytes = [256]string{
38 "\x1b[38;5;111m00 ", "\x1b[38;5;246m01 ",
39 "\x1b[38;5;246m02 ", "\x1b[38;5;246m03 ",
40 "\x1b[38;5;246m04 ", "\x1b[38;5;246m05 ",
41 "\x1b[38;5;246m06 ", "\x1b[38;5;246m07 ",
42 "\x1b[38;5;246m08 ", "\x1b[38;5;246m09 ",
43 "\x1b[38;5;246m0a ", "\x1b[38;5;246m0b ",
44 "\x1b[38;5;246m0c ", "\x1b[38;5;246m0d ",
45 "\x1b[38;5;246m0e ", "\x1b[38;5;246m0f ",
46 "\x1b[38;5;246m10 ", "\x1b[38;5;246m11 ",
47 "\x1b[38;5;246m12 ", "\x1b[38;5;246m13 ",
48 "\x1b[38;5;246m14 ", "\x1b[38;5;246m15 ",
49 "\x1b[38;5;246m16 ", "\x1b[38;5;246m17 ",
50 "\x1b[38;5;246m18 ", "\x1b[38;5;246m19 ",
51 "\x1b[38;5;246m1a ", "\x1b[38;5;246m1b ",
52 "\x1b[38;5;246m1c ", "\x1b[38;5;246m1d ",
53 "\x1b[38;5;246m1e ", "\x1b[38;5;246m1f ",
54 "\x1b[38;5;72m20\x1b[38;5;239m ", "\x1b[38;5;72m21\x1b[38;5;239m!",
55 "\x1b[38;5;72m22\x1b[38;5;239m\"", "\x1b[38;5;72m23\x1b[38;5;239m#",
56 "\x1b[38;5;72m24\x1b[38;5;239m$", "\x1b[38;5;72m25\x1b[38;5;239m%",
57 "\x1b[38;5;72m26\x1b[38;5;239m&", "\x1b[38;5;72m27\x1b[38;5;239m'",
58 "\x1b[38;5;72m28\x1b[38;5;239m(", "\x1b[38;5;72m29\x1b[38;5;239m)",
59 "\x1b[38;5;72m2a\x1b[38;5;239m*", "\x1b[38;5;72m2b\x1b[38;5;239m+",
60 "\x1b[38;5;72m2c\x1b[38;5;239m,", "\x1b[38;5;72m2d\x1b[38;5;239m-",
61 "\x1b[38;5;72m2e\x1b[38;5;239m.", "\x1b[38;5;72m2f\x1b[38;5;239m/",
62 "\x1b[38;5;72m30\x1b[38;5;239m0", "\x1b[38;5;72m31\x1b[38;5;239m1",
63 "\x1b[38;5;72m32\x1b[38;5;239m2", "\x1b[38;5;72m33\x1b[38;5;239m3",
64 "\x1b[38;5;72m34\x1b[38;5;239m4", "\x1b[38;5;72m35\x1b[38;5;239m5",
65 "\x1b[38;5;72m36\x1b[38;5;239m6", "\x1b[38;5;72m37\x1b[38;5;239m7",
66 "\x1b[38;5;72m38\x1b[38;5;239m8", "\x1b[38;5;72m39\x1b[38;5;239m9",
67 "\x1b[38;5;72m3a\x1b[38;5;239m:", "\x1b[38;5;72m3b\x1b[38;5;239m;",
68 "\x1b[38;5;72m3c\x1b[38;5;239m<", "\x1b[38;5;72m3d\x1b[38;5;239m=",
69 "\x1b[38;5;72m3e\x1b[38;5;239m>", "\x1b[38;5;72m3f\x1b[38;5;239m?",
70 "\x1b[38;5;72m40\x1b[38;5;239m@", "\x1b[38;5;72m41\x1b[38;5;239mA",
71 "\x1b[38;5;72m42\x1b[38;5;239mB", "\x1b[38;5;72m43\x1b[38;5;239mC",
72 "\x1b[38;5;72m44\x1b[38;5;239mD", "\x1b[38;5;72m45\x1b[38;5;239mE",
73 "\x1b[38;5;72m46\x1b[38;5;239mF", "\x1b[38;5;72m47\x1b[38;5;239mG",
74 "\x1b[38;5;72m48\x1b[38;5;239mH", "\x1b[38;5;72m49\x1b[38;5;239mI",
75 "\x1b[38;5;72m4a\x1b[38;5;239mJ", "\x1b[38;5;72m4b\x1b[38;5;239mK",
76 "\x1b[38;5;72m4c\x1b[38;5;239mL", "\x1b[38;5;72m4d\x1b[38;5;239mM",
77 "\x1b[38;5;72m4e\x1b[38;5;239mN", "\x1b[38;5;72m4f\x1b[38;5;239mO",
78 "\x1b[38;5;72m50\x1b[38;5;239mP", "\x1b[38;5;72m51\x1b[38;5;239mQ",
79 "\x1b[38;5;72m52\x1b[38;5;239mR", "\x1b[38;5;72m53\x1b[38;5;239mS",
80 "\x1b[38;5;72m54\x1b[38;5;239mT", "\x1b[38;5;72m55\x1b[38;5;239mU",
81 "\x1b[38;5;72m56\x1b[38;5;239mV", "\x1b[38;5;72m57\x1b[38;5;239mW",
82 "\x1b[38;5;72m58\x1b[38;5;239mX", "\x1b[38;5;72m59\x1b[38;5;239mY",
83 "\x1b[38;5;72m5a\x1b[38;5;239mZ", "\x1b[38;5;72m5b\x1b[38;5;239m[",
84 "\x1b[38;5;72m5c\x1b[38;5;239m\\", "\x1b[38;5;72m5d\x1b[38;5;239m]",
85 "\x1b[38;5;72m5e\x1b[38;5;239m^", "\x1b[38;5;72m5f\x1b[38;5;239m_",
86 "\x1b[38;5;72m60\x1b[38;5;239m`", "\x1b[38;5;72m61\x1b[38;5;239ma",
87 "\x1b[38;5;72m62\x1b[38;5;239mb", "\x1b[38;5;72m63\x1b[38;5;239mc",
88 "\x1b[38;5;72m64\x1b[38;5;239md", "\x1b[38;5;72m65\x1b[38;5;239me",
89 "\x1b[38;5;72m66\x1b[38;5;239mf", "\x1b[38;5;72m67\x1b[38;5;239mg",
90 "\x1b[38;5;72m68\x1b[38;5;239mh", "\x1b[38;5;72m69\x1b[38;5;239mi",
91 "\x1b[38;5;72m6a\x1b[38;5;239mj", "\x1b[38;5;72m6b\x1b[38;5;239mk",
92 "\x1b[38;5;72m6c\x1b[38;5;239ml", "\x1b[38;5;72m6d\x1b[38;5;239mm",
93 "\x1b[38;5;72m6e\x1b[38;5;239mn", "\x1b[38;5;72m6f\x1b[38;5;239mo",
94 "\x1b[38;5;72m70\x1b[38;5;239mp", "\x1b[38;5;72m71\x1b[38;5;239mq",
95 "\x1b[38;5;72m72\x1b[38;5;239mr", "\x1b[38;5;72m73\x1b[38;5;239ms",
96 "\x1b[38;5;72m74\x1b[38;5;239mt", "\x1b[38;5;72m75\x1b[38;5;239mu",
97 "\x1b[38;5;72m76\x1b[38;5;239mv", "\x1b[38;5;72m77\x1b[38;5;239mw",
98 "\x1b[38;5;72m78\x1b[38;5;239mx", "\x1b[38;5;72m79\x1b[38;5;239my",
99 "\x1b[38;5;72m7a\x1b[38;5;239mz", "\x1b[38;5;72m7b\x1b[38;5;239m{",
100 "\x1b[38;5;72m7c\x1b[38;5;239m|", "\x1b[38;5;72m7d\x1b[38;5;239m}",
101 "\x1b[38;5;72m7e\x1b[38;5;239m~", "\x1b[38;5;246m7f ",
102 "\x1b[38;5;246m80 ", "\x1b[38;5;246m81 ",
103 "\x1b[38;5;246m82 ", "\x1b[38;5;246m83 ",
104 "\x1b[38;5;246m84 ", "\x1b[38;5;246m85 ",
105 "\x1b[38;5;246m86 ", "\x1b[38;5;246m87 ",
106 "\x1b[38;5;246m88 ", "\x1b[38;5;246m89 ",
107 "\x1b[38;5;246m8a ", "\x1b[38;5;246m8b ",
108 "\x1b[38;5;246m8c ", "\x1b[38;5;246m8d ",
109 "\x1b[38;5;246m8e ", "\x1b[38;5;246m8f ",
110 "\x1b[38;5;246m90 ", "\x1b[38;5;246m91 ",
111 "\x1b[38;5;246m92 ", "\x1b[38;5;246m93 ",
112 "\x1b[38;5;246m94 ", "\x1b[38;5;246m95 ",
113 "\x1b[38;5;246m96 ", "\x1b[38;5;246m97 ",
114 "\x1b[38;5;246m98 ", "\x1b[38;5;246m99 ",
115 "\x1b[38;5;246m9a ", "\x1b[38;5;246m9b ",
116 "\x1b[38;5;246m9c ", "\x1b[38;5;246m9d ",
117 "\x1b[38;5;246m9e ", "\x1b[38;5;246m9f ",
118 "\x1b[38;5;246ma0 ", "\x1b[38;5;246ma1 ",
119 "\x1b[38;5;246ma2 ", "\x1b[38;5;246ma3 ",
120 "\x1b[38;5;246ma4 ", "\x1b[38;5;246ma5 ",
121 "\x1b[38;5;246ma6 ", "\x1b[38;5;246ma7 ",
122 "\x1b[38;5;246ma8 ", "\x1b[38;5;246ma9 ",
123 "\x1b[38;5;246maa ", "\x1b[38;5;246mab ",
124 "\x1b[38;5;246mac ", "\x1b[38;5;246mad ",
125 "\x1b[38;5;246mae ", "\x1b[38;5;246maf ",
126 "\x1b[38;5;246mb0 ", "\x1b[38;5;246mb1 ",
127 "\x1b[38;5;246mb2 ", "\x1b[38;5;246mb3 ",
128 "\x1b[38;5;246mb4 ", "\x1b[38;5;246mb5 ",
129 "\x1b[38;5;246mb6 ", "\x1b[38;5;246mb7 ",
130 "\x1b[38;5;246mb8 ", "\x1b[38;5;246mb9 ",
131 "\x1b[38;5;246mba ", "\x1b[38;5;246mbb ",
132 "\x1b[38;5;246mbc ", "\x1b[38;5;246mbd ",
133 "\x1b[38;5;246mbe ", "\x1b[38;5;246mbf ",
134 "\x1b[38;5;246mc0 ", "\x1b[38;5;246mc1 ",
135 "\x1b[38;5;246mc2 ", "\x1b[38;5;246mc3 ",
136 "\x1b[38;5;246mc4 ", "\x1b[38;5;246mc5 ",
137 "\x1b[38;5;246mc6 ", "\x1b[38;5;246mc7 ",
138 "\x1b[38;5;246mc8 ", "\x1b[38;5;246mc9 ",
139 "\x1b[38;5;246mca ", "\x1b[38;5;246mcb ",
140 "\x1b[38;5;246mcc ", "\x1b[38;5;246mcd ",
141 "\x1b[38;5;246mce ", "\x1b[38;5;246mcf ",
142 "\x1b[38;5;246md0 ", "\x1b[38;5;246md1 ",
143 "\x1b[38;5;246md2 ", "\x1b[38;5;246md3 ",
144 "\x1b[38;5;246md4 ", "\x1b[38;5;246md5 ",
145 "\x1b[38;5;246md6 ", "\x1b[38;5;246md7 ",
146 "\x1b[38;5;246md8 ", "\x1b[38;5;246md9 ",
147 "\x1b[38;5;246mda ", "\x1b[38;5;246mdb ",
148 "\x1b[38;5;246mdc ", "\x1b[38;5;246mdd ",
149 "\x1b[38;5;246mde ", "\x1b[38;5;246mdf ",
150 "\x1b[38;5;246me0 ", "\x1b[38;5;246me1 ",
151 "\x1b[38;5;246me2 ", "\x1b[38;5;246me3 ",
152 "\x1b[38;5;246me4 ", "\x1b[38;5;246me5 ",
153 "\x1b[38;5;246me6 ", "\x1b[38;5;246me7 ",
154 "\x1b[38;5;246me8 ", "\x1b[38;5;246me9 ",
155 "\x1b[38;5;246mea ", "\x1b[38;5;246meb ",
156 "\x1b[38;5;246mec ", "\x1b[38;5;246med ",
157 "\x1b[38;5;246mee ", "\x1b[38;5;246mef ",
158 "\x1b[38;5;246mf0 ", "\x1b[38;5;246mf1 ",
159 "\x1b[38;5;246mf2 ", "\x1b[38;5;246mf3 ",
160 "\x1b[38;5;246mf4 ", "\x1b[38;5;246mf5 ",
161 "\x1b[38;5;246mf6 ", "\x1b[38;5;246mf7 ",
162 "\x1b[38;5;246mf8 ", "\x1b[38;5;246mf9 ",
163 "\x1b[38;5;246mfa ", "\x1b[38;5;246mfb ",
164 "\x1b[38;5;246mfc ", "\x1b[38;5;246mfd ",
165 "\x1b[38;5;246mfe ", "\x1b[38;5;209mff ",
166 }
167
168 // hexSymbols is a direct lookup table combining 2 hex digits with either a
169 // space or a displayable ASCII symbol matching the byte's own ASCII value;
170 // this table was autogenerated by running the command
171 //
172 // seq 0 255 | ./hex-symbols.awk
173 var hexSymbols = [256]string{
174 `00 `, `01 `, `02 `, `03 `, `04 `, `05 `, `06 `, `07 `,
175 `08 `, `09 `, `0a `, `0b `, `0c `, `0d `, `0e `, `0f `,
176 `10 `, `11 `, `12 `, `13 `, `14 `, `15 `, `16 `, `17 `,
177 `18 `, `19 `, `1a `, `1b `, `1c `, `1d `, `1e `, `1f `,
178 `20 `, `21!`, `22"`, `23#`, `24$`, `25%`, `26&`, `27'`,
179 `28(`, `29)`, `2a*`, `2b+`, `2c,`, `2d-`, `2e.`, `2f/`,
180 `300`, `311`, `322`, `333`, `344`, `355`, `366`, `377`,
181 `388`, `399`, `3a:`, `3b;`, `3c<`, `3d=`, `3e>`, `3f?`,
182 `40@`, `41A`, `42B`, `43C`, `44D`, `45E`, `46F`, `47G`,
183 `48H`, `49I`, `4aJ`, `4bK`, `4cL`, `4dM`, `4eN`, `4fO`,
184 `50P`, `51Q`, `52R`, `53S`, `54T`, `55U`, `56V`, `57W`,
185 `58X`, `59Y`, `5aZ`, `5b[`, `5c\`, `5d]`, `5e^`, `5f_`,
186 "60`", `61a`, `62b`, `63c`, `64d`, `65e`, `66f`, `67g`,
187 `68h`, `69i`, `6aj`, `6bk`, `6cl`, `6dm`, `6en`, `6fo`,
188 `70p`, `71q`, `72r`, `73s`, `74t`, `75u`, `76v`, `77w`,
189 `78x`, `79y`, `7az`, `7b{`, `7c|`, `7d}`, `7e~`, `7f `,
190 `80 `, `81 `, `82 `, `83 `, `84 `, `85 `, `86 `, `87 `,
191 `88 `, `89 `, `8a `, `8b `, `8c `, `8d `, `8e `, `8f `,
192 `90 `, `91 `, `92 `, `93 `, `94 `, `95 `, `96 `, `97 `,
193 `98 `, `99 `, `9a `, `9b `, `9c `, `9d `, `9e `, `9f `,
194 `a0 `, `a1 `, `a2 `, `a3 `, `a4 `, `a5 `, `a6 `, `a7 `,
195 `a8 `, `a9 `, `aa `, `ab `, `ac `, `ad `, `ae `, `af `,
196 `b0 `, `b1 `, `b2 `, `b3 `, `b4 `, `b5 `, `b6 `, `b7 `,
197 `b8 `, `b9 `, `ba `, `bb `, `bc `, `bd `, `be `, `bf `,
198 `c0 `, `c1 `, `c2 `, `c3 `, `c4 `, `c5 `, `c6 `, `c7 `,
199 `c8 `, `c9 `, `ca `, `cb `, `cc `, `cd `, `ce `, `cf `,
200 `d0 `, `d1 `, `d2 `, `d3 `, `d4 `, `d5 `, `d6 `, `d7 `,
201 `d8 `, `d9 `, `da `, `db `, `dc `, `dd `, `de `, `df `,
202 `e0 `, `e1 `, `e2 `, `e3 `, `e4 `, `e5 `, `e6 `, `e7 `,
203 `e8 `, `e9 `, `ea `, `eb `, `ec `, `ed `, `ee `, `ef `,
204 `f0 `, `f1 `, `f2 `, `f3 `, `f4 `, `f5 `, `f6 `, `f7 `,
205 `f8 `, `f9 `, `fa `, `fb `, `fc `, `fd `, `fe `, `ff `,
206 }
207
208 const (
209 unknownStyle = 0
210 zeroStyle = 1
211 otherStyle = 2
212 asciiStyle = 3
213 allOnStyle = 4
214 )
215
216 // byteStyles turns bytes into one of several distinct visual types, which
217 // allows quickly telling when ANSI styles codes are repetitive and when
218 // they're actually needed
219 var byteStyles = [256]int{
220 zeroStyle, otherStyle, otherStyle, otherStyle,
221 otherStyle, otherStyle, otherStyle, otherStyle,
222 otherStyle, otherStyle, otherStyle, otherStyle,
223 otherStyle, otherStyle, otherStyle, otherStyle,
224 otherStyle, otherStyle, otherStyle, otherStyle,
225 otherStyle, otherStyle, otherStyle, otherStyle,
226 otherStyle, otherStyle, otherStyle, otherStyle,
227 otherStyle, otherStyle, otherStyle, otherStyle,
228 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
229 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
230 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
231 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
232 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
233 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
234 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
235 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
236 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
237 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
238 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
239 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
240 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
241 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
242 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
243 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
244 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
245 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
246 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
247 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
248 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
249 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
250 asciiStyle, asciiStyle, asciiStyle, asciiStyle,
251 asciiStyle, asciiStyle, asciiStyle, otherStyle,
252 otherStyle, otherStyle, otherStyle, otherStyle,
253 otherStyle, otherStyle, otherStyle, otherStyle,
254 otherStyle, otherStyle, otherStyle, otherStyle,
255 otherStyle, otherStyle, otherStyle, otherStyle,
256 otherStyle, otherStyle, otherStyle, otherStyle,
257 otherStyle, otherStyle, otherStyle, otherStyle,
258 otherStyle, otherStyle, otherStyle, otherStyle,
259 otherStyle, otherStyle, otherStyle, otherStyle,
260 otherStyle, otherStyle, otherStyle, otherStyle,
261 otherStyle, otherStyle, otherStyle, otherStyle,
262 otherStyle, otherStyle, otherStyle, otherStyle,
263 otherStyle, otherStyle, otherStyle, otherStyle,
264 otherStyle, otherStyle, otherStyle, otherStyle,
265 otherStyle, otherStyle, otherStyle, otherStyle,
266 otherStyle, otherStyle, otherStyle, otherStyle,
267 otherStyle, otherStyle, otherStyle, otherStyle,
268 otherStyle, otherStyle, otherStyle, otherStyle,
269 otherStyle, otherStyle, otherStyle, otherStyle,
270 otherStyle, otherStyle, otherStyle, otherStyle,
271 otherStyle, otherStyle, otherStyle, otherStyle,
272 otherStyle, otherStyle, otherStyle, otherStyle,
273 otherStyle, otherStyle, otherStyle, otherStyle,
274 otherStyle, otherStyle, otherStyle, otherStyle,
275 otherStyle, otherStyle, otherStyle, otherStyle,
276 otherStyle, otherStyle, otherStyle, otherStyle,
277 otherStyle, otherStyle, otherStyle, otherStyle,
278 otherStyle, otherStyle, otherStyle, otherStyle,
279 otherStyle, otherStyle, otherStyle, otherStyle,
280 otherStyle, otherStyle, otherStyle, otherStyle,
281 otherStyle, otherStyle, otherStyle, otherStyle,
282 otherStyle, otherStyle, otherStyle, otherStyle,
283 otherStyle, otherStyle, otherStyle, allOnStyle,
284 }
285
286 // writeMetaANSI shows metadata right before the ANSI-styled hex byte-view
287 func writeMetaANSI(w *bufio.Writer, fname string, fsize int, cfg config) {
288 if cfg.Title != "" {
289 fmt.Fprintf(w, "\x1b[4m%s\x1b[0m\n", cfg.Title)
290 w.WriteString("\n")
291 }
292
293 if fsize < 0 {
294 fmt.Fprintf(w, "• %s\n", fname)
295 } else {
296 const fs = "• %s \x1b[38;5;248m(%s bytes)\x1b[0m\n"
297 fmt.Fprintf(w, fs, fname, sprintCommas(fsize))
298 }
299
300 if cfg.Skip > 0 {
301 const fs = " \x1b[38;5;5mskipping first %s bytes\x1b[0m\n"
302 fmt.Fprintf(w, fs, sprintCommas(cfg.Skip))
303 }
304 if cfg.MaxBytes > 0 {
305 const fs = " \x1b[38;5;5mshowing only up to %s bytes\x1b[0m\n"
306 fmt.Fprintf(w, fs, sprintCommas(cfg.MaxBytes))
307 }
308 w.WriteString("\n")
309 }
310
311 // writeBufferANSI shows the hex byte-view using ANSI colors/styles
312 func writeBufferANSI(rc rendererConfig, first, second []byte) error {
313 // show a ruler every few lines to make eye-scanning easier
314 if rc.chunks%5 == 0 && rc.chunks > 0 {
315 writeRulerANSI(rc)
316 }
317
318 return writeLineANSI(rc, first, second)
319 }
320
321 // writeRulerANSI emits an indented ANSI-styled line showing spaced-out dots,
322 // so as to help eye-scan items on nearby output lines
323 func writeRulerANSI(rc rendererConfig) {
324 w := rc.out
325 if len(rc.ruler) == 0 {
326 w.WriteByte('\n')
327 return
328 }
329
330 w.WriteString("\x1b[38;5;248m")
331 indent := int(rc.offsetWidth) + len(padding)
332 writeSpaces(w, indent)
333 w.Write(rc.ruler)
334 w.WriteString("\x1b[0m\n")
335 }
336
337 func writeLineANSI(rc rendererConfig, first, second []byte) error {
338 w := rc.out
339
340 // start each line with the byte-offset for the 1st item shown on it
341 if rc.showOffsets {
342 writeStyledCounter(w, int(rc.offsetWidth), rc.offset)
343 w.WriteString(padding + "\x1b[48;5;254m")
344 } else {
345 w.WriteString(padding)
346 }
347
348 prevStyle := unknownStyle
349 for _, b := range first {
350 // using the slow/generic fmt.Fprintf is a performance bottleneck,
351 // since it's called for each input byte
352 // w.WriteString(styledHexBytes[b])
353
354 // this more complicated way of emitting output avoids repeating
355 // ANSI styles when dealing with bytes which aren't displayable
356 // ASCII symbols, thus emitting fewer bytes when dealing with
357 // general binary datasets; it makes no difference for plain-text
358 // ASCII input
359 style := byteStyles[b]
360 if style != prevStyle {
361 w.WriteString(styledHexBytes[b])
362 if style == asciiStyle {
363 // styling displayable ASCII symbols uses multiple different
364 // styles each time it happens, always forcing ANSI-style
365 // updates
366 style = unknownStyle
367 }
368 } else {
369 w.WriteString(hexSymbols[b])
370 }
371 prevStyle = style
372 }
373
374 w.WriteString("\x1b[0m")
375 if rc.showASCII {
376 writePlainASCII(w, first, second, int(rc.perLine))
377 }
378
379 return w.WriteByte('\n')
380 }
381
382 func writeStyledCounter(w *bufio.Writer, width int, n uint) {
383 var buf [32]byte
384 str := strconv.AppendUint(buf[:0], uint64(n), 10)
385
386 // left-pad the final result with leading spaces
387 writeSpaces(w, width-len(str))
388
389 var style bool
390 // emit leading part with 1 or 2 digits unstyled, ensuring the
391 // rest or the rendered number's string is a multiple of 3 long
392 if rem := len(str) % 3; rem != 0 {
393 w.Write(str[:rem])
394 str = str[rem:]
395 // next digit-group needs some styling
396 style = true
397 } else {
398 style = false
399 }
400
401 // alternate between styled/unstyled 3-digit groups
402 for len(str) > 0 {
403 if !style {
404 w.Write(str[:3])
405 } else {
406 w.WriteString("\x1b[38;5;248m")
407 w.Write(str[:3])
408 w.WriteString("\x1b[0m")
409 }
410
411 style = !style
412 str = str[3:]
413 }
414 }
File: ./nhex/config.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package nhex
26
27 import (
28 "bufio"
29 "bytes"
30 "flag"
31 "fmt"
32 )
33
34 const (
35 usageMaxBytes = `limit input up to n bytes; negative to disable`
36 usagePerLine = `how many bytes to show on each line`
37 usageSkip = `how many leading bytes to skip/ignore`
38 usageTitle = `use this to show a title/description`
39 usageTo = `the output format to use (plain or ansi)`
40 usagePlain = `show plain-text output, as opposed to ansi-styled output`
41 usageShowOffset = `start lines with the offset of the 1st byte shown on each`
42 usageShowASCII = `repeat all ASCII strings on the side, so they're searcheable`
43 )
44
45 const defaultOffsetCounterWidth = 8
46
47 const (
48 plainOutput = `plain`
49 ansiOutput = `ansi`
50 )
51
52 // config is the parsed cmd-line options given to the app
53 type config struct {
54 // MaxBytes limits how many bytes are shown; a negative value means no limit
55 MaxBytes int
56
57 // PerLine is how many bytes are shown per output line
58 PerLine int
59
60 // Skip is how many leading bytes to skip/ignore
61 Skip int
62
63 // OffsetCounterWidth is the max string-width; not exposed as a cmd-line option
64 OffsetCounterWidth uint
65
66 // Title is an optional title preceding the output proper
67 Title string
68
69 // To is the output format
70 To string
71
72 // Filenames is the list of input filenames
73 Filenames []string
74
75 // Ruler is a prerendered ruler to emit every few output lines
76 Ruler []byte
77
78 // ShowOffsets starts lines with the offset of the 1st byte shown on each
79 ShowOffsets bool
80
81 // ShowASCII shows a side-panel with searcheable ASCII-runs
82 ShowASCII bool
83 }
84
85 // parseFlags is the constructor for type config
86 func parseFlags(usage string) config {
87 flag.Usage = func() {
88 fmt.Fprintf(flag.CommandLine.Output(), "%s\n\nOptions\n\n", usage)
89 flag.PrintDefaults()
90 }
91
92 cfg := config{
93 MaxBytes: -1,
94 PerLine: 16,
95 OffsetCounterWidth: 0,
96 To: ansiOutput,
97 ShowOffsets: true,
98 ShowASCII: true,
99 }
100
101 plain := false
102 flag.IntVar(&cfg.MaxBytes, `max`, cfg.MaxBytes, usageMaxBytes)
103 flag.IntVar(&cfg.PerLine, `width`, cfg.PerLine, usagePerLine)
104 flag.IntVar(&cfg.Skip, `skip`, cfg.Skip, usageSkip)
105 flag.StringVar(&cfg.Title, `title`, cfg.Title, usageTitle)
106 flag.StringVar(&cfg.To, `to`, cfg.To, usageTo)
107 flag.BoolVar(&cfg.ShowOffsets, `n`, cfg.ShowOffsets, usageShowOffset)
108 flag.BoolVar(&cfg.ShowASCII, `ascii`, cfg.ShowASCII, usageShowASCII)
109 flag.BoolVar(&plain, `p`, plain, "alias for option `plain`")
110 flag.BoolVar(&plain, `plain`, plain, usagePlain)
111 flag.Parse()
112
113 if plain {
114 cfg.To = plainOutput
115 }
116
117 // normalize values for option -to
118 switch cfg.To {
119 case `text`, `plaintext`, `plain-text`:
120 cfg.To = plainOutput
121 }
122
123 cfg.Ruler = makeRuler(cfg.PerLine)
124 cfg.Filenames = flag.Args()
125 return cfg
126 }
127
128 // makeRuler prerenders a ruler-line, used to make the output lines breathe
129 func makeRuler(numitems int) []byte {
130 if n := numitems / 4; n > 0 {
131 var pat = []byte(` ·`)
132 return bytes.Repeat(pat, n)
133 }
134 return nil
135 }
136
137 // rendererConfig groups several arguments given to any of the rendering funcs
138 type rendererConfig struct {
139 // out is writer to send all output to
140 out *bufio.Writer
141
142 // offset is the byte-offset of the first byte shown on the current output
143 // line: if shown at all, it's shown at the start the line
144 offset uint
145
146 // chunks is the 0-based counter for byte-chunks/lines shown so far, which
147 // indirectly keeps track of when it's time to show a `breather` line
148 chunks uint
149
150 // ruler is the `ruler` content to show on `breather` lines
151 ruler []byte
152
153 // perLine is how many hex-encoded bytes are shown per line
154 perLine uint
155
156 // offsetWidth is the max string-width for the byte-offsets shown at the
157 // start of output lines, and determines those values' left-padding
158 offsetWidth uint
159
160 // showOffsets determines whether byte-offsets are shown at all
161 showOffsets bool
162
163 // showASCII determines whether the ASCII-panels are shown at all
164 showASCII bool
165 }
File: ./nhex/hex-styles.awk
1 #!/usr/bin/awk -f
2
3 # The MIT License (MIT)
4 #
5 # Copyright (c) 2026 pacman64
6 #
7 # Permission is hereby granted, free of charge, to any person obtaining a copy
8 # of this software and associated documentation files (the "Software"), to deal
9 # in the Software without restriction, including without limitation the rights
10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 # copies of the Software, and to permit persons to whom the Software is
12 # furnished to do so, subject to the following conditions:
13 #
14 # The above copyright notice and this permission notice shall be included in
15 # all copies or substantial portions of the Software.
16 #
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 # SOFTWARE.
24
25
26 # all 0 bits
27 $0 == 0 {
28 print "\"\\x1b[38;5;111m00 \","
29 next
30 }
31
32 # ascii symbol which need backslashing
33 $0 == 34 || $0 == 92 {
34 printf "\"\\x1b[38;5;72m%02x\\x1b[38;5;239m\\%c\",\n", $0 + 0, $0
35 next
36 }
37
38 # all other ascii symbol
39 32 <= $0 && $0 <= 126 {
40 printf "\"\\x1b[38;5;72m%02x\\x1b[38;5;239m%c\",\n", $0 + 0, $0
41 next
42 }
43
44 # all 1 bits
45 $0 == 255 {
46 print "\"\\x1b[38;5;209mff \","
47 next
48 }
49
50 # all other bytes
51 1 {
52 printf "\"\\x1b[38;5;246m%02x \",\n", $0 + 0
53 next
54 }
File: ./nhex/hex-symbols.awk
1 #!/usr/bin/awk -f
2
3 # The MIT License (MIT)
4 #
5 # Copyright (c) 2026 pacman64
6 #
7 # Permission is hereby granted, free of charge, to any person obtaining a copy
8 # of this software and associated documentation files (the "Software"), to deal
9 # in the Software without restriction, including without limitation the rights
10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 # copies of the Software, and to permit persons to whom the Software is
12 # furnished to do so, subject to the following conditions:
13 #
14 # The above copyright notice and this permission notice shall be included in
15 # all copies or substantial portions of the Software.
16 #
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 # SOFTWARE.
24
25
26 # ascii symbol which need backslashing
27 $0 == 34 || $0 == 92 {
28 printf "\"%02x\\%c\",\n", $0 + 0, $0
29 next
30 }
31
32 # all other ascii symbol
33 32 <= $0 && $0 <= 126 {
34 printf "\"%02x%c\",\n", $0 + 0, $0
35 next
36 }
37
38 # all other bytes
39 1 {
40 printf "\"%02x \",\n", $0 + 0
41 next
42 }
File: ./nhex/info.txt
1 nhex [options...] [filenames...]
2
3
4 Nice HEXadecimal is a simple hexadecimal viewer to easily inspect bytes
5 from files/data.
6
7 Each line shows the starting offset for the bytes shown, 16 of the bytes
8 themselves in base-16 notation, and any ASCII codes when the byte values
9 are in the typical ASCII range. The offsets shown are base-10.
10
11 The base-16 codes are color-coded, with most bytes shown in gray, while
12 all-1 and all-0 bytes are shown in orange and blue respectively.
13
14 All-0 bytes are the commonest kind in most binary file types and, along
15 with all-1 bytes are also a special case worth noticing when exploring
16 binary data, so it makes sense for them to stand out right away.
File: ./nhex/main.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package nhex
26
27 import (
28 "bufio"
29 "fmt"
30 "io"
31 "math"
32 "os"
33
34 _ "embed"
35 )
36
37 //go:embed info.txt
38 var usage string
39
40 func Main() {
41 if err := run(parseFlags(usage)); err != nil {
42 os.Stderr.WriteString(`nhex: `)
43 os.Stderr.WriteString(err.Error())
44 os.Stderr.WriteString("\n")
45 os.Exit(1)
46 return
47 }
48 }
49
50 func run(cfg config) error {
51 // f, _ := os.Create(`nh.prof`)
52 // defer f.Close()
53 // pprof.StartCPUProfile(f)
54 // defer pprof.StopCPUProfile()
55
56 w := bufio.NewWriterSize(os.Stdout, 32*1024)
57 defer w.Flush()
58
59 // with no filenames given, handle stdin and quit
60 if len(cfg.Filenames) == 0 {
61 return handle(w, os.Stdin, `<stdin>`, -1, cfg)
62 }
63
64 // show all files given
65 for i, fname := range cfg.Filenames {
66 if i > 0 {
67 w.WriteString("\n")
68 w.WriteString("\n")
69 }
70
71 err := handleFile(w, fname, cfg)
72 if err != nil {
73 return err
74 }
75 }
76
77 return nil
78 }
79
80 // handleFile is like handleReader, except it also shows file-related info
81 func handleFile(w *bufio.Writer, fname string, cfg config) error {
82 f, err := os.Open(fname)
83 if err != nil {
84 return err
85 }
86 defer f.Close()
87
88 stat, err := f.Stat()
89 if err != nil {
90 return handle(w, f, fname, -1, cfg)
91 }
92
93 fsize := int(stat.Size())
94 return handle(w, f, fname, fsize, cfg)
95 }
96
97 // handle shows some messages related to the input and the cmd-line options
98 // used, and then follows them by the hexadecimal byte-view
99 func handle(w *bufio.Writer, r io.Reader, name string, size int, cfg config) error {
100 skip(r, cfg.Skip)
101 if cfg.MaxBytes > 0 {
102 r = io.LimitReader(r, int64(cfg.MaxBytes))
103 }
104
105 // finish config setup based on the filesize, if a valid one was given
106 if cfg.OffsetCounterWidth < 1 {
107 if size < 1 {
108 cfg.OffsetCounterWidth = defaultOffsetCounterWidth
109 } else {
110 w := math.Log10(float64(size))
111 w = math.Max(math.Ceil(w), 1)
112 cfg.OffsetCounterWidth = uint(w)
113 }
114 }
115
116 switch cfg.To {
117 case plainOutput:
118 writeMetaPlain(w, name, size, cfg)
119 // when done, emit a new line in case only part of the last line is
120 // shown, which means no newline was emitted for it
121 defer w.WriteString("\n")
122 return render(w, r, cfg, writeBufferPlain)
123
124 case ansiOutput:
125 writeMetaANSI(w, name, size, cfg)
126 // when done, emit a new line in case only part of the last line is
127 // shown, which means no newline was emitted for it
128 defer w.WriteString("\x1b[0m\n")
129 return render(w, r, cfg, writeBufferANSI)
130
131 default:
132 const fs = `unsupported output format %q`
133 return fmt.Errorf(fs, cfg.To)
134 }
135 }
136
137 // skip ignores n bytes from the reader given
138 func skip(r io.Reader, n int) {
139 if n < 1 {
140 return
141 }
142
143 // use func Seek for input files, except for stdin, which you can't seek
144 if f, ok := r.(*os.File); ok && r != os.Stdin {
145 f.Seek(int64(n), io.SeekCurrent)
146 return
147 }
148 io.CopyN(io.Discard, r, int64(n))
149 }
150
151 // renderer is the type for the hex-view render funcs
152 type renderer func(rc rendererConfig, first, second []byte) error
153
154 // render reads all input and shows the hexadecimal byte-view for the input
155 // data via the rendering callback given
156 func render(w *bufio.Writer, r io.Reader, cfg config, fn renderer) error {
157 if cfg.PerLine < 1 {
158 cfg.PerLine = 16
159 }
160
161 rc := rendererConfig{
162 out: w,
163 offset: uint(cfg.Skip),
164 chunks: 0,
165 perLine: uint(cfg.PerLine),
166 ruler: cfg.Ruler,
167
168 offsetWidth: cfg.OffsetCounterWidth,
169 showOffsets: cfg.ShowOffsets,
170 showASCII: cfg.ShowASCII,
171 }
172
173 // calling func Read directly can sometimes result in chunks shorter
174 // than the max chunk-size, even when there are plenty of bytes yet
175 // to read; to avoid that, use a buffered-reader to explicitly fill
176 // a slice instead
177 br := bufio.NewReader(r)
178
179 // to show ASCII up to 1 full chunk ahead, 2 chunks are needed
180 cur := make([]byte, 0, cfg.PerLine)
181 ahead := make([]byte, 0, cfg.PerLine)
182
183 // the ASCII-panel's wide output requires staying 1 step/chunk behind,
184 // so to speak
185 cur, err := fillChunk(cur[:0], cfg.PerLine, br)
186 if len(cur) == 0 {
187 if err == io.EOF {
188 err = nil
189 }
190 return err
191 }
192
193 for {
194 ahead, err := fillChunk(ahead[:0], cfg.PerLine, br)
195 if err != nil && err != io.EOF {
196 return err
197 }
198
199 if len(ahead) == 0 {
200 // done, maybe except for an extra line of output
201 break
202 }
203
204 // show the byte-chunk on its own output line
205 err = fn(rc, cur, ahead)
206 if err != nil {
207 // probably a pipe was closed
208 return nil
209 }
210
211 rc.chunks++
212 rc.offset += uint(len(cur))
213 cur = cur[:copy(cur, ahead)]
214 }
215
216 // don't forget the last output line
217 if len(cur) > 0 {
218 return fn(rc, cur, nil)
219 }
220 return nil
221 }
222
223 // fillChunk tries to read the number of bytes given, appending them to the
224 // byte-slice given; this func returns an EOF error only when no bytes are
225 // read, which somewhat simplifies error-handling for the func caller
226 func fillChunk(chunk []byte, n int, br *bufio.Reader) ([]byte, error) {
227 // read buffered-bytes up to the max chunk-size
228 for i := 0; i < n; i++ {
229 b, err := br.ReadByte()
230 if err == nil {
231 chunk = append(chunk, b)
232 continue
233 }
234
235 if err == io.EOF && i > 0 {
236 return chunk, nil
237 }
238 return chunk, err
239 }
240
241 // got the full byte-count asked for
242 return chunk, nil
243 }
File: ./nhex/numbers.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package nhex
26
27 import (
28 "bufio"
29 "io"
30 "math"
31 "strconv"
32 "strings"
33 )
34
35 // loopThousandsGroups comes from my lib/package `mathplus`: that's why it
36 // handles negatives, even though this app only uses it with non-negatives.
37 func loopThousandsGroups(n int, fn func(i, n int)) {
38 // 0 doesn't have a log10
39 if n == 0 {
40 fn(0, 0)
41 return
42 }
43
44 sign := +1
45 if n < 0 {
46 n = -n
47 sign = -1
48 }
49
50 intLog1000 := int(math.Log10(float64(n)) / 3)
51 remBase := int(math.Pow10(3 * intLog1000))
52
53 for i := 0; remBase > 0; i++ {
54 group := (1000 * n) / remBase / 1000
55 fn(i, sign*group)
56 // if original number was negative, ensure only first
57 // group gives a negative input to the callback
58 sign = +1
59
60 n %= remBase
61 remBase /= 1000
62 }
63 }
64
65 // sprintCommas turns the non-negative number given into a readable string,
66 // where digits are grouped-separated by commas
67 func sprintCommas(n int) string {
68 var sb strings.Builder
69 loopThousandsGroups(n, func(i, n int) {
70 if i == 0 {
71 var buf [4]byte
72 sb.Write(strconv.AppendInt(buf[:0], int64(n), 10))
73 return
74 }
75 sb.WriteByte(',')
76 writePad0Sub1000Counter(&sb, uint(n))
77 })
78 return sb.String()
79 }
80
81 // writePad0Sub1000Counter is an alternative to fmt.Fprintf(w, `%03d`, n)
82 func writePad0Sub1000Counter(w io.Writer, n uint) {
83 // precondition is 0...999
84 if n > 999 {
85 w.Write([]byte(`???`))
86 return
87 }
88
89 var buf [3]byte
90 buf[0] = byte(n/100) + '0'
91 n %= 100
92 buf[1] = byte(n/10) + '0'
93 buf[2] = byte(n%10) + '0'
94 w.Write(buf[:])
95 }
96
97 // writeHex is faster than calling fmt.Fprintf(w, `%02x`, b): this
98 // matters because it's called for every byte of input which isn't
99 // all 0s or all 1s
100 func writeHex(w *bufio.Writer, b byte) {
101 const hexDigits = `0123456789abcdef`
102 w.WriteByte(hexDigits[b>>4])
103 w.WriteByte(hexDigits[b&0x0f])
104 }
File: ./nhex/plain.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package nhex
26
27 import (
28 "bufio"
29 "fmt"
30 "strconv"
31 )
32
33 // padding is the padding/spacing emitted across each output line, except for
34 // the breather/ruler lines
35 const padding = ` `
36
37 // writeMetaPlain shows metadata right before the plain-text hex byte-view
38 func writeMetaPlain(w *bufio.Writer, fname string, fsize int, cfg config) {
39 if cfg.Title != `` {
40 w.WriteString(cfg.Title)
41 w.WriteString("\n")
42 w.WriteString("\n")
43 }
44
45 if fsize < 0 {
46 fmt.Fprintf(w, "• %s\n", fname)
47 } else {
48 const fs = "• %s (%s bytes)\n"
49 fmt.Fprintf(w, fs, fname, sprintCommas(fsize))
50 }
51
52 if cfg.Skip > 0 {
53 const fs = " skipping first %s bytes\n"
54 fmt.Fprintf(w, fs, sprintCommas(cfg.Skip))
55 }
56 if cfg.MaxBytes > 0 {
57 const fs = " showing only up to %s bytes\n"
58 fmt.Fprintf(w, fs, sprintCommas(cfg.MaxBytes))
59 }
60 w.WriteString("\n")
61 }
62
63 // writeBufferPlain shows the hex byte-view withOUT using ANSI colors/styles
64 func writeBufferPlain(rc rendererConfig, first, second []byte) error {
65 // show a ruler every few lines to make eye-scanning easier
66 if rc.chunks%5 == 0 && rc.chunks > 0 {
67 rc.out.WriteByte('\n')
68 }
69
70 return writeLinePlain(rc, first, second)
71 }
72
73 func writeLinePlain(rc rendererConfig, first, second []byte) error {
74 w := rc.out
75
76 // start each line with the byte-offset for the 1st item shown on it
77 if rc.showOffsets {
78 writePlainCounter(w, int(rc.offsetWidth), rc.offset)
79 w.WriteByte(' ')
80 } else {
81 w.WriteString(padding)
82 }
83
84 for _, b := range first {
85 // fmt.Fprintf(w, ` %02x`, b)
86 //
87 // the commented part above was a performance bottleneck, since
88 // the slow/generic fmt.Fprintf was called for each input byte
89 w.WriteByte(' ')
90 writeHex(w, b)
91 }
92
93 if rc.showASCII {
94 writePlainASCII(w, first, second, int(rc.perLine))
95 }
96
97 return w.WriteByte('\n')
98 }
99
100 // writePlainCounter just emits a left-padded number
101 func writePlainCounter(w *bufio.Writer, width int, n uint) {
102 var buf [32]byte
103 str := strconv.AppendUint(buf[:0], uint64(n), 10)
104 writeSpaces(w, width-len(str))
105 w.Write(str)
106 }
107
108 // writeRulerPlain emits a breather line using a ruler-like pattern of spaces
109 // and dots, to guide the eye across the main output lines
110 // func writeRulerPlain(w *bufio.Writer, indent int, offset int, numitems int) {
111 // writeSpaces(w, indent)
112 // for i := 0; i < numitems-1; i++ {
113 // if (i+offset+1)%5 == 0 {
114 // w.WriteString(` `)
115 // } else {
116 // w.WriteString(` ·`)
117 // }
118 // }
119 // }
120
121 // writeSpaces bulk-emits the number of spaces given
122 func writeSpaces(w *bufio.Writer, n int) {
123 const spaces = ` `
124 for ; n > len(spaces); n -= len(spaces) {
125 w.WriteString(spaces)
126 }
127 if n > 0 {
128 w.WriteString(spaces[:n])
129 }
130 }
131
132 // writePlainASCII emits the side-panel showing all ASCII runs for each line
133 func writePlainASCII(w *bufio.Writer, first, second []byte, perline int) {
134 // prev keeps track of the previous byte, so spaces are added
135 // when bytes change from non-visible-ASCII to visible-ASCII
136 prev := byte(0)
137
138 spaces := 3*(perline-len(first)) + len(padding)
139
140 for _, b := range first {
141 if 32 < b && b < 127 {
142 if !(32 < prev && prev < 127) {
143 writeSpaces(w, spaces)
144 spaces = 1
145 }
146 w.WriteByte(b)
147 }
148 prev = b
149 }
150
151 for _, b := range second {
152 if 32 < b && b < 127 {
153 if !(32 < prev && prev < 127) {
154 writeSpaces(w, spaces)
155 spaces = 1
156 }
157 w.WriteByte(b)
158 }
159 prev = b
160 }
161 }
File: ./njson/njson.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package njson
26
27 import (
28 "bufio"
29 "encoding/json"
30 "errors"
31 "io"
32 "os"
33 )
34
35 const info = `
36 njson [filepath...]
37
38 Nice JSON shows JSON data as ANSI-styled indented lines, using 2 spaces for
39 each indentation level.
40 `
41
42 // indent is how many spaces each indentation level uses
43 const indent = 2
44
45 const (
46 // boolStyle is bluish, and very distinct from all other colors used
47 boolStyle = "\x1b[38;2;95;175;215m"
48
49 // keyStyle is magenta, and very distinct from normal strings
50 keyStyle = "\x1b[38;2;135;95;255m"
51
52 // nullStyle is a light-gray, just like syntax elements, but the word
53 // `null` is wide enough to stand out from syntax items at a glance
54 nullStyle = syntaxStyle
55
56 // positiveNumberStyle is a nice green
57 positiveNumberStyle = "\x1b[38;2;0;135;95m"
58
59 // negativeNumberStyle is a nice red
60 negativeNumberStyle = "\x1b[38;2;204;0;0m"
61
62 // zeroNumberStyle is a nice blue
63 zeroNumberStyle = "\x1b[38;2;0;95;215m"
64
65 // stringStyle used to be bluish, but it's better to keep it plain,
66 // which also minimizes how many different colors the output can show
67 stringStyle = ""
68
69 // syntaxStyle is a light-gray, not too light, not too dark
70 syntaxStyle = "\x1b[38;2;168;168;168m"
71 )
72
73 func Main() {
74 args := os.Args[1:]
75
76 if len(args) > 0 {
77 switch args[0] {
78 case `-h`, `--h`, `-help`, `--help`:
79 os.Stdout.WriteString(info[1:])
80 return
81 }
82 }
83
84 if len(args) > 0 && args[0] == `--` {
85 args = args[1:]
86 }
87
88 if len(args) > 1 {
89 showError(errors.New(`multiple inputs not allowed`))
90 os.Exit(1)
91 return
92 }
93
94 // figure out whether input should come from a named file or from stdin
95 name := `-`
96 if len(args) == 1 {
97 name = args[0]
98 }
99
100 var err error
101 if name == `-` {
102 // handle lack of filepath arg, or `-` as the filepath
103 err = niceJSON(os.Stdout, os.Stdin)
104 } else {
105 // handle being given a normal filepath
106 err = handleFile(os.Stdout, os.Args[1])
107 }
108
109 if err != nil && err != io.EOF {
110 showError(err)
111 os.Exit(1)
112 return
113 }
114 }
115
116 // showError standardizes how errors look in this app
117 func showError(err error) {
118 os.Stderr.WriteString(`njson: `)
119 os.Stderr.WriteString(err.Error())
120 os.Stderr.WriteString("\n")
121 }
122
123 // writeSpaces does what it says, minimizing calls to write-like funcs
124 func writeSpaces(w *bufio.Writer, n int) {
125 const spaces = ` `
126 for n >= len(spaces) {
127 w.WriteString(spaces)
128 n -= len(spaces)
129 }
130 if n > 0 {
131 w.WriteString(spaces[:n])
132 }
133 }
134
135 func handleFile(w io.Writer, path string) error {
136 // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
137 // resp, err := http.Get(path)
138 // if err != nil {
139 // return err
140 // }
141 // defer resp.Body.Close()
142 // return niceJSON(w, resp.Body)
143 // }
144
145 f, err := os.Open(path)
146 if err != nil {
147 // on windows, file-not-found error messages may mention `CreateFile`,
148 // even when trying to open files in read-only mode
149 return errors.New(`can't open file named ` + path)
150 }
151 defer f.Close()
152
153 return niceJSON(w, f)
154 }
155
156 func niceJSON(w io.Writer, r io.Reader) error {
157 bw := bufio.NewWriterSize(w, 32*1024)
158 defer bw.Flush()
159
160 dec := json.NewDecoder(r)
161 // using string-like json.Number values instead of float64 ones avoids
162 // unneeded reformatting of numbers; reformatting parsed float64 values
163 // can potentially even drop/change decimals, causing the output not to
164 // match the input digits exactly, which is best to avoid
165 dec.UseNumber()
166
167 t, err := dec.Token()
168 if err == io.EOF {
169 return errors.New(`empty input isn't valid JSON`)
170 }
171 if err != nil {
172 return err
173 }
174
175 if err := handleToken(bw, dec, t, 0, 0); err != nil {
176 return err
177 }
178 // don't forget to end the last output line
179 bw.WriteByte('\n')
180
181 if _, err := dec.Token(); err != io.EOF {
182 return errors.New(`unexpected trailing JSON data`)
183 }
184 return nil
185 }
186
187 func handleToken(w *bufio.Writer, d *json.Decoder, t json.Token, pre, level int) error {
188 switch t := t.(type) {
189 case json.Delim:
190 switch t {
191 case json.Delim('['):
192 return handleArray(w, d, pre, level)
193
194 case json.Delim('{'):
195 return handleObject(w, d, pre, level)
196
197 default:
198 // return fmt.Errorf(`unsupported JSON delimiter %v`, t)
199 return errors.New(`unsupported JSON delimiter`)
200 }
201
202 case nil:
203 return handleNull(w, pre)
204
205 case bool:
206 return handleBoolean(w, t, pre)
207
208 case string:
209 return handleString(w, t, pre)
210
211 case json.Number:
212 return handleNumber(w, t, pre)
213
214 default:
215 // return fmt.Errorf(`unsupported token type %T`, t)
216 return errors.New(`unsupported token type`)
217 }
218 }
219
220 func handleArray(w *bufio.Writer, d *json.Decoder, pre, level int) error {
221 for i := 0; true; i++ {
222 t, err := d.Token()
223 if err == io.EOF {
224 return errors.New(`end of JSON before array was closed`)
225 }
226 if err != nil {
227 return err
228 }
229
230 if t == json.Delim(']') {
231 if i == 0 {
232 writeSpaces(w, indent*pre)
233 w.WriteString(syntaxStyle + "[]\x1b[0m")
234 } else {
235 w.WriteString("\n")
236 writeSpaces(w, indent*level)
237 w.WriteString(syntaxStyle + "]\x1b[0m")
238 }
239 return nil
240 }
241
242 if i == 0 {
243 writeSpaces(w, indent*pre)
244 w.WriteString(syntaxStyle + "[\x1b[0m\n")
245 } else {
246 // this is a good spot to check for early-quit opportunities
247 w.WriteString(syntaxStyle + ",\x1b[0m\n")
248 }
249
250 if err := handleToken(w, d, t, level+1, level+1); err != nil {
251 return err
252 }
253 }
254
255 // make the compiler happy
256 return nil
257 }
258
259 func handleBoolean(w *bufio.Writer, b bool, pre int) error {
260 writeSpaces(w, indent*pre)
261 if b {
262 w.WriteString(boolStyle + "true\x1b[0m")
263 } else {
264 w.WriteString(boolStyle + "false\x1b[0m")
265 }
266 return nil
267 }
268
269 func handleKey(w *bufio.Writer, s string, pre int) error {
270 writeSpaces(w, indent*pre)
271 w.WriteString(syntaxStyle + "\"\x1b[0m" + keyStyle)
272 w.WriteString(s)
273 w.WriteString(syntaxStyle + "\":\x1b[0m ")
274 return nil
275 }
276
277 func handleNull(w *bufio.Writer, pre int) error {
278 writeSpaces(w, indent*pre)
279 w.WriteString(nullStyle + "null\x1b[0m")
280 return nil
281 }
282
283 // func handleNumber(w *bufio.Writer, n json.Number, pre int) error {
284 // writeSpaces(w, indent*pre)
285 // w.WriteString(numberStyle)
286 // w.WriteString(n.String())
287 // w.WriteString("\x1b[0m")
288 // return nil
289 // }
290
291 func handleNumber(w *bufio.Writer, n json.Number, pre int) error {
292 writeSpaces(w, indent*pre)
293 f, _ := n.Float64()
294 if f > 0 {
295 w.WriteString(positiveNumberStyle)
296 } else if f < 0 {
297 w.WriteString(negativeNumberStyle)
298 } else {
299 w.WriteString(zeroNumberStyle)
300 }
301 w.WriteString(n.String())
302 w.WriteString("\x1b[0m")
303 return nil
304 }
305
306 func handleObject(w *bufio.Writer, d *json.Decoder, pre, level int) error {
307 for i := 0; true; i++ {
308 t, err := d.Token()
309 if err == io.EOF {
310 return errors.New(`end of JSON before object was closed`)
311 }
312 if err != nil {
313 return err
314 }
315
316 if t == json.Delim('}') {
317 if i == 0 {
318 writeSpaces(w, indent*pre)
319 w.WriteString(syntaxStyle + "{}\x1b[0m")
320 } else {
321 w.WriteString("\n")
322 writeSpaces(w, indent*level)
323 w.WriteString(syntaxStyle + "}\x1b[0m")
324 }
325 return nil
326 }
327
328 if i == 0 {
329 writeSpaces(w, indent*pre)
330 w.WriteString(syntaxStyle + "{\x1b[0m\n")
331 } else {
332 // this is a good spot to check for early-quit opportunities
333 w.WriteString(syntaxStyle + ",\x1b[0m\n")
334 }
335
336 // the stdlib's JSON parser is supposed to complain about non-string
337 // keys anyway, but make sure just in case
338 k, ok := t.(string)
339 if !ok {
340 return errors.New(`expected key to be a string`)
341 }
342 if err := handleKey(w, k, level+1); err != nil {
343 return err
344 }
345
346 // handle value
347 t, err = d.Token()
348 if err != nil {
349 return err
350 }
351 if err := handleToken(w, d, t, 0, level+1); err != nil {
352 return err
353 }
354 }
355
356 // make the compiler happy
357 return nil
358 }
359
360 func needsEscaping(s string) bool {
361 for _, r := range s {
362 switch r {
363 case '"', '\\', '\t', '\r', '\n':
364 return true
365 }
366 }
367 return false
368 }
369
370 func handleString(w *bufio.Writer, s string, pre int) error {
371 writeSpaces(w, indent*pre)
372 w.WriteString(syntaxStyle + "\"\x1b[0m" + stringStyle)
373 if !needsEscaping(s) {
374 w.WriteString(s)
375 } else {
376 escapeString(w, s)
377 }
378 w.WriteString(syntaxStyle + "\"\x1b[0m")
379 return nil
380 }
381
382 func escapeString(w *bufio.Writer, s string) {
383 for _, r := range s {
384 switch r {
385 case '"', '\\':
386 w.WriteByte('\\')
387 w.WriteRune(r)
388 case '\t':
389 w.WriteByte('\\')
390 w.WriteByte('t')
391 case '\r':
392 w.WriteByte('\\')
393 w.WriteByte('r')
394 case '\n':
395 w.WriteByte('\\')
396 w.WriteByte('n')
397 default:
398 w.WriteRune(r)
399 }
400 }
401 }
File: ./nl/nl.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package nl
26
27 import (
28 "bufio"
29 "io"
30 "os"
31 "strconv"
32 "strings"
33 )
34
35 const info = `
36 nl [options...] [files...]
37
38 Number Lines from all the inputs. When not given any filepaths, the standard
39 input is used instead. Only non-empty lines advance the line-counter; also,
40 empty lines don't show the leading counter.
41
42 Options
43
44 -h, -help show this help message
45 -v [number] change starting number to count lines with (default is 1)
46 `
47
48 func Main() {
49 start := 1
50
51 args := os.Args[1:]
52 for len(args) > 0 {
53 switch args[0] {
54 case `-v`:
55 args = args[1:]
56 if len(args) == 0 {
57 os.Stderr.WriteString("missing starting number\n")
58 os.Exit(1)
59 return
60 }
61
62 s := strings.Replace(args[0], `_`, ``, -1)
63 n, err := strconv.ParseInt(s, 10, 64)
64 if err != nil {
65 os.Stderr.WriteString(`nl: invalid number: `)
66 os.Stderr.WriteString(err.Error())
67 os.Stderr.WriteString("\n")
68 os.Exit(1)
69 return
70 }
71
72 args = args[1:]
73 start = int(n)
74 continue
75
76 case `--help`:
77 os.Stderr.WriteString(info[1:])
78 return
79 }
80
81 break
82 }
83
84 if len(args) > 0 && args[0] == `--` {
85 args = args[1:]
86 }
87
88 if err := run(args, start); err != nil && err != io.EOF {
89 os.Stderr.WriteString(`nl: `)
90 os.Stderr.WriteString(err.Error())
91 os.Stderr.WriteString("\n")
92 os.Exit(1)
93 return
94 }
95 }
96
97 func run(paths []string, count int) error {
98 w := bufio.NewWriterSize(os.Stdout, 32*1024)
99 defer w.Flush()
100
101 for _, path := range paths {
102 if err := handleFile(w, path, &count); err != nil {
103 return err
104 }
105 }
106
107 if len(paths) == 0 {
108 if err := nl(w, os.Stdin, &count); err != nil {
109 return err
110 }
111 }
112 return nil
113 }
114
115 func handleFile(w *bufio.Writer, path string, count *int) error {
116 f, err := os.Open(path)
117 if err != nil {
118 return err
119 }
120 defer f.Close()
121 return nl(w, f, count)
122 }
123
124 func nl(w *bufio.Writer, r io.Reader, count *int) error {
125 const gb = 1024 * 1024 * 1024
126 sc := bufio.NewScanner(r)
127 sc.Buffer(nil, 8*gb)
128
129 const spaces = ` `
130 var buf [24]byte
131
132 for sc.Scan() {
133 s := sc.Bytes()
134
135 // only number non-empty lines, just like the standard `nl` command
136 if len(s) > 0 {
137 n := strconv.AppendInt(buf[:0], int64(*count), 10)
138
139 // right-align the line-count
140 if len(n) < len(spaces) {
141 w.WriteString(spaces[:len(spaces)-len(n)])
142 }
143 w.Write(n)
144
145 // separate the line count and the line with a few spaces
146 w.WriteString(spaces[:2])
147 }
148
149 w.Write(s)
150 if w.WriteByte('\n') != nil {
151 return io.EOF
152 }
153
154 if len(s) > 0 {
155 *count++
156 }
157 }
158
159 return sc.Err()
160 }
File: ./nn/nn.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package nn
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 "strings"
34 )
35
36 const info = `
37 nn [options...] [file...]
38
39
40 Nice Numbers is an app which renders the UTF-8 text it's given to make long
41 numbers much easier to read. It does so by alternating 3-digit groups which
42 are colored using ANSI-codes with plain/unstyled 3-digit groups.
43
44 Unlike the common practice of inserting commas between 3-digit groups, this
45 trick doesn't widen the original text, keeping alignments across lines the
46 same.
47
48 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
49 feeds.
50
51 All (optional) leading options start with either single or double-dash,
52 and most of them change the style/color used. Some of the options are,
53 shown in their single-dash form:
54
55 -h, -help show this help message
56
57 -b use a blue color
58 -blue use a blue color
59 -bold bold-style digits
60 -g use a green color
61 -gray use a gray color (default)
62 -green use a green color
63 -hi use a highlighting/inverse style
64 -m use a magenta color
65 -magenta use a magenta color
66 -o use an orange color
67 -orange use an orange color
68 -r use a red color
69 -red use a red color
70 -u underline digits
71 -underline underline digits
72 `
73
74 type config struct {
75 // style is the ANSI-style sequence to use verbatim
76 style string
77
78 // live is whether lines are flushed each time
79 live bool
80 }
81
82 func Main() {
83 var cfg config
84 cfg.live = true
85 args := os.Args[1:]
86
87 for len(args) > 0 {
88 switch args[0] {
89 case `-b`, `--b`, `-buffered`, `--buffered`:
90 cfg.live = false
91 args = args[1:]
92 continue
93
94 case `-h`, `--h`, `-help`, `--help`:
95 os.Stdout.WriteString(info[1:])
96 return
97 }
98
99 break
100 }
101
102 options := true
103 if len(args) > 0 && args[0] == `--` {
104 options = false
105 args = args[1:]
106 }
107
108 cfg.style, _ = lookupStyle(`gray`)
109
110 // if the first argument is 1 or 2 dashes followed by a supported
111 // style-name, change the style used
112 if options && len(args) > 0 && strings.HasPrefix(args[0], `-`) {
113 name := args[0]
114 name = strings.TrimPrefix(name, `-`)
115 name = strings.TrimPrefix(name, `-`)
116 args = args[1:]
117
118 // check if the `dedashed` argument is a supported style-name
119 if s, ok := lookupStyle(name); ok {
120 cfg.style = s
121 } else {
122 os.Stderr.WriteString(`nn: invalid style name `)
123 os.Stderr.WriteString(name)
124 os.Stderr.WriteString("\n")
125 os.Exit(1)
126 return
127 }
128 }
129
130 if cfg.live {
131 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
132 cfg.live = false
133 }
134 }
135
136 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
137 os.Stderr.WriteString(`nn: `)
138 os.Stderr.WriteString(err.Error())
139 os.Stderr.WriteString("\n")
140 os.Exit(1)
141 return
142 }
143 }
144
145 func run(w io.Writer, args []string, cfg config) error {
146 bw := bufio.NewWriterSize(w, 32*1024)
147 defer bw.Flush()
148
149 if len(args) == 0 {
150 return restyle(bw, os.Stdin, cfg)
151 }
152
153 for _, name := range args {
154 if err := handleFile(bw, name, cfg); err != nil {
155 return err
156 }
157 }
158 return nil
159 }
160
161 func handleFile(w *bufio.Writer, name string, cfg config) error {
162 if name == `` || name == `-` {
163 return restyle(w, os.Stdin, cfg)
164 }
165
166 f, err := os.Open(name)
167 if err != nil {
168 return errors.New(`can't read from file named "` + name + `"`)
169 }
170 defer f.Close()
171
172 return restyle(w, f, cfg)
173 }
174
175 func restyle(w *bufio.Writer, r io.Reader, cfg config) error {
176 const gb = 1024 * 1024 * 1024
177 sc := bufio.NewScanner(r)
178 sc.Buffer(nil, 8*gb)
179
180 for i := 0; sc.Scan(); i++ {
181 s := sc.Bytes()
182 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
183 s = s[3:]
184 }
185
186 restyleLine(w, s, cfg.style)
187 if w.WriteByte('\n') != nil {
188 return io.EOF
189 }
190
191 if cfg.live && w.Flush() != nil {
192 // a write error may be the consequence of stdout being closed,
193 // perhaps by another app along a pipe
194 return io.EOF
195 }
196 }
197 return sc.Err()
198 }
199
200 func lookupStyle(name string) (style string, ok bool) {
201 if alias, ok := styleAliases[name]; ok {
202 name = alias
203 }
204
205 style, ok = styles[name]
206 return style, ok
207 }
208
209 var styleAliases = map[string]string{
210 `b`: `blue`,
211 `g`: `green`,
212 `m`: `magenta`,
213 `o`: `orange`,
214 `p`: `purple`,
215 `r`: `red`,
216 `u`: `underline`,
217
218 `bolded`: `bold`,
219 `h`: `inverse`,
220 `hi`: `inverse`,
221 `highlight`: `inverse`,
222 `highlighted`: `inverse`,
223 `hilite`: `inverse`,
224 `hilited`: `inverse`,
225 `inv`: `inverse`,
226 `invert`: `inverse`,
227 `inverted`: `inverse`,
228 `underlined`: `underline`,
229
230 `bb`: `blueback`,
231 `bg`: `greenback`,
232 `bm`: `magentaback`,
233 `bo`: `orangeback`,
234 `bp`: `purpleback`,
235 `br`: `redback`,
236
237 `gb`: `greenback`,
238 `mb`: `magentaback`,
239 `ob`: `orangeback`,
240 `pb`: `purpleback`,
241 `rb`: `redback`,
242
243 `bblue`: `blueback`,
244 `bgray`: `grayback`,
245 `bgreen`: `greenback`,
246 `bmagenta`: `magentaback`,
247 `borange`: `orangeback`,
248 `bpurple`: `purpleback`,
249 `bred`: `redback`,
250
251 `backblue`: `blueback`,
252 `backgray`: `grayback`,
253 `backgreen`: `greenback`,
254 `backmagenta`: `magentaback`,
255 `backorange`: `orangeback`,
256 `backpurple`: `purpleback`,
257 `backred`: `redback`,
258 }
259
260 // styles turns style-names into the ANSI-code sequences used for the
261 // alternate groups of digits
262 var styles = map[string]string{
263 `blue`: "\x1b[38;2;0;95;215m",
264 `bold`: "\x1b[1m",
265 `gray`: "\x1b[38;2;168;168;168m",
266 `green`: "\x1b[38;2;0;135;95m",
267 `inverse`: "\x1b[7m",
268 `magenta`: "\x1b[38;2;215;0;255m",
269 `orange`: "\x1b[38;2;215;95;0m",
270 `plain`: "\x1b[0m",
271 `red`: "\x1b[38;2;204;0;0m",
272 `underline`: "\x1b[4m",
273
274 // `blue`: "\x1b[38;5;26m",
275 // `bold`: "\x1b[1m",
276 // `gray`: "\x1b[38;5;248m",
277 // `green`: "\x1b[38;5;29m",
278 // `inverse`: "\x1b[7m",
279 // `magenta`: "\x1b[38;5;99m",
280 // `orange`: "\x1b[38;5;166m",
281 // `plain`: "\x1b[0m",
282 // `red`: "\x1b[31m",
283 // `underline`: "\x1b[4m",
284
285 `blueback`: "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
286 `grayback`: "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
287 `greenback`: "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
288 `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
289 `orangeback`: "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
290 `purpleback`: "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
291 `redback`: "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
292 }
293
294 // restyleLine renders the line given, using ANSI-styles to make any long
295 // numbers in it more legible; this func doesn't emit a line-feed, which
296 // is up to its caller
297 func restyleLine(w *bufio.Writer, line []byte, style string) {
298 for len(line) > 0 {
299 i := indexDigit(line)
300 if i < 0 {
301 // no (more) digits to style for sure
302 w.Write(line)
303 return
304 }
305
306 // emit line before current digit-run
307 w.Write(line[:i])
308 // advance to the start of the current digit-run
309 line = line[i:]
310
311 // see where the digit-run ends
312 j := indexNonDigit(line)
313 if j < 0 {
314 // the digit-run goes until the end
315 restyleDigits(w, line, style)
316 return
317 }
318
319 // emit styled digit-run
320 restyleDigits(w, line[:j], style)
321 // skip right past the end of the digit-run
322 line = line[j:]
323 }
324 }
325
326 // indexDigit finds the index of the first digit in a string, or -1 when the
327 // string has no decimal digits
328 func indexDigit(s []byte) int {
329 for i := 0; i < len(s); i++ {
330 switch s[i] {
331 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
332 return i
333 }
334 }
335
336 // empty slice, or a slice without any digits
337 return -1
338 }
339
340 // indexNonDigit finds the index of the first non-digit in a string, or -1
341 // when the string is all decimal digits
342 func indexNonDigit(s []byte) int {
343 for i := 0; i < len(s); i++ {
344 switch s[i] {
345 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
346 continue
347 default:
348 return i
349 }
350 }
351
352 // empty slice, or a slice which only has digits
353 return -1
354 }
355
356 // restyleDigits renders a run of digits as alternating styled/unstyled runs
357 // of 3 digits, which greatly improves readability, and is the only purpose
358 // of this app; string is assumed to be all decimal digits
359 func restyleDigits(w *bufio.Writer, digits []byte, altStyle string) {
360 if len(digits) < 4 {
361 // digit sequence is short, so emit it as is
362 w.Write(digits)
363 return
364 }
365
366 // separate leading 0..2 digits which don't align with the 3-digit groups
367 i := len(digits) % 3
368 // emit leading digits unstyled, if there are any
369 w.Write(digits[:i])
370 // the rest is guaranteed to have a length which is a multiple of 3
371 digits = digits[i:]
372
373 // start by styling, unless there were no leading digits
374 style := i != 0
375
376 for len(digits) > 0 {
377 if style {
378 w.WriteString(altStyle)
379 w.Write(digits[:3])
380 w.Write([]byte{'\x1b', '[', '0', 'm'})
381 } else {
382 w.Write(digits[:3])
383 }
384
385 // advance to the next triple: the start of this func is supposed
386 // to guarantee this step always works
387 digits = digits[3:]
388
389 // alternate between styled and unstyled 3-digit groups
390 style = !style
391 }
392 }
File: ./nn/nn_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package nn
26
27 import (
28 "bufio"
29 "strings"
30 "testing"
31 )
32
33 func TestRestyleLine(t *testing.T) {
34 var (
35 r = "\x1b[0m"
36 d = string(styles[`gray`])
37 )
38
39 tests := map[string]string{
40 ``: ``,
41 `abc`: `abc`,
42 ` abc 123456 `: ` abc 123` + d + `456` + r + ` `,
43 ` 123456789 text`: ` 123` + d + `456` + r + `789 text`,
44 `0`: `0`,
45 `01`: `01`,
46 `012`: `012`,
47 `0123`: `0` + d + `123` + r,
48 `01234`: `01` + d + `234` + r,
49 `012345`: `012` + d + `345` + r,
50 `0123456`: `0` + d + `123` + r + `456`,
51 `01234567`: `01` + d + `234` + r + `567`,
52 `012345678`: `012` + d + `345` + r + `678`,
53 `0123456789`: `0` + d + `123` + r + `456` + d + `789` + r,
54 `01234567890`: `01` + d + `234` + r + `567` + d + `890` + r,
55 `012345678901`: `012` + d + `345` + r + `678` + d + `901` + r,
56
57 `0123456789012`: `0` + d + `123` + r + `456` + d + `789` + r + `012`,
58 `00321`: `00` + d + `321` + r,
59 `123.456789`: `123.` + `456` + d + `789` + r,
60 `123456.123456`: `123` + d + `456` + r + `.` + `123` + d + `456` + r,
61 }
62
63 for input, expected := range tests {
64 t.Run(input, func(t *testing.T) {
65 var b strings.Builder
66 w := bufio.NewWriter(&b)
67 restyleLine(w, []byte(input), d)
68 w.Flush()
69
70 if got := b.String(); got != expected {
71 t.Fatalf(`expected %q, but got %q instead`, expected, got)
72 }
73 })
74 }
75 }
File: ./now/info.txt
1 now [options...] [timezones/places...]
2
3 Show the current date and time for the places given, along with the local
4 date/time.
5
6 All (optional) leading options start with either single or double-dash:
7
8 -h, -help show this help message
File: ./now/lookup.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package now
26
27 import (
28 "strings"
29 "time"
30 )
31
32 var aliases = map[string]string{
33 // `istanbul`: `Asia/Istanbul`,
34 `istanbul`: `Europe/Istanbul`,
35
36 // `nicosia`: `Asia/Nicosia`,
37 `nicosia`: `Europe/Nicosia`,
38
39 `africa/abidjan`: `Africa/Abidjan`,
40 `africa/accra`: `Africa/Accra`,
41 `africa/addis ababa`: `Africa/Addis_Ababa`,
42 `africa/algiers`: `Africa/Algiers`,
43 `africa/asmara`: `Africa/Asmara`,
44 `africa/bamako`: `Africa/Bamako`,
45 `africa/bangui`: `Africa/Bangui`,
46 `africa/banjul`: `Africa/Banjul`,
47 `africa/bissau`: `Africa/Bissau`,
48 `africa/blantyre`: `Africa/Blantyre`,
49 `africa/brazzaville`: `Africa/Brazzaville`,
50 `africa/bujumbura`: `Africa/Bujumbura`,
51 `africa/cairo`: `Africa/Cairo`,
52 `africa/casablanca`: `Africa/Casablanca`,
53 `africa/ceuta`: `Africa/Ceuta`,
54 `africa/conakry`: `Africa/Conakry`,
55 `africa/dakar`: `Africa/Dakar`,
56 `africa/dar es salaam`: `Africa/Dar_es_Salaam`,
57 `africa/djibouti`: `Africa/Djibouti`,
58 `africa/douala`: `Africa/Douala`,
59 `africa/el aaiun`: `Africa/El_Aaiun`,
60 `africa/freetown`: `Africa/Freetown`,
61 `africa/gaborone`: `Africa/Gaborone`,
62 `africa/harare`: `Africa/Harare`,
63 `africa/johannesburg`: `Africa/Johannesburg`,
64 `africa/juba`: `Africa/Juba`,
65 `africa/kampala`: `Africa/Kampala`,
66 `africa/khartoum`: `Africa/Khartoum`,
67 `africa/kigali`: `Africa/Kigali`,
68 `africa/kinshasa`: `Africa/Kinshasa`,
69 `africa/lagos`: `Africa/Lagos`,
70 `africa/libreville`: `Africa/Libreville`,
71 `africa/lome`: `Africa/Lome`,
72 `africa/luanda`: `Africa/Luanda`,
73 `africa/lubumbashi`: `Africa/Lubumbashi`,
74 `africa/lusaka`: `Africa/Lusaka`,
75 `africa/malabo`: `Africa/Malabo`,
76 `africa/maputo`: `Africa/Maputo`,
77 `africa/maseru`: `Africa/Maseru`,
78 `africa/mbabane`: `Africa/Mbabane`,
79 `africa/mogadishu`: `Africa/Mogadishu`,
80 `africa/monrovia`: `Africa/Monrovia`,
81 `africa/nairobi`: `Africa/Nairobi`,
82 `africa/ndjamena`: `Africa/Ndjamena`,
83 `africa/niamey`: `Africa/Niamey`,
84 `africa/nouakchott`: `Africa/Nouakchott`,
85 `africa/ouagadougou`: `Africa/Ouagadougou`,
86 `africa/porto-novo`: `Africa/Porto-Novo`,
87 `africa/sao tome`: `Africa/Sao_Tome`,
88 `africa/timbuktu`: `Africa/Timbuktu`,
89 `africa/tripoli`: `Africa/Tripoli`,
90 `africa/tunis`: `Africa/Tunis`,
91 `africa/windhoek`: `Africa/Windhoek`,
92 `america/adak`: `America/Adak`,
93 `america/anchorage`: `America/Anchorage`,
94 `america/anguilla`: `America/Anguilla`,
95 `america/antigua`: `America/Antigua`,
96 `america/araguaina`: `America/Araguaina`,
97 `america/argentina/buenos aires`: `America/Argentina/Buenos_Aires`,
98 `america/argentina/catamarca`: `America/Argentina/Catamarca`,
99 `america/argentina/cordoba`: `America/Argentina/Cordoba`,
100 `america/argentina/jujuy`: `America/Argentina/Jujuy`,
101 `america/argentina/la rioja`: `America/Argentina/La_Rioja`,
102 `america/argentina/mendoza`: `America/Argentina/Mendoza`,
103 `america/argentina/rio gallegos`: `America/Argentina/Rio_Gallegos`,
104 `america/argentina/salta`: `America/Argentina/Salta`,
105 `america/argentina/san juan`: `America/Argentina/San_Juan`,
106 `america/argentina/san luis`: `America/Argentina/San_Luis`,
107 `america/argentina/tucuman`: `America/Argentina/Tucuman`,
108 `america/argentina/ushuaia`: `America/Argentina/Ushuaia`,
109 `america/aruba`: `America/Aruba`,
110 `america/asuncion`: `America/Asuncion`,
111 `america/atikokan`: `America/Atikokan`,
112 `america/atka`: `America/Atka`,
113 `america/bahia`: `America/Bahia`,
114 `america/bahia banderas`: `America/Bahia_Banderas`,
115 `america/barbados`: `America/Barbados`,
116 `america/belem`: `America/Belem`,
117 `america/belize`: `America/Belize`,
118 `america/blanc-sablon`: `America/Blanc-Sablon`,
119 `america/boa vista`: `America/Boa_Vista`,
120 `america/bogota`: `America/Bogota`,
121 `america/boise`: `America/Boise`,
122 `america/cambridge bay`: `America/Cambridge_Bay`,
123 `america/campo grande`: `America/Campo_Grande`,
124 `america/cancun`: `America/Cancun`,
125 `america/caracas`: `America/Caracas`,
126 `america/cayenne`: `America/Cayenne`,
127 `america/cayman`: `America/Cayman`,
128 `america/chicago`: `America/Chicago`,
129 `america/chihuahua`: `America/Chihuahua`,
130 `america/ciudad juarez`: `America/Ciudad_Juarez`,
131 `america/coral harbour`: `America/Coral_Harbour`,
132 `america/costa rica`: `America/Costa_Rica`,
133 `america/coyhaique`: `America/Coyhaique`,
134 `america/creston`: `America/Creston`,
135 `america/cuiaba`: `America/Cuiaba`,
136 `america/curacao`: `America/Curacao`,
137 `america/danmarkshavn`: `America/Danmarkshavn`,
138 `america/dawson`: `America/Dawson`,
139 `america/dawson creek`: `America/Dawson_Creek`,
140 `america/denver`: `America/Denver`,
141 `america/detroit`: `America/Detroit`,
142 `america/dominica`: `America/Dominica`,
143 `america/edmonton`: `America/Edmonton`,
144 `america/eirunepe`: `America/Eirunepe`,
145 `america/el salvador`: `America/El_Salvador`,
146 `america/ensenada`: `America/Ensenada`,
147 `america/fort nelson`: `America/Fort_Nelson`,
148 `america/fortaleza`: `America/Fortaleza`,
149 `america/glace bay`: `America/Glace_Bay`,
150 `america/goose bay`: `America/Goose_Bay`,
151 `america/grand turk`: `America/Grand_Turk`,
152 `america/grenada`: `America/Grenada`,
153 `america/guadeloupe`: `America/Guadeloupe`,
154 `america/guatemala`: `America/Guatemala`,
155 `america/guayaquil`: `America/Guayaquil`,
156 `america/guyana`: `America/Guyana`,
157 `america/halifax`: `America/Halifax`,
158 `america/havana`: `America/Havana`,
159 `america/hermosillo`: `America/Hermosillo`,
160 `america/indiana/indianapolis`: `America/Indiana/Indianapolis`,
161 `america/indiana/knox`: `America/Indiana/Knox`,
162 `america/indiana/marengo`: `America/Indiana/Marengo`,
163 `america/indiana/petersburg`: `America/Indiana/Petersburg`,
164 `america/indiana/tell city`: `America/Indiana/Tell_City`,
165 `america/indiana/vevay`: `America/Indiana/Vevay`,
166 `america/indiana/vincennes`: `America/Indiana/Vincennes`,
167 `america/indiana/winamac`: `America/Indiana/Winamac`,
168 `america/inuvik`: `America/Inuvik`,
169 `america/iqaluit`: `America/Iqaluit`,
170 `america/jamaica`: `America/Jamaica`,
171 `america/juneau`: `America/Juneau`,
172 `america/kentucky/louisville`: `America/Kentucky/Louisville`,
173 `america/kentucky/monticello`: `America/Kentucky/Monticello`,
174 `america/kralendijk`: `America/Kralendijk`,
175 `america/la paz`: `America/La_Paz`,
176 `america/lima`: `America/Lima`,
177 `america/los angeles`: `America/Los_Angeles`,
178 `america/lower princes`: `America/Lower_Princes`,
179 `america/maceio`: `America/Maceio`,
180 `america/managua`: `America/Managua`,
181 `america/manaus`: `America/Manaus`,
182 `america/marigot`: `America/Marigot`,
183 `america/martinique`: `America/Martinique`,
184 `america/matamoros`: `America/Matamoros`,
185 `america/mazatlan`: `America/Mazatlan`,
186 `america/menominee`: `America/Menominee`,
187 `america/merida`: `America/Merida`,
188 `america/metlakatla`: `America/Metlakatla`,
189 `america/mexico city`: `America/Mexico_City`,
190 `america/miquelon`: `America/Miquelon`,
191 `america/moncton`: `America/Moncton`,
192 `america/monterrey`: `America/Monterrey`,
193 `america/montevideo`: `America/Montevideo`,
194 `america/montreal`: `America/Montreal`,
195 `america/montserrat`: `America/Montserrat`,
196 `america/nassau`: `America/Nassau`,
197 `america/new york`: `America/New_York`,
198 `america/nipigon`: `America/Nipigon`,
199 `america/nome`: `America/Nome`,
200 `america/noronha`: `America/Noronha`,
201 `america/north dakota/beulah`: `America/North_Dakota/Beulah`,
202 `america/north dakota/center`: `America/North_Dakota/Center`,
203 `america/north dakota/new salem`: `America/North_Dakota/New_Salem`,
204 `america/nuuk`: `America/Nuuk`,
205 `america/ojinaga`: `America/Ojinaga`,
206 `america/panama`: `America/Panama`,
207 `america/pangnirtung`: `America/Pangnirtung`,
208 `america/paramaribo`: `America/Paramaribo`,
209 `america/phoenix`: `America/Phoenix`,
210 `america/port-au-prince`: `America/Port-au-Prince`,
211 `america/port of spain`: `America/Port_of_Spain`,
212 `america/porto acre`: `America/Porto_Acre`,
213 `america/porto velho`: `America/Porto_Velho`,
214 `america/puerto rico`: `America/Puerto_Rico`,
215 `america/punta arenas`: `America/Punta_Arenas`,
216 `america/rainy river`: `America/Rainy_River`,
217 `america/rankin inlet`: `America/Rankin_Inlet`,
218 `america/recife`: `America/Recife`,
219 `america/regina`: `America/Regina`,
220 `america/resolute`: `America/Resolute`,
221 `america/rio branco`: `America/Rio_Branco`,
222 `america/santa isabel`: `America/Santa_Isabel`,
223 `america/santarem`: `America/Santarem`,
224 `america/santiago`: `America/Santiago`,
225 `america/santo domingo`: `America/Santo_Domingo`,
226 `america/sao paulo`: `America/Sao_Paulo`,
227 `america/scoresbysund`: `America/Scoresbysund`,
228 `america/shiprock`: `America/Shiprock`,
229 `america/sitka`: `America/Sitka`,
230 `america/st barthelemy`: `America/St_Barthelemy`,
231 `america/st johns`: `America/St_Johns`,
232 `america/st kitts`: `America/St_Kitts`,
233 `america/st lucia`: `America/St_Lucia`,
234 `america/st thomas`: `America/St_Thomas`,
235 `america/st vincent`: `America/St_Vincent`,
236 `america/swift current`: `America/Swift_Current`,
237 `america/tegucigalpa`: `America/Tegucigalpa`,
238 `america/thule`: `America/Thule`,
239 `america/thunder bay`: `America/Thunder_Bay`,
240 `america/tijuana`: `America/Tijuana`,
241 `america/toronto`: `America/Toronto`,
242 `america/tortola`: `America/Tortola`,
243 `america/vancouver`: `America/Vancouver`,
244 `america/virgin`: `America/Virgin`,
245 `america/whitehorse`: `America/Whitehorse`,
246 `america/winnipeg`: `America/Winnipeg`,
247 `america/yakutat`: `America/Yakutat`,
248 `america/yellowknife`: `America/Yellowknife`,
249 `antarctica/casey`: `Antarctica/Casey`,
250 `antarctica/davis`: `Antarctica/Davis`,
251 `antarctica/dumontdurville`: `Antarctica/DumontDUrville`,
252 `antarctica/macquarie`: `Antarctica/Macquarie`,
253 `antarctica/mawson`: `Antarctica/Mawson`,
254 `antarctica/mcmurdo`: `Antarctica/McMurdo`,
255 `antarctica/palmer`: `Antarctica/Palmer`,
256 `antarctica/rothera`: `Antarctica/Rothera`,
257 `antarctica/syowa`: `Antarctica/Syowa`,
258 `antarctica/troll`: `Antarctica/Troll`,
259 `antarctica/vostok`: `Antarctica/Vostok`,
260 `arctic/longyearbyen`: `Arctic/Longyearbyen`,
261 `asia/aden`: `Asia/Aden`,
262 `asia/almaty`: `Asia/Almaty`,
263 `asia/amman`: `Asia/Amman`,
264 `asia/anadyr`: `Asia/Anadyr`,
265 `asia/aqtau`: `Asia/Aqtau`,
266 `asia/aqtobe`: `Asia/Aqtobe`,
267 `asia/ashgabat`: `Asia/Ashgabat`,
268 `asia/atyrau`: `Asia/Atyrau`,
269 `asia/baghdad`: `Asia/Baghdad`,
270 `asia/bahrain`: `Asia/Bahrain`,
271 `asia/baku`: `Asia/Baku`,
272 `asia/bangkok`: `Asia/Bangkok`,
273 `asia/barnaul`: `Asia/Barnaul`,
274 `asia/beirut`: `Asia/Beirut`,
275 `asia/bishkek`: `Asia/Bishkek`,
276 `asia/brunei`: `Asia/Brunei`,
277 `asia/chita`: `Asia/Chita`,
278 `asia/chongqing`: `Asia/Chongqing`,
279 `asia/colombo`: `Asia/Colombo`,
280 `asia/damascus`: `Asia/Damascus`,
281 `asia/dhaka`: `Asia/Dhaka`,
282 `asia/dili`: `Asia/Dili`,
283 `asia/dubai`: `Asia/Dubai`,
284 `asia/dushanbe`: `Asia/Dushanbe`,
285 `asia/famagusta`: `Asia/Famagusta`,
286 `asia/gaza`: `Asia/Gaza`,
287 `asia/harbin`: `Asia/Harbin`,
288 `asia/hebron`: `Asia/Hebron`,
289 `asia/ho chi minh`: `Asia/Ho_Chi_Minh`,
290 `asia/hong kong`: `Asia/Hong_Kong`,
291 `asia/hovd`: `Asia/Hovd`,
292 `asia/irkutsk`: `Asia/Irkutsk`,
293 `asia/istanbul`: `Asia/Istanbul`,
294 `asia/jakarta`: `Asia/Jakarta`,
295 `asia/jayapura`: `Asia/Jayapura`,
296 `asia/jerusalem`: `Asia/Jerusalem`,
297 `asia/kabul`: `Asia/Kabul`,
298 `asia/kamchatka`: `Asia/Kamchatka`,
299 `asia/karachi`: `Asia/Karachi`,
300 `asia/kashgar`: `Asia/Kashgar`,
301 `asia/kathmandu`: `Asia/Kathmandu`,
302 `asia/khandyga`: `Asia/Khandyga`,
303 `asia/kolkata`: `Asia/Kolkata`,
304 `asia/krasnoyarsk`: `Asia/Krasnoyarsk`,
305 `asia/kuala lumpur`: `Asia/Kuala_Lumpur`,
306 `asia/kuching`: `Asia/Kuching`,
307 `asia/kuwait`: `Asia/Kuwait`,
308 `asia/macau`: `Asia/Macau`,
309 `asia/magadan`: `Asia/Magadan`,
310 `asia/makassar`: `Asia/Makassar`,
311 `asia/manila`: `Asia/Manila`,
312 `asia/muscat`: `Asia/Muscat`,
313 `asia/nicosia`: `Asia/Nicosia`,
314 `asia/novokuznetsk`: `Asia/Novokuznetsk`,
315 `asia/novosibirsk`: `Asia/Novosibirsk`,
316 `asia/omsk`: `Asia/Omsk`,
317 `asia/oral`: `Asia/Oral`,
318 `asia/phnom penh`: `Asia/Phnom_Penh`,
319 `asia/pontianak`: `Asia/Pontianak`,
320 `asia/pyongyang`: `Asia/Pyongyang`,
321 `asia/qatar`: `Asia/Qatar`,
322 `asia/qostanay`: `Asia/Qostanay`,
323 `asia/qyzylorda`: `Asia/Qyzylorda`,
324 `asia/riyadh`: `Asia/Riyadh`,
325 `asia/sakhalin`: `Asia/Sakhalin`,
326 `asia/samarkand`: `Asia/Samarkand`,
327 `asia/seoul`: `Asia/Seoul`,
328 `asia/shanghai`: `Asia/Shanghai`,
329 `asia/singapore`: `Asia/Singapore`,
330 `asia/srednekolymsk`: `Asia/Srednekolymsk`,
331 `asia/taipei`: `Asia/Taipei`,
332 `asia/tashkent`: `Asia/Tashkent`,
333 `asia/tbilisi`: `Asia/Tbilisi`,
334 `asia/tehran`: `Asia/Tehran`,
335 `asia/tel aviv`: `Asia/Tel_Aviv`,
336 `asia/thimphu`: `Asia/Thimphu`,
337 `asia/tokyo`: `Asia/Tokyo`,
338 `asia/tomsk`: `Asia/Tomsk`,
339 `asia/ulaanbaatar`: `Asia/Ulaanbaatar`,
340 `asia/urumqi`: `Asia/Urumqi`,
341 `asia/ust-nera`: `Asia/Ust-Nera`,
342 `asia/vientiane`: `Asia/Vientiane`,
343 `asia/vladivostok`: `Asia/Vladivostok`,
344 `asia/yakutsk`: `Asia/Yakutsk`,
345 `asia/yangon`: `Asia/Yangon`,
346 `asia/yekaterinburg`: `Asia/Yekaterinburg`,
347 `asia/yerevan`: `Asia/Yerevan`,
348 `atlantic/azores`: `Atlantic/Azores`,
349 `atlantic/bermuda`: `Atlantic/Bermuda`,
350 `atlantic/canary`: `Atlantic/Canary`,
351 `atlantic/cape verde`: `Atlantic/Cape_Verde`,
352 `atlantic/faroe`: `Atlantic/Faroe`,
353 `atlantic/jan mayen`: `Atlantic/Jan_Mayen`,
354 `atlantic/madeira`: `Atlantic/Madeira`,
355 `atlantic/reykjavik`: `Atlantic/Reykjavik`,
356 `atlantic/south georgia`: `Atlantic/South_Georgia`,
357 `atlantic/st helena`: `Atlantic/St_Helena`,
358 `atlantic/stanley`: `Atlantic/Stanley`,
359 `australia/adelaide`: `Australia/Adelaide`,
360 `australia/brisbane`: `Australia/Brisbane`,
361 `australia/broken hill`: `Australia/Broken_Hill`,
362 `australia/canberra`: `Australia/Canberra`,
363 `australia/currie`: `Australia/Currie`,
364 `australia/darwin`: `Australia/Darwin`,
365 `australia/eucla`: `Australia/Eucla`,
366 `australia/hobart`: `Australia/Hobart`,
367 `australia/lindeman`: `Australia/Lindeman`,
368 `australia/lord howe`: `Australia/Lord_Howe`,
369 `australia/melbourne`: `Australia/Melbourne`,
370 `australia/perth`: `Australia/Perth`,
371 `australia/sydney`: `Australia/Sydney`,
372 `australia/yancowinna`: `Australia/Yancowinna`,
373 `etc/greenwich`: `Etc/Greenwich`,
374 `etc/uct`: `Etc/UCT`,
375 `etc/utc`: `Etc/UTC`,
376 `etc/universal`: `Etc/Universal`,
377 `etc/zulu`: `Etc/Zulu`,
378 `europe/amsterdam`: `Europe/Amsterdam`,
379 `europe/andorra`: `Europe/Andorra`,
380 `europe/astrakhan`: `Europe/Astrakhan`,
381 `europe/athens`: `Europe/Athens`,
382 `europe/belfast`: `Europe/Belfast`,
383 `europe/belgrade`: `Europe/Belgrade`,
384 `europe/berlin`: `Europe/Berlin`,
385 `europe/bratislava`: `Europe/Bratislava`,
386 `europe/brussels`: `Europe/Brussels`,
387 `europe/bucharest`: `Europe/Bucharest`,
388 `europe/budapest`: `Europe/Budapest`,
389 `europe/busingen`: `Europe/Busingen`,
390 `europe/chisinau`: `Europe/Chisinau`,
391 `europe/copenhagen`: `Europe/Copenhagen`,
392 `europe/dublin`: `Europe/Dublin`,
393 `europe/gibraltar`: `Europe/Gibraltar`,
394 `europe/guernsey`: `Europe/Guernsey`,
395 `europe/helsinki`: `Europe/Helsinki`,
396 `europe/isle of man`: `Europe/Isle_of_Man`,
397 `europe/istanbul`: `Europe/Istanbul`,
398 `europe/jersey`: `Europe/Jersey`,
399 `europe/kaliningrad`: `Europe/Kaliningrad`,
400 `europe/kirov`: `Europe/Kirov`,
401 `europe/kyiv`: `Europe/Kyiv`,
402 `europe/lisbon`: `Europe/Lisbon`,
403 `europe/ljubljana`: `Europe/Ljubljana`,
404 `europe/london`: `Europe/London`,
405 `europe/luxembourg`: `Europe/Luxembourg`,
406 `europe/madrid`: `Europe/Madrid`,
407 `europe/malta`: `Europe/Malta`,
408 `europe/mariehamn`: `Europe/Mariehamn`,
409 `europe/minsk`: `Europe/Minsk`,
410 `europe/monaco`: `Europe/Monaco`,
411 `europe/moscow`: `Europe/Moscow`,
412 `europe/nicosia`: `Europe/Nicosia`,
413 `europe/oslo`: `Europe/Oslo`,
414 `europe/paris`: `Europe/Paris`,
415 `europe/podgorica`: `Europe/Podgorica`,
416 `europe/prague`: `Europe/Prague`,
417 `europe/riga`: `Europe/Riga`,
418 `europe/rome`: `Europe/Rome`,
419 `europe/samara`: `Europe/Samara`,
420 `europe/san marino`: `Europe/San_Marino`,
421 `europe/sarajevo`: `Europe/Sarajevo`,
422 `europe/saratov`: `Europe/Saratov`,
423 `europe/simferopol`: `Europe/Simferopol`,
424 `europe/skopje`: `Europe/Skopje`,
425 `europe/sofia`: `Europe/Sofia`,
426 `europe/stockholm`: `Europe/Stockholm`,
427 `europe/tallinn`: `Europe/Tallinn`,
428 `europe/tirane`: `Europe/Tirane`,
429 `europe/tiraspol`: `Europe/Tiraspol`,
430 `europe/ulyanovsk`: `Europe/Ulyanovsk`,
431 `europe/vaduz`: `Europe/Vaduz`,
432 `europe/vatican`: `Europe/Vatican`,
433 `europe/vienna`: `Europe/Vienna`,
434 `europe/vilnius`: `Europe/Vilnius`,
435 `europe/volgograd`: `Europe/Volgograd`,
436 `europe/warsaw`: `Europe/Warsaw`,
437 `europe/zagreb`: `Europe/Zagreb`,
438 `europe/zurich`: `Europe/Zurich`,
439 `indian/antananarivo`: `Indian/Antananarivo`,
440 `indian/chagos`: `Indian/Chagos`,
441 `indian/christmas`: `Indian/Christmas`,
442 `indian/cocos`: `Indian/Cocos`,
443 `indian/comoro`: `Indian/Comoro`,
444 `indian/kerguelen`: `Indian/Kerguelen`,
445 `indian/mahe`: `Indian/Mahe`,
446 `indian/maldives`: `Indian/Maldives`,
447 `indian/mauritius`: `Indian/Mauritius`,
448 `indian/mayotte`: `Indian/Mayotte`,
449 `indian/reunion`: `Indian/Reunion`,
450 `pacific/apia`: `Pacific/Apia`,
451 `pacific/auckland`: `Pacific/Auckland`,
452 `pacific/bougainville`: `Pacific/Bougainville`,
453 `pacific/chatham`: `Pacific/Chatham`,
454 `pacific/chuuk`: `Pacific/Chuuk`,
455 `pacific/easter`: `Pacific/Easter`,
456 `pacific/efate`: `Pacific/Efate`,
457 `pacific/fakaofo`: `Pacific/Fakaofo`,
458 `pacific/fiji`: `Pacific/Fiji`,
459 `pacific/funafuti`: `Pacific/Funafuti`,
460 `pacific/galapagos`: `Pacific/Galapagos`,
461 `pacific/gambier`: `Pacific/Gambier`,
462 `pacific/guadalcanal`: `Pacific/Guadalcanal`,
463 `pacific/guam`: `Pacific/Guam`,
464 `pacific/honolulu`: `Pacific/Honolulu`,
465 `pacific/johnston`: `Pacific/Johnston`,
466 `pacific/kanton`: `Pacific/Kanton`,
467 `pacific/kiritimati`: `Pacific/Kiritimati`,
468 `pacific/kosrae`: `Pacific/Kosrae`,
469 `pacific/kwajalein`: `Pacific/Kwajalein`,
470 `pacific/majuro`: `Pacific/Majuro`,
471 `pacific/marquesas`: `Pacific/Marquesas`,
472 `pacific/midway`: `Pacific/Midway`,
473 `pacific/nauru`: `Pacific/Nauru`,
474 `pacific/niue`: `Pacific/Niue`,
475 `pacific/norfolk`: `Pacific/Norfolk`,
476 `pacific/noumea`: `Pacific/Noumea`,
477 `pacific/pago pago`: `Pacific/Pago_Pago`,
478 `pacific/palau`: `Pacific/Palau`,
479 `pacific/pitcairn`: `Pacific/Pitcairn`,
480 `pacific/pohnpei`: `Pacific/Pohnpei`,
481 `pacific/port moresby`: `Pacific/Port_Moresby`,
482 `pacific/rarotonga`: `Pacific/Rarotonga`,
483 `pacific/saipan`: `Pacific/Saipan`,
484 `pacific/samoa`: `Pacific/Samoa`,
485 `pacific/tahiti`: `Pacific/Tahiti`,
486 `pacific/tarawa`: `Pacific/Tarawa`,
487 `pacific/tongatapu`: `Pacific/Tongatapu`,
488 `pacific/wake`: `Pacific/Wake`,
489 `pacific/wallis`: `Pacific/Wallis`,
490 `pacific/yap`: `Pacific/Yap`,
491 `abidjan`: `Africa/Abidjan`,
492 `accra`: `Africa/Accra`,
493 `addis ababa`: `Africa/Addis_Ababa`,
494 `algiers`: `Africa/Algiers`,
495 `asmara`: `Africa/Asmara`,
496 `bamako`: `Africa/Bamako`,
497 `bangui`: `Africa/Bangui`,
498 `banjul`: `Africa/Banjul`,
499 `bissau`: `Africa/Bissau`,
500 `blantyre`: `Africa/Blantyre`,
501 `brazzaville`: `Africa/Brazzaville`,
502 `bujumbura`: `Africa/Bujumbura`,
503 `cairo`: `Africa/Cairo`,
504 `casablanca`: `Africa/Casablanca`,
505 `ceuta`: `Africa/Ceuta`,
506 `conakry`: `Africa/Conakry`,
507 `dakar`: `Africa/Dakar`,
508 `dar es salaam`: `Africa/Dar_es_Salaam`,
509 `djibouti`: `Africa/Djibouti`,
510 `douala`: `Africa/Douala`,
511 `el aaiun`: `Africa/El_Aaiun`,
512 `freetown`: `Africa/Freetown`,
513 `gaborone`: `Africa/Gaborone`,
514 `harare`: `Africa/Harare`,
515 `johannesburg`: `Africa/Johannesburg`,
516 `juba`: `Africa/Juba`,
517 `kampala`: `Africa/Kampala`,
518 `khartoum`: `Africa/Khartoum`,
519 `kigali`: `Africa/Kigali`,
520 `kinshasa`: `Africa/Kinshasa`,
521 `lagos`: `Africa/Lagos`,
522 `libreville`: `Africa/Libreville`,
523 `lome`: `Africa/Lome`,
524 `luanda`: `Africa/Luanda`,
525 `lubumbashi`: `Africa/Lubumbashi`,
526 `lusaka`: `Africa/Lusaka`,
527 `malabo`: `Africa/Malabo`,
528 `maputo`: `Africa/Maputo`,
529 `maseru`: `Africa/Maseru`,
530 `mbabane`: `Africa/Mbabane`,
531 `mogadishu`: `Africa/Mogadishu`,
532 `monrovia`: `Africa/Monrovia`,
533 `nairobi`: `Africa/Nairobi`,
534 `ndjamena`: `Africa/Ndjamena`,
535 `niamey`: `Africa/Niamey`,
536 `nouakchott`: `Africa/Nouakchott`,
537 `ouagadougou`: `Africa/Ouagadougou`,
538 `porto-novo`: `Africa/Porto-Novo`,
539 `sao tome`: `Africa/Sao_Tome`,
540 `timbuktu`: `Africa/Timbuktu`,
541 `tripoli`: `Africa/Tripoli`,
542 `tunis`: `Africa/Tunis`,
543 `windhoek`: `Africa/Windhoek`,
544 `adak`: `America/Adak`,
545 `anchorage`: `America/Anchorage`,
546 `anguilla`: `America/Anguilla`,
547 `antigua`: `America/Antigua`,
548 `araguaina`: `America/Araguaina`,
549 `buenos aires`: `America/Argentina/Buenos_Aires`,
550 `catamarca`: `America/Argentina/Catamarca`,
551 `cordoba`: `America/Argentina/Cordoba`,
552 `jujuy`: `America/Argentina/Jujuy`,
553 `la rioja`: `America/Argentina/La_Rioja`,
554 `mendoza`: `America/Argentina/Mendoza`,
555 `rio gallegos`: `America/Argentina/Rio_Gallegos`,
556 `salta`: `America/Argentina/Salta`,
557 `san juan`: `America/Argentina/San_Juan`,
558 `san luis`: `America/Argentina/San_Luis`,
559 `tucuman`: `America/Argentina/Tucuman`,
560 `ushuaia`: `America/Argentina/Ushuaia`,
561 `aruba`: `America/Aruba`,
562 `asuncion`: `America/Asuncion`,
563 `atikokan`: `America/Atikokan`,
564 `atka`: `America/Atka`,
565 `bahia`: `America/Bahia`,
566 `bahia banderas`: `America/Bahia_Banderas`,
567 `barbados`: `America/Barbados`,
568 `belem`: `America/Belem`,
569 `belize`: `America/Belize`,
570 `blanc-sablon`: `America/Blanc-Sablon`,
571 `boa vista`: `America/Boa_Vista`,
572 `bogota`: `America/Bogota`,
573 `boise`: `America/Boise`,
574 `cambridge bay`: `America/Cambridge_Bay`,
575 `campo grande`: `America/Campo_Grande`,
576 `cancun`: `America/Cancun`,
577 `caracas`: `America/Caracas`,
578 `cayenne`: `America/Cayenne`,
579 `cayman`: `America/Cayman`,
580 `chicago`: `America/Chicago`,
581 `chihuahua`: `America/Chihuahua`,
582 `ciudad juarez`: `America/Ciudad_Juarez`,
583 `coral harbour`: `America/Coral_Harbour`,
584 `costa rica`: `America/Costa_Rica`,
585 `coyhaique`: `America/Coyhaique`,
586 `creston`: `America/Creston`,
587 `cuiaba`: `America/Cuiaba`,
588 `curacao`: `America/Curacao`,
589 `danmarkshavn`: `America/Danmarkshavn`,
590 `dawson`: `America/Dawson`,
591 `dawson creek`: `America/Dawson_Creek`,
592 `denver`: `America/Denver`,
593 `detroit`: `America/Detroit`,
594 `dominica`: `America/Dominica`,
595 `edmonton`: `America/Edmonton`,
596 `eirunepe`: `America/Eirunepe`,
597 `el salvador`: `America/El_Salvador`,
598 `ensenada`: `America/Ensenada`,
599 `fort nelson`: `America/Fort_Nelson`,
600 `fortaleza`: `America/Fortaleza`,
601 `glace bay`: `America/Glace_Bay`,
602 `goose bay`: `America/Goose_Bay`,
603 `grand turk`: `America/Grand_Turk`,
604 `grenada`: `America/Grenada`,
605 `guadeloupe`: `America/Guadeloupe`,
606 `guatemala`: `America/Guatemala`,
607 `guayaquil`: `America/Guayaquil`,
608 `guyana`: `America/Guyana`,
609 `halifax`: `America/Halifax`,
610 `havana`: `America/Havana`,
611 `hermosillo`: `America/Hermosillo`,
612 `indianapolis`: `America/Indiana/Indianapolis`,
613 `knox`: `America/Indiana/Knox`,
614 `marengo`: `America/Indiana/Marengo`,
615 `petersburg`: `America/Indiana/Petersburg`,
616 `tell city`: `America/Indiana/Tell_City`,
617 `vevay`: `America/Indiana/Vevay`,
618 `vincennes`: `America/Indiana/Vincennes`,
619 `winamac`: `America/Indiana/Winamac`,
620 `inuvik`: `America/Inuvik`,
621 `iqaluit`: `America/Iqaluit`,
622 `jamaica`: `America/Jamaica`,
623 `juneau`: `America/Juneau`,
624 `louisville`: `America/Kentucky/Louisville`,
625 `monticello`: `America/Kentucky/Monticello`,
626 `kralendijk`: `America/Kralendijk`,
627 `la paz`: `America/La_Paz`,
628 `lima`: `America/Lima`,
629 `los angeles`: `America/Los_Angeles`,
630 `lower princes`: `America/Lower_Princes`,
631 `maceio`: `America/Maceio`,
632 `managua`: `America/Managua`,
633 `manaus`: `America/Manaus`,
634 `marigot`: `America/Marigot`,
635 `martinique`: `America/Martinique`,
636 `matamoros`: `America/Matamoros`,
637 `mazatlan`: `America/Mazatlan`,
638 `menominee`: `America/Menominee`,
639 `merida`: `America/Merida`,
640 `metlakatla`: `America/Metlakatla`,
641 `mexico city`: `America/Mexico_City`,
642 `miquelon`: `America/Miquelon`,
643 `moncton`: `America/Moncton`,
644 `monterrey`: `America/Monterrey`,
645 `montevideo`: `America/Montevideo`,
646 `montreal`: `America/Montreal`,
647 `montserrat`: `America/Montserrat`,
648 `nassau`: `America/Nassau`,
649 `new york`: `America/New_York`,
650 `nipigon`: `America/Nipigon`,
651 `nome`: `America/Nome`,
652 `noronha`: `America/Noronha`,
653 `beulah`: `America/North_Dakota/Beulah`,
654 `center`: `America/North_Dakota/Center`,
655 `new salem`: `America/North_Dakota/New_Salem`,
656 `nuuk`: `America/Nuuk`,
657 `ojinaga`: `America/Ojinaga`,
658 `panama`: `America/Panama`,
659 `pangnirtung`: `America/Pangnirtung`,
660 `paramaribo`: `America/Paramaribo`,
661 `phoenix`: `America/Phoenix`,
662 `port-au-prince`: `America/Port-au-Prince`,
663 `port of spain`: `America/Port_of_Spain`,
664 `porto acre`: `America/Porto_Acre`,
665 `porto velho`: `America/Porto_Velho`,
666 `puerto rico`: `America/Puerto_Rico`,
667 `punta arenas`: `America/Punta_Arenas`,
668 `rainy river`: `America/Rainy_River`,
669 `rankin inlet`: `America/Rankin_Inlet`,
670 `recife`: `America/Recife`,
671 `regina`: `America/Regina`,
672 `resolute`: `America/Resolute`,
673 `rio branco`: `America/Rio_Branco`,
674 `santa isabel`: `America/Santa_Isabel`,
675 `santarem`: `America/Santarem`,
676 `santiago`: `America/Santiago`,
677 `santo domingo`: `America/Santo_Domingo`,
678 `sao paulo`: `America/Sao_Paulo`,
679 `scoresbysund`: `America/Scoresbysund`,
680 `shiprock`: `America/Shiprock`,
681 `sitka`: `America/Sitka`,
682 `st barthelemy`: `America/St_Barthelemy`,
683 `st johns`: `America/St_Johns`,
684 `st kitts`: `America/St_Kitts`,
685 `st lucia`: `America/St_Lucia`,
686 `st thomas`: `America/St_Thomas`,
687 `st vincent`: `America/St_Vincent`,
688 `swift current`: `America/Swift_Current`,
689 `tegucigalpa`: `America/Tegucigalpa`,
690 `thule`: `America/Thule`,
691 `thunder bay`: `America/Thunder_Bay`,
692 `tijuana`: `America/Tijuana`,
693 `toronto`: `America/Toronto`,
694 `tortola`: `America/Tortola`,
695 `vancouver`: `America/Vancouver`,
696 `virgin`: `America/Virgin`,
697 `whitehorse`: `America/Whitehorse`,
698 `winnipeg`: `America/Winnipeg`,
699 `yakutat`: `America/Yakutat`,
700 `yellowknife`: `America/Yellowknife`,
701 `casey`: `Antarctica/Casey`,
702 `davis`: `Antarctica/Davis`,
703 `dumontdurville`: `Antarctica/DumontDUrville`,
704 `macquarie`: `Antarctica/Macquarie`,
705 `mawson`: `Antarctica/Mawson`,
706 `mcmurdo`: `Antarctica/McMurdo`,
707 `palmer`: `Antarctica/Palmer`,
708 `rothera`: `Antarctica/Rothera`,
709 `syowa`: `Antarctica/Syowa`,
710 `troll`: `Antarctica/Troll`,
711 `vostok`: `Antarctica/Vostok`,
712 `longyearbyen`: `Arctic/Longyearbyen`,
713 `aden`: `Asia/Aden`,
714 `almaty`: `Asia/Almaty`,
715 `amman`: `Asia/Amman`,
716 `anadyr`: `Asia/Anadyr`,
717 `aqtau`: `Asia/Aqtau`,
718 `aqtobe`: `Asia/Aqtobe`,
719 `ashgabat`: `Asia/Ashgabat`,
720 `atyrau`: `Asia/Atyrau`,
721 `baghdad`: `Asia/Baghdad`,
722 `bahrain`: `Asia/Bahrain`,
723 `baku`: `Asia/Baku`,
724 `bangkok`: `Asia/Bangkok`,
725 `barnaul`: `Asia/Barnaul`,
726 `beirut`: `Asia/Beirut`,
727 `bishkek`: `Asia/Bishkek`,
728 `brunei`: `Asia/Brunei`,
729 `chita`: `Asia/Chita`,
730 `chongqing`: `Asia/Chongqing`,
731 `colombo`: `Asia/Colombo`,
732 `damascus`: `Asia/Damascus`,
733 `dhaka`: `Asia/Dhaka`,
734 `dili`: `Asia/Dili`,
735 `dubai`: `Asia/Dubai`,
736 `dushanbe`: `Asia/Dushanbe`,
737 `famagusta`: `Asia/Famagusta`,
738 `gaza`: `Asia/Gaza`,
739 `harbin`: `Asia/Harbin`,
740 `hebron`: `Asia/Hebron`,
741 `ho chi minh`: `Asia/Ho_Chi_Minh`,
742 `hong kong`: `Asia/Hong_Kong`,
743 `hovd`: `Asia/Hovd`,
744 `irkutsk`: `Asia/Irkutsk`,
745 `jakarta`: `Asia/Jakarta`,
746 `jayapura`: `Asia/Jayapura`,
747 `jerusalem`: `Asia/Jerusalem`,
748 `kabul`: `Asia/Kabul`,
749 `kamchatka`: `Asia/Kamchatka`,
750 `karachi`: `Asia/Karachi`,
751 `kashgar`: `Asia/Kashgar`,
752 `kathmandu`: `Asia/Kathmandu`,
753 `khandyga`: `Asia/Khandyga`,
754 `kolkata`: `Asia/Kolkata`,
755 `krasnoyarsk`: `Asia/Krasnoyarsk`,
756 `kuala lumpur`: `Asia/Kuala_Lumpur`,
757 `kuching`: `Asia/Kuching`,
758 `kuwait`: `Asia/Kuwait`,
759 `macau`: `Asia/Macau`,
760 `magadan`: `Asia/Magadan`,
761 `makassar`: `Asia/Makassar`,
762 `manila`: `Asia/Manila`,
763 `muscat`: `Asia/Muscat`,
764 `novokuznetsk`: `Asia/Novokuznetsk`,
765 `novosibirsk`: `Asia/Novosibirsk`,
766 `omsk`: `Asia/Omsk`,
767 `oral`: `Asia/Oral`,
768 `phnom penh`: `Asia/Phnom_Penh`,
769 `pontianak`: `Asia/Pontianak`,
770 `pyongyang`: `Asia/Pyongyang`,
771 `qatar`: `Asia/Qatar`,
772 `qostanay`: `Asia/Qostanay`,
773 `qyzylorda`: `Asia/Qyzylorda`,
774 `riyadh`: `Asia/Riyadh`,
775 `sakhalin`: `Asia/Sakhalin`,
776 `samarkand`: `Asia/Samarkand`,
777 `seoul`: `Asia/Seoul`,
778 `shanghai`: `Asia/Shanghai`,
779 `singapore`: `Asia/Singapore`,
780 `srednekolymsk`: `Asia/Srednekolymsk`,
781 `taipei`: `Asia/Taipei`,
782 `tashkent`: `Asia/Tashkent`,
783 `tbilisi`: `Asia/Tbilisi`,
784 `tehran`: `Asia/Tehran`,
785 `tel aviv`: `Asia/Tel_Aviv`,
786 `thimphu`: `Asia/Thimphu`,
787 `tokyo`: `Asia/Tokyo`,
788 `tomsk`: `Asia/Tomsk`,
789 `ulaanbaatar`: `Asia/Ulaanbaatar`,
790 `urumqi`: `Asia/Urumqi`,
791 `ust-nera`: `Asia/Ust-Nera`,
792 `vientiane`: `Asia/Vientiane`,
793 `vladivostok`: `Asia/Vladivostok`,
794 `yakutsk`: `Asia/Yakutsk`,
795 `yangon`: `Asia/Yangon`,
796 `yekaterinburg`: `Asia/Yekaterinburg`,
797 `yerevan`: `Asia/Yerevan`,
798 `azores`: `Atlantic/Azores`,
799 `bermuda`: `Atlantic/Bermuda`,
800 `canary`: `Atlantic/Canary`,
801 `cape verde`: `Atlantic/Cape_Verde`,
802 `faroe`: `Atlantic/Faroe`,
803 `jan mayen`: `Atlantic/Jan_Mayen`,
804 `madeira`: `Atlantic/Madeira`,
805 `reykjavik`: `Atlantic/Reykjavik`,
806 `south georgia`: `Atlantic/South_Georgia`,
807 `st helena`: `Atlantic/St_Helena`,
808 `stanley`: `Atlantic/Stanley`,
809 `adelaide`: `Australia/Adelaide`,
810 `brisbane`: `Australia/Brisbane`,
811 `broken hill`: `Australia/Broken_Hill`,
812 `canberra`: `Australia/Canberra`,
813 `currie`: `Australia/Currie`,
814 `darwin`: `Australia/Darwin`,
815 `eucla`: `Australia/Eucla`,
816 `hobart`: `Australia/Hobart`,
817 `lindeman`: `Australia/Lindeman`,
818 `lord howe`: `Australia/Lord_Howe`,
819 `melbourne`: `Australia/Melbourne`,
820 `perth`: `Australia/Perth`,
821 `sydney`: `Australia/Sydney`,
822 `yancowinna`: `Australia/Yancowinna`,
823 `greenwich`: `Etc/Greenwich`,
824 `uct`: `Etc/UCT`,
825 `utc`: `Etc/UTC`,
826 `universal`: `Etc/Universal`,
827 `zulu`: `Etc/Zulu`,
828 `amsterdam`: `Europe/Amsterdam`,
829 `andorra`: `Europe/Andorra`,
830 `astrakhan`: `Europe/Astrakhan`,
831 `athens`: `Europe/Athens`,
832 `belfast`: `Europe/Belfast`,
833 `belgrade`: `Europe/Belgrade`,
834 `berlin`: `Europe/Berlin`,
835 `bratislava`: `Europe/Bratislava`,
836 `brussels`: `Europe/Brussels`,
837 `bucharest`: `Europe/Bucharest`,
838 `budapest`: `Europe/Budapest`,
839 `busingen`: `Europe/Busingen`,
840 `chisinau`: `Europe/Chisinau`,
841 `copenhagen`: `Europe/Copenhagen`,
842 `dublin`: `Europe/Dublin`,
843 `gibraltar`: `Europe/Gibraltar`,
844 `guernsey`: `Europe/Guernsey`,
845 `helsinki`: `Europe/Helsinki`,
846 `isle of man`: `Europe/Isle_of_Man`,
847 `jersey`: `Europe/Jersey`,
848 `kaliningrad`: `Europe/Kaliningrad`,
849 `kirov`: `Europe/Kirov`,
850 `kyiv`: `Europe/Kyiv`,
851 `lisbon`: `Europe/Lisbon`,
852 `ljubljana`: `Europe/Ljubljana`,
853 `london`: `Europe/London`,
854 `luxembourg`: `Europe/Luxembourg`,
855 `madrid`: `Europe/Madrid`,
856 `malta`: `Europe/Malta`,
857 `mariehamn`: `Europe/Mariehamn`,
858 `minsk`: `Europe/Minsk`,
859 `monaco`: `Europe/Monaco`,
860 `moscow`: `Europe/Moscow`,
861 `oslo`: `Europe/Oslo`,
862 `paris`: `Europe/Paris`,
863 `podgorica`: `Europe/Podgorica`,
864 `prague`: `Europe/Prague`,
865 `riga`: `Europe/Riga`,
866 `rome`: `Europe/Rome`,
867 `samara`: `Europe/Samara`,
868 `san marino`: `Europe/San_Marino`,
869 `sarajevo`: `Europe/Sarajevo`,
870 `saratov`: `Europe/Saratov`,
871 `simferopol`: `Europe/Simferopol`,
872 `skopje`: `Europe/Skopje`,
873 `sofia`: `Europe/Sofia`,
874 `stockholm`: `Europe/Stockholm`,
875 `tallinn`: `Europe/Tallinn`,
876 `tirane`: `Europe/Tirane`,
877 `tiraspol`: `Europe/Tiraspol`,
878 `ulyanovsk`: `Europe/Ulyanovsk`,
879 `vaduz`: `Europe/Vaduz`,
880 `vatican`: `Europe/Vatican`,
881 `vienna`: `Europe/Vienna`,
882 `vilnius`: `Europe/Vilnius`,
883 `volgograd`: `Europe/Volgograd`,
884 `warsaw`: `Europe/Warsaw`,
885 `zagreb`: `Europe/Zagreb`,
886 `zurich`: `Europe/Zurich`,
887 `antananarivo`: `Indian/Antananarivo`,
888 `chagos`: `Indian/Chagos`,
889 `christmas`: `Indian/Christmas`,
890 `cocos`: `Indian/Cocos`,
891 `comoro`: `Indian/Comoro`,
892 `kerguelen`: `Indian/Kerguelen`,
893 `mahe`: `Indian/Mahe`,
894 `maldives`: `Indian/Maldives`,
895 `mauritius`: `Indian/Mauritius`,
896 `mayotte`: `Indian/Mayotte`,
897 `reunion`: `Indian/Reunion`,
898 `apia`: `Pacific/Apia`,
899 `auckland`: `Pacific/Auckland`,
900 `bougainville`: `Pacific/Bougainville`,
901 `chatham`: `Pacific/Chatham`,
902 `chuuk`: `Pacific/Chuuk`,
903 `easter`: `Pacific/Easter`,
904 `efate`: `Pacific/Efate`,
905 `fakaofo`: `Pacific/Fakaofo`,
906 `fiji`: `Pacific/Fiji`,
907 `funafuti`: `Pacific/Funafuti`,
908 `galapagos`: `Pacific/Galapagos`,
909 `gambier`: `Pacific/Gambier`,
910 `guadalcanal`: `Pacific/Guadalcanal`,
911 `guam`: `Pacific/Guam`,
912 `honolulu`: `Pacific/Honolulu`,
913 `johnston`: `Pacific/Johnston`,
914 `kanton`: `Pacific/Kanton`,
915 `kiritimati`: `Pacific/Kiritimati`,
916 `kosrae`: `Pacific/Kosrae`,
917 `kwajalein`: `Pacific/Kwajalein`,
918 `majuro`: `Pacific/Majuro`,
919 `marquesas`: `Pacific/Marquesas`,
920 `midway`: `Pacific/Midway`,
921 `nauru`: `Pacific/Nauru`,
922 `niue`: `Pacific/Niue`,
923 `norfolk`: `Pacific/Norfolk`,
924 `noumea`: `Pacific/Noumea`,
925 `pago pago`: `Pacific/Pago_Pago`,
926 `palau`: `Pacific/Palau`,
927 `pitcairn`: `Pacific/Pitcairn`,
928 `pohnpei`: `Pacific/Pohnpei`,
929 `port moresby`: `Pacific/Port_Moresby`,
930 `rarotonga`: `Pacific/Rarotonga`,
931 `saipan`: `Pacific/Saipan`,
932 `samoa`: `Pacific/Samoa`,
933 `tahiti`: `Pacific/Tahiti`,
934 `tarawa`: `Pacific/Tarawa`,
935 `tongatapu`: `Pacific/Tongatapu`,
936 `wake`: `Pacific/Wake`,
937 `wallis`: `Pacific/Wallis`,
938 `yap`: `Pacific/Yap`,
939 }
940
941 // Lookup tries to find a timezone from the place/city name given
942 func Lookup(place string) (*time.Location, error) {
943 if loc, err := time.LoadLocation(place); err == nil {
944 return loc, err
945 }
946
947 if s, ok := lookupAlias(place); ok {
948 place = s
949 }
950
951 loc, err := time.LoadLocation(place)
952 return loc, err
953 }
954
955 // LookupName tries to find a timezone name from the place/city name given
956 func LookupName(place string) (string, bool) {
957 if s, ok := lookupAlias(place); ok {
958 return s, true
959 }
960
961 for _, s := range aliases {
962 if strings.EqualFold(place, s) {
963 return s, true
964 }
965 }
966 return place, false
967 }
968
969 // lookupAlias tries to find a timezone alias from the place/city name given
970 func lookupAlias(place string) (string, bool) {
971 key := strings.ToLower(place)
972 key = strings.ReplaceAll(key, `_`, ` `)
973 key = strings.ReplaceAll(key, `-`, ` `)
974
975 if s, ok := aliases[key]; ok {
976 return s, true
977 }
978 return place, false
979 }
File: ./now/main.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package now
26
27 import (
28 "fmt"
29 "io"
30 "os"
31 "time"
32
33 _ "embed"
34 )
35
36 //go:embed info.txt
37 var info string
38
39 const timeFormat = `2006-01-02 15:04:05 Mon Jan 02`
40
41 func Main() {
42 args := os.Args[1:]
43 if len(args) > 0 {
44 switch args[0] {
45 case `-h`, `--h`, `-help`, `--help`:
46 os.Stdout.WriteString(info[1:])
47 return
48 }
49 }
50
51 if len(args) > 0 && args[0] == `--` {
52 args = args[1:]
53 }
54
55 ok := true
56 w := os.Stdout
57
58 now := time.Now()
59 showDateTime(w, now)
60 place := now.Location().String()
61 fmt.Fprintf(w, " %s\n", place)
62
63 for _, place := range args {
64 loc, err := Lookup(place)
65 if err != nil {
66 fmt.Fprintln(os.Stderr, err.Error())
67 ok = false
68 continue
69 }
70
71 showDateTime(w, now.In(loc))
72 fmt.Fprintf(w, " %s\n", place)
73 }
74
75 if !ok {
76 os.Exit(1)
77 return
78 }
79 }
80
81 func showDateTime(w io.Writer, t time.Time) {
82 var buf [64]byte
83 w.Write(t.AppendFormat(buf[:0], timeFormat))
84 }
File: ./nts/nts.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package nts
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "os/exec"
33 "strings"
34 "time"
35 )
36
37 const info = `
38 nts [options...] [command...] [arguments...]
39
40 Nice TimeStamp emits each input line, starting it with an ANSI-style date/time
41 string and a tab. When given a command, it runs the command, timestamping its
42 resulting standard-output lines as they become available.
43
44 When not given a command, it timestamps lines from its standard input, as they
45 become available.
46
47 All (optional) leading options start with either single or double-dash:
48
49 -h, -help show this help message
50 -p, -plain don't style timestamps with ANSI-codes
51 `
52
53 func Main() {
54 style := "\x1b[48;2;218;218;218m\x1b[38;2;0;95;153m"
55 args := os.Args[1:]
56
57 if len(args) > 0 {
58 switch args[0] {
59 case `-h`, `--h`, `-help`, `--help`:
60 os.Stdout.WriteString(info[1:])
61 return
62
63 case `-p`, `--p`, `-plain`, `--plain`:
64 style = ``
65 args = args[1:]
66 }
67 }
68
69 options := true
70 if len(args) > 0 && args[0] == `--` {
71 options = false
72 args = args[1:]
73 }
74
75 // if the first argument is 1 or 2 dashes followed by a supported
76 // style-name, change the style used
77 if options && len(args) > 0 && strings.HasPrefix(args[0], `-`) {
78 name := args[0]
79 name = strings.TrimPrefix(name, `-`)
80 name = strings.TrimPrefix(name, `-`)
81 args = args[1:]
82
83 // check if the `dedashed` argument is a supported style-name
84 if s, ok := lookupStyle(name); ok {
85 style = s
86 } else {
87 os.Stderr.WriteString(`nts: invalid style name `)
88 os.Stderr.WriteString(name)
89 os.Stderr.WriteString("\n")
90 os.Exit(1)
91 return
92 }
93 }
94
95 err := run(os.Stdout, os.Stderr, os.Stdin, args, style)
96 if ee, ok := err.(*exec.ExitError); ok {
97 os.Exit(ee.ExitCode())
98 return
99 }
100 if err != nil && err != io.EOF {
101 os.Stderr.WriteString(`nts: `)
102 os.Stderr.WriteString(err.Error())
103 os.Stderr.WriteString("\n")
104 os.Exit(1)
105 return
106 }
107 }
108
109 func run(w1, w2 io.Writer, r io.Reader, args []string, style string) error {
110 bw1 := bufio.NewWriterSize(w1, 32*1024)
111 defer bw1.Flush()
112
113 if len(args) == 0 {
114 return timestamp(bw1, r, style)
115 }
116
117 bw2 := bufio.NewWriterSize(w2, 32*1024)
118 defer bw2.Flush()
119
120 ts := timestamper{w: bw2, style: style}
121 defer ts.Close()
122
123 cmd := exec.Command(args[0], args[1:]...)
124 cmd.Stdin = r
125 cmd.Stdout = &ts
126
127 if err := cmd.Start(); err != nil {
128 return err
129 }
130
131 return cmd.Wait()
132 }
133
134 func timestamp(w *bufio.Writer, r io.Reader, style string) error {
135 const gb = 1024 * 1024 * 1024
136 sc := bufio.NewScanner(r)
137 sc.Buffer(nil, 8*gb)
138
139 for i := 0; sc.Scan(); i++ {
140 s := sc.Bytes()
141 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
142 s = s[3:]
143 }
144
145 writeTimestamp(w, time.Now(), style)
146 w.WriteByte('\t')
147 w.Write(s)
148 w.WriteByte('\n')
149
150 if w.Flush() != nil {
151 // a write error may be the consequence of stdout being closed,
152 // perhaps by another app along a pipe
153 return io.EOF
154 }
155 }
156
157 return sc.Err()
158 }
159
160 func writeTimestamp(w *bufio.Writer, when time.Time, style string) {
161 var buf [64]byte
162 timestamp := when.AppendFormat(buf[:0], `2006-01-02 15:04:05`)
163
164 if len(style) == 0 {
165 w.Write(timestamp)
166 return
167 }
168
169 w.WriteString(style)
170 w.Write(timestamp)
171 w.WriteString("\x1b[0m")
172 }
173
174 func lookupStyle(name string) (style string, ok bool) {
175 if alias, ok := styleAliases[name]; ok {
176 name = alias
177 }
178
179 style, ok = styles[name]
180 return style, ok
181 }
182
183 var styleAliases = map[string]string{
184 `b`: `blue`,
185 `g`: `green`,
186 `m`: `magenta`,
187 `o`: `orange`,
188 `p`: `purple`,
189 `r`: `red`,
190 `u`: `underline`,
191
192 `bolded`: `bold`,
193 `h`: `inverse`,
194 `hi`: `inverse`,
195 `highlight`: `inverse`,
196 `highlighted`: `inverse`,
197 `hilite`: `inverse`,
198 `hilited`: `inverse`,
199 `inv`: `inverse`,
200 `invert`: `inverse`,
201 `inverted`: `inverse`,
202 `underlined`: `underline`,
203
204 `bb`: `blueback`,
205 `bg`: `greenback`,
206 `bm`: `magentaback`,
207 `bo`: `orangeback`,
208 `bp`: `purpleback`,
209 `br`: `redback`,
210
211 `gb`: `greenback`,
212 `mb`: `magentaback`,
213 `ob`: `orangeback`,
214 `pb`: `purpleback`,
215 `rb`: `redback`,
216
217 `bblue`: `blueback`,
218 `bgray`: `grayback`,
219 `bgreen`: `greenback`,
220 `bmagenta`: `magentaback`,
221 `borange`: `orangeback`,
222 `bpurple`: `purpleback`,
223 `bred`: `redback`,
224
225 `backblue`: `blueback`,
226 `backgray`: `grayback`,
227 `backgreen`: `greenback`,
228 `backmagenta`: `magentaback`,
229 `backorange`: `orangeback`,
230 `backpurple`: `purpleback`,
231 `backred`: `redback`,
232 }
233
234 // styles turns style-names into the ANSI-code sequences used for the
235 // alternate groups of digits
236 var styles = map[string]string{
237 `blue`: "\x1b[38;2;0;95;215m",
238 `bold`: "\x1b[1m",
239 `gray`: "\x1b[38;2;168;168;168m",
240 `green`: "\x1b[38;2;0;135;95m",
241 `inverse`: "\x1b[7m",
242 `magenta`: "\x1b[38;2;215;0;255m",
243 `none`: ``,
244 `orange`: "\x1b[38;2;215;95;0m",
245 `plain`: ``,
246 `red`: "\x1b[38;2;204;0;0m",
247 `underline`: "\x1b[4m",
248 `unstyled`: ``,
249
250 // `blue`: "\x1b[38;5;26m",
251 // `bold`: "\x1b[1m",
252 // `gray`: "\x1b[38;5;248m",
253 // `green`: "\x1b[38;5;29m",
254 // `inverse`: "\x1b[7m",
255 // `magenta`: "\x1b[38;5;99m",
256 // `orange`: "\x1b[38;5;166m",
257 // `plain`: "\x1b[0m",
258 // `red`: "\x1b[31m",
259 // `underline`: "\x1b[4m",
260
261 `blueback`: "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
262 `grayback`: "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
263 `greenback`: "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
264 `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
265 `orangeback`: "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
266 `purpleback`: "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
267 `redback`: "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
268 }
269
270 // timestamper is a writer which adds timestamps at the starts of lines; it
271 // assumes that the chunks given to its func Write are sequential fragments
272 // of text lines
273 type timestamper struct {
274 // w is the destination writer
275 w *bufio.Writer
276
277 // style is the ANSI-code to style timestamps with
278 style string
279
280 // last is to remember if the last byte was a line-feed
281 last byte
282
283 // anything is whether any bytes have been written yet: this both allows
284 // emitting the first line's timestamp, and to reliably check if an extra
285 // line-feed byte is needed at the end of the last line (in func Close)
286 anything bool
287 }
288
289 // Write precedes/starts each line with a timestamp and a tab
290 func (ts *timestamper) Write(p []byte) (n int, err error) {
291 var when time.Time
292 // flush tracks the presence of line-feeds, to flush the writer if so
293 flush := false
294 // timed avoids calling time.Now more than once
295 timed := false
296
297 if len(p) > 0 {
298 ts.last = p[len(p)-1]
299 }
300
301 if !ts.anything && len(p) > 0 {
302 when = time.Now()
303 timed = true
304
305 writeTimestamp(ts.w, when, ts.style)
306 ts.w.WriteByte('\t')
307 ts.anything = true
308 }
309
310 for len(p) > 0 {
311 i := bytes.IndexByte(p, '\n')
312 if i < 0 {
313 m, err := ts.w.Write(p)
314 n += m
315 if err != nil {
316 return n, err
317 }
318 break
319 }
320
321 m, err := ts.w.Write(p[:i+1])
322 n += m
323 if err != nil {
324 return n, err
325 }
326
327 flush = true
328
329 // don't add a timestamp at the very end of a chunk, since the next
330 // line will also start with its own timestamp, on the next Write
331 if i == len(p)-1 {
332 break
333 }
334
335 if !timed {
336 when = time.Now()
337 timed = true
338 }
339
340 writeTimestamp(ts.w, when, ts.style)
341 ts.w.WriteByte('\t')
342
343 p = p[i+1:]
344 }
345
346 if flush {
347 err = ts.w.Flush()
348 }
349 return n, err
350 }
351
352 // Close ensures non-empty output streams always end with a line-feed
353 func (ts *timestamper) Close() error {
354 if !ts.anything || ts.last == '\n' {
355 return nil
356 }
357 if err := ts.w.WriteByte('\n'); err != nil {
358 return err
359 }
360 return ts.w.Flush()
361 }
File: ./null/null.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package null
26
27 import (
28 "io"
29 "io/fs"
30 "os"
31 "os/exec"
32 )
33
34 const info = `
35 nil [options...] [command...] [args...]
36
37 Emit nothing to output and/or discard everything from input: this tool can
38 be used at either end of a pipe, or as a command-runner which discards the
39 command's standard output, but not its standard error.
40
41 The options are, available both in single and double-dash versions
42
43 -h, -help show this help message
44 `
45
46 func Main() {
47 args := os.Args[1:]
48
49 if len(args) > 0 {
50 switch args[0] {
51 case `-h`, `--h`, `-help`, `--help`:
52 os.Stdout.WriteString(info[1:])
53 return
54 }
55 }
56
57 if len(args) > 0 && args[0] == `--` {
58 args = args[1:]
59 }
60
61 if len(args) > 0 {
62 cmd := exec.Command(args[0], args[1:]...)
63 cmd.Stdin = os.Stdin
64 cmd.Stdout = io.Discard
65 cmd.Stderr = os.Stderr
66
67 if err := cmd.Start(); err != nil {
68 os.Stderr.WriteString(`nil: `)
69 os.Stderr.WriteString(err.Error())
70 os.Stderr.WriteString("\n")
71 os.Exit(1)
72 return
73 }
74
75 err := cmd.Wait()
76 if _, ok := err.(*exec.ExitError); ok {
77 err = nil
78 }
79
80 if err != nil {
81 os.Stderr.WriteString(`nil: `)
82 os.Stderr.WriteString(err.Error())
83 os.Stderr.WriteString("\n")
84 }
85
86 os.Exit(cmd.ProcessState.ExitCode())
87 return
88 }
89
90 info, err := os.Stdin.Stat()
91 if err != nil {
92 os.Stderr.WriteString(`nil: `)
93 os.Stderr.WriteString(err.Error())
94 os.Stderr.WriteString("\n")
95 os.Exit(1)
96 }
97
98 piped := int(info.Mode()&fs.ModeNamedPipe) != 0
99 if !piped {
100 return
101 }
102
103 var buf [32 * 1024]byte
104 for {
105 _, err := os.Stdin.Read(buf[:])
106 if err != nil {
107 break
108 }
109 }
110 }
File: ./pac/pac.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package pac
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "math"
32 "math/rand"
33 "os"
34 "strconv"
35 "strings"
36 "time"
37 )
38
39 const info = `
40 pac [options...] [args...]
41
42
43 Postfix Array Calculator runs command-line arguments to calculate numeric
44 results, also supporting arrays of numbers as its name indicates.
45
46 All (optional) leading options start with either single or double-dash:
47
48 -h, -help show this help message
49
50 Functions (and their shortcuts/aliases)
51
52 [ ... ] push multiple values as a single top array/value
53 a, anchor stick nearly-integers to their closest integer
54 abs the absolute value
55 b, bail dump all arrays/values and quit right away
56 bye, q, quit quit right away
57 c, clear, empty clear the stack empty
58 ceil, ceiling round numbers upward
59 cos the cosine
60 cosh the hyperbolic cosine
61 dump output each array/value on its own line
62 dup duplicate the top/latest value
63 dup2 duplicate the 2 top/latest values
64 e the number known as 'E'
65 exp exponential function e^x
66 f, flat, flatten gather all values from all arrays into a single one
67 floor round numbers downward
68 fuzz undo number-rounding
69 h, help show this help message
70 i, iota push numbers 1..n
71 l, len, length the item-count in the top/latest array/value
72 levels the item-count of the stack overall
73 ln, log natural logarithm
74 log10 base-10 logarithm
75 log2 base-2 logarithm
76 neg, negate flip the sign
77 over prepend top of the stack to beginning of the stack
78 p, print output the top/latest array/value on a single line
79 pi the number known as 'PI'
80 pop forget the top/latest value
81 pow10 10^x
82 pow2 2^x
83 rand, random, rnd emit n random numbers in range [0, 1)
84 round round numbers
85 s, swap, swap2 swap the 2 top/latest values
86 sgn, sign the sign function
87 shift forget the oldest value still in the stack
88 spread flatten vertically, so stack has just 1-item arrays
89 sin the sine
90 sinh the hyperbolic sine
91 tan the tangent
92 tanh the hyperbolic tangent
93 tau twice the number known as 'PI'
94 top forget all values but the top/latest
95 u, unit push the number 1 onto the stack n times
96
97 Examples
98
99 # push numbers on the stack; only the last one will show as the result
100 pac 4.5 -6.22 905_900
101
102 # push numbers on the stack, then flatten everything, so all values show
103 pac 4.5 -6.22 905_900 flat
104 `
105
106 // maxExactInt is the max integer value a float64 can represent exactly
107 const maxExactInt float64 = 9007199254740992.0
108
109 var (
110 needAtLeast1 = errors.New(`need at least 1 number or array on the stack`)
111 needAtLeast2 = errors.New(`need at least 2 numbers or arrays on the stack`)
112 )
113
114 func Main() {
115 args := os.Args[1:]
116
117 if len(args) > 0 {
118 switch args[0] {
119 case `-h`, `--h`, `-help`, `--help`:
120 os.Stdout.WriteString(info[1:])
121 return
122 }
123 }
124
125 if len(args) > 0 && args[0] == `--` {
126 args = args[1:]
127 }
128
129 if len(args) == 0 {
130 os.Stderr.WriteString(info[1:])
131 os.Exit(1)
132 return
133 }
134
135 if err := pac(os.Stdout, args); err != nil && err != io.EOF {
136 os.Stderr.WriteString(`pac: `)
137 os.Stderr.WriteString(err.Error())
138 os.Stderr.WriteString("\n")
139 os.Exit(1)
140 return
141 }
142 }
143
144 type context struct {
145 stack [][]float64
146 random *rand.Rand
147 }
148
149 func pac(w io.Writer, args []string) error {
150 var ctx context
151 ctx.stack = make([][]float64, 0, 64)
152 reset(&ctx)
153
154 if err := run(args, &ctx); err != nil {
155 return err
156 }
157
158 // if len(ctx.stack) > 1 {
159 // os.Stderr.WriteString("multiple results left on the stack\n")
160 // }
161 if len(ctx.stack) == 0 {
162 return errors.New(`no results left on the stack`)
163 }
164
165 show(w, &ctx)
166 return nil
167 }
168
169 func show(w io.Writer, ctx *context) {
170 if len(ctx.stack) == 0 {
171 return
172 }
173
174 bw := bufio.NewWriterSize(w, 32*1024)
175 defer bw.Flush()
176
177 var buf [24]byte
178 for i, f := range ctx.stack[len(ctx.stack)-1] {
179 if i > 0 {
180 if err := bw.WriteByte(' '); err != nil {
181 break
182 }
183 }
184 bw.Write(strconv.AppendFloat(buf[:0], f, 'f', -1, 64))
185 }
186 bw.WriteByte('\n')
187 }
188
189 func debug(w io.StringWriter, ctx *context) {
190 for i, v := range ctx.stack {
191 w.WriteString("\t")
192 w.WriteString(strconv.Itoa(i))
193 w.WriteString(":")
194 for _, f := range v {
195 w.WriteString(" ")
196 w.WriteString(strconv.FormatFloat(f, 'f', -1, 64))
197 }
198
199 w.WriteString("\n")
200 }
201
202 w.WriteString("\n")
203 }
204
205 func run(args []string, ctx *context) error {
206 for len(args) > 0 {
207 s := args[0]
208 args = args[1:]
209
210 for len(s) > 0 {
211 for len(s) > 0 && s[0] == ' ' {
212 s = s[1:]
213 }
214 if len(s) == 0 {
215 break
216 }
217
218 var cmd string
219 if i := strings.IndexByte(s, ' '); i >= 0 {
220 cmd = s[:i]
221 } else {
222 cmd = s
223 }
224 s = s[len(cmd):]
225
226 if f, err := strconv.ParseFloat(cmd, 64); err == nil {
227 ctx.stack = append(ctx.stack, []float64{f})
228 continue
229 }
230
231 if cmd == `[` {
232 rest, err := appendArray(ctx, args)
233 if err != nil {
234 return err
235 }
236 args = rest
237 continue
238 }
239
240 if cmd == `.` {
241 debug(os.Stderr, ctx)
242 continue
243 }
244
245 if f, ok := funcs[cmd]; ok {
246 if err := call(ctx, f, cmd); err != nil {
247 return err
248 }
249 continue
250 }
251
252 return errors.New(`unknown command ` + cmd)
253 }
254 }
255
256 return nil
257 }
258
259 func appendArray(ctx *context, args []string) ([]string, error) {
260 end := -1
261 for i, s := range args {
262 if s == `]` {
263 end = i
264 break
265 }
266 }
267
268 if end < 0 {
269 return args, errors.New(`missing the ']' to close array`)
270 }
271
272 items := args[:end]
273 rest := args[end+1:]
274
275 var sub context
276 err := run(items, &sub)
277 flatten(&sub)
278 if len(sub.stack) > 0 {
279 ctx.stack = append(ctx.stack, sub.stack[len(sub.stack)-1])
280 }
281 return rest, err
282 }
283
284 func call(ctx *context, f any, name string) error {
285 var err error
286
287 switch f := f.(type) {
288 case float64:
289 ctx.stack = append(ctx.stack, []float64{f})
290 case func(ctx *context) error:
291 err = f(ctx)
292 case func(x float64) float64:
293 err = loop(ctx, f)
294 case func(x, y float64) float64:
295 err = parallel(ctx, f)
296 case func(x, y float64) (float64, float64):
297 err = parallel2(ctx, f)
298 case func(ctx *context, n int) error:
299 err = repeat(ctx, f)
300 default:
301 err = errors.New(`unsupported function type`)
302 }
303
304 if err == io.EOF {
305 return io.EOF
306 }
307 if err != nil {
308 return errors.New(name + `: ` + err.Error())
309 }
310 return nil
311 }
312
313 func cloneArray(x []float64) []float64 {
314 clone := make([]float64, 0, len(x))
315 for _, f := range x {
316 clone = append(clone, f)
317 }
318 return clone
319 }
320
321 func debool(x bool) float64 {
322 if x {
323 return 1
324 } else {
325 return 0
326 }
327 }
328
329 var funcs = map[string]any{
330 `+`: add,
331 `-`: sub,
332 `*`: mul,
333 `/`: div,
334 `%`: math.Mod,
335 `^`: math.Pow,
336 `**`: math.Pow,
337
338 `add`: add,
339 `sub`: sub,
340 `mul`: mul,
341 `div`: div,
342 `mod`: math.Mod,
343 `modulus`: math.Mod,
344 `pow`: math.Pow,
345 `power`: math.Pow,
346
347 `m`: mul,
348
349 `=`: eql,
350 `==`: eql,
351 `!=`: neq,
352 `<>`: neq,
353 `<`: lt,
354 `<=`: lte,
355 `>`: gt,
356 `>=`: gte,
357
358 `eql`: eql,
359 `neq`: neq,
360 `lt`: lt,
361 `lte`: lte,
362 `gt`: gt,
363 `gte`: gte,
364
365 `a`: anchor,
366 `abs`: math.Abs,
367 `anchor`: anchor,
368 `ceil`: math.Ceil,
369 `ceiling`: math.Ceil,
370 `cos`: math.Cos,
371 `cosh`: math.Cosh,
372 `cosine`: math.Cos,
373 `div2`: div2,
374 `down`: math.Floor,
375 `exp`: math.Exp,
376 `floor`: math.Floor,
377 `hypot`: math.Hypot,
378 `hypotenuse`: math.Hypot,
379 `hypothenuse`: math.Hypot,
380 `mid`: mid,
381 `middle`: mid,
382 `neg`: negate,
383 `negate`: negate,
384 `ln`: math.Log,
385 `log`: math.Log,
386 `log10`: math.Log10,
387 `log2`: math.Log2,
388 `pow10`: pow10,
389 `pow2`: pow2,
390 `round`: math.Round,
391 `sgn`: sign,
392 `sign`: sign,
393 `sin`: math.Sin,
394 `sine`: math.Sin,
395 `sinh`: math.Sinh,
396 `tan`: math.Tan,
397 `tangent`: math.Tan,
398 `tanh`: math.Tanh,
399 `up`: math.Ceil,
400
401 `b`: bail,
402 `bail`: bail,
403 `bye`: bye,
404 `c`: empty,
405 `clear`: empty,
406 `d`: dup,
407 `dump`: dump,
408 `dup`: dup,
409 `dup2`: dup2,
410 `e`: math.E,
411 `empty`: empty,
412 `f`: flatten,
413 `flat`: flatten,
414 `flatten`: flatten,
415 `fuzz`: fuzz,
416 `h`: help,
417 `help`: help,
418 `i`: iota,
419 `inv`: invert,
420 `inverse`: invert,
421 `invert`: invert,
422 `iota`: iota,
423 `l`: toplen,
424 `len`: toplen,
425 `length`: toplen,
426 `levels`: levels,
427 `over`: over,
428 `p`: toprint,
429 `pi`: math.Pi,
430 `print`: toprint,
431 `pop`: pop,
432 `q`: bye,
433 `quit`: bye,
434 `rand`: random,
435 `random`: random,
436 `rnd`: random,
437 `s`: swap2,
438 `shift`: shift,
439 `spread`: spread,
440 `swap`: swap2,
441 `swap2`: swap2,
442 `tau`: 2 * math.Pi,
443 `top`: top,
444 `u`: unit,
445 `unit`: unit,
446 }
447
448 func invert(x float64) float64 { return 1 / x }
449 func negate(x float64) float64 { return -x }
450
451 func add(x, y float64) float64 { return x + y }
452 func sub(x, y float64) float64 { return x - y }
453 func mid(x, y float64) float64 { return 0.5 * (x + y) }
454 func mul(x, y float64) float64 { return x * y }
455 func div(x, y float64) float64 { return x / y }
456 func pow10(x float64) float64 { return math.Pow(10, x) }
457 func pow2(x float64) float64 { return math.Pow(2, x) }
458
459 func div2(x, y float64) (float64, float64) { return x / y, y / x }
460
461 func eql(x, y float64) float64 { return debool(x == y) }
462 func gt(x, y float64) float64 { return debool(x > y) }
463 func gte(x, y float64) float64 { return debool(x >= y) }
464 func lt(x, y float64) float64 { return debool(x < y) }
465 func lte(x, y float64) float64 { return debool(x <= y) }
466 func neq(x, y float64) float64 { return debool(x != y) }
467
468 func anchor(x float64) float64 {
469 const eps = 1e6
470 if i, f := math.Modf(x); -eps <= f && f <= +eps {
471 return float64(i)
472 }
473 return x
474 }
475
476 func sign(x float64) float64 {
477 if x > 0 {
478 return +1
479 }
480 if x < 0 {
481 return -1
482 }
483 return x
484 }
485
486 func bail(ctx *context) error {
487 dump(ctx)
488 return bye(ctx)
489 }
490
491 func bye(ctx *context) error {
492 return io.EOF
493 }
494
495 func dump(ctx *context) error {
496 if len(ctx.stack) == 0 {
497 return nil
498 }
499
500 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
501 defer bw.Flush()
502
503 var buf [24]byte
504 for _, v := range ctx.stack {
505 for i, f := range v {
506 if i > 0 {
507 if err := bw.WriteByte(' '); err != nil {
508 break
509 }
510 }
511 bw.Write(strconv.AppendFloat(buf[:0], f, 'f', -1, 64))
512 }
513
514 if err := bw.WriteByte('\n'); err != nil {
515 return io.EOF
516 }
517 }
518
519 return nil
520 }
521
522 func dup(ctx *context) error {
523 if len(ctx.stack) > 0 {
524 x := ctx.stack[len(ctx.stack)-1]
525 ctx.stack = append(ctx.stack, cloneArray(x))
526 }
527 return nil
528 }
529
530 func dup2(ctx *context) error {
531 if len(ctx.stack) < 2 {
532 return needAtLeast2
533 }
534
535 x := ctx.stack[len(ctx.stack)-2]
536 y := ctx.stack[len(ctx.stack)-1]
537 ctx.stack = append(ctx.stack, cloneArray(x))
538 ctx.stack = append(ctx.stack, cloneArray(y))
539 return nil
540 }
541
542 func empty(ctx *context) error {
543 ctx.stack = ctx.stack[:0]
544 return nil
545 }
546
547 func flatten(ctx *context) error {
548 n := 0
549 for _, v := range ctx.stack {
550 n += len(v)
551 }
552
553 top := make([]float64, 0, n)
554 for _, v := range ctx.stack {
555 top = append(top, v...)
556 }
557 ctx.stack = append(ctx.stack[:0], top)
558 return nil
559 }
560
561 // fuzz tries to undo a number-rounding, turning each number at the stack-top
562 // into a 3-item slice: the original number itself, surrounded by its lowest
563 // and highest values from the range it can be rounded from
564 func fuzz(ctx *context) error {
565 if len(ctx.stack) < 1 {
566 return needAtLeast1
567 }
568
569 top := ctx.stack[len(ctx.stack)-1]
570 if len(top) == 1 {
571 f, err := fuzzValue(top[0])
572 if err != nil {
573 return err
574 }
575 ctx.stack[len(ctx.stack)-1] = f
576 }
577
578 ctx.stack = ctx.stack[:len(ctx.stack)-1]
579 for _, f := range top {
580 fuzzed, err := fuzzValue(f)
581 if err != nil {
582 return err
583 }
584 ctx.stack = append(ctx.stack, fuzzed)
585 }
586 return nil
587 }
588
589 // fuzzValue tries to undo a number's rounding, returning a slice with the
590 // value itself, surrounded by its lowest and highest values from the range
591 // it can be rounded from
592 func fuzzValue(f float64) ([]float64, error) {
593 if math.IsNaN(f) || math.IsInf(f, 0) {
594 return []float64{f, f, f}, nil
595 }
596
597 // treat round numbers differently, by `fuzzing` with/around integers
598 if math.Mod(f, 1.0) != 0.0 {
599 lo := math.Floor(f)
600 hi := math.Ceil(f)
601 return []float64{lo, f, hi}, nil
602 }
603
604 // find the smallest power of 10 to fuzz with
605 fuzzBy := 1.0
606 for fuzzBy < maxExactInt/10 {
607 next := 10 * fuzzBy
608 if math.Mod(f, next) != 0.0 {
609 break
610 }
611 fuzzBy = next
612 }
613
614 lo := fuzzBy*math.Trunc(f/fuzzBy) - fuzzBy/2
615 hi := fuzzBy*math.Ceil(f/fuzzBy) + fuzzBy/2
616 return []float64{lo, f, hi}, nil
617 }
618
619 func help(ctx *context) error {
620 if _, err := os.Stdout.WriteString(info[1:]); err != nil {
621 return io.EOF
622 }
623 return nil
624 }
625
626 func iota(ctx *context, n int) error {
627 if n < 1 {
628 return nil
629 }
630
631 top := make([]float64, n)
632 for i := range top {
633 top[i] = float64(i + 1)
634 }
635 ctx.stack = append(ctx.stack, top)
636
637 return nil
638 }
639
640 func levels(ctx *context) error {
641 l := float64(len(ctx.stack))
642 ctx.stack = append(ctx.stack, []float64{l})
643 return nil
644 }
645
646 func over(ctx *context) error {
647 if len(ctx.stack) > 0 {
648 top := ctx.stack[len(ctx.stack)-1]
649 ctx.stack = append(ctx.stack, []float64{})
650 copy(ctx.stack[1:], ctx.stack[:len(ctx.stack)-1])
651 ctx.stack[0] = top
652 }
653 return nil
654 }
655
656 func pop(ctx *context) error {
657 if len(ctx.stack) > 0 {
658 ctx.stack = ctx.stack[:len(ctx.stack)-1]
659 }
660 return nil
661 }
662
663 func random(ctx *context, n int) error {
664 if n < 1 {
665 return nil
666 }
667
668 top := make([]float64, n)
669 for i := range top {
670 top[i] = ctx.random.Float64()
671 }
672 ctx.stack = append(ctx.stack, top)
673
674 return nil
675 }
676
677 func reset(ctx *context) error {
678 ctx.stack = ctx.stack[:0]
679 ctx.random = rand.New(rand.NewSource(time.Now().UnixNano()))
680 return nil
681 }
682
683 func shift(ctx *context) error {
684 for i := 1; i < len(ctx.stack); i++ {
685 ctx.stack[i-1] = ctx.stack[i]
686 }
687
688 if len(ctx.stack) > 0 {
689 ctx.stack = ctx.stack[:len(ctx.stack)-1]
690 }
691
692 return nil
693 }
694
695 func spread(ctx *context) error {
696 prev := ctx.stack
697 next := ctx.stack[:0]
698
699 for _, v := range prev {
700 for _, f := range v {
701 next = append(next, []float64{f})
702 }
703 }
704
705 ctx.stack = next
706 return nil
707 }
708
709 func toplen(ctx *context) error {
710 if len(ctx.stack) < 1 {
711 return needAtLeast1
712 }
713
714 top := ctx.stack[len(ctx.stack)-1]
715 ctx.stack = append(ctx.stack, []float64{float64(len(top))})
716 return nil
717 }
718
719 func swap2(ctx *context) error {
720 if len(ctx.stack) < 2 {
721 return needAtLeast2
722 }
723
724 x := ctx.stack[len(ctx.stack)-2]
725 y := ctx.stack[len(ctx.stack)-1]
726 ctx.stack[len(ctx.stack)-2] = y
727 ctx.stack[len(ctx.stack)-1] = x
728 return nil
729 }
730
731 func top(ctx *context) error {
732 if len(ctx.stack) < 1 {
733 return needAtLeast1
734 }
735
736 ctx.stack = append(ctx.stack[:0], ctx.stack[len(ctx.stack)-1])
737 return nil
738 }
739
740 func toprint(ctx *context) error {
741 show(os.Stdout, ctx)
742 return nil
743 }
744
745 func unit(ctx *context, n int) error {
746 if n < 1 {
747 return nil
748 }
749
750 top := make([]float64, n)
751 for i := range top {
752 top[i] = 1
753 }
754 ctx.stack = append(ctx.stack, top)
755
756 return nil
757 }
758
759 func loop(ctx *context, f func(x float64) float64) error {
760 if len(ctx.stack) < 1 {
761 return needAtLeast1
762 }
763
764 top := ctx.stack[len(ctx.stack)-1]
765 for i, v := range top {
766 top[i] = f(v)
767 }
768 return nil
769 }
770
771 func parallel(ctx *context, f func(x, y float64) float64) error {
772 if len(ctx.stack) < 2 {
773 return needAtLeast2
774 }
775
776 l := len(ctx.stack)
777 xa := ctx.stack[l-2]
778 ya := ctx.stack[l-1]
779 ctx.stack = ctx.stack[:l-2]
780
781 if len(xa) == 0 || len(ya) == 0 {
782 return nil
783 }
784
785 var za []float64
786 if len(xa) < len(ya) {
787 za = ya
788 } else {
789 za = xa
790 }
791
792 for i := range za {
793 za[i] = f(xa[i%len(xa)], ya[i%len(ya)])
794 }
795 ctx.stack = append(ctx.stack, za)
796 return nil
797 }
798
799 func parallel2(ctx *context, f func(x, y float64) (float64, float64)) error {
800 if len(ctx.stack) < 2 {
801 return needAtLeast2
802 }
803
804 l := len(ctx.stack)
805 xa := ctx.stack[l-2]
806 ya := ctx.stack[l-1]
807 ctx.stack = ctx.stack[:l-2]
808
809 if len(xa) == 0 || len(ya) == 0 {
810 return nil
811 }
812
813 var za []float64
814 if len(xa) < len(ya) {
815 za = ya
816 } else {
817 za = xa
818 }
819
820 r1 := make([]float64, len(za))
821 r2 := make([]float64, len(za))
822 for i := range r1 {
823 x, y := f(xa[i%len(xa)], ya[i%len(ya)])
824 r1[i] = x
825 r2[i] = y
826 }
827 ctx.stack = append(ctx.stack, r1)
828 ctx.stack = append(ctx.stack, r2)
829 return nil
830 }
831
832 func repeat(ctx *context, f func(ctx *context, n int) error) error {
833 if len(ctx.stack) < 1 {
834 return needAtLeast1
835 }
836
837 counts := ctx.stack[len(ctx.stack)-1]
838 ctx.stack = ctx.stack[:len(ctx.stack)-1]
839
840 for _, n := range counts {
841 if err := f(ctx, int(n)); err != nil {
842 return err
843 }
844 }
845
846 return nil
847 }
File: ./pac/pac_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package pac
26
27 import (
28 "strings"
29 "testing"
30 )
31
32 func TestTypes(t *testing.T) {
33 var ctx context
34 reset(&ctx)
35
36 for name, f := range funcs {
37 t.Run(name, func(t *testing.T) {
38 switch f.(type) {
39 case
40 float64,
41 func(x float64) float64,
42 func(x, y float64) float64,
43 func(x, y float64) (float64, float64),
44 func(ctx *context) error,
45 func(ctx *context, n int) error:
46 return
47
48 default:
49 t.Fatalf("%s: bad dispatch type %T", name, f)
50 }
51 })
52 }
53 }
54
55 func TestResults(t *testing.T) {
56 tests := map[string]string{
57 `0.0`: `0`,
58 `2 iota`: `1 2`,
59 }
60
61 for arguments, expected := range tests {
62 t.Run(arguments, func(t *testing.T) {
63 var w strings.Builder
64 if err := pac(&w, strings.Fields(arguments)); err != nil {
65 t.Fatal(err)
66 }
67
68 if got := strings.TrimSpace(w.String()); got != expected {
69 t.Fatalf("got %s, expected %s instead", got, expected)
70 }
71 })
72 }
73 }
File: ./pad/pad.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package pad
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "os"
32 "strings"
33 "unicode/utf8"
34 )
35
36 const info = `
37 pad [options...] [filenames...]
38
39 Pad lines with trailing spaces, so that all lines have the same number of
40 symbols. ANSI style-codes are also kept as given.
41
42 The options are, available both in single and double-dash versions
43
44 -h, -help show this help message
45 `
46
47 func Main() {
48 args := os.Args[1:]
49
50 if len(args) > 0 {
51 switch args[0] {
52 case `-h`, `--h`, `-help`, `--help`:
53 os.Stdout.WriteString(info[1:])
54 return
55 }
56 }
57
58 if len(args) > 0 && args[0] == `--` {
59 args = args[1:]
60 }
61
62 if err := run(args); err != nil {
63 os.Stderr.WriteString(`pad: `)
64 os.Stderr.WriteString(err.Error())
65 os.Stderr.WriteString("\n")
66 os.Exit(1)
67 return
68 }
69 }
70
71 type paddingData struct {
72 lines []string
73 max int
74 }
75
76 func run(paths []string) error {
77 var pd paddingData
78
79 for _, p := range paths {
80 if err := handleFile(&pd, p); err != nil {
81 return err
82 }
83 }
84
85 if len(paths) == 0 {
86 if err := handleReader(&pd, os.Stdin); err != nil {
87 return err
88 }
89 }
90
91 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
92 defer bw.Flush()
93
94 for _, line := range pd.lines {
95 bw.WriteString(line)
96 writeSpaces(bw, pd.max-countWidth(line))
97 if bw.WriteByte('\n') != nil {
98 break
99 }
100 }
101 return nil
102 }
103
104 func handleFile(pd *paddingData, path string) error {
105 f, err := os.Open(path)
106 if err != nil {
107 // on windows, file-not-found error messages may mention `CreateFile`,
108 // even when trying to open files in read-only mode
109 return errors.New(`can't open file named ` + path)
110 }
111 defer f.Close()
112 return handleReader(pd, f)
113 }
114
115 func handleReader(pd *paddingData, r io.Reader) error {
116 const gb = 1024 * 1024 * 1024
117 sc := bufio.NewScanner(r)
118 sc.Buffer(nil, 8*gb)
119
120 for i := 0; sc.Scan(); i++ {
121 line := sc.Text()
122 if i == 0 && strings.HasPrefix(line, "\xef\xbb\xbf") {
123 line = line[3:]
124 }
125
126 n := countWidth(line)
127 if pd.max < n {
128 pd.max = n
129 }
130 pd.lines = append(pd.lines, line)
131 }
132
133 return sc.Err()
134 }
135
136 func countWidth(s string) int {
137 width := 0
138
139 for len(s) > 0 {
140 i, j := indexEscapeSequence(s)
141 if i < 0 {
142 break
143 }
144 if j < 0 {
145 j = len(s)
146 }
147
148 width += utf8.RuneCountInString(s[:i])
149 s = s[j:]
150 }
151
152 // count trailing/all runes in strings which don't end with ANSI-sequences
153 width += utf8.RuneCountInString(s)
154 return width
155 }
156
157 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
158 // the multi-byte sequences starting with ESC[; the result is a pair of slice
159 // indices which can be independently negative when either the start/end of
160 // a sequence isn't found; given their fairly-common use, even the hyperlink
161 // ESC]8 sequences are supported
162 func indexEscapeSequence(s string) (int, int) {
163 var prev byte
164
165 for i := range s {
166 b := s[i]
167
168 if prev == '\x1b' && b == '[' {
169 j := indexLetter(s[i+1:])
170 if j < 0 {
171 return i, -1
172 }
173 return i - 1, i + 1 + j + 1
174 }
175
176 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
177 j := indexPair(s[i+1:], '\x1b', '\\')
178 if j < 0 {
179 return i, -1
180 }
181 return i - 1, i + 1 + j + 2
182 }
183
184 prev = b
185 }
186
187 return -1, -1
188 }
189
190 func indexLetter(s string) int {
191 for i, b := range s {
192 upper := b &^ 32
193 if 'A' <= upper && upper <= 'Z' {
194 return i
195 }
196 }
197
198 return -1
199 }
200
201 func indexPair(s string, x byte, y byte) int {
202 var prev byte
203
204 for i := range s {
205 b := s[i]
206 if prev == x && b == y && i > 0 {
207 return i
208 }
209 prev = b
210 }
211
212 return -1
213 }
214
215 // writeSpaces does what it says, minimizing calls to write-like funcs
216 func writeSpaces(w *bufio.Writer, n int) {
217 const spaces = ` `
218 if n < 1 {
219 return
220 }
221
222 for n >= len(spaces) {
223 w.WriteString(spaces)
224 n -= len(spaces)
225 }
226 w.WriteString(spaces[:n])
227 }
File: ./paramatch/paramatch.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package paramatch
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "regexp"
33 "strings"
34 )
35
36 const info = `
37 paramatch [options...] [regexes...]
38
39 PARAgraph MATCH tries to match all paragraphs containing any of the regexes
40 given.
41
42 This tool also offers a special "flat" mode, which further tests the regexes
43 against a flattened version of each current paragraph, where lines are joined
44 via single spaces, instead of line-feeds.
45
46 The options are, available both in single and double-dash versions
47
48 -f, -flat also check regexes with flat/space-joined paragraph lines
49 -h, -help show this help message
50 -i, -ins match regexes case-insensitively
51 `
52
53 type config struct {
54 // patterns are the regexes to test against
55 patterns []*regexp.Regexp
56
57 // shown counts how many paragraphs have been/shown matched so far
58 shown int
59
60 // flatChance is whether to also test regexes against `flattened` lines
61 flatChance bool
62
63 // live is whether to flush output each time a paragraph is emitted
64 live bool
65 }
66
67 func (cfg *config) dump(w *bufio.Writer, what []byte) error {
68 if len(what) == 0 {
69 return nil
70 }
71
72 if cfg.shown > 0 {
73 w.WriteByte('\n')
74 }
75 cfg.shown++
76
77 if _, err := w.Write(what); err != nil {
78 return io.EOF
79 }
80
81 if cfg.live && w.Flush() != nil {
82 return io.EOF
83 }
84 return nil
85 }
86
87 func (cfg config) match(line []byte) bool {
88 for _, pat := range cfg.patterns {
89 if pat.Match(line) {
90 return true
91 }
92 }
93 return false
94 }
95
96 func Main() {
97 var cfg config
98 cfg.live = true
99 insensitive := false
100 args := os.Args[1:]
101
102 for len(args) > 0 {
103 switch args[0] {
104 case `-b`, `--b`, `-buffered`, `--buffered`:
105 cfg.live = false
106 args = args[1:]
107 continue
108
109 case `-f`, `--f`, `-flat`, `--flat`, `-flattened`, `--flattened`:
110 cfg.flatChance = true
111 args = args[1:]
112 continue
113
114 case `-h`, `--h`, `-help`, `--help`:
115 os.Stdout.WriteString(info[1:])
116 return
117
118 case `-i`, `--i`, `-ins`, `--ins`, `-insensitive`, `--insensitive`:
119 insensitive = true
120 args = args[1:]
121 continue
122 }
123
124 if s := args[0]; s == `-` || s == `--` || !strings.HasPrefix(s, `-`) {
125 break
126 }
127
128 for s := args[0][1:]; len(s) > 0; s = s[1:] {
129 switch s[0] {
130 case 'b':
131 cfg.live = false
132 continue
133 case 'f':
134 cfg.flatChance = true
135 continue
136 case 'h':
137 os.Stdout.WriteString(info[1:])
138 return
139 case 'i':
140 insensitive = true
141 continue
142 default:
143 os.Stderr.WriteString(`paramatch: unsupported option `)
144 os.Stderr.WriteString(args[0])
145 os.Stderr.WriteString("\n")
146 os.Exit(1)
147 }
148 }
149
150 args = args[1:]
151 }
152
153 if len(args) > 0 && args[0] == `--` {
154 args = args[1:]
155 }
156
157 if cfg.live {
158 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
159 cfg.live = false
160 }
161 }
162
163 if len(args) == 0 {
164 os.Stderr.WriteString(info[1:])
165 os.Exit(1)
166 return
167 }
168
169 nerr := 0
170
171 for _, s := range args {
172 if insensitive && !strings.HasPrefix(s, `(?i)`) {
173 s = `(?i)` + s
174 }
175
176 pat, err := regexp.Compile(s)
177 if err != nil {
178 os.Stderr.WriteString(`paramatch: `)
179 os.Stderr.WriteString(err.Error())
180 os.Stderr.WriteString("\n")
181 nerr++
182 continue
183 }
184 cfg.patterns = append(cfg.patterns, pat)
185 }
186
187 if nerr > 0 {
188 os.Exit(1)
189 return
190 }
191
192 if err := run(cfg); err != nil && err != io.EOF {
193 os.Stderr.WriteString(`paramatch: `)
194 os.Stderr.WriteString(err.Error())
195 os.Stderr.WriteString("\n")
196 os.Exit(1)
197 return
198 }
199 }
200
201 func run(cfg config) error {
202 w := bufio.NewWriterSize(os.Stdout, 32*1024)
203 defer w.Flush()
204
205 // par is the current paragraph's lines joined by line-feeds
206 var par []byte
207 // flat is the current paragraph's lines joined by single spaces
208 var flat []byte
209
210 const gb = 1024 * 1024 * 1024
211 sc := bufio.NewScanner(os.Stdin)
212 sc.Buffer(nil, 8*gb)
213
214 didMatch := false
215
216 for i := 0; sc.Scan(); i++ {
217 line := sc.Bytes()
218 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
219 line = line[3:]
220 }
221
222 if emptyish(line) {
223 if !didMatch && cfg.flatChance && len(flat) > 0 {
224 didMatch = cfg.match(flat)
225 }
226
227 if !didMatch {
228 par = par[:0]
229 flat = flat[:0]
230 continue
231 }
232
233 if err := cfg.dump(w, par); err != nil {
234 return err
235 }
236
237 par = par[:0]
238 flat = flat[:0]
239 didMatch = false
240 continue
241 }
242
243 // avoid checking already-matched paragraphs
244 if !didMatch {
245 if len(flat) > 0 {
246 flat = append(flat, ' ')
247 }
248 flat = append(flat, line...)
249
250 didMatch = cfg.match(line)
251 if didMatch {
252 flat = flat[:0]
253 }
254 }
255
256 // remember all paragraph lines
257 par = append(par, line...)
258 par = append(par, '\n')
259 }
260
261 if err := sc.Err(); err != nil {
262 return err
263 }
264
265 if !didMatch && cfg.flatChance && len(flat) > 0 {
266 didMatch = cfg.match(flat)
267 }
268
269 if didMatch {
270 if err := cfg.dump(w, par); err != nil {
271 return err
272 }
273 }
274
275 return nil
276 }
277
278 func emptyish(line []byte) bool {
279 // trim leading spaces
280 for len(line) > 0 && line[0] == ' ' {
281 line = line[1:]
282 }
283
284 // trim trailing spaces
285 for len(line) > 0 && line[len(line)-1] == ' ' {
286 line = line[:len(line)-1]
287 }
288
289 return len(line) == 0
290 }
File: ./pbn/pbn.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package pbn
26
27 import (
28 "bufio"
29 "io"
30 "os"
31 "strconv"
32 "strings"
33 )
34
35 const info = `
36 pbn [options...] [line numbers...]
37
38 Pick (lines) By Number, using all the 1-based line-numbers given, or even
39 negative ones, which count backward from the last line. Input lines always
40 come from the standard input.
41
42 The options are, available both in single and double-dash versions
43
44 -h, -help show this help message
45 `
46
47 func Main() {
48 args := os.Args[1:]
49 buffered := false
50
51 if len(args) > 0 {
52 switch args[0] {
53 case `-b`, `--b`, `-buffered`, `--buffered`:
54 buffered = true
55 args = args[1:]
56
57 case `-h`, `--h`, `-help`, `--help`:
58 os.Stdout.WriteString(info[1:])
59 return
60 }
61 }
62
63 if len(args) > 0 && args[0] == `--` {
64 args = args[1:]
65 }
66
67 which := make([]int, 0, len(args))
68
69 for _, s := range args {
70 n, err := strconv.ParseInt(s, 10, 64)
71
72 if err != nil {
73 os.Stderr.WriteString(`pbn: `)
74 os.Stderr.WriteString(err.Error())
75 os.Stderr.WriteString("\n")
76 os.Exit(1)
77 return
78 }
79
80 if n == 0 {
81 os.Stderr.WriteString("indices can't be zero\n")
82 os.Exit(1)
83 return
84 }
85
86 i := n
87 if n > 0 {
88 i--
89 }
90
91 which = append(which, int(i))
92 }
93
94 liveLines := !buffered
95 if !buffered {
96 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
97 liveLines = false
98 }
99 }
100
101 if err := run(which, liveLines); err != nil {
102 os.Stderr.WriteString(`pbn: `)
103 os.Stderr.WriteString(err.Error())
104 os.Stderr.WriteString("\n")
105 os.Exit(1)
106 return
107 }
108 }
109
110 func run(which []int, liveLines bool) error {
111 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
112 defer bw.Flush()
113
114 const gb = 1024 * 1024 * 1024
115 sc := bufio.NewScanner(os.Stdin)
116 sc.Buffer(nil, 8*gb)
117
118 // see if lines even need to be stored
119 prev := 0
120 fullStreak := true
121 for _, i := range which {
122 if i < 1 || i < prev {
123 fullStreak = false
124 break
125 }
126 prev = i
127 }
128
129 var lines []string
130
131 for i := 0; sc.Scan() && len(which) > 0; i++ {
132 line := sc.Text()
133 if i == 0 && strings.HasPrefix(line, "\xef\xbb\xbf") {
134 line = line[3:]
135 }
136
137 if !fullStreak {
138 lines = append(lines, line)
139 }
140
141 for len(which) > 0 && i == which[0] {
142 if writeLine(bw, line, liveLines) != nil {
143 return nil
144 }
145 which = which[1:]
146 }
147 }
148
149 if err := sc.Err(); err != nil {
150 return err
151 }
152
153 for _, i := range which {
154 if i < 0 {
155 i += len(lines)
156 }
157
158 if 0 <= i && i < len(lines) {
159 if writeLine(bw, lines[i], liveLines) != nil {
160 return nil
161 }
162 }
163 }
164
165 return nil
166 }
167
168 func writeLine(w *bufio.Writer, s string, live bool) error {
169 w.WriteString(s)
170 if w.WriteByte('\n') != nil {
171 return nil
172 }
173
174 if live && w.Flush() != nil {
175 return io.EOF
176 }
177 return nil
178 }
File: ./pcol/pcol.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package pcol
26
27 import (
28 "bufio"
29 "io"
30 "os"
31 "strconv"
32 "strings"
33 )
34
35 const info = `
36 pcol [column names...]
37
38
39 Pick COLumns lets you select/reorder a subset of a table's columns, matching
40 the column names given using the first line from the standard input. Input
41 lines can be either space-separated or tab-separated; output lines are always
42 TSV (Tab-Separated Values) ones, where trailing tabs are added if any values
43 are missing.
44
45 When a column name isn't matched exactly, a case-insensitive match is tried:
46 if the latter also fails, number-matching is finally tried, before giving up
47 on that column name. Column numbers start at 1, and can be negative to count
48 backward from the last column.
49
50 All (optional) leading options start with either single or double-dash:
51
52 -h, -help show this help message
53 `
54
55 func Main() {
56 buffered := false
57 args := os.Args[1:]
58
59 if len(args) > 0 {
60 switch args[0] {
61 case `-b`, `--b`, `-buffered`, `--buffered`:
62 buffered = true
63 args = args[1:]
64
65 case `-h`, `--h`, `-help`, `--help`:
66 os.Stdout.WriteString(info[1:])
67 return
68 }
69 }
70
71 if len(args) > 0 && args[0] == `--` {
72 args = args[1:]
73 }
74
75 if len(args) == 0 {
76 os.Stderr.WriteString(info[1:])
77 return
78 }
79
80 liveLines := !buffered
81 if !buffered {
82 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
83 liveLines = false
84 }
85 }
86
87 if err := run(args, liveLines); err != nil && err != io.EOF {
88 os.Stderr.WriteString(`pcol: `)
89 os.Stderr.WriteString(err.Error())
90 os.Stderr.WriteString("\n")
91 os.Exit(1)
92 return
93 }
94 }
95
96 type itemFunc func(i int, s string) bool
97 type handler func(s string, f itemFunc)
98
99 func run(args []string, live bool) error {
100 w := bufio.NewWriterSize(os.Stdout, 32*1024)
101 defer w.Flush()
102
103 const gb = 1024 * 1024 * 1024
104 sc := bufio.NewScanner(os.Stdin)
105 sc.Buffer(nil, 8*gb)
106
107 var which []int
108 var fields []string
109 var handle handler
110
111 for i := 0; sc.Scan(); i++ {
112 s := sc.Text()
113 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
114 s = s[3:]
115 }
116
117 if i == 0 {
118 picks, h, ok := match(s, args)
119 if !ok {
120 return nil
121 }
122 which = picks
123 handle = h
124 fields = make([]string, 0, len(which))
125 }
126
127 fields = fields[:0]
128 handle(s, func(i int, s string) bool {
129 fields = append(fields, s)
130 return true
131 })
132
133 got := 0
134 for _, i := range which {
135 if 0 <= i && i < len(fields) {
136 if got > 0 {
137 w.WriteByte('\t')
138 }
139 w.WriteString(fields[i])
140 got++
141 }
142 }
143
144 if w.WriteByte('\n') != nil {
145 return io.EOF
146 }
147
148 if live && w.Flush() != nil {
149 return io.EOF
150 }
151 }
152
153 return sc.Err()
154 }
155
156 func match(s string, args []string) (which []int, handle handler, ok bool) {
157 if strings.IndexByte(s, '\t') >= 0 {
158 handle = loopItemsTSV
159 } else {
160 handle = loopItemsSSV
161 }
162
163 // count columns, so negative indices can be fixed with it
164 count := 0
165 handle(s, func(i int, s string) bool {
166 count++
167 return true
168 })
169
170 for _, arg := range args {
171 ok := false
172
173 // try exact matches
174 handle(s, func(i int, s string) bool {
175 if s == arg {
176 ok = true
177 which = append(which, i)
178 return false
179 }
180 return true
181 })
182
183 if ok {
184 continue
185 }
186
187 // try case-insensitive matches
188 handle(s, func(i int, s string) bool {
189 if strings.EqualFold(s, arg) {
190 ok = true
191 which = append(which, i)
192 return false
193 }
194 return true
195 })
196
197 if ok {
198 continue
199 }
200
201 // try 1-based indices, even negative ones
202 if n, err := strconv.Atoi(arg); err == nil {
203 if n < 0 {
204 n += count
205 } else if n > 0 {
206 n--
207 }
208
209 if 0 <= n && n < count {
210 which = append(which, n)
211 }
212 }
213 }
214
215 return which, handle, true
216 }
217
218 // loopItemsSSV loops over a line's items, allocation-free style; when given
219 // empty strings, the callback func is never called
220 func loopItemsSSV(s string, f itemFunc) {
221 s = trimTrailingSpaces(s)
222
223 for i := 0; true; i++ {
224 s = trimLeadingSpaces(s)
225 if len(s) == 0 {
226 return
227 }
228
229 j := strings.IndexByte(s, ' ')
230 if j < 0 {
231 if !f(i, s) {
232 return
233 }
234 return
235 }
236
237 if !f(i, s[:j]) {
238 return
239 }
240 s = s[j+1:]
241 }
242 }
243
244 func trimLeadingSpaces(s string) string {
245 for len(s) > 0 && s[0] == ' ' {
246 s = s[1:]
247 }
248 return s
249 }
250
251 func trimTrailingSpaces(s string) string {
252 for len(s) > 0 && s[len(s)-1] == ' ' {
253 s = s[:len(s)-1]
254 }
255 return s
256 }
257
258 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
259 // when given empty strings, the callback func is never called
260 func loopItemsTSV(s string, f itemFunc) {
261 if len(s) == 0 {
262 return
263 }
264
265 for i := 0; true; i++ {
266 j := strings.IndexByte(s, '\t')
267 if j < 0 {
268 if !f(i, s) {
269 return
270 }
271 return
272 }
273
274 if !f(i, s[:j]) {
275 return
276 }
277 s = s[j+1:]
278 }
279 }
File: ./plain/plain.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package plain
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 )
34
35 const info = `
36 plain [options...] [file...]
37
38
39 Turn potentially ANSI-styled plain-text into actual plain-text.
40
41 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
42 feeds.
43
44 All (optional) leading options start with either single or double-dash:
45
46 -h, -help show this help message
47 `
48
49 func Main() {
50 buffered := false
51 args := os.Args[1:]
52
53 if len(args) > 0 {
54 switch args[0] {
55 case `-b`, `--b`, `-buffered`, `--buffered`:
56 buffered = true
57 args = args[1:]
58
59 case `-h`, `--h`, `-help`, `--help`:
60 os.Stdout.WriteString(info[1:])
61 return
62 }
63 }
64
65 if len(args) > 0 && args[0] == `--` {
66 args = args[1:]
67 }
68
69 liveLines := !buffered
70 if !buffered {
71 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
72 liveLines = false
73 }
74 }
75
76 if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
77 os.Stderr.WriteString(`plain: `)
78 os.Stderr.WriteString(err.Error())
79 os.Stderr.WriteString("\n")
80 os.Exit(1)
81 return
82 }
83 }
84
85 func run(w io.Writer, args []string, live bool) error {
86 bw := bufio.NewWriterSize(w, 32*1024)
87 defer bw.Flush()
88
89 if len(args) == 0 {
90 return plain(bw, os.Stdin, live)
91 }
92
93 for _, name := range args {
94 if err := handleFile(bw, name, live); err != nil {
95 return err
96 }
97 }
98 return nil
99 }
100
101 func handleFile(w *bufio.Writer, name string, live bool) error {
102 if name == `` || name == `-` {
103 return plain(w, os.Stdin, live)
104 }
105
106 f, err := os.Open(name)
107 if err != nil {
108 return errors.New(`can't read from file named "` + name + `"`)
109 }
110 defer f.Close()
111
112 return plain(w, f, live)
113 }
114
115 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
116 // the multi-byte sequences starting with ESC[; the result is a pair of slice
117 // indices which can be independently negative when either the start/end of
118 // a sequence isn't found; given their fairly-common use, even the hyperlink
119 // ESC]8 sequences are supported
120 func indexEscapeSequence(s []byte) (int, int) {
121 var prev byte
122
123 for i, b := range s {
124 if prev == '\x1b' && b == '[' {
125 j := indexLetter(s[i+1:])
126 if j < 0 {
127 return i, -1
128 }
129 return i - 1, i + 1 + j + 1
130 }
131
132 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
133 j := indexPair(s[i+1:], '\x1b', '\\')
134 if j < 0 {
135 return i, -1
136 }
137 return i - 1, i + 1 + j + 2
138 }
139
140 prev = b
141 }
142
143 return -1, -1
144 }
145
146 func indexLetter(s []byte) int {
147 for i, b := range s {
148 upper := b &^ 32
149 if 'A' <= upper && upper <= 'Z' {
150 return i
151 }
152 }
153
154 return -1
155 }
156
157 func indexPair(s []byte, x byte, y byte) int {
158 var prev byte
159
160 for i, b := range s {
161 if prev == x && b == y && i > 0 {
162 return i
163 }
164 prev = b
165 }
166
167 return -1
168 }
169
170 func plain(w *bufio.Writer, r io.Reader, live bool) error {
171 const gb = 1024 * 1024 * 1024
172 sc := bufio.NewScanner(r)
173 sc.Buffer(nil, 8*gb)
174
175 for i := 0; sc.Scan(); i++ {
176 s := sc.Bytes()
177 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
178 s = s[3:]
179 }
180
181 for line := s; len(line) > 0; {
182 i, j := indexEscapeSequence(line)
183 if i < 0 {
184 w.Write(line)
185 break
186 }
187 if j < 0 {
188 j = len(line)
189 }
190
191 if i > 0 {
192 w.Write(line[:i])
193 }
194
195 line = line[j:]
196 }
197
198 if w.WriteByte('\n') != nil {
199 return io.EOF
200 }
201
202 if live && w.Flush() != nil {
203 return io.EOF
204 }
205 }
206
207 return sc.Err()
208 }
File: ./precho/precho.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package precho
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 )
33
34 const info = `
35 precho [options...] [words...]
36
37 PRecede ECHO emits a line with the words given as command-line arguments,
38 then copies all bytes from the standard input into the standard output.
39 This is a handy tool to add a missing header line with column names to
40 SSV (space-separated values) table data.
41
42 The options are, available both in single and double-dash versions
43
44 -h, -help show this help message
45 `
46
47 func Main() {
48 buffered := false
49 args := os.Args[1:]
50
51 if len(args) > 0 {
52 switch args[0] {
53 case `-b`, `--b`, `-buffered`, `--buffered`:
54 buffered = true
55 args = args[1:]
56
57 case `-h`, `--h`, `-help`, `--help`:
58 os.Stdout.WriteString(info[1:])
59 return
60 }
61 }
62
63 if len(args) > 0 && args[0] == `--` {
64 args = args[1:]
65 }
66
67 liveLines := !buffered
68 if !buffered {
69 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
70 liveLines = false
71 }
72 }
73
74 if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
75 os.Stderr.WriteString(`precho: `)
76 os.Stderr.WriteString(err.Error())
77 os.Stderr.WriteString("\n")
78 os.Exit(1)
79 return
80 }
81 }
82
83 func run(w io.Writer, args []string, live bool) error {
84 bw := bufio.NewWriterSize(w, 32*1024)
85 defer bw.Flush()
86
87 for i, s := range args {
88 if i > 0 {
89 bw.WriteByte(' ')
90 }
91 bw.WriteString(s)
92 }
93
94 if len(args) > 0 && bw.WriteByte('\n') != nil {
95 return io.EOF
96 }
97
98 if live && bw.Flush() != nil {
99 return io.EOF
100 }
101
102 return catl(bw, os.Stdin, live)
103 }
104
105 func catl(w *bufio.Writer, r io.Reader, live bool) error {
106 const gb = 1024 * 1024 * 1024
107 sc := bufio.NewScanner(r)
108 sc.Buffer(nil, 8*gb)
109
110 for i := 0; sc.Scan(); i++ {
111 s := sc.Bytes()
112 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
113 s = s[3:]
114 }
115
116 w.Write(s)
117 if w.WriteByte('\n') != nil {
118 return io.EOF
119 }
120
121 if live && w.Flush() != nil {
122 return io.EOF
123 }
124 }
125
126 return sc.Err()
127 }
File: ./pretsv/pretsv.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package pretsv
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 )
33
34 const info = `
35 pretsv [options...] [header names...]
36
37 PREcede TSV, emits a Tab-Separated-Values line using the arguments given,
38 following it with lines from the standard input. This is a handy tool to
39 add a missing header line with column names to TSV table data.
40
41 All (optional) leading options start with either single or double-dash:
42
43 -h, -help show this help message
44 `
45
46 func Main() {
47 buffered := false
48 args := os.Args[1:]
49
50 if len(args) > 0 {
51 switch args[0] {
52 case `-b`, `--b`, `-buffered`, `--buffered`:
53 buffered = true
54 args = args[1:]
55
56 case `-h`, `--h`, `-help`, `--help`:
57 os.Stdout.WriteString(info[1:])
58 return
59 }
60 }
61
62 if len(args) > 0 && args[0] == `--` {
63 args = args[1:]
64 }
65
66 liveLines := !buffered
67 if !buffered {
68 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
69 liveLines = false
70 }
71 }
72
73 if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
74 os.Stderr.WriteString(`pretsv: `)
75 os.Stderr.WriteString(err.Error())
76 os.Stderr.WriteString("\n")
77 os.Exit(1)
78 return
79 }
80 }
81
82 func run(w io.Writer, args []string, live bool) error {
83 bw := bufio.NewWriterSize(w, 32*1024)
84 defer bw.Flush()
85
86 for i, s := range args {
87 if i > 0 {
88 bw.WriteByte('\t')
89 }
90 bw.WriteString(s)
91 }
92
93 if len(args) > 0 && bw.WriteByte('\n') != nil {
94 return io.EOF
95 }
96
97 if live && bw.Flush() != nil {
98 return io.EOF
99 }
100
101 return catl(bw, os.Stdin, live)
102 }
103
104 func catl(w *bufio.Writer, r io.Reader, live bool) error {
105 const gb = 1024 * 1024 * 1024
106 sc := bufio.NewScanner(r)
107 sc.Buffer(nil, 8*gb)
108
109 for i := 0; sc.Scan(); i++ {
110 s := sc.Bytes()
111 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
112 s = s[3:]
113 }
114
115 w.Write(s)
116 if w.WriteByte('\n') != nil {
117 return io.EOF
118 }
119
120 if live && w.Flush() != nil {
121 return io.EOF
122 }
123 }
124
125 return sc.Err()
126 }
File: ./primes/primes.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package primes
26
27 import (
28 "bufio"
29 "math"
30 "os"
31 "strconv"
32 )
33
34 const info = `
35 primes [options...] [count...]
36
37
38 Show the first few prime numbers, starting from the lowest and showing one
39 per line. When not given how many primes to find, the default is 1 million.
40
41 All (optional) leading options start with either single or double-dash:
42
43 -h, -help show this help message
44 `
45
46 func Main() {
47 howMany := 1_000_000
48 if len(os.Args) > 1 {
49 switch os.Args[1] {
50 case `-h`, `--h`, `-help`, `--help`:
51 os.Stdout.WriteString(info[1:])
52 return
53 }
54
55 n, err := strconv.Atoi(os.Args[1])
56 if err != nil {
57 os.Stderr.WriteString(`primes: `)
58 os.Stderr.WriteString(err.Error())
59 os.Stderr.WriteString("\n")
60 os.Exit(1)
61 return
62 }
63
64 if n < 0 {
65 n = 0
66 }
67 howMany = n
68 }
69
70 primes(howMany)
71 }
72
73 func primes(left int) {
74 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
75 defer bw.Flush()
76
77 // 24 bytes are always enough for any 64-bit integer
78 var buf [24]byte
79
80 // 2 is the only even prime number
81 if left > 0 {
82 bw.WriteString("2\n")
83 left--
84 }
85
86 for n := uint64(3); left > 0; n += 2 {
87 if oddPrime(n) {
88 bw.Write(strconv.AppendUint(buf[:0], n, 10))
89 if err := bw.WriteByte('\n'); err != nil {
90 // assume errors come from closed stdout pipes
91 return
92 }
93 left--
94 }
95 }
96 }
97
98 // oddPrime assumes the number given to it is odd
99 func oddPrime(n uint64) bool {
100 max := uint64(math.Sqrt(float64(n)))
101 for div := uint64(3); div <= max; div += 2 {
102 if n%div == 0 {
103 return false
104 }
105 }
106 return true
107 }
File: ./ptsv/ptsv.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package ptsv
26
27 import (
28 "bufio"
29 "os"
30 "strconv"
31 "strings"
32 )
33
34 const info = `
35 ptsv [options...] [columns/indices...]
36
37 Pick Tab-Separated Values picks only the columns using the arguments given,
38 in the order given. Arguments are matched to columns, first exactly, failing
39 that case-insensitively, and failing even that, as 1-based indices, with
40 negative numbers counting backward from the last column.
41
42 Input is TSV (tab-separated values) lines from the standard input, where the
43 first/header line has all the column names.
44
45 The options are, available both in single and double-dash versions
46
47 -h, -help show this help message
48 `
49
50 func Main() {
51 args := os.Args[1:]
52
53 if len(args) > 0 {
54 switch args[0] {
55 case `-h`, `--h`, `-help`, `--help`:
56 os.Stdout.WriteString(info[1:])
57 return
58 }
59 }
60
61 if len(args) > 0 && args[0] == `--` {
62 args = args[1:]
63 }
64
65 // if len(args) == 0 {
66 // os.Stderr.WriteString(info[1:])
67 // os.Exit(1)
68 // return
69 // }
70
71 var items []string
72 indices := make([]int, 0, len(args))
73
74 sc := bufio.NewScanner(os.Stdin)
75 sc.Buffer(nil, 8*1024*1024*1024)
76
77 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
78 defer bw.Flush()
79
80 for i := 0; sc.Scan(); i++ {
81 line := sc.Text()
82
83 if i == 0 {
84 if strings.HasPrefix(line, "\xef\xbb\xbf") {
85 line = line[3:]
86 }
87
88 items = appendTSV(items[:0], line)
89
90 for _, s := range args {
91 if j := findColumn(items, s); j >= 0 {
92 indices = append(indices, j)
93 }
94 }
95 } else {
96 items = appendTSV(items[:0], line)
97 }
98
99 j := 0
100
101 for _, k := range indices {
102 if j > 0 {
103 bw.WriteByte('\t')
104 }
105 j++
106 if k < len(items) {
107 bw.WriteString(items[k])
108 }
109 }
110
111 if bw.WriteByte('\n') != nil {
112 return
113 }
114 }
115 }
116
117 func appendTSV(to []string, line string) []string {
118 for len(line) > 0 {
119 i := strings.IndexByte(line, '\t')
120 if i < 0 {
121 return append(to, line)
122 }
123
124 to = append(to, line[:i])
125 line = line[i+1:]
126 }
127
128 return to
129 }
130
131 // findColumn tries to find the 0-based column-index, given the first/header
132 // row, and a name/integer-index
133 func findColumn(items []string, name string) int {
134 // try exact match
135 for i, s := range items {
136 if s == name {
137 return i
138 }
139 }
140
141 // try case-insensitive match
142 for i, s := range items {
143 if strings.EqualFold(s, name) {
144 return i
145 }
146 }
147
148 // try a non-zero numeric index
149 i, err := strconv.Atoi(name)
150 if err != nil || i == 0 {
151 return -1
152 }
153
154 // make indices 0-based, and correct negative indices
155 if i > 0 {
156 i--
157 } else if i < 0 {
158 i += len(items)
159 }
160
161 // see if numeric index is in range
162 if 0 <= i && i < len(items) {
163 return i
164 }
165
166 return -1
167 }
File: ./realign/realign.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package realign
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "os"
32 "strings"
33 "unicode/utf8"
34 )
35
36 const info = `
37 realign [options...] [filenames...]
38
39 Realign all detected columns, right-aligning any detected numbers in any
40 column. ANSI style-codes are also kept as given.
41
42 The options are, available both in single and double-dash versions
43
44 -h, -help show this help message
45 -m, -max-columns use the row with the most items for the item-count
46 `
47
48 func Main() {
49 maxCols := false
50 args := os.Args[1:]
51
52 for len(args) > 0 {
53 if args[0] == `--` {
54 args = args[1:]
55 break
56 }
57
58 switch args[0] {
59 case `-h`, `--h`, `-help`, `--help`:
60 os.Stdout.WriteString(info[1:])
61 return
62
63 case
64 `-m`, `--m`,
65 `-maxcols`, `--maxcols`,
66 `-max-columns`, `--max-columns`:
67 maxCols = true
68 args = args[1:]
69 continue
70 }
71
72 break
73 }
74
75 if err := run(args, maxCols); err != nil {
76 os.Stderr.WriteString(`realign: `)
77 os.Stderr.WriteString(err.Error())
78 os.Stderr.WriteString("\n")
79 os.Exit(1)
80 return
81 }
82 }
83
84 // table has all summary info gathered from the data, along with the row
85 // themselves, stored as lines/strings
86 type table struct {
87 Columns int
88
89 Rows []string
90
91 MaxWidth []int
92
93 MaxIntWidth []int
94
95 MaxDotDecimals []int
96
97 LoopItems func(s string, max int, t *table, f itemFunc)
98
99 MaxColumns bool
100 }
101
102 type itemFunc func(i int, s string, t *table)
103
104 func run(paths []string, maxCols bool) error {
105 var res table
106 res.MaxColumns = maxCols
107
108 for _, p := range paths {
109 if err := handleFile(&res, p); err != nil {
110 return err
111 }
112 }
113
114 if len(paths) == 0 {
115 if err := handleReader(&res, os.Stdin); err != nil {
116 return err
117 }
118 }
119
120 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
121 defer bw.Flush()
122 realign(bw, res)
123 return nil
124 }
125
126 func handleFile(res *table, path string) error {
127 f, err := os.Open(path)
128 if err != nil {
129 // on windows, file-not-found error messages may mention `CreateFile`,
130 // even when trying to open files in read-only mode
131 return errors.New(`can't open file named ` + path)
132 }
133 defer f.Close()
134 return handleReader(res, f)
135 }
136
137 func handleReader(t *table, r io.Reader) error {
138 const gb = 1024 * 1024 * 1024
139 sc := bufio.NewScanner(r)
140 sc.Buffer(nil, 8*gb)
141
142 const maxInt = int(^uint(0) >> 1)
143 maxCols := maxInt
144
145 for i := 0; sc.Scan(); i++ {
146 s := sc.Text()
147 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
148 s = s[3:]
149 }
150
151 if len(s) == 0 {
152 if len(t.Rows) > 0 {
153 t.Rows = append(t.Rows, ``)
154 }
155 continue
156 }
157
158 t.Rows = append(t.Rows, s)
159
160 if t.Columns == 0 {
161 if t.LoopItems == nil {
162 if strings.IndexByte(s, '\t') >= 0 {
163 t.LoopItems = loopItemsTSV
164 } else {
165 t.LoopItems = loopItemsSSV
166 }
167 }
168
169 if !t.MaxColumns {
170 t.LoopItems(s, maxCols, t, updateColumnCount)
171 maxCols = t.Columns
172 }
173 }
174
175 t.LoopItems(s, maxCols, t, updateItem)
176 }
177
178 return sc.Err()
179 }
180
181 func updateColumnCount(i int, s string, t *table) {
182 t.Columns = i + 1
183 }
184
185 func updateItem(i int, s string, t *table) {
186 // ensure column-info-slices have enough room
187 if i >= len(t.MaxWidth) {
188 // update column-count if in max-columns mode
189 if t.MaxColumns {
190 t.Columns = i + 1
191 }
192 t.MaxWidth = append(t.MaxWidth, 0)
193 t.MaxIntWidth = append(t.MaxIntWidth, 0)
194 t.MaxDotDecimals = append(t.MaxDotDecimals, 0)
195 }
196
197 // keep track of widest rune-counts for each column
198 w := countWidth(s)
199 if t.MaxWidth[i] < w {
200 t.MaxWidth[i] = w
201 }
202
203 // update stats for numeric items
204 if isNumeric(s) {
205 iw, dd := countNumericWidths(s)
206 if t.MaxIntWidth[i] < iw {
207 t.MaxIntWidth[i] = iw
208 }
209 if t.MaxDotDecimals[i] < dd {
210 t.MaxDotDecimals[i] = dd
211 }
212 }
213 }
214
215 // loopItemsSSV loops over a line's items, allocation-free style; when given
216 // empty strings, the callback func is never called
217 func loopItemsSSV(s string, max int, t *table, f itemFunc) {
218 s = trimTrailingSpaces(s)
219
220 for i := 0; true; i++ {
221 s = trimLeadingSpaces(s)
222 if len(s) == 0 {
223 return
224 }
225
226 if i+1 == max {
227 f(i, s, t)
228 return
229 }
230
231 j := strings.IndexByte(s, ' ')
232 if j < 0 {
233 f(i, s, t)
234 return
235 }
236
237 f(i, s[:j], t)
238 s = s[j+1:]
239 }
240 }
241
242 func trimLeadingSpaces(s string) string {
243 for len(s) > 0 && s[0] == ' ' {
244 s = s[1:]
245 }
246 return s
247 }
248
249 func trimTrailingSpaces(s string) string {
250 for len(s) > 0 && s[len(s)-1] == ' ' {
251 s = s[:len(s)-1]
252 }
253 return s
254 }
255
256 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
257 // when given empty strings, the callback func is never called
258 func loopItemsTSV(s string, max int, t *table, f itemFunc) {
259 if len(s) == 0 {
260 return
261 }
262
263 for i := 0; true; i++ {
264 if i+1 == max {
265 f(i, s, t)
266 return
267 }
268
269 j := strings.IndexByte(s, '\t')
270 if j < 0 {
271 f(i, s, t)
272 return
273 }
274
275 f(i, s[:j], t)
276 s = s[j+1:]
277 }
278 }
279
280 func skipLeadingEscapeSequences(s string) string {
281 for len(s) >= 2 {
282 if s[0] != '\x1b' {
283 return s
284 }
285
286 switch s[1] {
287 case '[':
288 s = skipSingleLeadingANSI(s[2:])
289
290 case ']':
291 if len(s) < 3 || s[2] != '8' {
292 return s
293 }
294 s = skipSingleLeadingOSC(s[3:])
295
296 default:
297 return s
298 }
299 }
300
301 return s
302 }
303
304 func skipSingleLeadingANSI(s string) string {
305 for len(s) > 0 {
306 upper := s[0] &^ 32
307 s = s[1:]
308 if 'A' <= upper && upper <= 'Z' {
309 break
310 }
311 }
312
313 return s
314 }
315
316 func skipSingleLeadingOSC(s string) string {
317 var prev byte
318
319 for len(s) > 0 {
320 b := s[0]
321 s = s[1:]
322 if prev == '\x1b' && b == '\\' {
323 break
324 }
325 prev = b
326 }
327
328 return s
329 }
330
331 // isNumeric checks if a string is valid/useable as a number
332 func isNumeric(s string) bool {
333 if len(s) == 0 {
334 return false
335 }
336
337 s = skipLeadingEscapeSequences(s)
338 if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
339 s = s[1:]
340 }
341
342 s = skipLeadingEscapeSequences(s)
343 if len(s) == 0 {
344 return false
345 }
346 if s[0] == '.' {
347 return isDigits(s[1:])
348 }
349
350 digits := 0
351
352 for {
353 s = skipLeadingEscapeSequences(s)
354 if len(s) == 0 {
355 break
356 }
357
358 if s[0] == '.' {
359 return isDigits(s[1:])
360 }
361
362 if !('0' <= s[0] && s[0] <= '9') {
363 return false
364 }
365
366 digits++
367 s = s[1:]
368 }
369
370 s = skipLeadingEscapeSequences(s)
371 return len(s) == 0 && digits > 0
372 }
373
374 func isDigits(s string) bool {
375 if len(s) == 0 {
376 return false
377 }
378
379 digits := 0
380
381 for {
382 s = skipLeadingEscapeSequences(s)
383 if len(s) == 0 {
384 break
385 }
386
387 if '0' <= s[0] && s[0] <= '9' {
388 s = s[1:]
389 digits++
390 } else {
391 return false
392 }
393 }
394
395 s = skipLeadingEscapeSequences(s)
396 return len(s) == 0 && digits > 0
397 }
398
399 // countDecimals counts decimal digits from the string given, assuming it
400 // represents a valid/useable float64, when parsed
401 func countDecimals(s string) int {
402 dot := strings.IndexByte(s, '.')
403 if dot < 0 {
404 return 0
405 }
406
407 decs := 0
408 s = s[dot+1:]
409
410 for len(s) > 0 {
411 s = skipLeadingEscapeSequences(s)
412 if len(s) == 0 {
413 break
414 }
415 if '0' <= s[0] && s[0] <= '9' {
416 decs++
417 }
418 s = s[1:]
419 }
420
421 return decs
422 }
423
424 // countDotDecimals is like func countDecimals, but this one also includes
425 // the dot, when any decimals are present, else the count stays at 0
426 func countDotDecimals(s string) int {
427 decs := countDecimals(s)
428 if decs > 0 {
429 return decs + 1
430 }
431 return decs
432 }
433
434 // countNumericWidths counts both the integer-width (including leading sign,
435 // if found), and the dot-decimal trail from the string given, assuming it
436 // represents a valid/useable float64, when parsed
437 func countNumericWidths(s string) (intWidth int, dotDecimals int) {
438 sign := 0
439 if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
440 sign++
441 s = s[1:]
442 }
443
444 for len(s) > 0 {
445 s = skipLeadingEscapeSequences(s)
446 if len(s) == 0 {
447 break
448 }
449 if '0' <= s[0] && s[0] <= '9' {
450 intWidth++
451 } else {
452 break
453 }
454 s = s[1:]
455 }
456
457 intWidth += sign
458
459 if len(s) == 0 || s[0] != '.' {
460 return intWidth, dotDecimals
461 }
462
463 for len(s) > 0 {
464 s = skipLeadingEscapeSequences(s)
465 if len(s) == 0 {
466 break
467 }
468 if '0' <= s[0] && s[0] <= '9' {
469 dotDecimals++
470 }
471 s = s[1:]
472 }
473
474 if dotDecimals > 0 {
475 dotDecimals++
476 }
477 return intWidth, dotDecimals
478 }
479
480 func countWidth(s string) int {
481 width := 0
482
483 for len(s) > 0 {
484 i, j := indexEscapeSequence(s)
485 if i < 0 {
486 break
487 }
488 if j < 0 {
489 j = len(s)
490 }
491
492 width += utf8.RuneCountInString(s[:i])
493 s = s[j:]
494 }
495
496 // count trailing/all runes in strings which don't end with ANSI-sequences
497 width += utf8.RuneCountInString(s)
498 return width
499 }
500
501 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
502 // the multi-byte sequences starting with ESC[; the result is a pair of slice
503 // indices which can be independently negative when either the start/end of
504 // a sequence isn't found; given their fairly-common use, even the hyperlink
505 // ESC]8 sequences are supported
506 func indexEscapeSequence(s string) (int, int) {
507 var prev byte
508
509 for i := range s {
510 b := s[i]
511
512 if prev == '\x1b' && b == '[' {
513 j := indexLetter(s[i+1:])
514 if j < 0 {
515 return i, -1
516 }
517 return i - 1, i + 1 + j + 1
518 }
519
520 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
521 j := indexPair(s[i+1:], '\x1b', '\\')
522 if j < 0 {
523 return i, -1
524 }
525 return i - 1, i + 1 + j + 2
526 }
527
528 prev = b
529 }
530
531 return -1, -1
532 }
533
534 func indexLetter(s string) int {
535 for i, b := range s {
536 upper := b &^ 32
537 if 'A' <= upper && upper <= 'Z' {
538 return i
539 }
540 }
541
542 return -1
543 }
544
545 func indexPair(s string, x byte, y byte) int {
546 var prev byte
547
548 for i := range s {
549 b := s[i]
550 if prev == x && b == y && i > 0 {
551 return i
552 }
553 prev = b
554 }
555
556 return -1
557 }
558
559 func realign(w *bufio.Writer, t table) {
560 // decimal numbers can pad beyond their max-width
561 for i, _ := range t.MaxWidth {
562 w := t.MaxIntWidth[i] + t.MaxDotDecimals[i]
563 if t.MaxWidth[i] < w {
564 t.MaxWidth[i] = w
565 }
566 }
567
568 due := 0
569 showItem := func(i int, s string, t *table) {
570 if i > 0 {
571 due += 2
572 }
573
574 if isNumeric(s) {
575 dd := countDotDecimals(s)
576 rpad := t.MaxDotDecimals[i] - dd
577 width := countWidth(s)
578 lpad := t.MaxWidth[i] - (width + rpad) + due
579 writeSpaces(w, lpad)
580 w.WriteString(s)
581 due = rpad
582 return
583 }
584
585 writeSpaces(w, due)
586 w.WriteString(s)
587 due = t.MaxWidth[i] - countWidth(s)
588 }
589
590 for _, line := range t.Rows {
591 due = 0
592 if len(line) > 0 {
593 t.LoopItems(line, t.Columns, &t, showItem)
594 }
595 if w.WriteByte('\n') != nil {
596 break
597 }
598 }
599 }
600
601 // writeSpaces does what it says, minimizing calls to write-like funcs
602 func writeSpaces(w *bufio.Writer, n int) {
603 const spaces = ` `
604 if n < 1 {
605 return
606 }
607
608 for n >= len(spaces) {
609 w.WriteString(spaces)
610 n -= len(spaces)
611 }
612 w.WriteString(spaces[:n])
613 }
File: ./realign/realign_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package realign
26
27 import "testing"
28
29 func TestCountWidth(t *testing.T) {
30 tests := map[string]struct {
31 input string
32 expected int
33 }{
34 `empty`: {``, 0},
35 `empty ANSI`: {"\x1b[38;5;0;0;0m\x1b[0m", 0},
36 `simple plain`: {`abc def`, 7},
37 `unicode plain`: {`abc●def`, 7},
38 `simple ANSI`: {"abc \x1b[7mde\x1b[0mf", 7},
39 `unicode ANSI`: {"abc●\x1b[7mde\x1b[0mf", 7},
40 }
41
42 for name, tc := range tests {
43 t.Run(name, func(t *testing.T) {
44 got := countWidth(tc.input)
45 if got != tc.expected {
46 const fs = "expected width %d, got %d instead"
47 t.Errorf(fs, tc.expected, got)
48 }
49 })
50 }
51 }
52
53 func TestCountNumericWidths(t *testing.T) {
54 tests := map[string]struct {
55 intWidth int
56 dotDecimals int
57 }{
58 `123`: {3, 0},
59 `+123`: {4, 0},
60 `-123`: {4, 0},
61 `123.0`: {3, 2},
62 `+123.9`: {4, 2},
63 `-123.3`: {4, 2},
64 `123.01`: {3, 3},
65 `+123.93`: {4, 3},
66 `-123.344`: {4, 4},
67 }
68
69 for s, tc := range tests {
70 t.Run(s, func(t *testing.T) {
71 if !isNumeric(s) {
72 t.Errorf(`%s isn't numeric`, s)
73 return
74 }
75
76 iw, dd := countNumericWidths(s)
77 if iw != tc.intWidth || dd != tc.dotDecimals {
78 const fs = `expected (%d, %d), got (%d, %d) instead`
79 t.Errorf(fs, tc.intWidth, tc.dotDecimals, iw, dd)
80 }
81 })
82 }
83 }
File: ./reprose/reprose.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package reprose
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 "strconv"
34 "strings"
35 "unicode/utf8"
36 )
37
38 const info = `
39 reprose [options...] [max width...] [files...]
40
41 Reflow/trim lines of prose (text) to improve its legibility: this tool is
42 especially useful when the text is pasted from web-pages being viewed in
43 reader mode.
44
45 This tool also ensures no lines across inputs are accidentally joined,
46 since all lines it outputs end with line-feeds, even when the original
47 files don't.
48
49 The options are, available both in single and double-dash versions
50
51 -h, -help show this help message
52 `
53
54 type config struct {
55 // pos is the current position/symbol-count for the current output line
56 pos int
57
58 // maxWidth is the maximum number of symbols per line, when doable
59 maxWidth int
60
61 // liveLines is whether to flush every output line
62 liveLines bool
63 }
64
65 func Main() {
66 var cfg config
67 cfg.maxWidth = 80
68 buffered := false
69 args := os.Args[1:]
70
71 for len(args) > 0 {
72 switch args[0] {
73 case `-b`, `--b`, `-buffered`, `--buffered`:
74 buffered = true
75 args = args[1:]
76 continue
77
78 case `-h`, `--h`, `-help`, `--help`:
79 os.Stdout.WriteString(info[1:])
80 return
81 }
82
83 break
84 }
85
86 if len(args) > 0 {
87 s := strings.Replace(args[0], `_`, ``, -1)
88 if v, err := strconv.ParseInt(s, 10, 64); err == nil {
89 cfg.maxWidth = int(v)
90 args = args[1:]
91 }
92 }
93
94 if len(args) > 0 && args[0] == `--` {
95 args = args[1:]
96 }
97
98 cfg.liveLines = !buffered
99 if !buffered {
100 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
101 cfg.liveLines = false
102 }
103 }
104
105 if err := run(args, cfg); err != nil && err != io.EOF {
106 os.Stderr.WriteString(`reprose: `)
107 os.Stderr.WriteString(err.Error())
108 os.Stderr.WriteString("\n")
109 os.Exit(1)
110 return
111 }
112 }
113
114 func run(paths []string, cfg config) error {
115 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
116 defer bw.Flush()
117
118 for _, p := range paths {
119 if err := handleFile(bw, p, &cfg); err != nil {
120 return err
121 }
122 }
123
124 if len(paths) == 0 {
125 if err := handleReader(bw, os.Stdin, &cfg); err != nil {
126 return err
127 }
128 }
129
130 // end current line, if there's anything on it
131 if cfg.pos > 0 {
132 if bw.WriteByte('\n') != nil {
133 return io.EOF
134 }
135 }
136 return nil
137 }
138
139 func handleFile(w *bufio.Writer, path string, cfg *config) error {
140 f, err := os.Open(path)
141 if err != nil {
142 // on windows, file-not-found error messages may mention `CreateFile`,
143 // even when trying to open files in read-only mode
144 return errors.New(`can't open file named ` + path)
145 }
146 defer f.Close()
147 return handleReader(w, f, cfg)
148 }
149
150 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
151 const gb = 1024 * 1024 * 1024
152 sc := bufio.NewScanner(r)
153 sc.Buffer(nil, 8*gb)
154
155 for i := 0; sc.Scan(); i++ {
156 s := sc.Bytes()
157 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
158 s = s[3:]
159 }
160
161 s = trimTrailingSpaces(s)
162
163 // keep empty(ish) lines as empty lines
164 if len(s) == 0 {
165 if cfg.pos > 0 && w.WriteByte('\n') != nil {
166 return io.EOF
167 }
168 if w.WriteByte('\n') != nil {
169 return io.EOF
170 }
171 if cfg.liveLines && w.Flush() != nil {
172 return io.EOF
173 }
174 cfg.pos = 0
175 continue
176 }
177
178 // reflow all `words` from non-empty lines
179 for len(s) > 0 {
180 var word []byte
181 s = trimLeadingSpaces(s)
182 if i := bytes.IndexByte(s, ' '); i >= 0 {
183 word = s[:i]
184 s = s[i+1:]
185 } else {
186 word = s
187 s = nil
188 }
189
190 word = trimLeadingSpaces(word)
191 width := countWidth(word)
192 if width == 0 {
193 continue
194 }
195
196 if cfg.pos+width > cfg.maxWidth {
197 if cfg.pos > 0 && w.WriteByte('\n') != nil {
198 return io.EOF
199 }
200 if cfg.liveLines && w.Flush() != nil {
201 return io.EOF
202 }
203 cfg.pos = 0
204 }
205
206 if cfg.pos > 0 {
207 w.WriteByte(' ')
208 cfg.pos++
209 }
210 w.Write(word)
211 cfg.pos += width
212 }
213 }
214
215 return sc.Err()
216 }
217
218 func trimLeadingSpaces(s []byte) []byte {
219 for len(s) > 0 && s[0] == ' ' {
220 s = s[1:]
221 }
222 return s
223 }
224
225 func trimTrailingSpaces(s []byte) []byte {
226 for len(s) > 0 && s[len(s)-1] == ' ' {
227 s = s[:len(s)-1]
228 }
229 return s
230 }
231
232 func countWidth(s []byte) int {
233 width := 0
234
235 for len(s) > 0 {
236 i, j := indexEscapeSequence(s)
237 if i < 0 {
238 break
239 }
240 if j < 0 {
241 j = len(s)
242 }
243
244 width += utf8.RuneCount(s[:i])
245 s = s[j:]
246 }
247
248 // count trailing/all runes in strings which don't end with ANSI-sequences
249 width += utf8.RuneCount(s)
250 return width
251 }
252
253 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
254 // the multi-byte sequences starting with ESC[; the result is a pair of slice
255 // indices which can be independently negative when either the start/end of
256 // a sequence isn't found; given their fairly-common use, even the hyperlink
257 // ESC]8 sequences are supported
258 func indexEscapeSequence(s []byte) (int, int) {
259 var prev byte
260
261 for i := range s {
262 b := s[i]
263
264 if prev == '\x1b' && b == '[' {
265 j := indexLetter(s[i+1:])
266 if j < 0 {
267 return i, -1
268 }
269 return i - 1, i + 1 + j + 1
270 }
271
272 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
273 j := indexPair(s[i+1:], '\x1b', '\\')
274 if j < 0 {
275 return i, -1
276 }
277 return i - 1, i + 1 + j + 2
278 }
279
280 prev = b
281 }
282
283 return -1, -1
284 }
285
286 func indexLetter(s []byte) int {
287 for i, b := range s {
288 upper := b &^ 32
289 if 'A' <= upper && upper <= 'Z' {
290 return i
291 }
292 }
293
294 return -1
295 }
296
297 func indexPair(s []byte, x byte, y byte) int {
298 var prev byte
299
300 for i := range s {
301 b := s[i]
302 if prev == x && b == y && i > 0 {
303 return i
304 }
305 prev = b
306 }
307
308 return -1
309 }
File: ./restyle/restyle.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package restyle
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "strings"
33 )
34
35 const info = `
36 restyle [options...] [style...]
37
38
39 Change lines from the standard input, using ANSI-codes for the style/color
40 name given. Existing ANSI-styles from input lines are ignored, except when
41 inverse-styling lines.
42
43 The options are, available both in single and double-dash versions
44
45 -h, -help show this help message
46
47 Some of the colors/styles available are:
48
49 blue blueback
50 bold
51 gray grayback
52 green greenback
53 inverse
54 magenta magentaback
55 orange orangeback
56 purple purpleback
57 red redback
58 underline
59
60 Some style aliases are:
61
62 b blue bb blueback
63 g green gb greenback
64 m magenta mb magentaback
65 o orange ob orangeback
66 p purple pb purpleback
67 r red rb redback
68 i inverse (highlight)
69 u underline
70 `
71
72 var styleAliases = map[string]string{
73 `b`: `blue`,
74 `g`: `green`,
75 `m`: `magenta`,
76 `o`: `orange`,
77 `p`: `purple`,
78 `r`: `red`,
79 `u`: `underline`,
80
81 `bb`: `blueback`,
82 `bg`: `greenback`,
83 `bm`: `magentaback`,
84 `bo`: `orangeback`,
85 `bp`: `purpleback`,
86 `br`: `redback`,
87
88 `gb`: `greenback`,
89 `mb`: `magentaback`,
90 `ob`: `orangeback`,
91 `pb`: `purpleback`,
92 `rb`: `redback`,
93
94 `hi`: `inverse`,
95 `inv`: `inverse`,
96 `mag`: `magenta`,
97
98 `du`: `doubleunderline`,
99
100 `flip`: `inverse`,
101 `swap`: `inverse`,
102
103 `reset`: `plain`,
104 `highlight`: `inverse`,
105 `hilite`: `inverse`,
106 `invert`: `inverse`,
107 `inverted`: `inverse`,
108 `swapped`: `inverse`,
109
110 `dunderline`: `doubleunderline`,
111 `dunderlined`: `doubleunderline`,
112
113 `strikethrough`: `strike`,
114 `strikethru`: `strike`,
115 `struck`: `strike`,
116
117 `underlined`: `underline`,
118
119 `bblue`: `blueback`,
120 `bgray`: `grayback`,
121 `bgreen`: `greenback`,
122 `bmagenta`: `magentaback`,
123 `borange`: `orangeback`,
124 `bpurple`: `purpleback`,
125 `bred`: `redback`,
126
127 `bgblue`: `blueback`,
128 `bggray`: `grayback`,
129 `bggreen`: `greenback`,
130 `bgmag`: `magentaback`,
131 `bgmagenta`: `magentaback`,
132 `bgorange`: `orangeback`,
133 `bgpurple`: `purpleback`,
134 `bgred`: `redback`,
135
136 `bluebg`: `blueback`,
137 `graybg`: `grayback`,
138 `greenbg`: `greenback`,
139 `magbg`: `magentaback`,
140 `magentabg`: `magentaback`,
141 `orangebg`: `orangeback`,
142 `purplebg`: `purpleback`,
143 `redbg`: `redback`,
144
145 `backblue`: `blueback`,
146 `backgray`: `grayback`,
147 `backgreen`: `greenback`,
148 `backmag`: `magentaback`,
149 `backmagenta`: `magentaback`,
150 `backorange`: `orangeback`,
151 `backpurple`: `purpleback`,
152 `backred`: `redback`,
153 }
154
155 var styles = map[string]string{
156 `blue`: "\x1b[38;2;0;95;215m",
157 `bold`: "\x1b[1m",
158 `doubleunderline`: "\x1b[21m",
159 `gray`: "\x1b[38;2;168;168;168m",
160 `green`: "\x1b[38;2;0;135;95m",
161 `inverse`: "\x1b[7m",
162 `magenta`: "\x1b[38;2;215;0;255m",
163 `orange`: "\x1b[38;2;215;95;0m",
164 `plain`: "\x1b[0m",
165 `purple`: "\x1b[38;2;135;95;255m",
166 `red`: "\x1b[38;2;204;0;0m",
167 `strike`: "\x1b[9m",
168 `underline`: "\x1b[4m",
169
170 `blueback`: "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
171 `grayback`: "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
172 `greenback`: "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
173 `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
174 `orangeback`: "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
175 `purpleback`: "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
176 `redback`: "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
177 }
178
179 type config struct {
180 // style is the ANSI-code to style output lines
181 style string
182
183 // live is whether output lines are flushed immediately
184 live bool
185 }
186
187 func Main() {
188 buffered := false
189 args := os.Args[1:]
190
191 for len(args) > 0 {
192 switch args[0] {
193 case `-b`, `--b`, `-buffered`, `--buffered`:
194 buffered = true
195 args = args[1:]
196 continue
197
198 case `-h`, `--h`, `-help`, `--help`:
199 os.Stdout.WriteString(info[1:])
200 return
201 }
202
203 break
204 }
205
206 if len(args) > 0 && args[0] == `--` {
207 args = args[1:]
208 }
209
210 if len(args) == 0 {
211 os.Stderr.WriteString("restyle: forgot the style name\n")
212 os.Exit(1)
213 return
214 }
215
216 sname := args[0]
217 args = args[1:]
218
219 if strings.HasPrefix(sname, `--`) {
220 sname = sname[2:]
221 } else if strings.HasPrefix(sname, `-`) {
222 sname = sname[1:]
223 }
224
225 if alias, ok := styleAliases[sname]; ok {
226 sname = alias
227 }
228
229 style, ok := styles[sname]
230 if !ok {
231 os.Stderr.WriteString("restyle: no style named `")
232 os.Stderr.WriteString(sname)
233 os.Stderr.WriteString("`\n")
234 os.Exit(1)
235 return
236 }
237
238 liveLines := !buffered
239 if !buffered {
240 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
241 liveLines = false
242 }
243 }
244
245 var cfg config
246 cfg.style = style
247 cfg.live = liveLines
248
249 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
250 os.Stderr.WriteString(`restyle: `)
251 os.Stderr.WriteString(err.Error())
252 os.Stderr.WriteString("\n")
253 os.Exit(1)
254 return
255 }
256 }
257
258 func run(w io.Writer, paths []string, cfg config) error {
259 bw := bufio.NewWriterSize(w, 32*1024)
260 defer bw.Flush()
261
262 for _, path := range paths {
263 if err := handleFile(bw, path, cfg); err != nil {
264 return err
265 }
266 }
267
268 if len(paths) == 0 {
269 return handleReader(bw, os.Stdin, cfg)
270 }
271 return nil
272 }
273
274 func handleFile(w *bufio.Writer, path string, cfg config) error {
275 f, err := os.Open(path)
276 if err != nil {
277 return err
278 }
279 defer f.Close()
280 return handleReader(w, f, cfg)
281 }
282
283 func handleReader(w *bufio.Writer, r io.Reader, cfg config) error {
284 sc := bufio.NewScanner(os.Stdin)
285 sc.Buffer(nil, 8*1024*1024*1024)
286
287 invert := cfg.style == "\x1b[7m"
288
289 for i := 0; sc.Scan(); i++ {
290 s := sc.Bytes()
291 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
292 s = s[3:]
293 }
294
295 if invert {
296 writeInverted(w, s)
297 } else {
298 writePlain(w, s)
299 }
300
301 if w.WriteByte('\n') != nil {
302 return io.EOF
303 }
304
305 if cfg.live && w.Flush() != nil {
306 return io.EOF
307 }
308 }
309
310 return nil
311 }
312
313 func writeInverted(w *bufio.Writer, s []byte) {
314 w.WriteString("\x1b[7m")
315
316 for len(s) > 0 {
317 i, j := indexEscapeSequence(s)
318 if i < 0 {
319 w.Write(s)
320 break
321 }
322
323 if j < 0 {
324 j = len(s)
325 }
326
327 if i > 0 {
328 w.Write(s[:i])
329 }
330
331 s = s[j:]
332 }
333
334 w.WriteString("\x1b[0m")
335 }
336
337 func writePlain(w *bufio.Writer, s []byte) {
338 for len(s) > 0 {
339 i, j := indexEscapeSequence(s)
340 if i < 0 {
341 w.Write(s)
342 break
343 }
344
345 if j < 0 {
346 j = len(s)
347 }
348
349 if i > 0 {
350 w.Write(s[:i])
351 }
352
353 s = s[j:]
354 }
355 }
356
357 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
358 // the multi-byte sequences starting with ESC[; the result is a pair of slice
359 // indices which can be independently negative when either the start/end of
360 // a sequence isn't found; given their fairly-common use, even the hyperlink
361 // ESC]8 sequences are supported
362 func indexEscapeSequence(s []byte) (int, int) {
363 var prev byte
364
365 for i, b := range s {
366 if prev == '\x1b' && b == '[' {
367 j := indexLetter(s[i+1:])
368 if j < 0 {
369 return i, -1
370 }
371 return i - 1, i + 1 + j + 1
372 }
373
374 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
375 j := indexPair(s[i+1:], '\x1b', '\\')
376 if j < 0 {
377 return i, -1
378 }
379 return i - 1, i + 1 + j + 2
380 }
381
382 prev = b
383 }
384
385 return -1, -1
386 }
387
388 func indexLetter(s []byte) int {
389 for i, b := range s {
390 upper := b &^ 32
391 if 'A' <= upper && upper <= 'Z' {
392 return i
393 }
394 }
395
396 return -1
397 }
398
399 func indexPair(s []byte, x byte, y byte) int {
400 var prev byte
401
402 for i, b := range s {
403 if prev == x && b == y && i > 0 {
404 return i
405 }
406 prev = b
407 }
408
409 return -1
410 }
File: ./same/same.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package same
26
27 import (
28 "bytes"
29 "errors"
30 "fmt"
31 "io"
32 "os"
33 )
34
35 const info = `
36 same [options...] [file 1] [file 2]
37
38
39 Check if 2 files have the same contents. Output is emitted only for errors,
40 and only on the standard error, if so.
41
42 This tool is used for its return code, which is 0 when the files have the
43 same contents, 1 when they don't, or 2 when not given exactly 2 files to
44 compare, or if either file isn't found.
45
46 The options are, available both in single and double-dash versions
47
48 -h, -help show this help message
49 `
50
51 func Main() {
52 args := os.Args[1:]
53
54 if len(args) > 0 {
55 switch args[0] {
56 case `-h`, `--h`, `-help`, `--help`:
57 os.Stdout.WriteString(info[1:])
58 return
59 }
60 }
61
62 if len(args) > 0 && args[0] == `--` {
63 args = args[1:]
64 }
65
66 if len(args) != 2 {
67 fmt.Fprintf(os.Stderr, "given %d files, instead of 2\n", len(args))
68 // exit-status 1 is only for when the files have different contents
69 os.Exit(2)
70 return
71 }
72
73 same, err := run(args[0], args[1])
74 if err != nil && err != io.EOF {
75 os.Stderr.WriteString(`same: `)
76 os.Stderr.WriteString(err.Error())
77 os.Stderr.WriteString("\n")
78 // exit-status 1 is only for when the files have different contents
79 os.Exit(2)
80 return
81 }
82
83 if !same {
84 os.Exit(1)
85 return
86 }
87 }
88
89 func run(path1 string, path2 string) (same bool, err error) {
90 f1, err := os.Open(path1)
91 if err != nil {
92 // on windows, file-not-found error messages may mention `CreateFile`,
93 // even when trying to open files in read-only mode
94 return false, errors.New(`can't open file named ` + path1)
95 }
96 defer f1.Close()
97
98 if path1 == path2 {
99 return true, nil
100 }
101
102 f2, err := os.Open(path2)
103 if err != nil {
104 // on windows, file-not-found error messages may mention `CreateFile`,
105 // even when trying to open files in read-only mode
106 return false, errors.New(`can't open file named ` + path2)
107 }
108 defer f2.Close()
109
110 same, err = sameContents(f1, f2)
111 return same, err
112 }
113
114 // sameContents checks if 2 files have the same bytes/contents
115 func sameContents(f1, f2 *os.File) (same bool, err error) {
116 // don't read anything from files, if they're obviously different in size;
117 // files are always/already opened to check if filenames given are valid
118 if differentSize(f1, f2) {
119 return false, nil
120 }
121
122 const bufSize = 16 * 1024
123 var buf1 [bufSize]byte
124 var buf2 [bufSize]byte
125
126 for {
127 n, err1 := f1.Read(buf1[:])
128 chunk1 := buf1[:n]
129
130 n, err2 := f2.Read(buf2[:])
131 chunk2 := buf2[:n]
132
133 if err1 == io.EOF && err2 == io.EOF {
134 return bytes.Equal(chunk1, chunk2), nil
135 }
136
137 if err1 != nil {
138 return false, err1
139 }
140 if err2 != nil {
141 return false, err2
142 }
143
144 if !bytes.Equal(chunk1, chunk2) {
145 return false, nil
146 }
147 }
148 }
149
150 // differentSize returns whether the 2 files given can be determined to have
151 // different non-zero file-sizes: this is because some special files falsely
152 // advertise being empty; in case of errors false is returned, but the files
153 // can still have different sizes, but that isn't known for sure
154 func differentSize(f1, f2 *os.File) bool {
155 info1, err := f1.Stat()
156 if err != nil {
157 // can't tell size for sure
158 return false
159 }
160
161 info2, err := f2.Stat()
162 if err != nil {
163 // can't tell size for sure
164 return false
165 }
166
167 size1 := info1.Size()
168 size2 := info2.Size()
169
170 if size1 == 0 || size2 == 0 {
171 // some special files falsely advertise being empty
172 return false
173 }
174 return size1 != size2
175 }
File: ./sbs/config.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package sbs
26
27 import (
28 "math"
29 )
30
31 const (
32 // tabstop is the space-count used for tab-expansion
33 tabstop = 4
34
35 // maxAutoWidth is the output max-width, chosen to fit very old monitors
36 maxAutoWidth = 79
37 )
38
39 // chooseNumColumns implements heuristics to auto-pick the number of columns
40 // to show: this func is used when the app is using data from standard-input
41 func chooseNumColumns(lines []string, separator string) int {
42 if len(lines) == 0 {
43 return 1
44 }
45
46 // sepw is the separator width
47 sepw := width(separator)
48
49 // see if lines can even fit a single column
50 if !columnsCanFit(1, lines, sepw) {
51 return 1
52 }
53
54 start := int(maxAutoWidth)
55 if sepw > 0 {
56 start = int(maxAutoWidth / sepw)
57 }
58
59 // starting from the max possible columns which may fit, keep trying
60 // with 1 fewer column, until the columns fit
61 for ncols := start; ncols > 1; ncols-- {
62 if columnsCanFit(ncols, lines, sepw) {
63 // success: found the most columns which fit
64 return ncols
65 }
66 }
67
68 // avoid multiple columns if some lines are too wide
69 return 1
70 }
71
72 // columnsCanFit checks whether the number of columns given would fit the
73 // display max-width constant
74 func columnsCanFit(ncols int, lines []string, gap int) bool {
75 if ncols < 1 {
76 // avoid surprises when called with non-sense column counts
77 return true
78 }
79
80 // stack-allocate the backing-array behind slice maxw
81 var buf [maxAutoWidth / 2]int
82 maxw := buf[:0]
83
84 // find the column max-height, to chunk lines into columns
85 h := int(math.Ceil(float64(len(lines)) / float64(ncols)))
86
87 // find column max-width by looping over chunks of lines
88 for len(lines) >= h {
89 w := findMaxWidth(lines[:h])
90 maxw = append(maxw, w)
91 lines = lines[h:]
92 }
93
94 // don't forget the last column
95 if len(lines) > 0 {
96 w := findMaxWidth(lines)
97 maxw = append(maxw, w)
98 }
99
100 // remember to add the gaps/separators between columns, along with
101 // all the individual column max-widths
102 w := (ncols - 1) * gap
103 for _, n := range maxw {
104 w += n
105 }
106
107 // do the columns fit?
108 return w <= maxAutoWidth
109 }
110
111 // findMaxWidth finds the max width in the slice given, ignoring ANSI codes
112 func findMaxWidth(lines []string) int {
113 maxw := 0
114 for _, s := range lines {
115 w := width(s)
116 if w > maxw {
117 maxw = w
118 }
119 }
120 return maxw
121 }
File: ./sbs/info.txt
1 sbs [options...] [columns...] [filenames...]
2
3
4 Show lines Side By Side: this app is made for content which normally scrolls
5 a long way downward. You can either just pipe text into it, or give multiple
6 filenames to read lines from those: a common use-case is to pipe its results
7 to `less -MKiCRS` or some other viewer command.
8
9 When given an explicit `0` for the number of columns, it figures out the most
10 columns which fit 80-symbols lines, or just 1 column if that's not possible.
11
12 When given no filenames, this app reads from stdin; when giving it filenames,
13 you can use a dash to use stdin along with the files.
14
15 All (optional) leading options start with either single or double-dash:
16
17 -h, -help show this help message
18 -s, -simple, -no-sep don't put a separator symbol between columns
19 -sep, -separator use the next argument as the column-separator
File: ./sbs/io.go
1 /*
2 The MIT License (MIT)
3
4 # Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package sbs
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "strings"
32 )
33
34 // errDoneWriting is a dummy error used to signal the app should quit early
35 // without showing an actual error
36 var errDoneWriting = errors.New(`done writing`)
37
38 // padding is a ring-buffer only used by func pad, to minimize calls to Write
39 var padding = [64]byte{
40 ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
41 ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
42 ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
43 ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
44 ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
45 ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
46 ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
47 ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
48 }
49
50 // writeSpaces emits the number of spaces given, while minimizing calls
51 // to Write by reusing a ring-buffer
52 func writeSpaces(w *bufio.Writer, spaces int) (n int, err error) {
53 if spaces <= 0 {
54 return 0, nil
55 }
56
57 // emit all full-buffer writes
58 for l := len(padding); spaces >= l; spaces -= l {
59 m, err := w.Write(padding[:])
60 n += m
61
62 if err != nil {
63 return n, err
64 }
65 }
66
67 // emit any remainder bytes
68 if spaces > 0 {
69 m, err := w.Write(padding[:spaces])
70 return n + m, err
71 }
72 return n, nil
73 }
74
75 // newScanner standardizes how line-scanners are setup in this app
76 func newScanner(r io.Reader) *bufio.Scanner {
77 const maxbufsize = 8 * 1024 * 1024 * 1024
78 sc := bufio.NewScanner(r)
79 sc.Buffer(nil, maxbufsize)
80 return sc
81 }
82
83 // padWrite emits the string given, following it with spaces to fill the
84 // width given if string is shorter than that
85 func padWrite(w *bufio.Writer, s string, n int) {
86 w.WriteString(s)
87 writeSpaces(w, n-width(s))
88 }
89
90 // writeItem emits the string given, followed by any padding needed, as well
91 // as ANSI-style clearing, again if needed
92 func writeItem(w *bufio.Writer, s string, width int) {
93 padWrite(w, s, width)
94 if needsStyleReset(s) {
95 w.WriteString("\x1b[0m")
96 }
97 }
98
99 func needsStyleReset(s string) bool {
100 return strings.Contains(s, "\x1b[") && !strings.HasSuffix(s, "\x1b[0m")
101 }
File: ./sbs/io_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package sbs
26
27 import (
28 "bufio"
29 "strconv"
30 "strings"
31 "testing"
32 )
33
34 func TestWriteSpaces(t *testing.T) {
35 spaces := strings.Repeat(` `, 20_000)
36
37 for n := -20; n < len(spaces); n++ {
38 t.Run(strconv.Itoa(n), func(t *testing.T) {
39 var sb strings.Builder
40 w := bufio.NewWriter(&sb)
41 writeSpaces(w, n)
42 w.Flush()
43
44 if n < 0 {
45 // avoid slicing with negative values
46 n = 0
47 }
48
49 expected := spaces[:n]
50 got := sb.String()
51
52 if got != expected {
53 const fs = `expected %d spaces, but got %d instead`
54 t.Fatalf(fs, len(expected), len(got))
55 }
56 })
57 }
58 }
File: ./sbs/main.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package sbs
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "math"
32 "os"
33 "strconv"
34 "strings"
35
36 _ "embed"
37 )
38
39 //go:embed info.txt
40 var usage string
41
42 func Main() {
43 sep := ` █ `
44 args := os.Args[1:]
45
46 if len(args) > 0 {
47 switch args[0] {
48 case `-h`, `--h`, `-help`, `--help`:
49 os.Stderr.WriteString(usage)
50 return
51
52 case `-s`, `--s`, `-simple`, `--simple`, `-no-sep`, `--no-sep`:
53 sep = ` `
54 args = args[1:]
55
56 case `-sep`, `--sep`, `-separator`, `--separator`:
57 if len(args) < 2 {
58 const msg = "missing value to change column separator\n"
59 os.Stderr.WriteString(msg)
60 os.Exit(1)
61 return
62 }
63 sep = args[1]
64 args = args[2:]
65 }
66 }
67
68 ncols := 0
69 if len(args) > 0 {
70 f, err := strconv.ParseFloat(args[0], 64)
71 if err == nil && !math.IsInf(f, 0) && !math.IsNaN(f) {
72 ncols = int(f)
73 args = args[1:]
74 }
75 }
76
77 if len(args) > 0 && args[0] == `--` {
78 args = args[1:]
79 }
80
81 err := run(args, ncols, sep)
82 if err != nil && err != errDoneWriting {
83 os.Stderr.WriteString(`sbs: `)
84 os.Stderr.WriteString(err.Error())
85 os.Exit(1)
86 return
87 }
88 }
89
90 func run(args []string, ncols int, separator string) error {
91 paths := args
92 if len(paths) == 0 {
93 paths = []string{`-`}
94 }
95
96 if separator == `` {
97 separator = ` `
98 }
99
100 lines, err := gatherLines(paths)
101 if err != nil {
102 return err
103 }
104
105 // choose a default number of columns, if not given an explicit one
106 if ncols < 1 {
107 ncols = chooseNumColumns(lines, separator)
108 }
109
110 w := bufio.NewWriterSize(os.Stdout, 32*1024)
111 defer w.Flush()
112 return showLines(w, lines, ncols, separator)
113 }
114
115 // gatherLines slurps all text-lines from all the filepaths given, expanding
116 // any tabs found; a single dash as a pathname means stdin
117 func gatherLines(paths []string) ([]string, error) {
118 var lines []string
119 var sb strings.Builder
120
121 dashes := 0
122 for _, s := range paths {
123 if s == `-` {
124 dashes++
125 }
126 }
127 if dashes > 1 {
128 return lines, errors.New(`can't use "-" (stdin) more than once`)
129 }
130
131 for _, s := range paths {
132 err := handleNamedInput(s, func(r io.Reader) error {
133 sc := newScanner(r)
134
135 for sc.Scan() {
136 s := sc.Text()
137 if strings.IndexByte(s, '\t') >= 0 {
138 sb.Reset()
139 expand(s, tabstop, &sb)
140 s = strings.Clone(sb.String())
141 }
142 lines = append(lines, s)
143 }
144
145 return sc.Err()
146 })
147
148 if err != nil {
149 return lines, err
150 }
151 }
152
153 return lines, nil
154 }
155
156 // handleNamedInput makes opening/closing files more convenient by using
157 // callbacks to handle processing; this func also handles recognizing `-`
158 // as meaning stdin
159 func handleNamedInput(path string, fn func(r io.Reader) error) error {
160 if path == `-` {
161 return fn(os.Stdin)
162 }
163
164 f, err := os.Open(path)
165 if err != nil {
166 return err
167 }
168 defer f.Close()
169 return fn(f)
170 }
171
172 // showLines handles the use-case of showing/rearranging lines from a single
173 // input source (presumably standard input) into several columns
174 func showLines(w *bufio.Writer, lines []string, ncols int, sep string) error {
175 if ncols < 1 {
176 return nil
177 }
178
179 if ncols == 1 {
180 for _, s := range lines {
181 w.WriteString(s)
182 err := w.WriteByte('\n')
183 if err != nil {
184 // assume error probably results from a closed stdout
185 // pipe, so quit the app right away without complaining
186 return err
187 }
188 }
189 return nil
190 }
191
192 // nothing to show, so don't even bother
193 if len(lines) == 0 {
194 return nil
195 }
196
197 cols, height := splitLines(lines, ncols)
198 widths := make([]int, 0, len(cols))
199 for _, c := range cols {
200 // find the max width of all lines of the current column
201 maxw := 0
202 for _, v := range c {
203 w := width(v)
204 if w > maxw {
205 maxw = w
206 }
207 }
208
209 widths = append(widths, maxw)
210 }
211
212 // endSep is right-trimmed to avoid unneeded trailing spaces on output
213 // lines whose last column is an empty/missing input line
214 endSep := strings.TrimRight(sep, ` `)
215
216 // show columns side by side
217 for r := 0; r < height; r++ {
218 for c := 0; c < len(cols); c++ {
219 badr := r >= len(cols[c])
220
221 // clearly separate columns visually
222 if c > 0 {
223 if c == len(cols)-1 && (badr || cols[c][r] == ``) {
224 // avoid unneeded trailing spaces
225 w.WriteString(endSep)
226 } else {
227 w.WriteString(sep)
228 }
229 }
230
231 if badr {
232 // exceeding items for this (last) column
233 continue
234 }
235
236 // pad all columns, except the last
237 width := 0
238 if c < len(cols)-1 {
239 width = widths[c]
240 }
241
242 // emit maybe-padded column
243 writeItem(w, cols[c][r], width)
244 }
245
246 // end the line
247 err := w.WriteByte('\n')
248 if err != nil {
249 // probably a pipe was closed
250 return nil
251 }
252 }
253
254 return nil
255 }
File: ./sbs/strings.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package sbs
26
27 import (
28 "math"
29 "strings"
30 "unicode/utf8"
31 )
32
33 // expand replaces all tabs with correctly-padded tabstops, turning all tabs
34 // each into 1 or more spaces, as appropriate
35 func expand(s string, tabstop int, sb *strings.Builder) {
36 sb.Reset()
37 numrunes := 0
38
39 for _, r := range s {
40 switch r {
41 case '\t':
42 numspaces := tabstop - numrunes%tabstop
43 for i := 0; i < numspaces; i++ {
44 sb.WriteRune(' ')
45 }
46 numrunes += numspaces
47
48 default:
49 sb.WriteRune(r)
50 numrunes++
51 }
52 }
53 }
54
55 // width calculates visually-correct string widths
56 func width(s string) int {
57 return utf8.RuneCountInString(s) - ansiLength(s)
58 }
59
60 // ansiLength calculates how many bytes ANSI-codes take in the string given:
61 // func width uses this to calculate visually-correct string widths
62 func ansiLength(s string) int {
63 n := 0
64 prev := rune(0)
65 ansi := false
66 for _, r := range s {
67 if ansi {
68 n++
69 }
70
71 if ansi && r == 'm' {
72 ansi = false
73 continue
74 }
75
76 if prev == '\x1b' && r == '[' {
77 n += 2 // count the 2-item starter-sequence `\x1b[`
78 ansi = true
79 }
80 prev = r
81 }
82 return n
83 }
84
85 // splitLines turns an array of lines into sub-arrays of lines, so they can
86 // be shown side by side later on
87 func splitLines(lines []string, ncols int) (cols [][]string, maxheight int) {
88 n := ncols
89 hfrac := float64(len(lines)) / float64(n)
90 h := int(math.Ceil(hfrac))
91
92 cols = make([][]string, 0, n)
93 for len(lines) > h {
94 cols = append(cols, lines[:h])
95 lines = lines[h:]
96 }
97 if len(lines) != 0 {
98 cols = append(cols, lines)
99 }
100 return cols, h
101 }
102
103 // indexLine handles slice-indexing by returning an empty string when the
104 // index given is invalid, which helps simplify other funcs' control-flow
105 // func indexLine(lines []string, i int) (line string, ok bool) {
106 // if 0 <= i && i < len(lines) {
107 // return lines[i], true
108 // }
109 // return ``, false
110 // }
File: ./sbs/strings_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package sbs
26
27 import (
28 "strings"
29 "testing"
30 )
31
32 func TestExpand(t *testing.T) {
33 tests := map[string]struct {
34 input string
35 tabstop int
36 expected string
37 }{
38 `empty`: {``, 4, ``},
39 `indent 1`: {"\tabc", 4, ` abc`},
40 `indent 2`: {"\t\tabc", 4, ` abc`},
41 `indent 2 (mix tab/space)`: {"\t \tabc", 4, ` abc`},
42 }
43
44 for name, tc := range tests {
45 t.Run(name, func(t *testing.T) {
46 var sb strings.Builder
47 expand(tc.input, tc.tabstop, &sb)
48
49 if got := strings.Clone(sb.String()); got != tc.expected {
50 const fs = `input %q, tabstop %d: got %q, instead of %q`
51 t.Fatalf(fs, tc.input, tc.tabstop, got, tc.expected)
52 }
53 })
54 }
55 }
56
57 func TestANSILength(t *testing.T) {
58 tests := map[string]struct {
59 input string
60 expected int
61 }{
62 `empty`: {``, 0},
63 `no ansi escapes`: {`abc def`, 0},
64 `simple ansi escapes`: {"\x1b[38;5;120mabc def\x1b[0m", 15},
65 }
66
67 for name, tc := range tests {
68 t.Run(name, func(t *testing.T) {
69 if got := ansiLength(tc.input); got != tc.expected {
70 const fs = `input %q: got %d, instead of %d`
71 t.Fatalf(fs, tc.input, got, tc.expected)
72 }
73 })
74 }
75 }
File: ./seq/seq.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package seq
26
27 import (
28 "bufio"
29 "io"
30 "math/big"
31 "os"
32 "strconv"
33 "strings"
34 )
35
36 const info = `
37 seq [options...] [first...] [increment...] [last]
38
39 Emit a sequence of numbers, one per line.
40
41 Options
42
43 -w pad with leading zeros
44 `
45
46 func Main() {
47 zeroPad := false
48 var numbuf [3]*big.Rat
49
50 args := os.Args[1:]
51 nums := numbuf[:0]
52
53 for len(args) > 0 {
54 switch args[0] {
55 case `-h`, `--h`, `-help`, `--help`, `help`:
56 os.Stdout.WriteString(info[1:])
57 return
58
59 case `-w`:
60 zeroPad = true
61 args = args[1:]
62 continue
63 }
64
65 n := big.NewRat(0, 1)
66 if v, ok := n.SetString(args[0]); ok {
67 if len(nums) == cap(numbuf) {
68 os.Stderr.WriteString(info[1:])
69 os.Exit(1)
70 return
71 }
72
73 nums = append(nums, v)
74 args = args[1:]
75 continue
76 }
77
78 break
79 }
80
81 if len(args) > 0 && args[0] == `--` {
82 args = args[1:]
83 }
84
85 switch len(nums) {
86 case 0:
87 os.Stderr.WriteString(info[1:])
88 os.Exit(1)
89 return
90
91 case 1:
92 numbuf[2] = numbuf[0]
93 numbuf[0] = big.NewRat(1, 1)
94 numbuf[1] = big.NewRat(1, 1)
95
96 case 2:
97 numbuf[2] = numbuf[1]
98 numbuf[1] = big.NewRat(1, 1)
99 }
100
101 if numbuf[1].Sign() == 0 {
102 return
103 }
104
105 first, incr, last := numbuf[0], numbuf[1], numbuf[2]
106 if err := seq(first, incr, last, zeroPad); err != nil && err != io.EOF {
107 os.Stderr.WriteString(`seq: `)
108 os.Stderr.WriteString(err.Error())
109 os.Stderr.WriteString("\n")
110 os.Exit(1)
111 return
112 }
113 }
114
115 func seq(first, incr, last *big.Rat, zeroPad bool) error {
116 if first.IsInt() && incr.IsInt() && last.IsInt() {
117 f := int(first.Num().Int64())
118 i := int(incr.Num().Int64())
119 l := int(last.Num().Int64())
120 return seqInt(f, i, l, zeroPad)
121 }
122
123 return seqFrac(first, incr, last, zeroPad)
124 }
125
126 func seqInt(first, incr, last int, zeroPad bool) error {
127 if incr == 0 {
128 return nil
129 }
130
131 w := bufio.NewWriterSize(os.Stdout, 32*1024)
132 defer w.Flush()
133
134 var buf [24]byte
135 var maxlen int
136
137 cur := first
138 if zeroPad {
139 var n int
140 n = len(strconv.AppendInt(buf[:0], int64(first), 10))
141 if maxlen < n {
142 maxlen = n
143 }
144 n = len(strconv.AppendInt(buf[:0], int64(last), 10))
145 if maxlen < n {
146 maxlen = n
147 }
148 }
149
150 for {
151 if incr > 0 && cur > last {
152 break
153 }
154 if incr < 0 && cur < last {
155 break
156 }
157
158 s := strconv.AppendInt(buf[:0], int64(cur), 10)
159 if maxlen > 0 {
160 writeZeros(w, maxlen-len(s))
161 }
162 w.Write(s)
163
164 if w.WriteByte('\n') != nil {
165 return io.EOF
166 }
167
168 cur += incr
169 }
170
171 return nil
172 }
173
174 func seqFrac(first, incr, last *big.Rat, zeroPad bool) error {
175 incrSign := incr.Sign()
176 if incrSign == 0 {
177 return nil
178 }
179
180 w := bufio.NewWriterSize(os.Stdout, 32*1024)
181 defer w.Flush()
182
183 var maxlen, prec int
184
185 var p int
186 p = maxPrec(first)
187 if prec < p {
188 prec = p
189 }
190 p = maxPrec(incr)
191 if prec < p {
192 prec = p
193 }
194 p = maxPrec(last)
195 if prec < p {
196 prec = p
197 }
198
199 cur := first
200 if zeroPad {
201 var n int
202 n = len(first.FloatString(prec))
203 if maxlen < n {
204 maxlen = n
205 }
206 n = len(last.FloatString(prec))
207 if maxlen < n {
208 maxlen = n
209 }
210 }
211
212 for {
213 if incrSign > 0 && cur.Cmp(last) > 0 {
214 break
215 }
216 if incrSign < 0 && cur.Cmp(last) < 0 {
217 break
218 }
219
220 s := cur.FloatString(prec)
221 if maxlen > 0 {
222 writeZeros(w, maxlen-len(s))
223 }
224 w.WriteString(s)
225
226 if w.WriteByte('\n') != nil {
227 return io.EOF
228 }
229
230 cur = cur.Add(cur, incr)
231 }
232
233 return nil
234 }
235
236 func maxPrec(n *big.Rat) int {
237 if p, exact := n.FloatPrec(); exact {
238 return p
239 }
240
241 s := n.FloatString(50)
242 i := strings.IndexByte(s, '.')
243 if i < 0 {
244 return 0
245 }
246
247 s = s[i+1:]
248 for len(s) > 0 && s[len(s)-1] == '0' {
249 s = s[:len(s)-1]
250 }
251 return len(s)
252 }
253
254 func writeZeros(w *bufio.Writer, n int) {
255 const (
256 half = `00000000000000000000000000000000`
257 zeros = half + half
258 )
259
260 for n >= len(zeros) {
261 w.WriteString(zeros)
262 n -= len(zeros)
263 }
264 if n > 0 {
265 w.WriteString(zeros[:n])
266 }
267 }
File: ./setint/setint.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package setint
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "os"
32 "strings"
33 )
34
35 const info = `
36 setint [options...] [files...]
37
38 SET INTersection emits only the lines which are common to all the input
39 files given, never emitting the same line more than once.
40
41 All (optional) leading options start with either single or double-dash:
42
43 -h, -help show this help message
44 -i, -ins match lines case-insensitively
45 `
46
47 type config struct {
48 // w is a writer to start emitting known results during the last input
49 w *bufio.Writer
50
51 // live controls whether each line output is immediately flushed
52 live bool
53
54 // sensitive controls case-sensitivity for the tally across inputs
55 sensitive bool
56 }
57
58 func Main() {
59 var cfg config
60 cfg.live = true
61 cfg.sensitive = true
62 args := os.Args[1:]
63
64 if len(args) > 0 {
65 switch args[0] {
66 case `-b`, `--b`, `-buffered`, `--buffered`:
67 cfg.live = false
68 args = args[1:]
69
70 case `-h`, `--h`, `-help`, `--help`:
71 os.Stdout.WriteString(info[1:])
72 return
73
74 case `-i`, `--i`, `-ins`, `--ins`, `-insensitive`, `--insensitive`:
75 cfg.sensitive = false
76 args = args[1:]
77 }
78 }
79
80 if len(args) > 0 && args[0] == `--` {
81 args = args[1:]
82 }
83
84 if cfg.live {
85 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
86 cfg.live = false
87 }
88 }
89
90 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
91 os.Stderr.WriteString(`setint: `)
92 os.Stderr.WriteString(err.Error())
93 os.Stderr.WriteString("\n")
94 os.Exit(1)
95 return
96 }
97 }
98
99 type intersector struct {
100 // lines has all the lines across inputs, without repetitions
101 lines []string
102
103 // tally counts how many inputs have each entry
104 tally map[string]int
105
106 // dedup is to ensure each input can only tally a value once
107 dedup map[string]struct{}
108
109 // target auto-increments to keep track of the target tally for items
110 target int
111
112 // leadKey remembers the calculated tally-key for the first eligible
113 // line in the queued-up list
114 leadKey string
115
116 // lastInput is to start emitting output before the end of all inputs
117 lastInput bool
118
119 // empty keeps track of empty inputs, since these mean an empty output
120 empty bool
121 }
122
123 // prepare does what's needed right before handling the next input
124 func (in *intersector) prepare(r io.Reader, cfg config) {
125 in.empty = true
126 clear(in.dedup)
127 in.target++
128 in.seekLead(cfg)
129 }
130
131 // seekLead discards leading items which will provably never make it to the
132 // output for sure due to their tally being too low; udpates `leadKey` to
133 // avoid recalculating it in the update method
134 func (in *intersector) seekLead(cfg config) {
135 if !in.lastInput {
136 return
137 }
138
139 for len(in.lines) > 0 {
140 key := in.lines[0]
141 if !cfg.sensitive {
142 key = strings.ToLower(key)
143 }
144
145 if in.tally[key] >= in.target-1 {
146 in.leadKey = key
147 break
148 }
149
150 in.lines = in.lines[1:]
151 }
152 }
153
154 // update handles the line given
155 func (in *intersector) update(line string, cfg config) error {
156 in.empty = false
157
158 if _, ok := in.dedup[line]; ok {
159 return nil
160 }
161
162 in.dedup[line] = struct{}{}
163
164 key := line
165 if !cfg.sensitive {
166 key = strings.ToLower(key)
167 }
168
169 t, ok := in.tally[key]
170 if !ok {
171 in.lines = append(in.lines, line)
172 }
173
174 t++
175 in.tally[key] = t
176
177 sureLead := in.lastInput && t == in.target && key == in.leadKey
178
179 if !sureLead {
180 return nil
181 }
182
183 // the current lead makes it to the output
184
185 in.lines = in.lines[1:]
186
187 cfg.w.WriteString(line)
188 if cfg.w.WriteByte('\n') != nil {
189 return io.EOF
190 }
191
192 if cfg.live && cfg.w.Flush() != nil {
193 return io.EOF
194 }
195
196 // the (former) lead making it may open room for an uninterrupted streak
197 // of other lines whose tally makes them part of the output
198
199 for len(in.lines) > 0 {
200 line := in.lines[0]
201 key := line
202 if !cfg.sensitive {
203 key = strings.ToLower(key)
204 }
205
206 if in.tally[key] != in.target {
207 break
208 }
209 in.lines = in.lines[1:]
210
211 cfg.w.WriteString(line)
212 if cfg.w.WriteByte('\n') != nil {
213 return io.EOF
214 }
215
216 if cfg.live && cfg.w.Flush() != nil {
217 return io.EOF
218 }
219 }
220
221 // don't forget there may be later leads about to make it
222 in.seekLead(cfg)
223
224 return nil
225 }
226
227 // show what's left of the output entries, after all inputs are handled
228 func (in intersector) show(w *bufio.Writer, cfg config) error {
229 defer w.Flush()
230
231 if in.empty {
232 return nil
233 }
234
235 for _, line := range in.lines {
236 key := line
237 if !cfg.sensitive {
238 key = strings.ToLower(key)
239 }
240
241 if in.tally[key] != in.target {
242 continue
243 }
244
245 w.WriteString(line)
246 if w.WriteByte('\n') != nil {
247 return io.EOF
248 }
249 }
250
251 return nil
252 }
253
254 func run(w io.Writer, paths []string, cfg config) error {
255 // ignore duplicate file names
256 got := make(map[string]struct{})
257 unique := make([]string, 0, len(paths))
258 for _, s := range paths {
259 if _, ok := got[s]; ok {
260 continue
261 }
262 got[s] = struct{}{}
263 unique = append(unique, s)
264 }
265
266 cfg.w = bufio.NewWriterSize(w, 32*1024)
267 defer cfg.w.Flush()
268
269 // with a single input, the tool simply avoids duplicates; use special
270 // funcs to save memory, in that case
271 if len(unique) == 0 {
272 return dedup(cfg.w, os.Stdin, cfg)
273 }
274 if len(unique) == 1 {
275 return dedupFile(cfg.w, unique[0], cfg)
276 }
277
278 var in intersector
279 in.tally = make(map[string]int)
280 in.dedup = make(map[string]struct{})
281
282 for i, name := range unique {
283 in.lastInput = i == len(unique)-1
284 if err := handleFile(name, &in, cfg); err != nil {
285 return err
286 }
287
288 // as soon as there's an empty input, the result is empty for sure
289 if in.empty {
290 break
291 }
292 }
293
294 return in.show(cfg.w, cfg)
295 }
296
297 func handleFile(name string, in *intersector, cfg config) error {
298 if name == `` || name == `-` {
299 return handleReader(os.Stdin, in, cfg)
300 }
301
302 f, err := os.Open(name)
303 if err != nil {
304 return errors.New(`can't read from file named "` + name + `"`)
305 }
306 defer f.Close()
307
308 return handleReader(f, in, cfg)
309 }
310
311 func handleReader(r io.Reader, in *intersector, cfg config) error {
312 // any empty input makes the whole output empty
313 if in.empty {
314 return nil
315 }
316
317 in.prepare(r, cfg)
318
319 const gb = 1024 * 1024 * 1024
320 sc := bufio.NewScanner(r)
321 sc.Buffer(nil, 8*gb)
322
323 for sc.Scan() {
324 in.update(sc.Text(), cfg)
325 }
326 return sc.Err()
327 }
328
329 func dedupFile(w *bufio.Writer, path string, cfg config) error {
330 if path == `` || path == `-` {
331 return dedup(w, os.Stdin, cfg)
332 }
333
334 f, err := os.Open(path)
335 if err != nil {
336 return errors.New(`can't read from file named "` + path + `"`)
337 }
338 defer f.Close()
339
340 return dedup(w, f, cfg)
341 }
342
343 func dedup(w *bufio.Writer, r io.Reader, cfg config) error {
344 defer w.Flush()
345
346 const gb = 1024 * 1024 * 1024
347 sc := bufio.NewScanner(r)
348 sc.Buffer(nil, 8*gb)
349
350 got := make(map[string]struct{})
351
352 for sc.Scan() {
353 line := sc.Text()
354 key := line
355 if !cfg.sensitive {
356 key = strings.ToLower(key)
357 }
358
359 if _, ok := got[key]; ok {
360 continue
361 }
362 got[key] = struct{}{}
363
364 w.Write(sc.Bytes())
365 if w.WriteByte('\n') != nil {
366 return io.EOF
367 }
368
369 if cfg.live && w.Flush() != nil {
370 return io.EOF
371 }
372 }
373
374 return sc.Err()
375 }
File: ./setint/setint_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package setint
26
27 import (
28 "bufio"
29 "encoding/base64"
30 "strings"
31 "testing"
32
33 cryptorand "crypto/rand"
34 mathrand "math/rand"
35 )
36
37 func TestIntersections(t *testing.T) {
38 testIntersection(t,
39 "3\n4\n5\n6\n", "3\n4\n5\n6\na\n3\n5\na\n", "2\n3\n4\n5\n6\n7\n")
40
41 const m = 16
42
43 for maxab := 0; maxab < m; maxab++ {
44 for maxa := 0; maxa < m; maxa++ {
45 for maxb := 0; maxb < m; maxb++ {
46 for maxlen := m / 2; maxlen < m; maxlen++ {
47 ab, a, b := mkInputs(maxab, maxa, maxb, maxlen)
48 testIntersection(t, ab, a, b)
49 }
50 }
51 }
52 }
53 }
54
55 func testIntersection(t *testing.T, ab, a, b string) {
56 var cfg config
57 var dst strings.Builder
58 cfg.w = bufio.NewWriterSize(&dst, 32*1024)
59
60 var p intersector
61 p.tally = make(map[string]int)
62 p.dedup = make(map[string]struct{})
63
64 handleReader(strings.NewReader(a), &p, cfg)
65 handleReader(strings.NewReader(b), &p, cfg)
66 p.show(cfg.w, cfg)
67
68 res := dst.String()
69 ab, _ = strings.CutSuffix(ab, "\n")
70 res, _ = strings.CutSuffix(res, "\n")
71 if ab == res {
72 return
73 }
74
75 t.Errorf("results not matching:\n%s\nvs\n%s", ab, res)
76 // t.Errorf(
77 // "results not matching (%d/%d/%d|%d//%d):\n%s\nvs\n%s",
78 // maxab, maxa, maxb, len(ab), len(res), ab, res,
79 // )
80 }
81
82 func testRandomIntersection(t *testing.T, maxab, maxa, maxb, maxlen int) {
83 ab, a, b := mkInputs(maxab, maxa, maxb, maxlen)
84 testIntersection(t, ab, a, b)
85 }
86
87 func mkInputs(maxab, maxa, maxb, maxlen int) (ab, a, b string) {
88 buf := make([]byte, maxlen)
89
90 var abs []string
91 dedup := make(map[string]struct{})
92 for i := 0; i < maxab; i++ {
93 s := makeJunkLine(buf)
94 if _, ok := dedup[s]; ok {
95 // resulting set-intersection can't have duplicates
96 continue
97 }
98 abs = append(abs, s)
99 dedup[s] = struct{}{}
100 }
101
102 var as []string
103 for i := 0; i < maxa; i++ {
104 s := makeJunkLine(buf)
105 as = append(as, s)
106 }
107 as = append(as, as...)
108 as = append(as, abs...)
109
110 // don't shuffle lines from the first input, since output is supposed to
111 // keep order of first appearance, and shuffling would prevent that
112
113 var bs []string
114 for i := 0; i < maxab; i++ {
115 s := makeJunkLine(buf)
116 bs = append(bs, s)
117 }
118 bs = append(bs, bs...)
119 bs = append(bs, abs...)
120
121 // shuffle lines from the second input, since order of later appearances
122 // of common lines shouldn't matter
123 mathrand.Shuffle(len(bs), func(i, j int) {
124 bs[i], bs[j] = bs[j], bs[i]
125 })
126
127 if len(abs) > 0 {
128 ab = strings.Join(abs, "\n") + "\n"
129 }
130 if len(as) > 0 {
131 a = strings.Join(as, "\n") + "\n"
132 }
133 if len(bs) > 0 {
134 b = strings.Join(bs, "\n") + "\n"
135 }
136 return ab, a, b
137 }
138
139 func makeJunkLine(buf []byte) string {
140 // n, _ := cryptorand.Read(buf[:])
141 // s := string(buf[:n])
142 // s = strings.Replace(s, "\n", "", -1)
143 // s = strings.Replace(s, "\r", "", -1)
144 // return s
145
146 n, _ := cryptorand.Read(buf[:])
147 return base64.StdEncoding.EncodeToString(buf[:n])
148 }
File: ./setsub/setsub.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package setsub
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "os"
32 "strings"
33 )
34
35 const info = `
36 setsub [options...] [files...]
37
38 SET SUBtraction emits only the lines from the first input given which don't
39 appear in later inputs, never emitting the same line more than once.
40
41 All (optional) leading options start with either single or double-dash:
42
43 -h, -help show this help message
44 -i, -ins match lines case-insensitively
45 `
46
47 type config struct {
48 // sensitive controls case-sensitivity when comparing input lines
49 sensitive bool
50
51 // live controls whether each line output is immediately flushed; this
52 // is used only when handling a single input
53 live bool
54 }
55
56 func Main() {
57 var cfg config
58 cfg.sensitive = true
59 cfg.live = true
60 args := os.Args[1:]
61
62 if len(args) > 0 {
63 switch args[0] {
64 case `-b`, `--b`, `-buffered`, `--buffered`:
65 cfg.live = false
66 args = args[1:]
67
68 case `-h`, `--h`, `-help`, `--help`:
69 os.Stdout.WriteString(info[1:])
70 return
71
72 case `-i`, `--i`, `-ins`, `--ins`, `-insensitive`, `--insensitive`:
73 cfg.sensitive = false
74 args = args[1:]
75 }
76 }
77
78 if len(args) > 0 && args[0] == `--` {
79 args = args[1:]
80 }
81
82 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
83 os.Stderr.WriteString(`setsub: `)
84 os.Stderr.WriteString(err.Error())
85 os.Stderr.WriteString("\n")
86 os.Exit(1)
87 return
88 }
89 }
90
91 type subtractor struct {
92 // lines has all the lines from all the inputs, without repetitions
93 lines []string
94
95 // passing keeps track of whether a line/key makes it to the output
96 passing map[string]bool
97
98 // firstInput decides whether lines are added/allowed for output or not
99 firstInput bool
100 }
101
102 func run(w io.Writer, paths []string, cfg config) error {
103 // ignore duplicate file names
104 got := make(map[string]struct{})
105 unique := make([]string, 0, len(paths))
106 for _, s := range paths {
107 if _, ok := got[s]; ok {
108 continue
109 }
110 got[s] = struct{}{}
111 unique = append(unique, s)
112 }
113
114 bw := bufio.NewWriterSize(w, 32*1024)
115 defer bw.Flush()
116
117 // using a single input is equivalent to a `dedup`: this is handled
118 // separately both for efficiency and for the `live` output of lines
119 if len(unique) == 0 {
120 return runDedup(bw, `-`, cfg)
121 }
122 if len(unique) == 1 {
123 return runDedup(bw, unique[0], cfg)
124 }
125
126 // gather data from all inputs
127
128 var sub subtractor
129 sub.passing = make(map[string]bool)
130
131 for i, name := range unique {
132 sub.firstInput = i == 0
133 if err := handleFile(name, &sub, cfg); err != nil {
134 return err
135 }
136 }
137
138 // show lines still remaining
139
140 for _, line := range sub.lines {
141 key := line
142 if !cfg.sensitive {
143 key = strings.ToLower(key)
144 }
145
146 if !sub.passing[key] {
147 continue
148 }
149
150 bw.WriteString(line)
151 if bw.WriteByte('\n') != nil {
152 return nil
153 }
154 }
155
156 return nil
157 }
158
159 func handleFile(name string, sub *subtractor, cfg config) error {
160 if name == `` || name == `-` {
161 return handleReader(os.Stdin, sub, cfg)
162 }
163
164 f, err := os.Open(name)
165 if err != nil {
166 return errors.New(`can't read from file named "` + name + `"`)
167 }
168 defer f.Close()
169
170 return handleReader(f, sub, cfg)
171 }
172
173 func handleReader(r io.Reader, sub *subtractor, cfg config) error {
174 const gb = 1024 * 1024 * 1024
175 sc := bufio.NewScanner(r)
176 sc.Buffer(nil, 8*gb)
177
178 for sc.Scan() {
179 line := sc.Text()
180 key := line
181 if !cfg.sensitive {
182 key = strings.ToLower(key)
183 }
184
185 passing, ok := sub.passing[key]
186
187 if sub.firstInput {
188 // first input, and the line hasn't appeared before
189 if !ok {
190 sub.lines = append(sub.lines, line)
191 sub.passing[key] = true
192 }
193 continue
194 }
195
196 // avoid storing more entries on keys which haven't appeared in
197 // the first input
198 if passing && ok {
199 sub.passing[key] = false
200 }
201 }
202
203 return sc.Err()
204 }
205
206 func runDedup(w *bufio.Writer, path string, cfg config) error {
207 if cfg.live {
208 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
209 cfg.live = false
210 }
211 }
212
213 return dedupFile(w, path, cfg)
214 }
215
216 func dedupFile(w *bufio.Writer, path string, cfg config) error {
217 if path == `` || path == `-` {
218 return dedup(w, os.Stdin, cfg)
219 }
220
221 f, err := os.Open(path)
222 if err != nil {
223 return errors.New(`can't read from file named "` + path + `"`)
224 }
225 defer f.Close()
226
227 return dedup(w, f, cfg)
228 }
229
230 func dedup(w *bufio.Writer, r io.Reader, cfg config) error {
231 const gb = 1024 * 1024 * 1024
232 sc := bufio.NewScanner(r)
233 sc.Buffer(nil, 8*gb)
234
235 got := make(map[string]struct{})
236
237 for sc.Scan() {
238 line := sc.Text()
239 key := line
240 if !cfg.sensitive {
241 key = strings.ToLower(key)
242 }
243
244 if _, ok := got[key]; ok {
245 continue
246 }
247 got[key] = struct{}{}
248
249 w.Write(sc.Bytes())
250 if w.WriteByte('\n') != nil {
251 return io.EOF
252 }
253
254 if cfg.live && w.Flush() != nil {
255 return io.EOF
256 }
257 }
258
259 return sc.Err()
260 }
File: ./shame512/shame512.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package shame512
26
27 import (
28 "bufio"
29 "crypto/sha512"
30 "encoding/hex"
31 "errors"
32 "io"
33 "io/fs"
34 "os"
35 "path/filepath"
36 "runtime"
37 "sort"
38 "sync"
39 )
40
41 const info = `
42 shame512 [options...] [files/folders...]
43
44 Group files by common/same SHA-512 hashes: having the same hash means a high
45 chance of having the same contents, even though that's not a guarantee.
46
47 Files and folder names can be given together for convenience: folder names
48 are searched recursively to find all files, including in any subfolders.
49
50 All (optional) leading options start with either single or double-dash:
51
52 -h, -help show this help message
53 -j, -json show results as JSON instead
54 `
55
56 func Main() {
57 args := os.Args[1:]
58 show := showGroups
59
60 if len(args) > 0 {
61 switch args[0] {
62 case `-h`, `--h`, `-help`, `--help`:
63 os.Stdout.WriteString(info[1:])
64 return
65
66 case `-j`, `--j`, `-json`, `--json`:
67 show = showJSON
68 args = args[1:]
69 }
70 }
71
72 if len(args) > 0 && args[0] == `--` {
73 args = args[1:]
74 }
75
76 if len(args) == 0 {
77 args = []string{`.`}
78 }
79
80 // paths has all filepaths given, ignoring repetitions
81 paths, ok := findAllFiles(args)
82 if !ok {
83 os.Exit(1)
84 return
85 }
86
87 checksums := make([]string, len(paths))
88 findChecksums(paths, checksums)
89
90 // group filepaths by common checksum, remembering the index of the
91 // first item for each group, so the groups can be sorted by the
92 // original order their first filepath came from the command-line
93 indices := make(map[string]int)
94 groups := make(map[string][]string)
95 for i, chsum := range checksums {
96 if chsum == `` {
97 continue
98 }
99 g, ok := groups[chsum]
100 if !ok {
101 indices[chsum] = i
102 }
103 groups[chsum] = append(g, paths[i])
104 }
105
106 // keys has the sorted checksums
107 keys := make([]string, len(groups))
108 for chsum := range groups {
109 keys = append(keys, chsum)
110 }
111 sort.SliceStable(keys, func(i, j int) bool {
112 x := indices[keys[i]]
113 y := indices[keys[j]]
114 return x < y
115 })
116
117 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
118 defer bw.Flush()
119
120 show(bw, keys, groups)
121 }
122
123 // findAllFiles can be given a mix of file/folder paths, finding all files
124 // recursively in folders, avoiding duplicates
125 func findAllFiles(paths []string) (files []string, success bool) {
126 walk := filepath.WalkDir
127 got := make(map[string]struct{})
128 success = true
129
130 for _, path := range paths {
131 if _, ok := got[path]; ok {
132 continue
133 }
134 got[path] = struct{}{}
135
136 // a dash means standard input
137 if path == `-` {
138 files = append(files, path)
139 continue
140 }
141
142 info, err := os.Stat(path)
143 if os.IsNotExist(err) {
144 // on windows, file-not-found messages may mention `CreateFile`,
145 // even when trying to open files in read-only mode
146 err = errors.New(`can't find file/folder named ` + path)
147 }
148
149 if err != nil {
150 showError(path, err)
151 success = false
152 continue
153 }
154
155 if !info.IsDir() {
156 files = append(files, path)
157 continue
158 }
159
160 err = walk(path, func(path string, info fs.DirEntry, err error) error {
161 path, err = filepath.Abs(path)
162 if err != nil {
163 showError(path, err)
164 success = false
165 return err
166 }
167
168 if _, ok := got[path]; ok {
169 if info.IsDir() {
170 return fs.SkipDir
171 }
172 return nil
173 }
174 got[path] = struct{}{}
175
176 if info.IsDir() {
177 return nil
178 }
179
180 files = append(files, path)
181 return nil
182 })
183
184 if err != nil {
185 showError(path, err)
186 success = false
187 }
188 }
189
190 return files, success
191 }
192
193 func showError(path string, err error) {
194 if path != `` {
195 os.Stderr.WriteString(path)
196 os.Stderr.WriteString(`: `)
197 }
198 os.Stderr.WriteString(err.Error())
199 os.Stderr.WriteString("\n")
200 }
201
202 func findChecksums(paths []string, sums []string) {
203 var tasks sync.WaitGroup
204 // the number of tasks is always known in advance
205 tasks.Add(len(paths))
206
207 // permissions is buffered to limit concurrency to the core-count
208 permissions := make(chan struct{}, runtime.NumCPU())
209 defer close(permissions)
210
211 for i, path := range paths {
212 // wait until some concurrency-room is available, before proceeding
213 permissions <- struct{}{}
214
215 go func(i int, path string) {
216 defer tasks.Done()
217
218 chsum, err := sha(path)
219 if err != nil {
220 chsum = ``
221 showError(path, err)
222 }
223
224 sums[i] = chsum
225 <-permissions
226 }(i, path)
227 }
228
229 // wait for all tasks to finish
230 tasks.Wait()
231 }
232
233 // sha calculates a checksum for a file's contents
234 func sha(path string) (string, error) {
235 f, err := os.Open(path)
236 if err != nil {
237 return ``, err
238 }
239 defer f.Close()
240
241 sha := sha512.New()
242 _, err = io.Copy(sha, f)
243 if err != nil {
244 return ``, err
245 }
246
247 // buf has room to fit a SHA-512 hash exactly: while its hexadecimal-ASCII
248 // rendition is 128 bytes, the checksum itself is 64 bytes
249 var buf [64]byte
250 return hex.EncodeToString(sha.Sum(buf[:0])), err
251 }
252
253 func showGroups(w *bufio.Writer, keys []string, groups map[string][]string) {
254 shown := 0
255
256 for _, chsum := range keys {
257 if chsum == `` {
258 continue
259 }
260
261 if shown > 0 && w.WriteByte('\n') != nil {
262 return
263 }
264
265 w.WriteString(chsum)
266 if w.WriteByte('\n') != nil {
267 return
268 }
269
270 files := groups[chsum]
271
272 for _, path := range files {
273 w.WriteString(path)
274 if w.WriteByte('\n') != nil {
275 return
276 }
277 }
278
279 shown++
280 }
281
282 return
283 }
284
285 func showJSON(w *bufio.Writer, keys []string, groups map[string][]string) {
286 if len(keys) == 0 {
287 w.WriteString("{}\n")
288 return
289 }
290
291 const indent = ` `
292
293 shown := 0
294
295 w.WriteString("{")
296
297 for _, chsum := range keys {
298 files := groups[chsum]
299 if len(files) == 0 {
300 continue
301 }
302
303 if shown > 0 {
304 w.WriteByte(',')
305 }
306 if w.WriteByte('\n') != nil {
307 return
308 }
309
310 w.WriteString(indent)
311
312 // checksum strings never need escaping
313
314 w.WriteByte('"')
315 w.WriteString(chsum)
316 if _, err := w.WriteString(`": [` + "\n"); err != nil {
317 return
318 }
319
320 for i, path := range files {
321 if i > 0 && w.WriteByte('\n') != nil {
322 return
323 }
324
325 w.WriteString(indent + indent)
326
327 w.WriteByte('"')
328 w.WriteString(path)
329 if i == len(files)-1 {
330 w.WriteByte('"')
331 } else {
332 w.WriteString(`",`)
333 }
334 }
335
336 w.WriteString("\n" + indent + `]`)
337
338 shown++
339 }
340
341 if shown > 0 {
342 w.WriteString("\n}\n")
343 } else {
344 w.WriteString("}\n")
345 }
346 return
347 }
File: ./skip/skip.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package skip
26
27 import (
28 "bufio"
29 "io"
30 "os"
31 "strconv"
32 "strings"
33 )
34
35 const info = `
36 skip [options...] [max lines...] [files...]
37
38 Skip at most the first n lines, or skip the first line by default. When
39 not given any filepaths, the standard input is used instead.
40
41 Options
42
43 -n [number] change max number of lines to skip (default is 1)
44 `
45
46 type config struct {
47 skip int
48 liveLines bool
49 }
50
51 func Main() {
52 var cfg config
53 cfg.skip = 1
54 cfg.liveLines = true
55
56 args := os.Args[1:]
57 for len(args) > 0 {
58 switch args[0] {
59 case `-b`, `--b`, `-buffered`, `--buffered`:
60 cfg.liveLines = false
61 args = args[1:]
62 continue
63
64 case `-n`:
65 args = args[1:]
66 if len(args) == 0 {
67 os.Stderr.WriteString("missing number of lines\n")
68 os.Exit(1)
69 return
70 }
71
72 s := strings.Replace(args[0], `_`, ``, -1)
73 n, err := strconv.ParseInt(s, 10, 64)
74 if err != nil {
75 os.Stderr.WriteString(`skip: invalid number: `)
76 os.Stderr.WriteString(err.Error())
77 os.Stderr.WriteString("\n")
78 os.Exit(1)
79 return
80 }
81
82 args = args[1:]
83 cfg.skip = int(n)
84 continue
85
86 case `--help`:
87 os.Stderr.WriteString(info[1:])
88 return
89 }
90
91 break
92 }
93
94 if len(args) > 0 {
95 s := strings.Replace(args[0], `_`, ``, -1)
96 if n, err := strconv.ParseInt(s, 10, 64); err == nil {
97 args = args[1:]
98 cfg.skip = int(n)
99 }
100 }
101
102 if len(args) > 0 && args[0] == `--` {
103 args = args[1:]
104 }
105
106 if cfg.liveLines {
107 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
108 cfg.liveLines = false
109 }
110 }
111
112 if err := run(args, &cfg); err != nil && err != io.EOF {
113 os.Stderr.WriteString(`skip: `)
114 os.Stderr.WriteString(err.Error())
115 os.Stderr.WriteString("\n")
116 os.Exit(1)
117 return
118 }
119 }
120
121 func run(paths []string, cfg *config) error {
122 w := bufio.NewWriterSize(os.Stdout, 32*1024)
123 defer w.Flush()
124
125 for _, path := range paths {
126 if err := handleFile(w, path, cfg); err != nil {
127 return err
128 }
129 }
130
131 if len(paths) == 0 {
132 if err := handleLines(w, os.Stdin, cfg); err != nil {
133 return err
134 }
135 }
136 return nil
137 }
138
139 func handleFile(w *bufio.Writer, path string, cfg *config) error {
140 f, err := os.Open(path)
141 if err != nil {
142 return err
143 }
144 defer f.Close()
145 return handleLines(w, f, cfg)
146 }
147
148 func handleLines(w *bufio.Writer, r io.Reader, cfg *config) error {
149 const gb = 1024 * 1024 * 1024
150 sc := bufio.NewScanner(r)
151 sc.Buffer(nil, 8*gb)
152
153 for sc.Scan() {
154 if cfg.skip > 0 {
155 cfg.skip--
156 continue
157 }
158
159 w.Write(sc.Bytes())
160 if w.WriteByte('\n') != nil {
161 return io.EOF
162 }
163
164 if cfg.liveLines && w.Flush() != nil {
165 return io.EOF
166 }
167 }
168
169 return sc.Err()
170 }
File: ./skiplast/skiplast.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package skiplast
26
27 import (
28 "bufio"
29 "io"
30 "os"
31 "strconv"
32 "strings"
33 )
34
35 const info = `
36 skiplast [options...] [max lines...] [files...]
37
38 Keep all but the last n lines, or skip the last line by default. When not
39 given any filepaths, the standard input is used instead.
40
41 All (optional) leading options start with either single or double-dash:
42
43 -h, -help show this help message
44 `
45
46 type ringBuffer struct {
47 next int
48 max int
49 items []string
50 }
51
52 func (rb *ringBuffer) append(s string) (previous string) {
53 if rb.next < len(rb.items) {
54 previous = rb.items[rb.next]
55 rb.items[rb.next] = s
56 } else if len(rb.items) < rb.max {
57 rb.items = append(rb.items, s)
58 } else if len(rb.items) > 0 {
59 previous = rb.items[0]
60 rb.items[0] = s
61 }
62
63 rb.next++
64 if rb.next >= rb.max {
65 rb.next = 0
66 }
67 return previous
68 }
69
70 func Main() {
71 var latest ringBuffer
72 latest.max = 1
73 buffered := false
74 args := os.Args[1:]
75
76 if len(args) > 0 {
77 switch args[0] {
78 case `-b`, `--b`, `-buffered`, `--buffered`:
79 buffered = true
80 args = args[1:]
81
82 case `-h`, `--h`, `-help`, `--help`:
83 os.Stdout.WriteString(info[1:])
84 return
85 }
86 }
87
88 if len(args) > 0 {
89 s := strings.Replace(args[0], `_`, ``, -1)
90 n, err := strconv.ParseInt(s, 10, 64)
91 if err == nil {
92 latest.max = int(n)
93 args = args[1:]
94 }
95 }
96
97 if len(args) > 0 && args[0] == `--` {
98 args = args[1:]
99 }
100
101 if latest.max <= 0 {
102 return
103 }
104
105 if latest.max <= 1_000 {
106 latest.items = make([]string, 0, latest.max)
107 }
108
109 liveLines := !buffered
110 if !buffered {
111 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
112 liveLines = false
113 }
114 }
115
116 if err := run(args, &latest, liveLines); err != nil && err != io.EOF {
117 os.Stderr.WriteString(`skiplast: `)
118 os.Stderr.WriteString(err.Error())
119 os.Stderr.WriteString("\n")
120 os.Exit(1)
121 return
122 }
123 }
124
125 type config struct {
126 w *bufio.Writer
127 live bool
128 }
129
130 func run(paths []string, rb *ringBuffer, live bool) error {
131 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
132 defer bw.Flush()
133
134 var cfg config
135 cfg.w = bw
136 cfg.live = live
137
138 for _, path := range paths {
139 if err := handleFile(path, rb, cfg); err != nil {
140 return err
141 }
142 }
143
144 if len(paths) == 0 {
145 if err := visit(os.Stdin, rb, cfg); err != nil {
146 return err
147 }
148 }
149 return nil
150 }
151
152 func handleFile(path string, rb *ringBuffer, cfg config) error {
153 f, err := os.Open(path)
154 if err != nil {
155 return err
156 }
157 defer f.Close()
158 return visit(f, rb, cfg)
159 }
160
161 func visit(r io.Reader, rb *ringBuffer, cfg config) error {
162 const gb = 1024 * 1024 * 1024
163 sc := bufio.NewScanner(r)
164 sc.Buffer(nil, 8*gb)
165
166 for sc.Scan() {
167 prev := rb.append(sc.Text())
168 if len(rb.items) < rb.max {
169 continue
170 }
171
172 cfg.w.WriteString(prev)
173
174 if cfg.w.WriteByte('\n') != nil {
175 return io.EOF
176 }
177
178 if !cfg.live {
179 continue
180 }
181
182 if cfg.w.Flush() != nil {
183 return io.EOF
184 }
185 }
186
187 return sc.Err()
188 }
File: ./squeeze/squeeze.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package squeeze
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 )
34
35 const info = `
36 squeeze [filenames...]
37
38 Ignore leading/trailing spaces (and carriage-returns) on lines, also turning
39 all runs of multiple consecutive spaces into single spaces. Spaces around
40 tabs are ignored as well.
41 `
42
43 func Main() {
44 buffered := false
45 args := os.Args[1:]
46
47 if len(args) > 0 {
48 switch args[0] {
49 case `-b`, `--b`, `-buffered`, `--buffered`:
50 buffered = true
51 args = args[1:]
52
53 case `-h`, `--h`, `-help`, `--help`:
54 os.Stdout.WriteString(info[1:])
55 return
56 }
57 }
58
59 if len(args) > 0 && args[0] == `--` {
60 args = args[1:]
61 }
62
63 liveLines := !buffered
64 if !buffered {
65 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
66 liveLines = false
67 }
68 }
69
70 if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
71 os.Stderr.WriteString(`squeeze: `)
72 os.Stderr.WriteString(err.Error())
73 os.Stderr.WriteString("\n")
74 os.Exit(1)
75 return
76 }
77 }
78
79 func run(w io.Writer, args []string, live bool) error {
80 bw := bufio.NewWriterSize(w, 32*1024)
81 defer bw.Flush()
82
83 if len(args) == 0 {
84 return squeeze(bw, os.Stdin, live)
85 }
86
87 for _, name := range args {
88 if err := handleFile(bw, name, live); err != nil {
89 return err
90 }
91 }
92 return nil
93 }
94
95 func handleFile(w *bufio.Writer, name string, live bool) error {
96 if name == `` || name == `-` {
97 return squeeze(w, os.Stdin, live)
98 }
99
100 f, err := os.Open(name)
101 if err != nil {
102 return errors.New(`can't read from file named "` + name + `"`)
103 }
104 defer f.Close()
105
106 return squeeze(w, f, live)
107 }
108
109 func squeeze(w *bufio.Writer, r io.Reader, live bool) error {
110 const gb = 1024 * 1024 * 1024
111 sc := bufio.NewScanner(r)
112 sc.Buffer(nil, 8*gb)
113
114 for i := 0; sc.Scan(); i++ {
115 s := sc.Bytes()
116 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
117 s = s[3:]
118 }
119
120 writeSqueezed(w, s)
121 if w.WriteByte('\n') != nil {
122 return io.EOF
123 }
124
125 if live && w.Flush() != nil {
126 return io.EOF
127 }
128 }
129
130 return sc.Err()
131 }
132
133 func writeSqueezed(w *bufio.Writer, s []byte) {
134 // ignore leading spaces
135 for len(s) > 0 && s[0] == ' ' {
136 s = s[1:]
137 }
138
139 // ignore trailing spaces
140 for len(s) > 0 && s[len(s)-1] == ' ' {
141 s = s[:len(s)-1]
142 }
143
144 space := false
145
146 for len(s) > 0 {
147 switch s[0] {
148 case ' ':
149 s = s[1:]
150 space = true
151
152 case '\t':
153 s = s[1:]
154 space = false
155 for len(s) > 0 && s[0] == ' ' {
156 s = s[1:]
157 }
158 w.WriteByte('\t')
159
160 default:
161 if space {
162 w.WriteByte(' ')
163 space = false
164 }
165 w.WriteByte(s[0])
166 s = s[1:]
167 }
168 }
169 }
File: ./squomp/squomp.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package squomp
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 )
34
35 const info = `
36 squomp [filenames...]
37
38 Squeeze non-empty lines and stomp empty(-ish) lines.
39
40 Ignore leading/trailing spaces (and carriage-returns) on lines, also turning
41 all runs of multiple consecutive spaces into single spaces. Spaces around
42 tabs are ignored as well.
43
44 Also, ignore leading/trailing empty lines, and turn runs of multiple empty
45 lines into single empty lines. Empty-ish lines, or lines with only spaces in
46 them, are also treated like empty ones.
47 `
48
49 type config struct {
50 nonEmpty int
51 emptyRun int
52 liveLines bool
53 }
54
55 func Main() {
56 var cfg config
57 cfg.liveLines = true
58 args := os.Args[1:]
59
60 for len(args) > 0 {
61 switch args[0] {
62 case `-b`, `--b`, `-buffered`, `--buffered`:
63 cfg.liveLines = false
64 args = args[1:]
65 continue
66
67 case `-h`, `--h`, `-help`, `--help`:
68 os.Stdout.WriteString(info[1:])
69 return
70 }
71
72 break
73 }
74
75 if len(args) > 0 && args[0] == `--` {
76 args = args[1:]
77 }
78
79 if cfg.liveLines {
80 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
81 cfg.liveLines = false
82 }
83 }
84
85 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
86 os.Stderr.WriteString(`squomp: `)
87 os.Stderr.WriteString(err.Error())
88 os.Stderr.WriteString("\n")
89 os.Exit(1)
90 return
91 }
92 }
93
94 func run(w io.Writer, args []string, cfg config) error {
95 bw := bufio.NewWriterSize(w, 32*1024)
96 defer bw.Flush()
97
98 if len(args) == 0 {
99 return squomp(bw, os.Stdin, &cfg)
100 }
101
102 for _, name := range args {
103 if err := handleFile(bw, name, &cfg); err != nil {
104 return err
105 }
106 }
107 return nil
108 }
109
110 func handleFile(w *bufio.Writer, name string, cfg *config) error {
111 if name == `` || name == `-` {
112 return squomp(w, os.Stdin, cfg)
113 }
114
115 f, err := os.Open(name)
116 if err != nil {
117 return errors.New(`can't read from file named "` + name + `"`)
118 }
119 defer f.Close()
120
121 return squomp(w, f, cfg)
122 }
123
124 func squomp(w *bufio.Writer, r io.Reader, cfg *config) error {
125 const gb = 1024 * 1024 * 1024
126 sc := bufio.NewScanner(r)
127 sc.Buffer(nil, 8*gb)
128
129 for i := 0; sc.Scan(); i++ {
130 s := sc.Bytes()
131 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
132 s = s[3:]
133 }
134
135 // trim leading spaces, so the empty-length check which follows can
136 // also detect/ignore empty-ish lines, that is lines with only spaces
137 for len(s) > 0 && s[0] == ' ' {
138 s = s[1:]
139 }
140
141 if len(s) == 0 {
142 if cfg.nonEmpty == 0 {
143 continue
144 }
145 cfg.emptyRun++
146 }
147
148 if cfg.emptyRun > 0 {
149 cfg.emptyRun = 0
150 if w.WriteByte('\n') != nil {
151 return io.EOF
152 }
153 }
154
155 cfg.nonEmpty++
156
157 writeSqueezed(w, s)
158 if w.WriteByte('\n') != nil {
159 return io.EOF
160 }
161
162 if cfg.liveLines && w.Flush() != nil {
163 return io.EOF
164 }
165 }
166
167 return sc.Err()
168 }
169
170 func writeSqueezed(w *bufio.Writer, s []byte) {
171 // ignore leading spaces
172 for len(s) > 0 && s[0] == ' ' {
173 s = s[1:]
174 }
175
176 // ignore trailing spaces
177 for len(s) > 0 && s[len(s)-1] == ' ' {
178 s = s[:len(s)-1]
179 }
180
181 space := false
182
183 for len(s) > 0 {
184 switch s[0] {
185 case ' ':
186 s = s[1:]
187 space = true
188
189 case '\t':
190 s = s[1:]
191 space = false
192 for len(s) > 0 && s[0] == ' ' {
193 s = s[1:]
194 }
195 w.WriteByte('\t')
196
197 default:
198 if space {
199 w.WriteByte(' ')
200 space = false
201 }
202 w.WriteByte(s[0])
203 s = s[1:]
204 }
205 }
206 }
File: ./stomp/stomp.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package stomp
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 )
34
35 const info = `
36 stomp [files...]
37
38 Turn runs of empty lines into single empty lines, effectively squeezing
39 paragraphs vertically, so to speak; runs of empty lines both at the start
40 and at the end are completely ignored.
41
42 All (optional) leading options start with either single or double-dash:
43
44 -h, -help show this help message
45 `
46
47 type config struct {
48 nonEmpty int
49 emptyRun int
50 liveLines bool
51 }
52
53 func Main() {
54 var cfg config
55 cfg.liveLines = true
56 args := os.Args[1:]
57
58 for len(args) > 0 {
59 switch args[0] {
60 case `-b`, `--b`, `-buffered`, `--buffered`:
61 cfg.liveLines = false
62 args = args[1:]
63 continue
64
65 case `-h`, `--h`, `-help`, `--help`:
66 os.Stdout.WriteString(info[1:])
67 return
68 }
69
70 break
71 }
72
73 if len(args) > 0 && args[0] == `--` {
74 args = args[1:]
75 }
76
77 if cfg.liveLines {
78 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
79 cfg.liveLines = false
80 }
81 }
82
83 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
84 os.Stderr.WriteString(`stomp: `)
85 os.Stderr.WriteString(err.Error())
86 os.Stderr.WriteString("\n")
87 os.Exit(1)
88 return
89 }
90 }
91
92 func run(w io.Writer, args []string, cfg config) error {
93 bw := bufio.NewWriterSize(w, 32*1024)
94 defer bw.Flush()
95
96 dashes := 0
97 for _, name := range args {
98 if name == `-` {
99 dashes++
100 }
101 if dashes > 1 {
102 return errors.New(`can't read stdin (dash) more than once`)
103 }
104 }
105
106 if len(args) == 0 {
107 return stomp(bw, os.Stdin, &cfg)
108 }
109
110 for _, name := range args {
111 if err := handleFile(bw, name, &cfg); err != nil {
112 return err
113 }
114 }
115 return nil
116 }
117
118 func handleFile(w *bufio.Writer, name string, cfg *config) error {
119 if name == `` || name == `-` {
120 return stomp(w, os.Stdin, cfg)
121 }
122
123 f, err := os.Open(name)
124 if err != nil {
125 return errors.New(`can't read from file named "` + name + `"`)
126 }
127 defer f.Close()
128
129 return stomp(w, f, cfg)
130 }
131
132 func stomp(w *bufio.Writer, r io.Reader, cfg *config) error {
133 const gb = 1024 * 1024 * 1024
134 sc := bufio.NewScanner(r)
135 sc.Buffer(nil, 8*gb)
136
137 for i := 0; sc.Scan(); i++ {
138 s := sc.Bytes()
139 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
140 s = s[3:]
141 }
142
143 if len(s) == 0 {
144 if cfg.nonEmpty == 0 {
145 continue
146 }
147 cfg.emptyRun++
148 }
149
150 if cfg.emptyRun > 0 {
151 cfg.emptyRun = 0
152 if w.WriteByte('\n') != nil {
153 return io.EOF
154 }
155 }
156
157 cfg.nonEmpty++
158
159 w.Write(s)
160 if w.WriteByte('\n') != nil {
161 return io.EOF
162 }
163
164 if cfg.liveLines && w.Flush() != nil {
165 return io.EOF
166 }
167 }
168
169 return sc.Err()
170 }
File: ./sym/sym.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package sym
26
27 import (
28 "bufio"
29 "io"
30 "os"
31 )
32
33 const info = `
34 sym [names...]
35
36 Lookup various hard-to-type symbols by their common names/aliases. All/any
37 dashes in the names given are ignored, and are thus supported, so to speak.
38
39 All (optional) leading options start with either single or double-dash:
40
41 -h, -help show this help message
42 `
43
44 func Main() {
45 args := os.Args[1:]
46
47 if len(args) > 0 {
48 switch args[0] {
49 case `-h`, `--h`, `-help`, `--help`:
50 os.Stdout.WriteString(info[1:])
51 return
52 }
53 }
54
55 if len(args) > 0 && args[0] == `--` {
56 args = args[1:]
57 }
58
59 if err := run(args); err != nil && err != io.EOF {
60 os.Stderr.WriteString(`sym: `)
61 os.Stderr.WriteString(err.Error())
62 os.Stderr.WriteString("\n")
63 os.Exit(1)
64 return
65 }
66 }
67
68 func run(args []string) error {
69 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
70 defer bw.Flush()
71
72 if err := showSymbols(bw, args); err != nil {
73 return err
74 }
75 return nil
76 }
File: ./sym/tables.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package sym
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "sort"
32 "strings"
33 )
34
35 const (
36 base64 = `` +
37 `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=`
38
39 faces = `` +
40 `😀😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏😐😑😒😓` +
41 `😔😕😖😗😘😙😚😛😜😝😞😟😠😡😢😣😤😥😦😧` +
42 `😨😩😪😫😬😭😮😯😰😱😲😳😴😵😶😷🙁🙂🙃🙄` +
43 `🧐👶🤓🤐🤑🤒🤔🤕🤗🤠🤡🤢🤣🤤🤥🤧🤨🤩🤪🤫` +
44 `🤬🤭🤮🤯`
45
46 punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
47
48 visibleASCII = `` +
49 "!\"#$%&'()*+,-./0123456789:;<=>?@" +
50 "ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
51 )
52
53 // `crlf`: "\r\n",
54 // `lf`: "\n",
55 // `linefeed`: "\n",
56 // `tab`: "\t",
57
58 // names2symbols translates common names for common symbols
59 var names2symbols = map[string]string{
60 `altcolon`: `:`,
61 `altquestion`: `?`,
62 `altverticalbar`: `|`,
63 `almost`: `≈`,
64 `ampersand`: `&`,
65 `apostrophe`: `’`,
66 `approximately`: `≅`,
67 `asterisk`: `*`,
68 `backquote`: "`",
69 `backslash`: `\`,
70 `ball`: `●`,
71 `bell`: `🔔`,
72 `block`: `█`,
73 `bomb`: `💣`,
74 `bullet`: `•`,
75 `caca`: `💩`,
76 `caret`: `^`,
77 `check`: `✅`,
78 `cherries`: `🍒`,
79 `cloud`: `☁️`,
80 `cloverleaf`: `🍀`,
81 `colon`: `:`,
82 `comma`: `,`,
83 `commaspace`: `, `,
84 `copyright`: `©`,
85 `cross`: `❌`,
86 `crying`: `😭`,
87 `decimals`: `0123456789`,
88 `degree`: `°`,
89 `diameter`: `⌀`,
90 `diamond`: `💎`,
91 `document`: `📄`,
92 `dollar`: `$`,
93 `doublequote`: `"`,
94 `ellipsis`: `…`,
95 `email`: `@`,
96 `emdash`: `—`,
97 `emptyset`: `∅`,
98 `endash`: `–`,
99 `equals`: `=`,
100 `escape`: ``,
101 `euro`: `€`,
102 `exclamation`: `!`,
103 `eyes`: `👀`,
104 `fire`: `🔥`,
105 `folder`: `📂`,
106 `fullmoon`: `🌕`,
107 `grapes`: `🍇`,
108 `greatereq`: `≥`,
109 `happy`: `😀`,
110 `heart`: `❤️`,
111 `hole`: `○`,
112 `hyphen`: `-`,
113 `laughter`: `😄`,
114 `lemon`: `🍋`,
115 `less`: `<`,
116 `lesseq`: `≤`,
117 `lightning`: `🌩️`,
118 `ldquote`: `“`,
119 `lol`: `😂`,
120 `lsquote`: `‘`,
121 `mdot`: `·`,
122 `more`: `>`,
123 `multiplication`: `×`,
124 `musicalnote`: `🎵`,
125 `not`: `¬`,
126 `notequals`: `≠`,
127 `orange`: `🍊`,
128 `party`: `🥳`,
129 `partypopper`: `🎉`,
130 `percent`: `%`,
131 `period`: `.`,
132 `pineapple`: `🍍`,
133 `plus`: `+`,
134 `question`: `?`,
135 `rain`: `🌧️`,
136 `rdquote`: `”`,
137 `rofl`: `🤣`,
138 `semicolon`: `;`,
139 `sharp`: `#`,
140 `slash`: `/`,
141 `slasher`: `⧸`,
142 `smile`: `🙂`,
143 `smirk`: `😏`,
144 `snowflake`: `❄️`,
145 `space`: ` `,
146 `sparkles`: `✨`,
147 `square`: `■`,
148 `singlequote`: `'`,
149 `star`: `⭐`,
150 `sun`: `☀️`,
151 `supersmile`: `😊`,
152 `tilde`: `~`,
153 `verticalbar`: `|`,
154 `vellipsis`: `⋮`,
155 `warning`: `⚠️`,
156 `watermelon`: `🍉`,
157
158 `base64`: base64,
159 `faces`: faces,
160
161 `visibleascii`: visibleASCII,
162
163 `alpha`: `α`,
164 `beta`: `β`,
165 `delta`: `δ`,
166 `epsilon`: `ε`,
167 `gamma`: `γ`,
168 `lambda`: `λ`,
169 `omega`: `ω`,
170 `pi`: `π`,
171 `sigma`: `σ`,
172 `tau`: `τ`,
173 `theta`: `θ`,
174
175 `capitalsigma`: `Σ`,
176
177 `asciiletters`: `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`,
178 `greek`: `αβγδεζηθικλμνξοπρστυφχψω`,
179 `hexadecimals`: `0123456789ABCDEF`,
180 `infinity`: `∞`,
181 `lowercase`: `abcdefghijklmnopqrstuvwxyz`,
182 `lowercasehex`: `0123456789abcdef`,
183 `math`: `+-×÷²³±`,
184 `octaldigits`: `01234567`,
185 `other`: `✓✗✔❌`,
186 `plusminus`: `±`,
187 `product`: `Π`,
188 `punctuation`: punctuation,
189 `uppercase`: `ABCDEFGHIJKLMNOPQRSTUVWXYZ`,
190 `uppercasegreek`: `ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ`,
191
192 `eur`: `🇪🇺`,
193
194 `afg`: `🇦🇫`,
195 `ala`: `🇦🇽`,
196 `alb`: `🇦🇱`,
197 `dza`: `🇩🇿`,
198 `asm`: `🇦🇸`,
199 `and`: `🇦🇩`,
200 `ago`: `🇦🇴`,
201 `aia`: `🇦🇮`,
202 `ata`: `🇦🇶`,
203 `atg`: `🇦🇬`,
204 `arg`: `🇦🇷`,
205 `arm`: `🇦🇲`,
206 `abw`: `🇦🇼`,
207 `aus`: `🇦🇺`,
208 `aut`: `🇦🇹`,
209 `aze`: `🇦🇿`,
210 `bhs`: `🇧🇸`,
211 `bhr`: `🇧🇭`,
212 `bgd`: `🇧🇩`,
213 `brb`: `🇧🇧`,
214 `blr`: `🇧🇾`,
215 `bel`: `🇧🇪`,
216 `blz`: `🇧🇿`,
217 `ben`: `🇧🇯`,
218 `bmu`: `🇧🇲`,
219 `btn`: `🇧🇹`,
220 `bol`: `🇧🇴`,
221 `bes`: `🇧🇶`,
222 `bih`: `🇧🇦`,
223 `bwa`: `🇧🇼`,
224 `bvt`: `🇧🇻`,
225 `bra`: `🇧🇷`,
226 `iot`: `🇮🇴`,
227 `brn`: `🇧🇳`,
228 `bgr`: `🇧🇬`,
229 `bfa`: `🇧🇫`,
230 `bdi`: `🇧🇮`,
231 `cpv`: `🇨🇻`,
232 `khm`: `🇰🇭`,
233 `cmr`: `🇨🇲`,
234 `can`: `🇨🇦`,
235 `cym`: `🇰🇾`,
236 `caf`: `🇨🇫`,
237 `tcd`: `🇹🇩`,
238 `chl`: `🇨🇱`,
239 `chn`: `🇨🇳`,
240 `cxr`: `🇨🇽`,
241 `cck`: `🇨🇨`,
242 `col`: `🇨🇴`,
243 `com`: `🇰🇲`,
244 `cod`: `🇨🇩`,
245 `cog`: `🇨🇬`,
246 `cok`: `🇨🇰`,
247 `cri`: `🇨🇷`,
248 `civ`: `🇨🇮`,
249 `hrv`: `🇭🇷`,
250 `cub`: `🇨🇺`,
251 `cuw`: `🇨🇼`,
252 `cyp`: `🇨🇾`,
253 `cze`: `🇨🇿`,
254 `dnk`: `🇩🇰`,
255 `dji`: `🇩🇯`,
256 `dma`: `🇩🇲`,
257 `dom`: `🇩🇴`,
258 `ecu`: `🇪🇨`,
259 `egy`: `🇪🇬`,
260 `slv`: `🇸🇻`,
261 `gnq`: `🇬🇶`,
262 `eri`: `🇪🇷`,
263 `est`: `🇪🇪`,
264 `swz`: `🇸🇿`,
265 `eth`: `🇪🇹`,
266 `flk`: `🇫🇰`,
267 `fro`: `🇫🇴`,
268 `fji`: `🇫🇯`,
269 `fin`: `🇫🇮`,
270 `fra`: `🇫🇷`,
271 `guf`: `🇬🇫`,
272 `pyf`: `🇵🇫`,
273 `atf`: `🇹🇫`,
274 `gab`: `🇬🇦`,
275 `gmb`: `🇬🇲`,
276 `geo`: `🇬🇪`,
277 `deu`: `🇩🇪`,
278 `gha`: `🇬🇭`,
279 `gib`: `🇬🇮`,
280 `grc`: `🇬🇷`,
281 `grl`: `🇬🇱`,
282 `grd`: `🇬🇩`,
283 `glp`: `🇬🇵`,
284 `gum`: `🇬🇺`,
285 `gtm`: `🇬🇹`,
286 `ggy`: `🇬🇬`,
287 `gin`: `🇬🇳`,
288 `gnb`: `🇬🇼`,
289 `guy`: `🇬🇾`,
290 `hti`: `🇭🇹`,
291 `hmd`: `🇭🇲`,
292 `vat`: `🇻🇦`,
293 `hnd`: `🇭🇳`,
294 `hkg`: `🇭🇰`,
295 `hun`: `🇭🇺`,
296 `isl`: `🇮🇸`,
297 `ind`: `🇮🇳`,
298 `idn`: `🇮🇩`,
299 `irn`: `🇮🇷`,
300 `irq`: `🇮🇶`,
301 `irl`: `🇮🇪`,
302 `imn`: `🇮🇲`,
303 `isr`: `🇮🇱`,
304 `ita`: `🇮🇹`,
305 `jam`: `🇯🇲`,
306 `jpn`: `🇯🇵`,
307 `jey`: `🇯🇪`,
308 `jor`: `🇯🇴`,
309 `kaz`: `🇰🇿`,
310 `ken`: `🇰🇪`,
311 `kir`: `🇰🇮`,
312 `prk`: `🇰🇵`,
313 `kor`: `🇰🇷`,
314 `kwt`: `🇰🇼`,
315 `kgz`: `🇰🇬`,
316 `lao`: `🇱🇦`,
317 `lva`: `🇱🇻`,
318 `lbn`: `🇱🇧`,
319 `lso`: `🇱🇸`,
320 `lbr`: `🇱🇷`,
321 `lby`: `🇱🇾`,
322 `lie`: `🇱🇮`,
323 `ltu`: `🇱🇹`,
324 `lux`: `🇱🇺`,
325 `mac`: `🇲🇴`,
326 `mkd`: `🇲🇰`,
327 `mdg`: `🇲🇬`,
328 `mwi`: `🇲🇼`,
329 `mys`: `🇲🇾`,
330 `mdv`: `🇲🇻`,
331 `mli`: `🇲🇱`,
332 `mlt`: `🇲🇹`,
333 `mhl`: `🇲🇭`,
334 `mtq`: `🇲🇶`,
335 `mrt`: `🇲🇷`,
336 `mus`: `🇲🇺`,
337 `myt`: `🇾🇹`,
338 `mex`: `🇲🇽`,
339 `fsm`: `🇫🇲`,
340 `mda`: `🇲🇩`,
341 `mco`: `🇲🇨`,
342 `mng`: `🇲🇳`,
343 `mne`: `🇲🇪`,
344 `msr`: `🇲🇸`,
345 `mar`: `🇲🇦`,
346 `moz`: `🇲🇿`,
347 `mmr`: `🇲🇲`,
348 `nam`: `🇳🇦`,
349 `nru`: `🇳🇷`,
350 `npl`: `🇳🇵`,
351 `nld`: `🇳🇱`,
352 `ncl`: `🇳🇨`,
353 `nzl`: `🇳🇿`,
354 `nic`: `🇳🇮`,
355 `ner`: `🇳🇪`,
356 `nga`: `🇳🇬`,
357 `niu`: `🇳🇺`,
358 `nfk`: `🇳🇫`,
359 `mnp`: `🇲🇵`,
360 `nor`: `🇳🇴`,
361 `omn`: `🇴🇲`,
362 `pak`: `🇵🇰`,
363 `plw`: `🇵🇼`,
364 `pse`: `🇵🇸`,
365 `pan`: `🇵🇦`,
366 `png`: `🇵🇬`,
367 `pry`: `🇵🇾`,
368 `per`: `🇵🇪`,
369 `phl`: `🇵🇭`,
370 `pcn`: `🇵🇳`,
371 `pol`: `🇵🇱`,
372 `prt`: `🇵🇹`,
373 `pri`: `🇵🇷`,
374 `qat`: `🇶🇦`,
375 `reu`: `🇷🇪`,
376 `rou`: `🇷🇴`,
377 `rus`: `🇷🇺`,
378 `rwa`: `🇷🇼`,
379 `blm`: `🇧🇱`,
380 `shn`: `🇸🇭`,
381 `kna`: `🇰🇳`,
382 `lca`: `🇱🇨`,
383 `maf`: `🇲🇫`,
384 `spm`: `🇵🇲`,
385 `vct`: `🇻🇨`,
386 `wsm`: `🇼🇸`,
387 `smr`: `🇸🇲`,
388 `stp`: `🇸🇹`,
389 `sau`: `🇸🇦`,
390 `sen`: `🇸🇳`,
391 `srb`: `🇷🇸`,
392 `syc`: `🇸🇨`,
393 `sle`: `🇸🇱`,
394 `sgp`: `🇸🇬`,
395 `sxm`: `🇸🇽`,
396 `svk`: `🇸🇰`,
397 `svn`: `🇸🇮`,
398 `slb`: `🇸🇧`,
399 `som`: `🇸🇴`,
400 `zaf`: `🇿🇦`,
401 `sgs`: `🇬🇸`,
402 `ssd`: `🇸🇸`,
403 `esp`: `🇪🇸`,
404 `lka`: `🇱🇰`,
405 `sdn`: `🇸🇩`,
406 `sur`: `🇸🇷`,
407 `sjm`: `🇸🇯`,
408 `swe`: `🇸🇪`,
409 `che`: `🇨🇭`,
410 `syr`: `🇸🇾`,
411 `twn`: `🇹🇼`,
412 `tjk`: `🇹🇯`,
413 `tza`: `🇹🇿`,
414 `tha`: `🇹🇭`,
415 `tls`: `🇹🇱`,
416 `tgo`: `🇹🇬`,
417 `tkl`: `🇹🇰`,
418 `ton`: `🇹🇴`,
419 `tto`: `🇹🇹`,
420 `tun`: `🇹🇳`,
421 `tur`: `🇹🇷`,
422 `tkm`: `🇹🇲`,
423 `tca`: `🇹🇨`,
424 `tuv`: `🇹🇻`,
425 `uga`: `🇺🇬`,
426 `ukr`: `🇺🇦`,
427 `gbr`: `🇬🇧`,
428 `uae`: `🇦🇪`,
429 `umi`: `🇺🇲`,
430 `usa`: `🇺🇸`,
431 `ury`: `🇺🇾`,
432 `uzb`: `🇺🇿`,
433 `vut`: `🇻🇺`,
434 `ven`: `🇻🇪`,
435 `vnm`: `🇻🇳`,
436 `vgb`: `🇻🇬`,
437 `vir`: `🇻🇮`,
438 `wlf`: `🇼🇫`,
439 `esh`: `🇪🇭`,
440 `yem`: `🇾🇪`,
441 `zmb`: `🇿🇲`,
442 `zwe`: `🇿🇼`,
443 }
444
445 // symbolAliases resolves aliases for keys used in table name2symbols
446 var symbolAliases = map[string]string{
447 `adash`: `hyphen`,
448 `aeq`: `almost`,
449 `alphabet`: `lowercase`,
450 `altqmark`: `altquestion`,
451 `altque`: `altquestion`,
452 `altques`: `altquestion`,
453 `altquest`: `altquestion`,
454 `altvbar`: `altverticalbar`,
455 `altverbar`: `altverticalbar`,
456 `altvertbar`: `altverticalbar`,
457 `amp`: `ampersand`,
458 `apos`: `apostrophe`,
459 `approx`: `approximately`,
460 `ast`: `asterisk`,
461 `atleast`: `greatereq`,
462 `atmost`: `lesseq`,
463 `backtick`: `backquote`,
464 `bang`: `exclamation`,
465 `bigsigma`: `capitalsigma`,
466 `bquo`: `backquote`,
467 `bquote`: `backquote`,
468 `bslash`: `backslash`,
469 `capsigma`: `capitalsigma`,
470 `cdot`: `mdot`,
471 `cherry`: `cherries`,
472 `circle`: `ball`,
473 `cquote`: `rdquote`,
474 `crap`: `caca`,
475 `cry`: `crying`,
476 `cryin`: `crying`,
477 `dash`: `emdash`,
478 `decimal`: `decimals`,
479 `dec`: `decimals`,
480 `deg`: `degree`,
481 `digits`: `decimals`,
482 `doc`: `document`,
483 `dot`: `period`,
484 `dquo`: `doublequote`,
485 `dquote`: `doublequote`,
486 `ellip`: `ellipsis`,
487 `eps`: `epsilon`,
488 `eq`: `equals`,
489 `equal`: `equals`,
490 `esc`: `escape`,
491 `excl`: `exclamation`,
492 `exclam`: `exclamation`,
493 `file`: `document`,
494 `fullstop`: `period`,
495 `gem`: `diamond`,
496 `grape`: `grapes`,
497 `geq`: `greatereq`,
498 `grin`: `happy`,
499 `hash`: `sharp`,
500 `hellip`: `ellipsis`,
501 `hellipsis`: `ellipsis`,
502 `hex`: `hexadecimals`,
503 `hexa`: `hexadecimals`,
504 `hexadec`: `hexadecimals`,
505 `hexadecimal`: `hexadecimals`,
506 `hexdigits`: `hexadecimals`,
507 `inf`: `infinity`,
508 `infty`: `infinity`,
509 `latin`: `lowercase`,
510 `laugh`: `laughter`,
511 `ldquo`: `ldquote`,
512 `leq`: `lesseq`,
513 `letters`: `lowercase`,
514 `lower`: `lowercase`,
515 `loweralpha`: `alpha`,
516 `lowerbeta`: `beta`,
517 `lowercasegreek`: `greek`,
518 `lowergreek`: `greek`,
519 `lowerdelta`: `delta`,
520 `lowerepsilon`: `epsilon`,
521 `lowergamma`: `gamma`,
522 `lowerhex`: `lowercasehex`,
523 `lowerlambda`: `lambda`,
524 `loweromega`: `omega`,
525 `lowerpi`: `pi`,
526 `lowersigma`: `sigma`,
527 `lowertau`: `tau`,
528 `lowertheta`: `theta`,
529 `lsquo`: `lsquote`,
530 `mdash`: `emdash`,
531 `midascii`: `visibleascii`,
532 `minus`: `hyphen`,
533 `moon`: `fullmoon`,
534 `multiply`: `multiplication`,
535 `music`: `musicalnote`,
536 `ndash`: `endash`,
537 `neq`: `notequals`,
538 `note`: `musicalnote`,
539 `notequal`: `notequals`,
540 `oct`: `octaldigits`,
541 `octal`: `octaldigits`,
542 `octals`: `octaldigits`,
543 `octdigits`: `octaldigits`,
544 `oquote`: `ldquote`,
545 `pipe`: `verticalbar`,
546 `prod`: `product`,
547 `punct`: `punctuation`,
548 `rdquo`: `rdquote`,
549 `rsquo`: `apostrophe`,
550 `rsquote`: `apostrophe`,
551 `semi`: `semicolon`,
552 `shit`: `caca`,
553 `slashier`: `slasher`,
554 `snow`: `snowflake`,
555 `sparkle`: `sparkles`,
556 `squo`: `singlequote`,
557 `squote`: `singlequote`,
558 `sum`: `capitalsigma`,
559 `summation`: `capitalsigma`,
560 `upper`: `uppercase`,
561 `uppercasehex`: `hexadecimals`,
562 `uppergreek`: `uppercasegreek`,
563 `upperhex`: `hexadecimals`,
564 `vbar`: `verticalbar`,
565 `vellip`: `vellipsis`,
566 `verbar`: `verticalbar`,
567 `vertbar`: `verticalbar`,
568 `visascii`: `visibleascii`,
569 }
570
571 func showSymbols(w *bufio.Writer, names []string) error {
572 if len(names) == 0 {
573 return showAllSymbols(w)
574 }
575
576 for _, name := range names {
577 // normalize symbol names
578 name = strings.TrimSpace(name)
579 name = strings.ToLower(name)
580 name = strings.ReplaceAll(name, ` `, ``)
581 name = strings.ReplaceAll(name, `-`, ``)
582 name = strings.ReplaceAll(name, `_`, ``)
583
584 if len(name) == 0 {
585 return errors.New(`no symbol name given`)
586 }
587
588 // handle regular symbol-name lookups
589 s := name
590 if alias, ok := symbolAliases[name]; ok {
591 s = alias
592 }
593 sym, ok := names2symbols[s]
594 if !ok {
595 return errors.New(`no symbol named ` + name + ` found`)
596 }
597
598 w.WriteString(sym)
599 if w.WriteByte('\n') != nil {
600 return io.EOF
601 }
602 }
603
604 return nil
605 }
606
607 func showAllSymbols(w *bufio.Writer) error {
608 var keys []string
609 got := make(map[string]struct{}, len(names2symbols)+len(symbolAliases))
610
611 for k := range names2symbols {
612 got[k] = struct{}{}
613 keys = append(keys, k)
614 }
615
616 for k := range symbolAliases {
617 if _, ok := got[k]; ok {
618 continue
619 }
620 got[k] = struct{}{}
621 keys = append(keys, k)
622 }
623
624 sort.Strings(keys)
625
626 for _, s := range keys {
627 k := s
628 if alias, ok := symbolAliases[k]; ok {
629 k = alias
630 }
631
632 w.WriteString(s)
633 w.WriteByte('\t')
634 w.WriteString(names2symbols[k])
635 if w.WriteByte('\n') != nil {
636 return io.EOF
637 }
638 }
639
640 return nil
641 }
File: ./sym/tables_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package sym
26
27 import "testing"
28
29 func TestAliases(t *testing.T) {
30 for alias, name := range symbolAliases {
31 t.Run(alias, func(t *testing.T) {
32 const fs = `alias %q becomes %q, which has no associated symbol`
33 if _, ok := names2symbols[name]; !ok {
34 t.Errorf(fs, alias, name)
35 }
36 })
37 }
38 }
39
40 func TestUniqueness(t *testing.T) {
41 reverse := make(map[string][]string)
42 for k, v := range names2symbols {
43 reverse[v] = append(reverse[v], k)
44 }
45
46 for v, keys := range reverse {
47 t.Run(v, func(t *testing.T) {
48 if len(keys) > 1 {
49 t.Errorf(`value %q appears %d times`, v, len(keys))
50 }
51 })
52 }
53 }
File: ./tacl/tacl.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package tacl
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 "strings"
34 )
35
36 const info = `
37 tacl [options...] [file...]
38
39 TAC Lines emits text lines in backward-order, last ones first.
40
41 Unlike "tac" (reverse-order "cat"), TAC Lines ensures lines across inputs
42 are never joined by accident, when an input's last line doesn't end with a
43 line-feed.
44
45 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
46 feeds. Leading BOM (byte-order marks) on first lines are also ignored.
47
48 All (optional) leading options start with either single or double-dash:
49
50 -h, -help show this help message
51 -0, -null turn null-byte-delimited chunks into proper lines
52 `
53
54 type config struct {
55 lines []string
56 null bool
57 }
58
59 func Main() {
60 var cfg config
61 args := os.Args[1:]
62
63 for len(args) > 0 {
64 switch args[0] {
65 case `-0`, `--0`, `-null`, `--null`:
66 cfg.null = true
67 args = args[1:]
68 continue
69
70 case `-h`, `--h`, `-help`, `--help`:
71 os.Stdout.WriteString(info[1:])
72 return
73 }
74
75 break
76 }
77
78 if len(args) > 0 && args[0] == `--` {
79 args = args[1:]
80 }
81
82 if err := run(os.Stdout, args, &cfg); err != nil && err != io.EOF {
83 os.Stderr.WriteString(`tacl: `)
84 os.Stderr.WriteString(err.Error())
85 os.Stderr.WriteString("\n")
86 os.Exit(1)
87 return
88 }
89
90 w := bufio.NewWriterSize(os.Stdout, 32*1024)
91 defer w.Flush()
92
93 for i := len(cfg.lines) - 1; i >= 0; i-- {
94 w.WriteString(cfg.lines[i])
95 if w.WriteByte('\n') != nil {
96 break
97 }
98 }
99 }
100
101 func run(w io.Writer, args []string, cfg *config) error {
102 dashes := 0
103 for _, name := range args {
104 if name == `-` {
105 dashes++
106 }
107 if dashes > 1 {
108 return errors.New(`can't read stdin (dash) more than once`)
109 }
110 }
111
112 if len(args) == 0 {
113 return getLines(os.Stdin, cfg)
114 }
115
116 for _, name := range args {
117 if name == `-` {
118 if err := getLines(os.Stdin, cfg); err != nil {
119 return err
120 }
121 continue
122 }
123
124 if err := handleFile(name, cfg); err != nil {
125 return err
126 }
127 }
128 return nil
129 }
130
131 func handleFile(name string, cfg *config) error {
132 if name == `` || name == `-` {
133 return getLines(os.Stdin, cfg)
134 }
135
136 f, err := os.Open(name)
137 if err != nil {
138 return errors.New(`can't read from file named "` + name + `"`)
139 }
140 defer f.Close()
141
142 return getLines(f, cfg)
143 }
144
145 func getLines(r io.Reader, cfg *config) error {
146 const gb = 1024 * 1024 * 1024
147 sc := bufio.NewScanner(r)
148 sc.Buffer(nil, 8*gb)
149 if cfg.null {
150 sc.Split(splitNull)
151 }
152
153 for i := 0; sc.Scan(); i++ {
154 s := sc.Text()
155 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
156 s = s[3:]
157 }
158
159 cfg.lines = append(cfg.lines, sc.Text())
160 }
161
162 return sc.Err()
163 }
164
165 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
166 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
167 // handle leading null-terminated line, if found in the current chunk
168 if i := bytes.IndexByte(data, 0); i >= 0 {
169 return i + 1, data[:i], nil
170 }
171
172 // request more data, in case there's a null coming up later
173 if !atEOF {
174 return 0, nil, nil
175 }
176
177 // handle non-empty non-terminated last chunk
178 if len(data) > 0 {
179 return len(data), data, bufio.ErrFinalToken
180 }
181
182 // handle empty non-terminated last chunk
183 return 0, nil, bufio.ErrFinalToken
184 }
File: ./tail/tail.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 /*
26 Note
27
28 A previous attempt was trying to act more like the standard `tail` app
29 for efficiency, by going backwards on the list of files, reading chunks
30 backwards to reach the starting position in the right file for (up to)
31 the last n lines.
32
33 That attempt had an intermittent bug, and was scrapped in favor of the
34 naive/slow approach used here, forward-scanning lines and keeping them
35 into a ring-buffer.
36 */
37
38 package tail
39
40 import (
41 "bufio"
42 "io"
43 "os"
44 "strconv"
45 "strings"
46 )
47
48 const info = `
49 tail [options...] [files...]
50
51 Keep at most the last n lines, or keep the last 10 lines by default. When not
52 given any filepaths, the standard input is used instead.
53
54 Options
55
56 -n [number] change max number of lines (default is 10)
57 `
58
59 type ringBuffer struct {
60 next int
61 max int
62 items []string
63 }
64
65 func (rb *ringBuffer) append(s string) {
66 if rb.next < len(rb.items) {
67 rb.items[rb.next] = s
68 } else if len(rb.items) < rb.max {
69 rb.items = append(rb.items, s)
70 } else if len(rb.items) > 0 {
71 rb.items[0] = s
72 }
73
74 rb.next++
75 if rb.next >= rb.max {
76 rb.next = 0
77 }
78 }
79
80 func Main() {
81 var latest ringBuffer
82 latest.max = 10
83
84 args := os.Args[1:]
85 for len(args) > 0 {
86 switch args[0] {
87 case `-n`:
88 args = args[1:]
89 if len(args) == 0 {
90 os.Stderr.WriteString("missing number of lines\n")
91 os.Exit(1)
92 return
93 }
94
95 s := strings.Replace(args[0], `_`, ``, -1)
96 n, err := strconv.ParseInt(s, 10, 64)
97 if err != nil {
98 os.Stderr.WriteString(`tail: invalid number: `)
99 os.Stderr.WriteString(err.Error())
100 os.Stderr.WriteString("\n")
101 os.Exit(1)
102 return
103 }
104
105 args = args[1:]
106 latest.max = int(n)
107 continue
108
109 case `--help`:
110 os.Stderr.WriteString(info[1:])
111 return
112 }
113
114 break
115 }
116
117 if len(args) > 0 && args[0] == `--` {
118 args = args[1:]
119 }
120
121 if latest.max <= 0 {
122 return
123 }
124
125 if latest.max <= 1_000 {
126 latest.items = make([]string, 0, latest.max)
127 }
128
129 if err := run(args, &latest); err != nil {
130 os.Stderr.WriteString(`tail: `)
131 os.Stderr.WriteString(err.Error())
132 os.Stderr.WriteString("\n")
133 os.Exit(1)
134 return
135 }
136
137 show(os.Stdout, latest)
138 }
139
140 func run(paths []string, rb *ringBuffer) error {
141 for _, path := range paths {
142 if err := handleFile(path, rb); err != nil {
143 return err
144 }
145 }
146
147 if len(paths) == 0 {
148 if err := visit(os.Stdin, rb); err != nil {
149 return err
150 }
151 }
152 return nil
153 }
154
155 func show(w io.Writer, rb ringBuffer) {
156 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
157 defer bw.Flush()
158
159 for i := rb.next; i < len(rb.items); i++ {
160 bw.WriteString(rb.items[i])
161 if err := bw.WriteByte('\n'); err != nil {
162 return
163 }
164 }
165
166 for i := 0; i < len(rb.items) && i < rb.next; i++ {
167 bw.WriteString(rb.items[i])
168 if err := bw.WriteByte('\n'); err != nil {
169 return
170 }
171 }
172 }
173
174 func handleFile(path string, rb *ringBuffer) error {
175 f, err := os.Open(path)
176 if err != nil {
177 return err
178 }
179 defer f.Close()
180 return visit(f, rb)
181 }
182
183 func visit(r io.Reader, rb *ringBuffer) error {
184 const gb = 1024 * 1024 * 1024
185 sc := bufio.NewScanner(r)
186 sc.Buffer(nil, 8*gb)
187
188 for sc.Scan() {
189 rb.append(sc.Text())
190 }
191 return sc.Err()
192 }
File: ./tam/tam.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package tam
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "os/exec"
33 "time"
34 )
35
36 // unixInfo is the help message for the non-Windows versions of this tool
37 const unixInfo = `
38 tam [custom options...] [command] [arguments...]
39
40
41 Time And Memory (and exit) runs the command given, then shows a single line
42 with the following info on the standard error:
43
44 - total time elapsed
45 - total user-mode time spent overall
46 - total system/kernel time spent overall
47 - maximum actual memory used (RSS) in 1024-byte kilobytes
48 - the exit/status code, where 0 means success, and anything else error
49
50 The options are, available both in single and double-dash versions
51
52 -h, -help show this help message
53 -n, -name also show command name and arguments
54 `
55
56 func Main() {
57 full := false
58 buffered := false
59 args := os.Args[1:]
60
61 for len(args) > 0 {
62 switch args[0] {
63 case `-b`, `--b`, `-buffered`, `--buffered`:
64 buffered = true
65 args = args[1:]
66 continue
67
68 case `-f`, `--f`, `-full`, `--full`, `-n`, `--n`, `-name`, `--name`:
69 full = true
70 args = args[1:]
71 continue
72
73 case `-h`, `--h`, `-help`, `--help`:
74 os.Stdout.WriteString(info[1:])
75 return
76 }
77
78 break
79 }
80
81 if len(args) > 0 && args[0] == `--` {
82 args = args[1:]
83 }
84
85 if len(args) == 0 {
86 os.Stderr.WriteString(info[1:])
87 os.Exit(1)
88 return
89 }
90
91 liveLines := !buffered
92 if len(args) > 0 && !buffered {
93 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
94 liveLines = false
95 }
96 }
97
98 res, err := run(args, liveLines)
99 if _, ok := err.(*exec.ExitError); ok {
100 err = nil
101 }
102
103 if err != nil {
104 os.Stderr.WriteString(`tam: `)
105 os.Stderr.WriteString(err.Error())
106 os.Stderr.WriteString("\n")
107 if res.exitCode == 0 {
108 res.exitCode = 1
109 }
110 }
111
112 if full {
113 for i, s := range args {
114 if i > 0 {
115 os.Stderr.WriteString(` `)
116 }
117 os.Stderr.WriteString(s)
118 }
119 os.Stderr.WriteString(` `)
120 }
121 showResults(res)
122 os.Exit(res.exitCode)
123 return
124 }
125
126 func run(args []string, liveLines bool) (res results, err error) {
127 if len(args) == 0 {
128 return results{}, nil
129 }
130
131 start := time.Now()
132
133 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
134 defer bw.Flush()
135
136 cmd := exec.Command(args[0], args[1:]...)
137 cmd.Stdin = os.Stdin
138 if liveLines {
139 cmd.Stdout = lineFlusher{bw}
140 } else {
141 cmd.Stdout = bw
142 }
143 cmd.Stderr = os.Stderr
144
145 bw.Flush()
146
147 if err := cmd.Start(); err != nil {
148 return makeResults(cmd.ProcessState, start), err
149 }
150
151 if err := cmd.Wait(); err != nil {
152 return makeResults(cmd.ProcessState, start), err
153 }
154
155 bw.Flush()
156
157 return makeResults(cmd.ProcessState, start), err
158 }
159
160 type lineFlusher struct {
161 bw *bufio.Writer
162 }
163
164 func (lf lineFlusher) Write(p []byte) (n int, err error) {
165 n, err = lf.bw.Write(p)
166 if err != nil {
167 return n, err
168 }
169
170 if bytes.IndexByte(p, '\n') >= 0 {
171 err = lf.bw.Flush()
172 }
173
174 return n, err
175 }
File: ./tam/tam_unix.go
1 //go:build linux || freebsd || openbsd || netbsd || darwin
2 // +build linux freebsd openbsd netbsd darwin
3
4 /*
5 The MIT License (MIT)
6
7 Copyright (c) 2026 pacman64
8
9 Permission is hereby granted, free of charge, to any person obtaining a copy of
10 this software and associated documentation files (the "Software"), to deal
11 in the Software without restriction, including without limitation the rights to
12 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
13 of the Software, and to permit persons to whom the Software is furnished to do
14 so, subject to the following conditions:
15
16 The above copyright notice and this permission notice shall be included in all
17 copies or substantial portions of the Software.
18
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 SOFTWARE.
26 */
27
28 package tam
29
30 import (
31 "os"
32 "strconv"
33 "syscall"
34 "time"
35 )
36
37 const info = unixInfo
38
39 type results struct {
40 exitCode int
41 totalTime time.Duration
42 userTime time.Duration
43 systemTime time.Duration
44 maxMemory int
45 }
46
47 func makeResults(s *os.ProcessState, start time.Time) results {
48 if s == nil {
49 return results{exitCode: 127}
50 }
51
52 maxRSS := 0
53 if info, ok := s.SysUsage().(*syscall.Rusage); ok {
54 maxRSS = int(info.Maxrss)
55 }
56
57 return results{
58 exitCode: s.ExitCode(),
59 userTime: s.UserTime(),
60 systemTime: s.SystemTime(),
61 totalTime: time.Now().Sub(start),
62 maxMemory: maxRSS,
63 }
64 }
65
66 func showResults(res results) {
67 const gap = ` `
68 var buf [256]byte
69 s := buf[:0]
70
71 s = append(s, `real `...)
72 s = strconv.AppendFloat(s, res.totalTime.Seconds(), 'f', 2, 64)
73 s = append(s, gap...)
74 s = append(s, `user `...)
75 s = strconv.AppendFloat(s, res.userTime.Seconds(), 'f', 2, 64)
76 s = append(s, gap...)
77 s = append(s, `sys `...)
78 s = strconv.AppendFloat(s, res.systemTime.Seconds(), 'f', 2, 64)
79 s = append(s, gap...)
80 s = append(s, `mem `...)
81 s = strconv.AppendInt(s, int64(res.maxMemory), 10)
82 s = append(s, gap...)
83 s = append(s, `exit `...)
84 s = strconv.AppendInt(s, int64(res.exitCode), 10)
85 s = append(s, '\n')
86
87 os.Stderr.Write(s)
88 }
File: ./tam/tam_windows.go
1 //go:build windows
2 // +build windows
3
4 /*
5 The MIT License (MIT)
6
7 Copyright (c) 2026 pacman64
8
9 Permission is hereby granted, free of charge, to any person obtaining a copy of
10 this software and associated documentation files (the "Software"), to deal
11 in the Software without restriction, including without limitation the rights to
12 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
13 of the Software, and to permit persons to whom the Software is furnished to do
14 so, subject to the following conditions:
15
16 The above copyright notice and this permission notice shall be included in all
17 copies or substantial portions of the Software.
18
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 SOFTWARE.
26 */
27
28 package tam
29
30 import (
31 "os"
32 "strconv"
33 "time"
34 )
35
36 const info = `
37 tam [custom options...] [command] [arguments...]
38
39
40 Time And Memory (and exit) runs the command given, then shows a single line
41 with the following info on the standard error:
42
43 - total time elapsed
44 - total user-mode time spent overall
45 - total system/kernel time spent overall
46 - the exit/status code, where 0 means success, and anything else error
47
48 Despite its name, no info on the max-memory used is available on the Windows
49 version of this tool.
50
51 The options are, available both in single and double-dash versions
52
53 -h, -help show this help message
54 `
55
56 type results struct {
57 exitCode int
58 totalTime time.Duration
59 userTime time.Duration
60 systemTime time.Duration
61 }
62
63 func makeResults(s *os.ProcessState, start time.Time) results {
64 if s == nil {
65 return results{exitCode: 127}
66 }
67
68 return results{
69 exitCode: s.ExitCode(),
70 userTime: s.UserTime(),
71 systemTime: s.SystemTime(),
72 totalTime: time.Now().Sub(start),
73 }
74 }
75
76 func showResults(res results) {
77 const gap = ` `
78 var buf [256]byte
79 s := buf[:0]
80
81 s = append(s, `real `...)
82 s = strconv.AppendFloat(s, res.totalTime.Seconds(), 'f', 2, 64)
83 s = append(s, gap...)
84 s = append(s, `user `...)
85 s = strconv.AppendFloat(s, res.userTime.Seconds(), 'f', 2, 64)
86 s = append(s, gap...)
87 s = append(s, `sys `...)
88 s = strconv.AppendFloat(s, res.systemTime.Seconds(), 'f', 2, 64)
89 s = append(s, gap...)
90 s = append(s, `exit `...)
91 s = append(s, '\n')
92
93 os.Stderr.Write(s)
94 }
File: ./tcatl/tcatl.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package tcatl
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 "unicode/utf8"
34 )
35
36 const info = `
37 tcatl [options...] [file...]
38
39
40 Title and Concatenate lines emits lines from all the named sources given,
41 preceding each file's contents with its name, using an ANSI reverse style.
42
43 The name "-" stands for the standard input. When no names are given, the
44 standard input is used by default.
45
46 All (optional) leading options start with either single or double-dash:
47
48 -h, -help show this help message
49 -0, -null turn null-byte-delimited chunks into proper lines
50 `
51
52 type config struct {
53 null bool
54 liveLines bool
55 }
56
57 func Main() {
58 var cfg config
59 cfg.liveLines = true
60 args := os.Args[1:]
61
62 for len(args) > 0 {
63 switch args[0] {
64 case `-0`, `--0`, `-null`, `--null`:
65 cfg.null = true
66 args = args[1:]
67 continue
68
69 case `-b`, `--b`, `-buffered`, `--buffered`:
70 cfg.liveLines = false
71 args = args[1:]
72 continue
73
74 case `-h`, `--h`, `-help`, `--help`:
75 os.Stdout.WriteString(info[1:])
76 return
77 }
78
79 break
80 }
81
82 if len(args) > 0 && args[0] == `--` {
83 args = args[1:]
84 }
85
86 if cfg.liveLines {
87 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
88 cfg.liveLines = false
89 }
90 }
91
92 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
93 os.Stderr.WriteString(`tcatl: `)
94 os.Stderr.WriteString(err.Error())
95 os.Stderr.WriteString("\n")
96 os.Exit(1)
97 return
98 }
99 }
100
101 func run(w io.Writer, args []string, cfg config) error {
102 bw := bufio.NewWriterSize(w, 32*1024)
103 defer bw.Flush()
104
105 dashes := 0
106 for _, name := range args {
107 if name == `-` {
108 dashes++
109 }
110 if dashes > 1 {
111 break
112 }
113 }
114
115 if len(args) == 0 {
116 return tcatl(bw, os.Stdin, `<stdin>`, cfg)
117 }
118
119 var stdin []byte
120 gotStdin := false
121
122 for _, name := range args {
123 if name == `-` {
124 if dashes == 1 {
125 if err := tcatl(bw, os.Stdin, `<stdin>`, cfg); err != nil {
126 return err
127 }
128 continue
129 }
130
131 if !gotStdin {
132 data, err := io.ReadAll(os.Stdin)
133 if err != nil {
134 return err
135 }
136 stdin = data
137 gotStdin = true
138 }
139
140 bw.Write(stdin)
141 if len(stdin) > 0 && stdin[len(stdin)-1] != '\n' {
142 bw.WriteByte('\n')
143 }
144
145 if cfg.liveLines && bw.Flush() != nil {
146 return io.EOF
147 }
148
149 continue
150 }
151
152 if err := handleFile(bw, name, cfg); err != nil {
153 return err
154 }
155 }
156 return nil
157 }
158
159 func handleFile(w *bufio.Writer, name string, cfg config) error {
160 if name == `` || name == `-` {
161 return tcatl(w, os.Stdin, `<stdin>`, cfg)
162 }
163
164 f, err := os.Open(name)
165 if err != nil {
166 return errors.New(`can't read from file named "` + name + `"`)
167 }
168 defer f.Close()
169
170 return tcatl(w, f, name, cfg)
171 }
172
173 func tcatl(w *bufio.Writer, r io.Reader, name string, cfg config) error {
174 w.WriteString("\x1b[7m")
175 w.WriteString(name)
176 writeSpaces(w, 80-utf8.RuneCountInString(name))
177 w.WriteString("\x1b[0m\n")
178
179 if w.Flush() != nil {
180 // a write error may be the consequence of stdout being closed,
181 // perhaps by another app along a pipe
182 return io.EOF
183 }
184
185 if !cfg.liveLines {
186 return catlFast(w, r, cfg.null)
187 }
188
189 const gb = 1024 * 1024 * 1024
190 sc := bufio.NewScanner(r)
191 sc.Buffer(nil, 8*gb)
192 if cfg.null {
193 sc.Split(splitNull)
194 }
195
196 for i := 0; sc.Scan(); i++ {
197 s := sc.Bytes()
198 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
199 s = s[3:]
200 }
201
202 w.Write(s)
203 if w.WriteByte('\n') != nil {
204 return io.EOF
205 }
206
207 if w.Flush() != nil {
208 return io.EOF
209 }
210 }
211
212 return sc.Err()
213 }
214
215 func catlFast(w *bufio.Writer, r io.Reader, null bool) error {
216 var buf [32 * 1024]byte
217 var last byte = '\n'
218
219 for i := 0; true; i++ {
220 n, err := r.Read(buf[:])
221 if n > 0 && err == io.EOF {
222 err = nil
223 }
224 if err == io.EOF {
225 if last != '\n' {
226 w.WriteByte('\n')
227 }
228 return nil
229 }
230
231 if err != nil {
232 return err
233 }
234
235 chunk := buf[:n]
236 if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
237 chunk = chunk[3:]
238 }
239
240 // change nulls into line-feeds to handle null-terminated lines
241 if null {
242 for i, b := range chunk {
243 if b == 0 {
244 chunk[i] = '\n'
245 }
246 }
247 }
248
249 if len(chunk) >= 1 {
250 if _, err := w.Write(chunk); err != nil {
251 return io.EOF
252 }
253 last = chunk[len(chunk)-1]
254 }
255 }
256
257 return nil
258 }
259
260 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
261 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
262 // handle leading null-terminated line, if found in the current chunk
263 if i := bytes.IndexByte(data, 0); i >= 0 {
264 return i + 1, data[:i], nil
265 }
266
267 // request more data, in case there's a null coming up later
268 if !atEOF {
269 return 0, nil, nil
270 }
271
272 // handle non-empty non-terminated last chunk
273 if len(data) > 0 {
274 return len(data), data, bufio.ErrFinalToken
275 }
276
277 // handle empty non-terminated last chunk
278 return 0, nil, bufio.ErrFinalToken
279 }
280
281 // writeSpaces bulk-emits the number of spaces given
282 func writeSpaces(w *bufio.Writer, n int) {
283 const spaces = ` `
284 for ; n > len(spaces); n -= len(spaces) {
285 w.WriteString(spaces)
286 }
287 if n > 0 {
288 w.WriteString(spaces[:n])
289 }
290 }
File: ./teletype/teletype.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package teletype
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "os"
32 "strings"
33 "time"
34 "unicode/utf8"
35 )
36
37 const info = `
38 teletype [options...] [files...]
39
40 Simulate the cadence of old-fashioned teletype machines, by slowing down
41 the output of ASCII/UTF-8 symbols from the inputs given.
42
43 All (optional) leading options start with either single or double-dash:
44
45 -h, -help show this help message
46 `
47
48 func Main() {
49 args := os.Args[1:]
50
51 if len(args) > 0 {
52 switch args[0] {
53 case `-h`, `--h`, `-help`, `--help`:
54 os.Stdout.WriteString(info[1:])
55 return
56 }
57 }
58
59 if len(args) > 0 && args[0] == `--` {
60 args = args[1:]
61 }
62
63 if err := run(os.Stdout, args); err != nil && err != io.EOF {
64 os.Stderr.WriteString(`teletype: `)
65 os.Stderr.WriteString(err.Error())
66 os.Stderr.WriteString("\n")
67 os.Exit(1)
68 return
69 }
70 }
71
72 func run(w io.Writer, args []string) error {
73 dashes := 0
74 for _, name := range args {
75 if name == `-` {
76 dashes++
77 }
78 if dashes > 1 {
79 return errors.New(`can't read stdin (dash) more than once`)
80 }
81 }
82
83 if len(args) == 0 {
84 return teletype(w, os.Stdin)
85 }
86
87 for _, name := range args {
88 if name == `-` {
89 if err := teletype(w, os.Stdin); err != nil {
90 return err
91 }
92 continue
93 }
94
95 if err := handleFile(w, name); err != nil {
96 return err
97 }
98 }
99 return nil
100 }
101
102 func handleFile(w io.Writer, name string) error {
103 if name == `` || name == `-` {
104 return teletype(w, os.Stdin)
105 }
106
107 f, err := os.Open(name)
108 if err != nil {
109 return errors.New(`can't read from file named "` + name + `"`)
110 }
111 defer f.Close()
112
113 return teletype(w, f)
114 }
115
116 func teletype(w io.Writer, r io.Reader) error {
117 const gb = 1024 * 1024 * 1024
118 sc := bufio.NewScanner(r)
119 sc.Buffer(nil, 8*gb)
120
121 for i := 0; sc.Scan(); i++ {
122 s := sc.Text()
123 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
124 s = s[3:]
125 }
126
127 var buf [4]byte
128 for _, r := range s {
129 time.Sleep(15 * time.Millisecond)
130 if _, err := w.Write(utf8.AppendRune(buf[:0], r)); err != nil {
131 return io.EOF
132 }
133 }
134
135 time.Sleep(750 * time.Millisecond)
136 if _, err := w.Write([]byte{'\n'}); err != nil {
137 return io.EOF
138 }
139 }
140
141 return sc.Err()
142 }
File: ./timer/timer.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package timer
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "os/exec"
33 "os/signal"
34 "strconv"
35 "sync"
36 "time"
37 )
38
39 const (
40 // gap has the spaces between current timer value and current date/time
41 gap = ` `
42
43 // clear has enough spaces in it to cover any chronograph output
44 clear = "\r" + ` ` + "\r"
45
46 dateTimeFormat = `2006-01-02 15:04:05 Jan Mon`
47
48 forceQuitCode = 255
49 )
50
51 const (
52 // every = 100 * time.Millisecond
53 // chronoFormat = `15:04:05.0`
54
55 every = 1000 * time.Millisecond
56 chronoFormat = `15:04:05`
57 )
58
59 const info = `
60 timer [options...] [command...] [args...]
61
62
63 Run a live timer/chronograph on stderr, always showing below all lines
64 from stdin, which update stdout as they come.
65
66 When stdin (or the command run) is over, this app simply quits showing how
67 long it ran, by extending its last visible timer line on stderr.
68
69 You can also use this app without piping anything to its stdin or giving it
70 any command to run, acting as a simple live clock until you force-quit it,
71 via Control+C, or end the stdin stream via Control+D.
72
73 To ensure any stderr lines don't clash with the chronograph output line,
74 you can also run this app with arguments to have it run a command, thus
75 guaranteeing the subtask's stdout and stderr lines show without ever being
76 mixed up.
77
78 Note: by default this app assumes command output is plain-text lines, so a
79 command's non-empty binary stdout may be corrupted in subtle ways; use the
80 binary option to avoid that.
81
82 The options are, available both in single and double-dash versions
83
84 -b, -bin, -binary treat stdin or a command's stdout as binary data
85 -h, -help show this help message
86 `
87
88 func Main() {
89 bin := false
90 args := os.Args[1:]
91
92 if len(args) > 0 {
93 switch args[0] {
94 case `-h`, `--h`, `-help`, `--help`:
95 os.Stdout.WriteString(info[1:])
96 return
97
98 case `-b`, `--b`, `-bin`, `--bin`, `-binary`, `--binary`:
99 bin = true
100 args = args[1:]
101 }
102 }
103
104 if len(args) > 0 && args[0] == `--` {
105 args = args[1:]
106 }
107
108 if len(args) > 0 {
109 os.Exit(runTask(args[0], args[1:], bin))
110 }
111
112 err := chronograph(os.Stdin, nil, bin)
113 if exit, ok := err.(exit); ok {
114 os.Exit(exit.code)
115 }
116
117 if err != nil {
118 showError(err)
119 os.Exit(1)
120 }
121 }
122
123 // exit is a custom error-type which isn't for showing, but for quitting the
124 // app right away instead
125 type exit struct {
126 code int
127 }
128
129 // Error is only to satisfy the error interface, and not for showing
130 func (e exit) Error() string {
131 os.Exit(e.code)
132 return `quitting right away`
133 }
134
135 // runTask handles running the app in `subtask-mode`
136 func runTask(name string, args []string, bin bool) (exitCode int) {
137 cmd := exec.Command(name, args...)
138 cmd.Stdin = os.Stdin
139
140 stdout, err := cmd.StdoutPipe()
141 if err != nil {
142 showError(err)
143 return 1
144 }
145 defer stdout.Close()
146
147 stderr, err := cmd.StderrPipe()
148 if err != nil {
149 showError(err)
150 return 1
151 }
152 defer stderr.Close()
153
154 if err := cmd.Start(); err != nil {
155 showError(err)
156 return 1
157 }
158
159 err = chronograph(stdout, stderr, bin)
160 if exit, ok := err.(exit); ok {
161 return exit.code
162 }
163 if err != nil {
164 showError(err)
165 }
166
167 if err := cmd.Wait(); err != nil {
168 showError(err)
169 }
170 return cmd.ProcessState.ExitCode()
171 }
172
173 func showError(err error) {
174 if err != nil {
175 os.Stderr.WriteString(err.Error())
176 os.Stderr.WriteString("\n")
177 }
178 }
179
180 // chronograph runs a live chronograph, showing the time elapsed: 2 input
181 // sources for lines are handled concurrently, one destined for the app's
182 // stdout, the other for the app's stderr, without interfering with the
183 // chronograph lines, which also show on stderr
184 func chronograph(stdout io.Reader, stderr io.Reader, binaryOut bool) error {
185 start := time.Now()
186 t := time.NewTicker(every)
187 // s is a buffer used to minimize actual writes to final stdout/stderr
188 s := make([]byte, 0, 1024)
189
190 // start showing the timer right away
191 s = startChronoLine(s[:0], start, start)
192 os.Stderr.Write(s)
193
194 // stopped is a special event to handle force-quitting the app
195 stopped := make(chan os.Signal, 1)
196 defer close(stopped)
197 signal.Notify(stopped, os.Interrupt)
198
199 // errors will no longer travel when both input streams are over
200 errors := make(chan error)
201 var waitAllOutput sync.WaitGroup
202 waitAllOutput.Add(2)
203
204 // binary-mode only concerns stdin or stdout, and never stderr
205 readOutput := readLines
206 if binaryOut {
207 readOutput = readBytes
208 }
209
210 // relay stdout data asynchronously
211 outChunks := make(chan []byte)
212 go func() {
213 defer waitAllOutput.Done()
214 if stdout != nil {
215 errors <- readOutput(stdout, outChunks)
216 }
217 }()
218
219 // relay stderr data asynchronously
220 errLines := make(chan []byte)
221 go func() {
222 defer waitAllOutput.Done()
223 if stderr != nil {
224 errors <- readLines(stderr, errLines)
225 }
226 }()
227
228 // quit is a special event which happens when both input streams are over
229 quit := make(chan struct{})
230 defer close(quit)
231 go func() {
232 waitAllOutput.Wait()
233 close(errors)
234 quit <- struct{}{}
235 }()
236
237 for {
238 select {
239 case now := <-t.C:
240 s = append(s[:0], clear...)
241 s = startChronoLine(s, start, now)
242 os.Stderr.Write(s)
243
244 case chunk := <-outChunks:
245 if chunk == nil {
246 // filter out junk for needlessly-chatty pipes, while still
247 // keeping actual empty lines
248 continue
249 }
250
251 // write-order of the next 3 steps matters, to avoid mixing up
252 // lines since stdout and stderr lines can show up together, if
253 // not handled correctly
254
255 os.Stderr.WriteString(clear)
256
257 s = append(s[:0], chunk...)
258 // add an extra line-feed byte only when in plain-text mode
259 if !binaryOut {
260 s = append(s, '\n')
261 }
262
263 // assume (final) stdout write errors are due to a closed pipe,
264 // so quit this app successfully right away
265 if _, err := os.Stdout.Write(s); err != nil {
266 t.Stop()
267 s = startChronoLine(s[:0], start, time.Now())
268 s = endChronoLine(s, start)
269 os.Stderr.Write(s)
270 return nil
271 }
272
273 s = startChronoLine(s[:0], start, time.Now())
274 os.Stderr.Write(s)
275
276 case line := <-errLines:
277 if line == nil {
278 // filter out junk for needlessly-chatty pipes, while still
279 // keeping actual empty lines
280 continue
281 }
282
283 s = append(append(append(s[:0], clear...), line...), '\n')
284 s = startChronoLine(s, start, time.Now())
285 s = endChronoLine(s, start)
286 os.Stderr.Write(s)
287
288 case err := <-errors:
289 if err == nil {
290 continue
291 }
292 os.Stderr.WriteString(clear)
293 showError(err)
294 s = startChronoLine(s[:0], start, time.Now())
295 os.Stderr.Write(s)
296
297 case <-quit:
298 os.Stderr.Write(endChronoLine(s[:0], start))
299 return exit{0}
300
301 case <-stopped:
302 t.Stop()
303 os.Stderr.Write(endChronoLine(s[:0], start))
304 return exit{forceQuitCode}
305 }
306 }
307 }
308
309 // readBytes is the binary-mode alternative to func readLines for a command's
310 // standard output
311 func readBytes(r io.Reader, chunks chan<- []byte) error {
312 defer close(chunks)
313
314 if r == nil {
315 return nil
316 }
317
318 var buf [16 * 1024]byte
319
320 for {
321 n, err := r.Read(buf[:])
322
323 if err == io.EOF {
324 if n > 0 {
325 chunks <- buf[:n]
326 }
327 return nil
328 }
329
330 if err != nil {
331 return err
332 }
333
334 if n < 1 {
335 continue
336 }
337
338 chunks <- buf[:n]
339 }
340 }
341
342 // readLines is run twice asynchronously, so that both stdin and stderr lines
343 // are handled independently, which matters when running a subtask
344 func readLines(r io.Reader, lines chan<- []byte) error {
345 defer close(lines)
346
347 // when not handling a subtask, this func will be called with a nil
348 // reader, since without a subtask, there's no stderr to read from
349 if r == nil {
350 return nil
351 }
352
353 const gb = 1024 * 1024 * 1024
354 sc := bufio.NewScanner(r)
355 sc.Buffer(nil, 8*gb)
356
357 for i := 0; sc.Scan(); i++ {
358 s := sc.Bytes()
359 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
360 s = s[3:]
361 }
362 lines <- s
363 }
364 return sc.Err()
365 }
366
367 func startChronoLine(buf []byte, start, now time.Time) []byte {
368 dt := now.Sub(start)
369 buf = time.Time{}.Add(dt).AppendFormat(buf, chronoFormat)
370 buf = append(buf, gap...)
371 buf = now.AppendFormat(buf, dateTimeFormat)
372 return buf
373 }
374
375 func endChronoLine(buf []byte, start time.Time) []byte {
376 secs := time.Since(start).Seconds()
377 buf = append(buf, gap...)
378 buf = strconv.AppendFloat(buf, secs, 'f', 4, 64)
379 buf = append(buf, " seconds\n"...)
380 return buf
381 }
File: ./tinytools.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package main
26
27 import (
28 "bufio"
29 "fmt"
30 "math"
31 "os"
32 "sort"
33 "strconv"
34 "strings"
35 "time"
36 "unicode/utf8"
37
38 "./ngron"
39 "./now"
40 )
41
42 func args() {
43 args := os.Args[1:]
44 if len(args) == 0 {
45 return
46 }
47
48 w := bufio.NewWriterSize(os.Stdout, 32*1024)
49 defer w.Flush()
50
51 for _, s := range args {
52 w.WriteString(s)
53 if w.WriteByte('\n') != nil {
54 break
55 }
56 }
57 }
58
59 func clear() {
60 if len(os.Args) > 1 {
61 switch os.Args[1] {
62 case `-h`, `--h`, `-help`, `--help`, `help`:
63 os.Stdout.WriteString("Clear the screen\n")
64 return
65
66 case `-x`:
67 // clear all but the scrollback buffer
68 os.Stdout.WriteString("\x1b[H\x1b[2J")
69 return
70 }
71 }
72
73 os.Stdout.WriteString("\x1b[H\x1b[2J\x1b[3J")
74 }
75
76 func cls() {
77 if len(os.Args) > 1 {
78 switch os.Args[1] {
79 case `-h`, `--h`, `-help`, `--help`, `help`:
80 os.Stdout.WriteString("Clear the Screen\n")
81 return
82 }
83 }
84
85 os.Stdout.WriteString("\x1b[H\x1b[2J\x1b[3J")
86 }
87
88 const divInfo = `
89 div [options...] [x...] [y]
90
91 DIVide 2 numbers in 3 different ways, showing each result in its own line.
92
93 When given 2 numbers, the results are x / y, y / x, and 1 - (x / y), where
94 x is the smaller number, and y is the larger number.
95
96 When given just 1 number, the second number is 1 by default, showing you the
97 inverse of the number given explicitly, among the other results.
98
99 The options are, available both in single and double-dash versions
100
101 -h, -help show this help message
102 `
103
104 func div() {
105 args := os.Args[1:]
106
107 if len(args) > 0 {
108 switch args[0] {
109 case `-h`, `--h`, `-help`, `--help`:
110 os.Stdout.WriteString(divInfo[1:])
111 return
112 }
113 }
114
115 if len(args) > 0 && args[0] == `--` {
116 args = args[1:]
117 }
118
119 var nums []float64
120
121 for len(args) > 0 {
122 f, err := strconv.ParseFloat(args[0], 64)
123 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
124 nums = append(nums, f)
125 args = args[1:]
126 continue
127 }
128
129 break
130 }
131
132 switch len(nums) {
133 case 0:
134 os.Stderr.WriteString(divInfo[1:])
135 os.Exit(1)
136 return
137
138 case 1:
139 nums = append(nums, nums[0])
140 nums[0] = 1
141 }
142
143 if len(nums)%2 != 0 {
144 os.Stderr.WriteString(divInfo[1:])
145 os.Exit(1)
146 return
147 }
148
149 for len(nums) >= 2 {
150 x, y := nums[0], nums[1]
151 nums = nums[2:]
152
153 if x > y {
154 x, y = y, x
155 }
156 comp := 1 - (x / y)
157
158 const prec = 6
159 os.Stdout.WriteString(strconv.FormatFloat(x/y, 'f', prec, 64) + "\n")
160 os.Stdout.WriteString(strconv.FormatFloat(y/x, 'f', prec, 64) + "\n")
161 os.Stdout.WriteString(strconv.FormatFloat(comp, 'f', prec, 64) + "\n")
162 }
163 }
164
165 func echo() {
166 args := os.Args[1:]
167 if len(args) == 0 {
168 os.Stdout.WriteString("\n")
169 return
170 }
171
172 w := bufio.NewWriterSize(os.Stdout, 32*1024)
173 defer w.Flush()
174
175 for i, s := range args {
176 if i > 0 {
177 if w.WriteByte(' ') != nil {
178 return
179 }
180 }
181 w.WriteString(s)
182 }
183
184 w.WriteByte('\n')
185 }
186
187 func echobar() {
188 const (
189 half = ` `
190 spaces = half + half
191 )
192
193 args := os.Args[1:]
194 if len(args) == 0 {
195 os.Stdout.WriteString("\x1b[7m" + spaces + "\x1b[0m\n")
196 return
197 }
198
199 w := bufio.NewWriterSize(os.Stdout, 32*1024)
200 defer w.Flush()
201
202 w.WriteString("\x1b[7m")
203
204 left := len(spaces)
205
206 for i, s := range args {
207 if i > 0 {
208 if w.WriteByte(' ') != nil {
209 return
210 }
211 left--
212 }
213 w.WriteString(s)
214 left -= utf8.RuneCountInString(s)
215 }
216
217 if 0 < left && left < len(spaces) {
218 w.WriteString(spaces[:left])
219 }
220 w.WriteString("\x1b[0m\n")
221 }
222
223 const falseInfo = `
224 false [options...]
225
226 Quit right away, using exit code 1.
227
228 All (optional) leading options start with either single or double-dash:
229
230 -h, -help show this help message
231 `
232
233 func falseMain() {
234 if len(os.Args) > 1 {
235 switch os.Args[1] {
236 case `-h`, `--h`, `-help`, `--help`, `help`:
237 os.Stdout.WriteString(falseInfo[1:])
238 return
239 }
240 }
241
242 os.Exit(1)
243 return
244 }
245
246 func hecho() {
247 args := os.Args[1:]
248 if len(args) == 0 {
249 os.Stdout.WriteString("\n")
250 return
251 }
252
253 w := bufio.NewWriterSize(os.Stdout, 32*1024)
254 defer w.Flush()
255
256 w.WriteString("\x1b[7m")
257
258 for i, s := range args {
259 if i > 0 {
260 if err := w.WriteByte(' '); err != nil {
261 return
262 }
263 }
264 w.WriteString(s)
265 }
266
267 w.WriteString("\x1b[0m\n")
268 }
269
270 const mopInfo = `
271 mop [options...] [x...] [y]
272
273 Multiple OPerations runs multiple arithmetic calculations on 2 numbers,
274 showing each result in its own line.
275
276 The options are, available both in single and double-dash versions
277
278 -h, -help show this help message
279 `
280
281 func mop() {
282 args := os.Args[1:]
283
284 if len(args) > 0 {
285 switch args[0] {
286 case `-h`, `--h`, `-help`, `--help`:
287 os.Stdout.WriteString(mopInfo[1:])
288 return
289 }
290 }
291
292 if len(args) > 0 && args[0] == `--` {
293 args = args[1:]
294 }
295
296 var nums []float64
297
298 for len(args) > 0 {
299 f, err := strconv.ParseFloat(args[0], 64)
300 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
301 nums = append(nums, f)
302 args = args[1:]
303 continue
304 }
305
306 break
307 }
308
309 if len(nums) == 0 || len(nums)%2 != 0 {
310 os.Stderr.WriteString(mopInfo[1:])
311 os.Exit(1)
312 return
313 }
314
315 for len(nums) >= 2 {
316 x, y := nums[0], nums[1]
317 nums = nums[2:]
318
319 if x > y {
320 x, y = y, x
321 }
322
323 fmt.Printf("%.6f + %.6f ~ %.6f\n", x, y, x+y)
324 fmt.Printf("%.6f - %.6f ~ %.6f\n", x, y, x-y)
325 fmt.Printf("%.6f * %.6f ~ %.6f\n", x, y, x*y)
326 fmt.Printf("%.6f / %.6f ~ %.6f\n", x, y, x/y)
327 fmt.Printf("%.6f / %.6f ~ %.6f\n", y, x, y/x)
328 fmt.Printf("%.6f %% %.6f ~ %.6f\n", x, y, math.Mod(x, y))
329 fmt.Printf("%.6f %% %.6f ~ %.6f\n", y, x, math.Mod(y, x))
330 fmt.Printf("%.6f ** %.6f ~ %.6f\n", x, y, math.Pow(x, y))
331 fmt.Printf("%.6f ** %.6f ~ %.6f\n", y, x, math.Pow(y, x))
332 }
333 }
334
335 func nothing() {
336 // deliberately do nothing
337 }
338
339 const rulerInfo = `
340 ruler [width...]
341
342 Emit a line with a ruler-like pattern, which helps check the width of output
343 lines right above it. If given a valid number, it's used as the ruler width,
344 or 80 is used as the default width.
345
346 The options are, available both in single and double-dash versions
347
348 -h, -help show this help message
349 `
350
351 func ruler() {
352 args := os.Args[1:]
353
354 if len(args) > 0 {
355 switch args[0] {
356 case `-h`, `--h`, `-help`, `--help`, `help`:
357 os.Stdout.WriteString(rulerInfo[1:])
358 return
359 }
360 }
361
362 if len(args) > 0 && args[0] == `--` {
363 args = args[1:]
364 }
365
366 width := 80
367 if len(args) > 0 {
368 if n, err := strconv.Atoi(args[0]); err == nil {
369 width = n
370 }
371 }
372
373 // avoid a single line-feed byte for empty rulers
374 if width < 1 {
375 return
376 }
377
378 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
379 defer bw.Flush()
380
381 for width >= 10 {
382 if _, err := bw.WriteString(`····╵····│`); err != nil {
383 return
384 }
385 width -= 10
386 }
387
388 if width >= 5 {
389 bw.WriteString(`····╵`)
390 width -= 5
391 }
392
393 for width > 0 {
394 bw.WriteRune('·')
395 width--
396 }
397
398 bw.WriteByte('\n')
399 }
400
401 const sleepInfo = `
402 sleep [options...] [duration/seconds...]
403
404 Wait for the amount of time given.
405
406 All (optional) leading options start with either single or double-dash:
407
408 -h, -help show this help message
409 `
410
411 func sleep() {
412 args := os.Args[1:]
413
414 if len(args) > 0 {
415 switch args[0] {
416 case `-h`, `--h`, `-help`, `--help`, `help`:
417 os.Stdout.WriteString(sleepInfo[1:])
418 return
419 }
420 }
421
422 if len(args) > 0 && args[0] == `--` {
423 args = args[1:]
424 }
425
426 if len(args) == 0 {
427 os.Stderr.WriteString(sleepInfo[1:])
428 // os.Stderr.WriteString("forgot the duration to wait for\n")
429 os.Exit(1)
430 return
431 }
432
433 var delay time.Duration
434
435 for _, s := range args {
436 f, err := strconv.ParseFloat(s, 64)
437 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
438 if f < 0 {
439 f = 0
440 }
441 delay += time.Duration(f * float64(time.Second))
442 continue
443 }
444
445 if d, err := time.ParseDuration(s); err == nil {
446 delay += d
447 continue
448 }
449
450 os.Stderr.WriteString("invalid duration " + s)
451 os.Stderr.WriteString("\n")
452 os.Exit(1)
453 return
454 }
455
456 time.Sleep(delay)
457 }
458
459 const timezonesInfo = `
460 timezones [options...] [places...]
461
462 Lookup full timezone names from the city/place names given.
463
464 All (optional) leading options start with either single or double-dash:
465
466 -h, -help show this help message
467 `
468
469 func timezones() {
470 args := os.Args[1:]
471
472 if len(args) > 0 {
473 switch args[0] {
474 case `-h`, `--h`, `-help`, `--help`, `help`:
475 os.Stdout.WriteString(timezonesInfo[1:])
476 return
477 }
478 }
479
480 if len(args) > 0 && args[0] == `--` {
481 args = args[1:]
482 }
483
484 if len(args) == 0 {
485 os.Stderr.WriteString(timezonesInfo[1:])
486 return
487 }
488
489 nerr := 0
490 w := bufio.NewWriterSize(os.Stdout, 32*1024)
491 defer w.Flush()
492
493 for _, place := range args {
494 name, ok := now.LookupName(place)
495
496 if !ok {
497 w.Flush()
498 fmt.Fprintf(os.Stderr, "timezone for %q not found\n", place)
499 nerr++
500 continue
501 }
502
503 w.WriteString(name)
504 w.WriteByte('\n')
505 }
506
507 if nerr > 0 {
508 w.Flush()
509 os.Exit(1)
510 return
511 }
512 }
513
514 const toolsInfo = `
515 tools [options...]
516
517 Show all tools (officially) available.
518
519 All (optional) leading options start with either single or double-dash:
520
521 -a, -aliases, -all also show all aliases available, after the tools
522 -h, -help show this help message
523 `
524
525 func tools() {
526 showAliases := false
527 if len(os.Args) > 1 {
528 switch os.Args[1] {
529 case `-a`, `--a`, `-aliases`, `--aliases`, `-all`, `--all`:
530 showAliases = true
531
532 case `-h`, `--h`, `-help`, `--help`, `help`:
533 os.Stdout.WriteString(toolsInfo[1:])
534 return
535 }
536 }
537
538 n := len(mains)
539 if n < len(aliases) {
540 n = len(aliases)
541 }
542
543 names := make([]string, 0, n)
544 for k := range mains {
545 names = append(names, k)
546 }
547
548 sort.Strings(names)
549
550 for _, s := range names {
551 fmt.Fprintln(os.Stdout, s)
552 }
553
554 if !showAliases {
555 return
556 }
557
558 names = names[:0]
559 for k := range aliases {
560 names = append(names, k)
561 }
562
563 sort.Strings(names)
564
565 for _, s := range names {
566 fmt.Fprintf(os.Stdout, "%s -> %s\n", s, aliases[s])
567 }
568 }
569
570 const trueInfo = `
571 true [options...]
572
573 Quit right away successfully, using exit code 0.
574
575 All (optional) leading options start with either single or double-dash:
576
577 -h, -help show this help message
578 `
579
580 func trueMain() {
581 if len(os.Args) > 1 {
582 switch os.Args[1] {
583 case `-h`, `--h`, `-help`, `--help`, `help`:
584 os.Stdout.WriteString(trueInfo[1:])
585 return
586 }
587 }
588 }
589
590 const ungronInfo = `
591 ungron [options...]
592
593 Turn GRON-style lines into valid JSON. This is the same as directly running
594 the 'ngron' tool with the '-u' option.
595
596 All (optional) leading options start with either single or double-dash:
597
598 -h, -help show this help message
599 `
600
601 func ungronMain() {
602 args := os.Args[1:]
603
604 for _, s := range args {
605 switch s {
606 case `-h`, `--h`, `-help`, `--help`, `help`:
607 os.Stdout.WriteString(ungronInfo[1:])
608 return
609 }
610 }
611
612 gronArgs := make([]string, 0, len(os.Args)+1)
613 gronArgs = append(gronArgs, os.Args[0])
614 gronArgs = append(gronArgs, `-u`)
615 gronArgs = append(gronArgs, args...)
616 os.Args = gronArgs
617 ngron.Main()
618 }
619
620 const yesInfo = `
621 yes [options...] [message...]
622
623 Keep emitting the line with the message given, or "y" (short for "yes") by
624 default.
625
626 All (optional) leading options start with either single or double-dash:
627
628 -h, -help show this help message
629 `
630
631 func yes() {
632 args := os.Args[1:]
633
634 if len(args) > 0 {
635 switch args[0] {
636 case `-h`, `--h`, `-help`, `--help`, `help`:
637 os.Stdout.WriteString(yesInfo[1:])
638 return
639 }
640 }
641
642 if len(args) > 0 && args[0] == `--` {
643 args = args[1:]
644 }
645
646 msg := `y`
647 if len(args) > 0 {
648 msg = strings.Join(args, ` `)
649 }
650
651 w := bufio.NewWriterSize(os.Stdout, 32*1024)
652 defer w.Flush()
653
654 for {
655 w.WriteString(msg)
656 if w.WriteByte('\n') != nil {
657 break
658 }
659 }
660 }
File: ./tolower/tolower.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package tolower
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 "unicode"
34 "unicode/utf8"
35 )
36
37 const info = `
38 tolower [options...] [files...]
39
40 Turn all uppercase letters into lowercase ones.
41
42 All (optional) leading options start with either single or double-dash:
43
44 -h, -help show this help message
45 `
46
47 func Main() {
48 buffered := false
49 args := os.Args[1:]
50
51 if len(args) > 0 {
52 switch args[0] {
53 case `-b`, `--b`, `-buffered`, `--buffered`:
54 buffered = true
55 args = args[1:]
56
57 case `-h`, `--h`, `-help`, `--help`:
58 os.Stdout.WriteString(info[1:])
59 return
60 }
61 }
62
63 if len(args) > 0 && args[0] == `--` {
64 args = args[1:]
65 }
66
67 liveLines := !buffered
68 if !buffered {
69 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
70 liveLines = false
71 }
72 }
73
74 if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
75 os.Stderr.WriteString(`tolower: `)
76 os.Stderr.WriteString(err.Error())
77 os.Stderr.WriteString("\n")
78 os.Exit(1)
79 return
80 }
81 }
82
83 func run(w io.Writer, args []string, live bool) error {
84 bw := bufio.NewWriterSize(w, 32*1024)
85 defer bw.Flush()
86
87 if len(args) == 0 {
88 return toLower(bw, os.Stdin, live)
89 }
90
91 for _, name := range args {
92 if err := handleFile(bw, name, live); err != nil {
93 return err
94 }
95 }
96 return nil
97 }
98
99 func handleFile(w *bufio.Writer, name string, live bool) error {
100 if name == `` || name == `-` {
101 return toLower(w, os.Stdin, live)
102 }
103
104 f, err := os.Open(name)
105 if err != nil {
106 return errors.New(`can't read from file named "` + name + `"`)
107 }
108 defer f.Close()
109
110 return toLower(w, f, live)
111 }
112
113 func toLower(w *bufio.Writer, r io.Reader, live bool) error {
114 const gb = 1024 * 1024 * 1024
115 sc := bufio.NewScanner(r)
116 sc.Buffer(nil, 8*gb)
117
118 for i := 0; sc.Scan(); i++ {
119 s := sc.Bytes()
120 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
121 s = s[3:]
122 }
123
124 if needsLowercasing(s) {
125 writeLowercase(w, s)
126 } else {
127 w.Write(s)
128 }
129
130 if w.WriteByte('\n') != nil {
131 return io.EOF
132 }
133
134 if live && w.Flush() != nil {
135 return io.EOF
136 }
137 }
138
139 return sc.Err()
140 }
141
142 func needsLowercasing(src []byte) bool {
143 for _, b := range src {
144 if b > 127 {
145 return true
146 }
147 if 'A' <= b && b <= 'Z' {
148 return true
149 }
150 }
151
152 return false
153 }
154
155 func writeLowercase(w *bufio.Writer, src []byte) {
156 for len(src) > 0 {
157 r, size := utf8.DecodeRune(src)
158 w.WriteRune(unicode.ToLower(r))
159 src = src[size:]
160 }
161 }
File: ./transpose/transpose.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package transpose
26
27 import (
28 "bufio"
29 "io"
30 "os"
31 "strings"
32 )
33
34 const info = `
35 transpose [options...] [files...]
36
37 Switch rows and columns from tables. Output is always TSV (tab-separated
38 values) lines. Input lines are auto-detected as either space(s)-separated
39 fields or TSV by default, but there's an option to force splitting by tabs,
40 should TSV-auto-detection fail for some inputs.
41
42 All (optional) leading options start with either single or double-dash:
43
44 -h, -help show this help message
45 -t, -tsv split lines by tabs, no matter the first input line
46 `
47
48 func Main() {
49 tsv := false
50 args := os.Args[1:]
51
52 if len(args) > 0 {
53 switch args[0] {
54 case `-h`, `--h`, `-help`, `--help`:
55 os.Stdout.WriteString(info[1:])
56 return
57
58 case `-t`, `--t`, `-tsv`, `--tsv`:
59 tsv = true
60 args = args[1:]
61 }
62 }
63
64 if len(args) > 0 && args[0] == `--` {
65 args = args[1:]
66 }
67
68 if err := run(args, tsv); err != nil && err != io.EOF {
69 os.Stderr.WriteString(`transpose: `)
70 os.Stderr.WriteString(err.Error())
71 os.Stderr.WriteString("\n")
72 os.Exit(1)
73 return
74 }
75 }
76
77 type table struct {
78 // maxWidth is the highest item-count in any row read
79 maxWidth int
80
81 // rows remembers all split items for later transposition
82 rows [][]string
83
84 // split is the func to split items from input lines
85 split func(s string) []string
86 }
87
88 func run(paths []string, tsv bool) error {
89 var t table
90 if tsv {
91 t.split = splitTSV
92 }
93
94 // gather items
95
96 for _, path := range paths {
97 if err := handleFile(path, &t); err != nil {
98 return err
99 }
100 }
101
102 if len(paths) == 0 {
103 if err := handleReader(os.Stdin, &t); err != nil {
104 return err
105 }
106 }
107
108 // show transpose
109
110 w := bufio.NewWriterSize(os.Stdout, 32*1024)
111 defer w.Flush()
112
113 for i := 0; i < t.maxWidth; i++ {
114 for j, row := range t.rows {
115 if j > 0 {
116 w.WriteByte('\t')
117 }
118 if i < len(row) {
119 w.WriteString(row[i])
120 }
121 }
122
123 if w.WriteByte('\n') != nil {
124 return io.EOF
125 }
126 }
127
128 return nil
129 }
130
131 func handleFile(path string, t *table) error {
132 f, err := os.Open(path)
133 if err != nil {
134 return err
135 }
136 defer f.Close()
137 return handleReader(f, t)
138 }
139
140 func handleReader(r io.Reader, t *table) error {
141 const gb = 1024 * 1024 * 1024
142 sc := bufio.NewScanner(r)
143 sc.Buffer(nil, 8*gb)
144
145 for i := 0; sc.Scan(); i++ {
146 s := sc.Text()
147 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
148 s = s[3:]
149 }
150
151 if t.split == nil {
152 if strings.IndexByte(s, '\t') >= 0 {
153 t.split = splitTSV
154 } else {
155 t.split = strings.Fields
156 }
157 }
158
159 items := t.split(s)
160 if t.maxWidth < len(items) {
161 t.maxWidth = len(items)
162 }
163 t.rows = append(t.rows, items)
164 }
165
166 return sc.Err()
167 }
168
169 func splitTSV(s string) []string {
170 if len(s) == 0 {
171 return nil
172 }
173
174 n := count(s, '\t') + 1
175 tsv := make([]string, 0, n)
176
177 for len(s) > 0 {
178 i := strings.IndexByte(s, '\t')
179 if i < 0 {
180 tsv = append(tsv, s)
181 break
182 }
183 tsv = append(tsv, s[:i])
184 s = s[i+1:]
185 }
186
187 return tsv
188 }
189
190 func count(src string, what byte) int {
191 got := 0
192 for i := range src {
193 if src[i] == what {
194 got++
195 }
196 }
197 return got
198 }
File: ./underline/underline.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package underline
26
27 import (
28 "bufio"
29 "bytes"
30 "errors"
31 "io"
32 "os"
33 "strconv"
34 )
35
36 const info = `
37 underline [options...] [every...] [files...]
38
39 Underline every nth line, or every 5th line by default.
40
41 All (optional) leading options start with either single or double-dash:
42
43 -h, -help show this help message
44 -header, -t, -top start by underlining the first line instead
45 `
46
47 type config struct {
48 n int
49 every int
50 top bool
51 liveLines bool
52 }
53
54 func Main() {
55 var cfg config
56 cfg.every = 5
57 cfg.liveLines = true
58 args := os.Args[1:]
59
60 for len(args) > 0 {
61 switch args[0] {
62 case `-b`, `--b`, `-buffered`, `--buffered`:
63 cfg.liveLines = false
64 args = args[1:]
65 continue
66
67 case `-h`, `--h`, `-help`, `--help`:
68 os.Stdout.WriteString(info[1:])
69 return
70
71 case `-header`, `--header`, `-t`, `--t`, `-top`, `--top`:
72 cfg.top = true
73 args = args[1:]
74 continue
75 }
76
77 break
78 }
79
80 if len(args) > 0 {
81 if n, err := strconv.ParseUint(args[0], 10, 64); err == nil {
82 cfg.every = int(n)
83 args = args[1:]
84 }
85 }
86
87 if len(args) > 0 && args[0] == `--` {
88 args = args[1:]
89 }
90
91 if cfg.liveLines {
92 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
93 cfg.liveLines = false
94 }
95 }
96
97 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
98 os.Stderr.WriteString(`underline: `)
99 os.Stderr.WriteString(err.Error())
100 os.Stderr.WriteString("\n")
101 os.Exit(1)
102 return
103 }
104 }
105
106 func run(w io.Writer, args []string, cfg config) error {
107 bw := bufio.NewWriterSize(w, 32*1024)
108 defer bw.Flush()
109
110 dashes := 0
111 for _, name := range args {
112 if name == `-` {
113 dashes++
114 }
115 if dashes > 1 {
116 return errors.New(`can't read stdin (dash) more than once`)
117 }
118 }
119
120 if len(args) == 0 {
121 return handleReader(bw, os.Stdin, &cfg)
122 }
123
124 for _, name := range args {
125 if name == `-` {
126 if err := handleReader(bw, os.Stdin, &cfg); err != nil {
127 return err
128 }
129 continue
130 }
131
132 if err := handleFile(bw, name, &cfg); err != nil {
133 return err
134 }
135 }
136 return nil
137 }
138
139 func handleFile(w *bufio.Writer, name string, cfg *config) error {
140 if name == `` || name == `-` {
141 return handleReader(w, os.Stdin, cfg)
142 }
143
144 f, err := os.Open(name)
145 if err != nil {
146 return errors.New(`can't read from file named "` + name + `"`)
147 }
148 defer f.Close()
149
150 return handleReader(w, f, cfg)
151 }
152
153 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
154 const gb = 1024 * 1024 * 1024
155 sc := bufio.NewScanner(r)
156 sc.Buffer(nil, 8*gb)
157
158 every := cfg.every
159
160 for i := 0; sc.Scan(); i++ {
161 s := sc.Bytes()
162 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
163 s = s[3:]
164 }
165
166 cfg.n++
167 var u bool
168 if cfg.top {
169 u = every > 0 && (cfg.n == 1 || ((cfg.n-1)%every == 0))
170 } else {
171 u = (every > 0 && cfg.n%every == 0 && cfg.n != 1) || every == 1
172 }
173
174 if u {
175 if underline(w, s) != nil {
176 return io.EOF
177 }
178 } else {
179 w.Write(s)
180 if w.WriteByte('\n') != nil {
181 return io.EOF
182 }
183 }
184
185 if cfg.liveLines && w.Flush() != nil {
186 return io.EOF
187 }
188 }
189
190 return sc.Err()
191 }
192
193 func underline(w *bufio.Writer, s []byte) error {
194 w.WriteString("\x1b[4m")
195 for len(s) > 0 {
196 i := bytes.Index(s, []byte("\x1b[0m"))
197 if i < 0 {
198 break
199 }
200
201 j := i + len("\x1b[0m")
202 w.Write(s[:j])
203 w.WriteString("\x1b[4m")
204 s = s[j:]
205 }
206
207 if len(s) > 0 {
208 w.Write(s)
209 }
210
211 if _, err := w.WriteString("\x1b[0m\n"); err != nil {
212 return io.EOF
213 }
214 return nil
215 }
File: ./units/units.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package units
26
27 import (
28 "bufio"
29 "fmt"
30 "io"
31 "math"
32 "os"
33 "sort"
34 "strconv"
35 "strings"
36 )
37
38 const info = `
39 units [options...] [quantities / source units...]
40
41 Convert quantities from weird units into equivalent better ones, usually from
42 the international systems of measurements: think kilometers instead of miles.
43
44 All (optional) leading options start with either single or double-dash:
45
46 -h, -help show this help message
47 `
48
49 func Main() {
50 args := os.Args[1:]
51 if len(args) > 0 {
52 switch args[0] {
53 case `-h`, `--h`, `-help`, `--help`:
54 os.Stdout.WriteString(info[1:])
55 return
56 }
57 }
58
59 if len(args) > 0 && args[0] == `--` {
60 args = args[1:]
61 }
62
63 if err := run(os.Stdout, args); err != nil {
64 os.Stderr.WriteString(`units: `)
65 os.Stderr.WriteString(err.Error())
66 os.Stderr.WriteString("\n")
67 os.Exit(1)
68 return
69 }
70 }
71
72 func run(w io.Writer, args []string) error {
73 bw := bufio.NewWriterSize(w, 32*1024)
74 defer bw.Flush()
75
76 if len(args) == 0 {
77 dumpAll(bw, []float64{1})
78 return nil
79 }
80
81 from := ``
82 var values []float64
83
84 for _, s := range args {
85 f, err := strconv.ParseFloat(s, 64)
86 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
87 values = append(values, f)
88 continue
89 }
90
91 if len(values) == 0 {
92 values = append(values, 1)
93 }
94 from = strings.ToLower(s)
95
96 if dump(bw, values, from) {
97 values = values[:0]
98 from = ``
99 continue
100 }
101
102 return fmt.Errorf("unit %q not supported", from)
103 }
104
105 if len(values) > 0 {
106 dumpAll(bw, values)
107 return nil
108 }
109
110 if from != `` && !dump(bw, values, from) {
111 return fmt.Errorf("unit %q not supported", from)
112 }
113 return nil
114 }
115
116 func dump(w *bufio.Writer, values []float64, unit string) bool {
117 if s, ok := aliases[unit]; ok {
118 unit = s
119 }
120
121 c, ok := mainConverters[unit]
122 if !ok {
123 c, ok = otherConverters[unit]
124 if !ok {
125 return false
126 }
127 }
128
129 const fs = "%.4f %-4s = %.4f %s\n"
130
131 if len(values) == 0 {
132 res := c.Mul + c.Add
133 fmt.Fprintf(w, fs, 1.0, unit, res, c.To)
134 return true
135 }
136
137 for _, v := range values {
138 res := c.Mul*v + c.Add
139 fmt.Fprintf(w, fs, v, unit, res, c.To)
140 }
141 return true
142 }
143
144 func dumpAll(w *bufio.Writer, values []float64) bool {
145 if len(values) == 0 {
146 values = []float64{1}
147 }
148
149 units := make([]string, 0, len(mainConverters))
150 for k := range mainConverters {
151 units = append(units, k)
152 }
153 sort.Strings(units)
154
155 for _, from := range units {
156 dump(w, values, from)
157 }
158 return true
159 }
160
161 var aliases = map[string]string{
162 `acre`: `ac`,
163 `acres`: `ac`,
164 `days`: `day`,
165 `foot`: `ft`,
166 `feet`: `ft`,
167 `feet2`: `ft²`,
168 `feet3`: `ft³`,
169 `foot2`: `ft²`,
170 `foot3`: `ft³`,
171 `ft2`: `ft²`,
172 `ft3`: `ft³`,
173 `gallon`: `gal`,
174 `gallons`: `gal`,
175 `gals`: `gal`,
176 `inch`: `in`,
177 `inches`: `in`,
178 `mile`: `mi`,
179 `miles`: `mi`,
180 `mile²`: `mi²`,
181 `miles²`: `mi²`,
182 `minute`: `min`,
183 `minutes`: `min`,
184 `nmile`: `nmi`,
185 `nmiles`: `nmi`,
186 `ounce`: `oz`,
187 `ounces`: `oz`,
188 `ozs`: `oz`,
189 `weeks`: `week`,
190 `wk`: `week`,
191 `wks`: `week`,
192 `yard`: `yd`,
193 `yards`: `yd`,
194 `yard2`: `yd²`,
195 `yard²`: `yd²`,
196 `yards2`: `yd²`,
197 `yards²`: `yd²`,
198 `yds`: `yd`,
199 `yds2`: `yd²`,
200 `yds²`: `yd²`,
201 }
202
203 type converter struct {
204 To string
205 Mul float64
206 Add float64
207 }
208
209 var mainConverters = map[string]converter{
210 `ft`: converter{`m`, 0.3048, 0},
211 `gal`: converter{`L`, 3.785411784, 0},
212 `in`: converter{`cm`, 2.54, 0},
213 `mi`: converter{`km`, 1.609344, 0},
214 `mpg`: converter{`kpl`, 0.425143707, 0},
215 `mph`: converter{`kph`, 1.609344, 0},
216 `nmi`: converter{`km`, 1.852, 0},
217 `oz`: converter{`g`, 28.349523125, 0},
218 `yd`: converter{`m`, 0.9144, 0},
219 }
220
221 var otherConverters = map[string]converter{
222 `ac`: converter{`m²`, 4046.8564224, 0},
223 `day`: converter{`s`, 24 * 60 * 60, 0},
224 `ft²`: converter{`m²`, 0.09290304, 0},
225 `ft³`: converter{`m³`, 0.028316846592, 0},
226 `mi²`: converter{`km²`, 2.5899881103360, 0},
227 `min`: converter{`s`, 60, 0},
228 `psi`: converter{`Pa`, 6894.757293168, 0},
229 `week`: converter{`s`, 7 * 24 * 60 * 60, 0},
230 `yd²`: converter{`m²`, 0.83612736, 0},
231 }
File: ./utfate/utfate.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package utfate
26
27 import (
28 "bufio"
29 "bytes"
30 "encoding/binary"
31 "errors"
32 "io"
33 "os"
34 "unicode"
35 "unicode/utf16"
36 )
37
38 const info = `
39 utfate [options...] [file...]
40
41 This app turns plain-text input into UTF-8. Supported input formats are
42
43 - ASCII
44 - UTF-8
45 - UTF-8 with a leading BOM
46 - UTF-16 BE
47 - UTF-16 LE
48 - UTF-32 BE
49 - UTF-32 LE
50
51 All (optional) leading options start with either single or double-dash:
52
53 -h, -help show this help message
54 `
55
56 func Main() {
57 if len(os.Args) > 1 {
58 switch os.Args[1] {
59 case `-h`, `--h`, `-help`, `--help`:
60 os.Stdout.WriteString(info[1:])
61 return
62 }
63 }
64
65 if err := run(os.Stdout, os.Args[1:]); err != nil && err != io.EOF {
66 os.Stderr.WriteString(`utfate: `)
67 os.Stderr.WriteString(err.Error())
68 os.Stderr.WriteString("\n")
69 os.Exit(1)
70 return
71 }
72 }
73
74 func run(w io.Writer, args []string) error {
75 bw := bufio.NewWriterSize(w, 32*1024)
76 defer bw.Flush()
77
78 for _, path := range args {
79 if err := handleFile(bw, path); err != nil {
80 return err
81 }
82 }
83
84 if len(args) == 0 {
85 return utfate(bw, os.Stdin)
86 }
87 return nil
88 }
89
90 func handleFile(w *bufio.Writer, name string) error {
91 if name == `-` {
92 return utfate(w, os.Stdin)
93 }
94
95 f, err := os.Open(name)
96 if err != nil {
97 return errors.New(`can't read from file named "` + name + `"`)
98 }
99 defer f.Close()
100
101 return utfate(w, f)
102 }
103
104 func utfate(w io.Writer, r io.Reader) error {
105 br := bufio.NewReader(r)
106 bw := bufio.NewWriterSize(w, 32*1024)
107 defer bw.Flush()
108
109 lead, err := br.Peek(4)
110 if err != nil && err != io.EOF {
111 return err
112 }
113
114 if bytes.HasPrefix(lead, []byte{'\x00', '\x00', '\xfe', '\xff'}) {
115 br.Discard(4)
116 return utf32toUTF8(bw, br, binary.BigEndian)
117 }
118
119 if bytes.HasPrefix(lead, []byte{'\xff', '\xfe', '\x00', '\x00'}) {
120 br.Discard(4)
121 return utf32toUTF8(bw, br, binary.LittleEndian)
122 }
123
124 if bytes.HasPrefix(lead, []byte{'\xfe', '\xff'}) {
125 br.Discard(2)
126 return utf16toUTF8(bw, br, readBytePairBE)
127 }
128
129 if bytes.HasPrefix(lead, []byte{'\xff', '\xfe'}) {
130 br.Discard(2)
131 return utf16toUTF8(bw, br, readBytePairLE)
132 }
133
134 if bytes.HasPrefix(lead, []byte{'\xef', '\xbb', '\xbf'}) {
135 br.Discard(3)
136 return handleUTF8(bw, br)
137 }
138
139 return handleUTF8(bw, br)
140 }
141
142 func handleUTF8(w *bufio.Writer, r *bufio.Reader) error {
143 for {
144 c, _, err := r.ReadRune()
145 if c == unicode.ReplacementChar {
146 return errors.New(`invalid UTF-8 stream`)
147 }
148 if err == io.EOF {
149 return nil
150 }
151 if err != nil {
152 return err
153 }
154
155 if _, err := w.WriteRune(c); err != nil {
156 return io.EOF
157 }
158 }
159 }
160
161 // fancyHandleUTF8 is kept only for reference, as its attempts at being clever
162 // don't seem to speed things up much when given ASCII input
163 func fancyHandleUTF8(w *bufio.Writer, r *bufio.Reader) error {
164 lookahead := 1
165 maxAhead := r.Size() / 2
166
167 for {
168 // look ahead to check for ASCII runs
169 ahead, err := r.Peek(lookahead)
170 if err == io.EOF {
171 return nil
172 }
173 if err != nil {
174 return err
175 }
176
177 // copy leading ASCII runs
178 n := leadASCII(ahead)
179 if n > 0 {
180 w.Write(ahead[:n])
181 r.Discard(n)
182 }
183
184 // adapt lookahead size
185 if n == len(ahead) && lookahead < maxAhead {
186 lookahead *= 2
187 } else if lookahead > 1 {
188 lookahead /= 2
189 }
190
191 if n == len(ahead) {
192 continue
193 }
194
195 c, _, err := r.ReadRune()
196 if c == unicode.ReplacementChar {
197 return errors.New(`invalid UTF-8 stream`)
198 }
199 if err == io.EOF {
200 return nil
201 }
202 if err != nil {
203 return err
204 }
205
206 if _, err := w.WriteRune(c); err != nil {
207 return io.EOF
208 }
209 }
210 }
211
212 // leadASCII is used by func fancyHandleUTF8
213 func leadASCII(buf []byte) int {
214 for i, b := range buf {
215 if b >= 128 {
216 return i
217 }
218 }
219 return len(buf)
220 }
221
222 // readPairFunc narrows source-code lines below
223 type readPairFunc func(*bufio.Reader) (byte, byte, error)
224
225 // utf16toUTF8 handles UTF-16 inputs for func utfate
226 func utf16toUTF8(w *bufio.Writer, r *bufio.Reader, read2 readPairFunc) error {
227 for {
228 a, b, err := read2(r)
229 if err == io.EOF {
230 return nil
231 }
232 if err != nil {
233 return err
234 }
235
236 c := rune(256*int(a) + int(b))
237 if utf16.IsSurrogate(c) {
238 a, b, err := read2(r)
239 if err == io.EOF {
240 return nil
241 }
242 if err != nil {
243 return err
244 }
245
246 next := rune(256*int(a) + int(b))
247 c = utf16.DecodeRune(c, next)
248 }
249
250 if _, err := w.WriteRune(c); err != nil {
251 return io.EOF
252 }
253 }
254 }
255
256 // readBytePairBE gets you a pair of bytes in big-endian (original) order
257 func readBytePairBE(br *bufio.Reader) (byte, byte, error) {
258 a, err := br.ReadByte()
259 if err != nil {
260 return a, 0, err
261 }
262
263 b, err := br.ReadByte()
264 return a, b, err
265 }
266
267 // readBytePairLE gets you a pair of bytes in little-endian order
268 func readBytePairLE(br *bufio.Reader) (byte, byte, error) {
269 a, b, err := readBytePairBE(br)
270 return b, a, err
271 }
272
273 // utf32toUTF8 handles UTF-32 inputs for func utfate
274 func utf32toUTF8(w *bufio.Writer, r *bufio.Reader, o binary.ByteOrder) error {
275 var n uint32
276 for {
277 err := binary.Read(r, o, &n)
278 if err == io.EOF {
279 return nil
280 }
281 if err != nil {
282 return err
283 }
284
285 if _, err := w.WriteRune(rune(n)); err != nil {
286 return io.EOF
287 }
288 }
289 }
File: ./waveout/bytes.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package waveout
26
27 import (
28 "encoding/binary"
29 "fmt"
30 "io"
31 "math"
32 )
33
34 // aiff header format
35 //
36 // http://paulbourke.net/dataformats/audio/
37 //
38 // wav header format
39 //
40 // http://soundfile.sapp.org/doc/WaveFormat/
41 // http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
42 // https://docs.fileformat.com/audio/wav/
43
44 const (
45 // maxInt helps convert float64 values into int16 ones
46 maxInt = 1<<15 - 1
47
48 // wavIntPCM declares integer PCM sound-data in a wav header
49 wavIntPCM = 1
50
51 // wavFloatPCM declares floating-point PCM sound-data in a wav header
52 wavFloatPCM = 3
53 )
54
55 // emitInt16LE writes a 16-bit signed integer in little-endian byte order
56 func emitInt16LE(w io.Writer, f float64) {
57 // binary.Write(w, binary.LittleEndian, int16(maxInt*f))
58 var buf [2]byte
59 binary.LittleEndian.PutUint16(buf[:2], uint16(int16(maxInt*f)))
60 w.Write(buf[:2])
61 }
62
63 // emitFloat32LE writes a 32-bit float in little-endian byte order
64 func emitFloat32LE(w io.Writer, f float64) {
65 var buf [4]byte
66 binary.LittleEndian.PutUint32(buf[:4], math.Float32bits(float32(f)))
67 w.Write(buf[:4])
68 }
69
70 // emitInt16BE writes a 16-bit signed integer in big-endian byte order
71 func emitInt16BE(w io.Writer, f float64) {
72 // binary.Write(w, binary.BigEndian, int16(maxInt*f))
73 var buf [2]byte
74 binary.BigEndian.PutUint16(buf[:2], uint16(int16(maxInt*f)))
75 w.Write(buf[:2])
76 }
77
78 // emitFloat32BE writes a 32-bit float in big-endian byte order
79 func emitFloat32BE(w io.Writer, f float64) {
80 var buf [4]byte
81 binary.BigEndian.PutUint32(buf[:4], math.Float32bits(float32(f)))
82 w.Write(buf[:4])
83 }
84
85 // wavSettings is an item in the type2wavSettings table
86 type wavSettings struct {
87 Type byte
88 BitsPerSample byte
89 }
90
91 // type2wavSettings encodes values used when emitting wav headers
92 var type2wavSettings = map[sampleFormat]wavSettings{
93 int16LE: {wavIntPCM, 16},
94 float32LE: {wavFloatPCM, 32},
95 }
96
97 // emitWaveHeader writes the start of a valid .wav file: since it also starts
98 // the wav data section and emits its size, you only need to write all samples
99 // after calling this func
100 func emitWaveHeader(w io.Writer, cfg outputConfig) error {
101 const fmtChunkSize = 16
102 duration := cfg.MaxTime
103 numchan := uint32(len(cfg.Scripts))
104 sampleRate := cfg.SampleRate
105
106 ws, ok := type2wavSettings[cfg.Samples]
107 if !ok {
108 const fs = `internal error: invalid output-format code %d`
109 return fmt.Errorf(fs, cfg.Samples)
110 }
111 kind := uint16(ws.Type)
112 bps := uint32(ws.BitsPerSample)
113
114 // byte rate
115 br := sampleRate * bps * numchan / 8
116 // data size in bytes
117 dataSize := uint32(float64(br) * duration)
118 // total file size
119 totalSize := uint32(dataSize + 44)
120
121 // general descriptor
122 w.Write([]byte(`RIFF`))
123 binary.Write(w, binary.LittleEndian, uint32(totalSize))
124 w.Write([]byte(`WAVE`))
125
126 // fmt chunk
127 w.Write([]byte(`fmt `))
128 binary.Write(w, binary.LittleEndian, uint32(fmtChunkSize))
129 binary.Write(w, binary.LittleEndian, uint16(kind))
130 binary.Write(w, binary.LittleEndian, uint16(numchan))
131 binary.Write(w, binary.LittleEndian, uint32(sampleRate))
132 binary.Write(w, binary.LittleEndian, uint32(br))
133 binary.Write(w, binary.LittleEndian, uint16(bps*numchan/8))
134 binary.Write(w, binary.LittleEndian, uint16(bps))
135
136 // start data chunk
137 w.Write([]byte(`data`))
138 binary.Write(w, binary.LittleEndian, uint32(dataSize))
139 return nil
140 }
File: ./waveout/config.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package waveout
26
27 import (
28 "errors"
29 "fmt"
30 "math"
31 "os"
32 "strconv"
33 "strings"
34 "time"
35 )
36
37 // config has all the parsed cmd-line options
38 type config struct {
39 // Scripts has the source codes of all scripts for all channels
40 Scripts []string
41
42 // To is the output format
43 To string
44
45 // MaxTime is the play duration of the resulting sound
46 MaxTime float64
47
48 // SampleRate is the number of samples per second for all channels
49 SampleRate uint
50 }
51
52 // parseFlags is the constructor for type config
53 func parseFlags(usage string) (config, error) {
54 cfg := config{
55 To: `wav`,
56 MaxTime: math.NaN(),
57 SampleRate: 48_000,
58 }
59
60 args := os.Args[1:]
61 if len(args) == 0 {
62 fmt.Fprint(os.Stderr, usage)
63 os.Exit(0)
64 return cfg, nil
65 }
66
67 for _, s := range args {
68 switch s {
69 case `help`, `-h`, `--h`, `-help`, `--help`:
70 fmt.Fprint(os.Stdout, usage)
71 os.Exit(0)
72 return cfg, nil
73 }
74
75 err := cfg.handleArg(s)
76 if err != nil {
77 return cfg, err
78 }
79 }
80
81 if math.IsNaN(cfg.MaxTime) {
82 cfg.MaxTime = 1
83 }
84 if cfg.MaxTime < 0 {
85 const fs = `error: given negative duration %f`
86 return cfg, fmt.Errorf(fs, cfg.MaxTime)
87 }
88 return cfg, nil
89 }
90
91 func (c *config) handleArg(s string) error {
92 switch s {
93 case `44.1k`, `44.1K`:
94 c.SampleRate = 44_100
95 return nil
96
97 case `48k`, `48K`:
98 c.SampleRate = 48_000
99 return nil
100
101 case `dat`, `DAT`:
102 c.SampleRate = 48_000
103 return nil
104
105 case `cd`, `cda`, `CD`, `CDA`:
106 c.SampleRate = 44_100
107 return nil
108 }
109
110 // handle output-format names and their aliases
111 if kind, ok := name2type[s]; ok {
112 c.To = kind
113 return nil
114 }
115
116 // handle time formats, except when they're pure numbers
117 if math.IsNaN(c.MaxTime) {
118 dur, derr := ParseDuration(s)
119 if derr == nil {
120 c.MaxTime = float64(dur) / float64(time.Second)
121 return nil
122 }
123 }
124
125 // handle sample-rate, given either in hertz or kilohertz
126 lc := strings.ToLower(s)
127 if strings.HasSuffix(lc, `khz`) {
128 lc = strings.TrimSuffix(lc, `khz`)
129 khz, err := strconv.ParseFloat(lc, 64)
130 if err != nil || isBadNumber(khz) || khz <= 0 {
131 const fs = `invalid sample-rate frequency %q`
132 return fmt.Errorf(fs, s)
133 }
134 c.SampleRate = uint(1_000 * khz)
135 return nil
136 } else if strings.HasSuffix(lc, `hz`) {
137 lc = strings.TrimSuffix(lc, `hz`)
138 hz, err := strconv.ParseUint(lc, 10, 64)
139 if err != nil {
140 const fs = `invalid sample-rate frequency %q`
141 return fmt.Errorf(fs, s)
142 }
143 c.SampleRate = uint(hz)
144 return nil
145 }
146
147 c.Scripts = append(c.Scripts, s)
148 return nil
149 }
150
151 type encoding byte
152 type headerType byte
153 type sampleFormat byte
154
155 const (
156 directEncoding encoding = 1
157 uriEncoding encoding = 2
158
159 noHeader headerType = 1
160 wavHeader headerType = 2
161
162 int16BE sampleFormat = 1
163 int16LE sampleFormat = 2
164 float32BE sampleFormat = 3
165 float32LE sampleFormat = 4
166 )
167
168 // name2type normalizes keys used for type2settings
169 var name2type = map[string]string{
170 `datauri`: `data-uri`,
171 `dataurl`: `data-uri`,
172 `data-uri`: `data-uri`,
173 `data-url`: `data-uri`,
174 `uri`: `data-uri`,
175 `url`: `data-uri`,
176
177 `raw`: `raw`,
178 `raw16be`: `raw16be`,
179 `raw16le`: `raw16le`,
180 `raw32be`: `raw32be`,
181 `raw32le`: `raw32le`,
182
183 `audio/x-wav`: `wave-16`,
184 `audio/x-wave`: `wave-16`,
185 `wav`: `wave-16`,
186 `wave`: `wave-16`,
187 `wav16`: `wave-16`,
188 `wave16`: `wave-16`,
189 `wav-16`: `wave-16`,
190 `wave-16`: `wave-16`,
191 `x-wav`: `wave-16`,
192 `x-wave`: `wave-16`,
193
194 `wav16uri`: `wave-16-uri`,
195 `wave-16-uri`: `wave-16-uri`,
196
197 `wav32uri`: `wave-32-uri`,
198 `wave-32-uri`: `wave-32-uri`,
199
200 `wav32`: `wave-32`,
201 `wave32`: `wave-32`,
202 `wav-32`: `wave-32`,
203 `wave-32`: `wave-32`,
204 }
205
206 // outputSettings are format-specific settings which are controlled by the
207 // output-format option on the cmd-line
208 type outputSettings struct {
209 Encoding encoding
210 Header headerType
211 Samples sampleFormat
212 }
213
214 // type2settings translates output-format names into the specific settings
215 // these imply
216 var type2settings = map[string]outputSettings{
217 ``: {directEncoding, wavHeader, int16LE},
218
219 `data-uri`: {uriEncoding, wavHeader, int16LE},
220 `raw`: {directEncoding, noHeader, int16LE},
221 `raw16be`: {directEncoding, noHeader, int16BE},
222 `raw16le`: {directEncoding, noHeader, int16LE},
223 `wave-16`: {directEncoding, wavHeader, int16LE},
224 `wave-16-uri`: {uriEncoding, wavHeader, int16LE},
225
226 `raw32be`: {directEncoding, noHeader, float32BE},
227 `raw32le`: {directEncoding, noHeader, float32LE},
228 `wave-32`: {directEncoding, wavHeader, float32LE},
229 `wave-32-uri`: {uriEncoding, wavHeader, float32LE},
230 }
231
232 // outputConfig has all the info the core of this app needs to make sound
233 type outputConfig struct {
234 // Scripts has the source codes of all scripts for all channels
235 Scripts []string
236
237 // MaxTime is the play duration of the resulting sound
238 MaxTime float64
239
240 // SampleRate is the number of samples per second for all channels
241 SampleRate uint32
242
243 // all the configuration details needed to emit output
244 outputSettings
245 }
246
247 // newOutputConfig is the constructor for type outputConfig, translating the
248 // cmd-line info from type config
249 func newOutputConfig(cfg config) (outputConfig, error) {
250 oc := outputConfig{
251 Scripts: cfg.Scripts,
252 MaxTime: cfg.MaxTime,
253 SampleRate: uint32(cfg.SampleRate),
254 }
255
256 if len(oc.Scripts) == 0 {
257 return oc, errors.New(`no formulas given`)
258 }
259
260 outFmt := strings.ToLower(strings.TrimSpace(cfg.To))
261 if alias, ok := name2type[outFmt]; ok {
262 outFmt = alias
263 }
264
265 set, ok := type2settings[outFmt]
266 if !ok {
267 const fs = `unsupported output format %q`
268 return oc, fmt.Errorf(fs, cfg.To)
269 }
270
271 oc.outputSettings = set
272 return oc, nil
273 }
274
275 // mimeType gives the format's corresponding MIME type, or an empty string
276 // if the type isn't URI-encodable
277 func (oc outputConfig) mimeType() string {
278 if oc.Header == wavHeader {
279 return `audio/x-wav`
280 }
281 return ``
282 }
283
284 func isBadNumber(f float64) bool {
285 return math.IsNaN(f) || math.IsInf(f, 0)
286 }
File: ./waveout/config_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package waveout
26
27 import "testing"
28
29 func TestTables(t *testing.T) {
30 for _, kind := range name2type {
31 // ensure all canonical format values are aliased to themselves
32 if _, ok := name2type[kind]; !ok {
33 const fs = `canonical format %q not set`
34 t.Fatalf(fs, kind)
35 }
36 }
37
38 for k, kind := range name2type {
39 // ensure each setting leads somewhere
40 set, ok := type2settings[kind]
41 if !ok {
42 const fs = `type alias %q has no setting for it`
43 t.Fatalf(fs, k)
44 }
45
46 // ensure all encoding codes are valid in the next step
47 switch set.Encoding {
48 case directEncoding, uriEncoding:
49 // ok
50 default:
51 const fs = `invalid encoding (code %d) from settings for %q`
52 t.Fatalf(fs, set.Encoding, kind)
53 }
54
55 // also ensure all header codes are valid
56 switch set.Header {
57 case noHeader, wavHeader:
58 // ok
59 default:
60 const fs = `invalid header (code %d) from settings for %q`
61 t.Fatalf(fs, set.Header, kind)
62 }
63
64 // as well as all sample-format codes
65 switch set.Samples {
66 case int16BE, int16LE, float32BE, float32LE:
67 // ok
68 default:
69 const fs = `invalid sample-format (code %d) from settings for %q`
70 t.Fatalf(fs, set.Header, kind)
71 }
72 }
73 }
File: ./waveout/durations.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package waveout
26
27 import (
28 "errors"
29 "math"
30 "strconv"
31 "strings"
32 "time"
33 )
34
35 const (
36 day = 24 * time.Hour
37 week = 7 * day
38 normalYear = 365 * day
39
40 secondsInMinute = 60
41 secondsInHour = 3_600
42 secondsInDay = 24 * secondsInHour
43 secondsInWeek = 7 * secondsInDay
44 )
45
46 var (
47 ErrMisplacedDots = errors.New(`misplaced decimal dot in time-duration`)
48 )
49
50 // ParseDuration extends the stdlib time-duration parser to also allow some
51 // commonly-used time notations like
52 //
53 // MM:SS minutes and seconds
54 // HH:MM:SS hours, minutes, and seconds
55 // DD:HH:MM:SS days, hours, minutes, and seconds
56 // WW:DD:HH:MM:SS weeks, days, hours, minutes, and seconds
57 //
58 // Decimals are also supported, but only for the final seconds field.
59 // Again, this function also supports the stdlib time-duration notation.
60 func ParseDuration(s string) (time.Duration, error) {
61 s = strings.TrimSpace(s)
62 if s == "" {
63 return 0, errors.New(`can't parse time values from empty strings`)
64 }
65
66 // handle shortcuts for time units such as weeks and (normal) years
67 if s[len(s)-1] == 'w' {
68 f, err := strconv.ParseFloat(s[:len(s)-1], 64)
69 if err != nil {
70 return 0, err
71 }
72 return time.Duration(float64(week) * f), nil
73 }
74 if s[len(s)-1] == 'y' {
75 f, err := strconv.ParseFloat(s[:len(s)-1], 64)
76 if err != nil {
77 return 0, err
78 }
79 return time.Duration(float64(normalYear) * f), nil
80 }
81
82 // see if the stdlib can handle it directly
83 d, err := time.ParseDuration(s)
84 if err == nil {
85 return d, nil
86 }
87
88 return parseColonDuration(s)
89 }
90
91 // durationFragments helps func parseDuration keep track of all fields without
92 // depending on functionality from external packages, such as strings.Split
93 type durationFragments struct {
94 weeks int
95 days int
96 hours int
97 minutes int
98 seconds int
99
100 numfields int
101 }
102
103 func (f *durationFragments) update(n int) error {
104 f.numfields++
105 switch f.numfields {
106 case 1:
107 f.seconds = n
108 return nil
109
110 case 2:
111 f.minutes = f.seconds
112 f.seconds = n
113 return nil
114
115 case 3:
116 f.hours = f.minutes
117 f.minutes = f.seconds
118 f.seconds = n
119 return nil
120
121 case 4:
122 f.days = f.hours
123 f.hours = f.minutes
124 f.minutes = f.seconds
125 f.seconds = n
126 return nil
127
128 case 5:
129 f.weeks = f.days
130 f.days = f.hours
131 f.hours = f.minutes
132 f.minutes = f.seconds
133 f.seconds = n
134 return nil
135
136 default:
137 // weeks are the largest constant time unit there is
138 return errors.New(`semicolon-separated time fields stop at weeks`)
139 }
140 }
141
142 func (f durationFragments) duration() time.Duration {
143 d := time.Duration(f.weeks) * week
144 d += time.Duration(f.days) * day
145 d += time.Duration(f.hours) * time.Hour
146 d += time.Duration(f.minutes) * time.Minute
147 d += time.Duration(f.seconds) * time.Second
148 return d
149 }
150
151 // parseColonDuration handles HH:MM:SS-like strings for func ParseDuration
152 func parseColonDuration(s string) (time.Duration, error) {
153 n := 0 // value for current field
154 dec := false // was a decimal point found?
155 numdigits := 0 // how many digits current field has
156 frags := durationFragments{}
157
158 for _, r := range s {
159 switch r {
160 case '.':
161 // handle decimals
162 if dec {
163 return 0, ErrMisplacedDots
164 }
165 dec = true
166 // remember value for seconds
167 if err := frags.update(n); err != nil {
168 return 0, err
169 }
170 numdigits = 0
171 n = 0
172
173 case ':':
174 // switch to next fragment/group
175 if dec {
176 return 0, ErrMisplacedDots
177 }
178 if err := frags.update(n); err != nil {
179 return 0, err
180 }
181 numdigits = 0
182 n = 0
183
184 default:
185 // update value in current field
186 if r < '0' || r > '9' {
187 const m1 = `non-digits found in what's supposed`
188 const m2 = `to be a valid numeric substring`
189 const msg = m1 + ` ` + m2
190 return 0, errors.New(msg)
191 }
192 n *= 10
193 n += int(r - '0')
194 numdigits++
195 }
196 }
197
198 // handle subsecond values: seconds are already counted for in this case
199 if dec {
200 return frags.duration() + fractionalSecond(n, numdigits), nil
201 }
202
203 // remember value for seconds
204 if err := frags.update(n); err != nil {
205 return 0, err
206 }
207 return frags.duration(), nil
208 }
209
210 // fractionalSecond turns the int-pair (mantissa, -log10) into the sub-second
211 // time-duration it represents
212 func fractionalSecond(fraction int, numdigits int) time.Duration {
213 nd := math.Pow10(numdigits)
214 return time.Duration(fraction) * time.Second / time.Duration(int64(nd))
215 }
File: ./waveout/info.txt
1 waveout [options...] [duration...] [formulas...]
2
3
4 This app emits wave-sound binary data using the script(s) given. Scripts
5 give you the float64-related functionality you may expect, from numeric
6 operations to several math functions. When given 1 formula, the result is
7 mono; when given 2 formulas (left and right), the result is stereo, and so
8 on.
9
10 Output is always uncompressed audio: `waveout` can emit that as is, or as a
11 base64-encoded data-URI, which you can use as a `src` attribute value in an
12 HTML audio tag. Output duration is 1 second by default, but you can change
13 that too by using a recognized time format.
14
15 The first recognized time format is the familiar hh:mm:ss, where the hours
16 are optional, and where seconds can have a decimal part after it.
17
18 The second recognized time format uses 1-letter shortcuts instead of colons
19 for each time component, each of which is optional: `h` stands for hour, `m`
20 for minutes, and `s` for seconds.
21
22
23 Output Formats
24
25 encoding header samples endian more info
26
27 wav direct wave int16 little default format
28
29 wav16 direct wave int16 little alias for `wav`
30 wav32 direct wave float32 little
31 uri data-URI wave int16 little MIME type is audio/x-wav
32
33 raw direct none int16 little
34 raw16le direct none int16 little alias for `raw`
35 raw32le direct none float32 little
36 raw16be direct none int16 big
37 raw32be direct none float32 big
38
39
40 Concrete Examples
41
42 # low-tones commonly used in club music as beats
43 waveout 2s 'sin(10 * tau * exp(-20 * u)) * exp(-2 * u)' > club-beats.wav
44
45 # 1 minute and 5 seconds of static-like random noise
46 waveout 1m5s 'rand()' > random-noise.wav
47
48 # many bell-like clicks in quick succession; can be a cellphone's ringtone
49 waveout 'sin(2048 * tau * t) * exp(-50 * (t%0.1))' > ringtone.wav
50
51 # 10-second sound resembling a door-ajar warning
52 waveout 10 'sin(660 * tau * (t%1)) * exp(-10 * (t%1))' > door-ajar.wav
53
54 # (vaguely) similar to the door-opening sound from a home alarm
55 waveout 'sin(4096 * tau * t) * exp(-10 * (t%0.1))' > home-alarm.wav
56
57 # watch your ears: quickly increases frequency up to 2khz
58 waveout 'sin(2_000 * t * tau * t)' > frequency-sweep.wav
59
60 # 1-second 400hz test tone
61 waveout 'sin(400 * tau * t)' > test-tone-400.wav
62
63 # 2s of a 440hz test tone, also called an A440 sound
64 waveout 2s 'sin(440 * tau * t)' > a440.wav
65
66 # 1s 400hz test tone with sudden volume drop at the end, to avoid clip
67 waveout 'sin(400 * tau * t) * min(1, exp(-100*(t-0.9)))' > nice-tone.wav
68
69 # old ringtone used in north america
70 waveout '0.5*sin(350 * tau * t) + 0.5*sin(450 * tau * t)' > na-ringtone.wav
71
72 # 20 seconds of periodic pings
73 waveout 20s 'sin(800 * tau * u) * exp(-20 * u)' > pings.wav
74
75 # 2 seconds of a european-style dial-tone
76 waveout 2s '(sin(350 * tau * t) + sin(450 * tau * t)) / 2' > dial-tone.wav
77
78 # 4 seconds of a north-american-style busy-phone signal
79 waveout 4s '(u < 0.5) * (sin(480*tau * t) + sin(620*tau * t)) / 2' > na-busy.wav
80
81 # hit the 51st key on a synthetic piano-like instrument
82 waveout 'sin(tau * 440 * 2**((51 - 49)/12) * t) * exp(-10*u)' > piano-key.wav
83
84 # hit of a synthetic snare-like sound
85 waveout 'random() * exp(-10 * t)' > synth-snare.wav
86
87 # a stereotypical `laser` sound
88 waveout 'sin(100 * tau * exp(-40 * t))' > laser.wav
File: ./waveout/main.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package waveout
26
27 import (
28 "bufio"
29 "encoding/base64"
30 "errors"
31 "fmt"
32 "io"
33 "os"
34
35 _ "embed"
36 )
37
38 //go:embed info.txt
39 var usage string
40
41 func Main() {
42 cfg, err := parseFlags(usage)
43 if err != nil {
44 fmt.Fprintln(os.Stderr, err.Error())
45 os.Exit(1)
46 return
47 }
48
49 oc, err := newOutputConfig(cfg)
50 if err != nil {
51 fmt.Fprintln(os.Stderr, err.Error())
52 os.Exit(1)
53 return
54 }
55
56 addDetermFuncs()
57
58 if err := run(oc); err != nil {
59 fmt.Fprintln(os.Stderr, err.Error())
60 os.Exit(1)
61 return
62 }
63 }
64
65 func run(cfg outputConfig) error {
66 // f, err := os.Create(`waveout.prof`)
67 // if err != nil {
68 // return err
69 // }
70 // defer f.Close()
71
72 // pprof.StartCPUProfile(f)
73 // defer pprof.StopCPUProfile()
74
75 w := bufio.NewWriterSize(os.Stdout, 64*1024)
76 defer w.Flush()
77
78 switch cfg.Encoding {
79 case directEncoding:
80 return runDirect(w, cfg)
81
82 case uriEncoding:
83 mtype := cfg.mimeType()
84 if mtype == `` {
85 return errors.New(`internal error: no MIME type`)
86 }
87
88 fmt.Fprintf(w, `data:%s;base64,`, mtype)
89 enc := base64.NewEncoder(base64.StdEncoding, w)
90 defer enc.Close()
91 return runDirect(enc, cfg)
92
93 default:
94 const fs = `internal error: wrong output-encoding code %d`
95 return fmt.Errorf(fs, cfg.Encoding)
96 }
97 }
98
99 // type2emitter chooses sample-emitter funcs from the format given
100 var type2emitter = map[sampleFormat]func(io.Writer, float64){
101 int16LE: emitInt16LE,
102 int16BE: emitInt16BE,
103 float32LE: emitFloat32LE,
104 float32BE: emitFloat32BE,
105 }
106
107 // runDirect emits sound-data bytes: this func can be called with writers
108 // which keep bytes as given, or with re-encoders, such as base64 writers
109 func runDirect(w io.Writer, cfg outputConfig) error {
110 switch cfg.Header {
111 case noHeader:
112 // do nothing, while avoiding error
113
114 case wavHeader:
115 emitWaveHeader(w, cfg)
116
117 default:
118 const fs = `internal error: wrong header code %d`
119 return fmt.Errorf(fs, cfg.Header)
120 }
121
122 emitter, ok := type2emitter[cfg.Samples]
123 if !ok {
124 const fs = `internal error: wrong output-format code %d`
125 return fmt.Errorf(fs, cfg.Samples)
126 }
127
128 if len(cfg.Scripts) == 1 {
129 return emitMono(w, cfg, emitter)
130 }
131 return emit(w, cfg, emitter)
132 }
File: ./waveout/scripts.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package waveout
26
27 import (
28 "io"
29 "math"
30 "math/rand"
31 "time"
32
33 "../fmscripts"
34 )
35
36 // makeDefs makes extra funcs and values available to scripts
37 func makeDefs(cfg outputConfig) map[string]any {
38 // copy extra built-in funcs
39 defs := make(map[string]any, len(extras)+6+5)
40 for k, v := range extras {
41 defs[k] = v
42 }
43
44 // add extra variables
45 defs[`t`] = 0.0
46 defs[`u`] = 0.0
47 defs[`d`] = cfg.MaxTime
48 defs[`dur`] = cfg.MaxTime
49 defs[`duration`] = cfg.MaxTime
50 defs[`end`] = cfg.MaxTime
51
52 // add pseudo-random funcs
53
54 seed := time.Now().UnixNano()
55 r := rand.New(rand.NewSource(seed))
56
57 rand := func() float64 {
58 return random01(r)
59 }
60 randomf := func() float64 {
61 return random(r)
62 }
63 rexpf := func(scale float64) float64 {
64 return rexp(r, scale)
65 }
66 rnormf := func(mu, sigma float64) float64 {
67 return rnorm(r, mu, sigma)
68 }
69
70 defs[`rand`] = rand
71 defs[`rand01`] = rand
72 defs[`random`] = randomf
73 defs[`rexp`] = rexpf
74 defs[`rnorm`] = rnormf
75
76 return defs
77 }
78
79 type emitFunc = func(io.Writer, float64)
80
81 // emit runs the formulas given to emit all wave samples
82 func emit(w io.Writer, cfg outputConfig, emit emitFunc) error {
83 var c fmscripts.Compiler
84 defs := makeDefs(cfg)
85
86 programs := make([]fmscripts.Program, 0, len(cfg.Scripts))
87 tvars := make([]*float64, 0, len(cfg.Scripts))
88 uvars := make([]*float64, 0, len(cfg.Scripts))
89
90 for _, s := range cfg.Scripts {
91 p, err := c.Compile(s, defs)
92 if err != nil {
93 return err
94 }
95 programs = append(programs, p)
96 t, _ := p.Get(`t`)
97 u, _ := p.Get(`u`)
98 tvars = append(tvars, t)
99 uvars = append(uvars, u)
100 }
101
102 dt := 1.0 / float64(cfg.SampleRate)
103 end := cfg.MaxTime
104
105 for i := 0.0; true; i++ {
106 now := dt * i
107 if now >= end {
108 return nil
109 }
110
111 _, u := math.Modf(now)
112
113 for j, p := range programs {
114 *tvars[j] = now
115 *uvars[j] = u
116 emit(w, p.Run())
117 }
118 }
119 return nil
120 }
121
122 // emitMono runs the formula given to emit all single-channel wave samples
123 func emitMono(w io.Writer, cfg outputConfig, emit emitFunc) error {
124 var c fmscripts.Compiler
125 mono, err := c.Compile(cfg.Scripts[0], makeDefs(cfg))
126 if err != nil {
127 return err
128 }
129
130 t, _ := mono.Get(`t`)
131 u, needsu := mono.Get(`u`)
132
133 dt := 1.0 / float64(cfg.SampleRate)
134 end := cfg.MaxTime
135
136 // update variable `u` only if script uses it: this can speed things
137 // up considerably when that variable isn't used
138 if needsu {
139 for i := 0.0; true; i++ {
140 now := dt * i
141 if now >= end {
142 return nil
143 }
144
145 *t = now
146 _, *u = math.Modf(now)
147 emit(w, mono.Run())
148 }
149 return nil
150 }
151
152 for i := 0.0; true; i++ {
153 now := dt * i
154 if now >= end {
155 return nil
156 }
157
158 *t = now
159 emit(w, mono.Run())
160 }
161 return nil
162 }
163
164 // // emitStereo runs the formula given to emit all 2-channel wave samples
165 // func emitStereo(w io.Writer, cfg outputConfig, emit emitFunc) error {
166 // defs := makeDefs(cfg)
167 // var c fmscripts.Compiler
168 // left, err := c.Compile(cfg.Scripts[0], defs)
169 // if err != nil {
170 // return err
171 // }
172 // right, err := c.Compile(cfg.Scripts[1], defs)
173 // if err != nil {
174 // return err
175 // }
176
177 // lt, _ := left.Get(`t`)
178 // rt, _ := right.Get(`t`)
179 // lu, luok := left.Get(`u`)
180 // ru, ruok := right.Get(`u`)
181
182 // dt := 1.0 / float64(cfg.SampleRate)
183 // end := cfg.MaxTime
184
185 // // update variable `u` only if script uses it: this can speed things
186 // // up considerably when that variable isn't used
187 // updateu := func(float64) {}
188 // if luok || ruok {
189 // updateu = func(now float64) {
190 // _, u := math.Modf(now)
191 // *lu = u
192 // *ru = u
193 // }
194 // }
195
196 // for i := 0.0; true; i++ {
197 // now := dt * i
198 // if now >= end {
199 // return nil
200 // }
201
202 // *rt = now
203 // *lt = now
204 // updateu(now)
205
206 // // most software seems to emit stereo pairs in left-right order
207 // emit(w, left.Run())
208 // emit(w, right.Run())
209 // }
210
211 // // keep the compiler happy
212 // return nil
213 // }
File: ./waveout/stdlib.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package waveout
26
27 import (
28 "math"
29 "math/rand"
30
31 "../fmscripts"
32 "../mathplus"
33 )
34
35 // tau is exactly 1 loop around a circle, which is handy to turn frequencies
36 // into trigonometric angles, since they're measured in radians
37 const tau = 2 * math.Pi
38
39 // extras has funcs beyond what the script built-ins offer: those built-ins
40 // are for general math calculations, while these are specific for sound
41 // effects, other sound-related calculations, or to make pseudo-random values
42 var extras = map[string]any{
43 `hihat`: hihat,
44 }
45
46 // addDetermFuncs does what it says, ensuring these funcs are optimizable when
47 // they're given all-constant expressions as inputs
48 func addDetermFuncs() {
49 fmscripts.DefineDetFuncs(map[string]any{
50 `ascale`: mathplus.AnchoredScale,
51 `awrap`: mathplus.AnchoredWrap,
52 `clamp`: mathplus.Clamp,
53 `epa`: mathplus.Epanechnikov,
54 `epanechnikov`: mathplus.Epanechnikov,
55 `fract`: mathplus.Fract,
56 `gauss`: mathplus.Gauss,
57 `horner`: mathplus.Polyval,
58 `logistic`: mathplus.Logistic,
59 `mix`: mathplus.Mix,
60 `polyval`: mathplus.Polyval,
61 `scale`: mathplus.Scale,
62 `sign`: mathplus.Sign,
63 `sinc`: mathplus.Sinc,
64 `smoothstep`: mathplus.SmoothStep,
65 `step`: mathplus.Step,
66 `tricube`: mathplus.Tricube,
67 `unwrap`: mathplus.Unwrap,
68 `wrap`: mathplus.Wrap,
69
70 `drop`: dropsince,
71 `dropfrom`: dropsince,
72 `dropoff`: dropsince,
73 `dropsince`: dropsince,
74 `kick`: kick,
75 `kicklow`: kicklow,
76 `piano`: piano,
77 `pianokey`: piano,
78 `pickval`: pickval,
79 `pickvalue`: pickval,
80 `sched`: schedule,
81 `schedule`: schedule,
82 `timeval`: timeval,
83 `timevalues`: timeval,
84 })
85 }
86
87 // random01 returns a random value in 0 .. 1
88 func random01(r *rand.Rand) float64 {
89 return r.Float64()
90 }
91
92 // random returns a random value in -1 .. +1
93 func random(r *rand.Rand) float64 {
94 return (2 * r.Float64()) - 1
95 }
96
97 // rexp returns an exponentially-distributed random value using the scale
98 // (expected value) given
99 func rexp(r *rand.Rand, scale float64) float64 {
100 return scale * r.ExpFloat64()
101 }
102
103 // rnorm returns a normally-distributed random value using the mean and
104 // standard deviation given
105 func rnorm(r *rand.Rand, mu, sigma float64) float64 {
106 return r.NormFloat64()*sigma + mu
107 }
108
109 // make sample for a synthetic-drum kick
110 func kick(t float64, f, k float64) float64 {
111 const p = 0.085
112 return math.Sin(tau*f*math.Pow(p, t)) * math.Exp(-k*t)
113 }
114
115 // make sample for a heavier-sounding synthetic-drum kick
116 func kicklow(t float64, f, k float64) float64 {
117 const p = 0.08
118 return math.Sin(tau*f*math.Pow(p, t)) * math.Exp(-k*t)
119 }
120
121 // make sample for a synthetic hi-hat hit
122 func hihat(t float64, k float64) float64 {
123 return rand.Float64() * math.Exp(-k*t)
124 }
125
126 // schedule rearranges time, without being a time machine
127 func schedule(t float64, period, delay float64) float64 {
128 v := t + (1 - delay)
129 if v < 0 {
130 return 0
131 }
132 return math.Mod(v*period, period)
133 }
134
135 // make sample for a synthetic piano key being hit
136 func piano(t float64, n float64) float64 {
137 p := (math.Floor(n) - 49) / 12
138 f := 440 * math.Pow(2, p)
139 return math.Sin(tau * f * t)
140 }
141
142 // multiply rest of a formula with this for a quick volume drop at the end:
143 // this is handy to avoid clips when sounds end playing
144 func dropsince(t float64, start float64) float64 {
145 // return math.Min(1, math.Exp(-100*(t-start)))
146 if t <= start {
147 return 1
148 }
149 return math.Exp(-100 * (t - start))
150 }
151
152 // pickval requires at least 3 args, the first 2 being the current time and
153 // each slot's duration, respectively: these 2 are followed by all the values
154 // to pick for all time slots
155 func pickval(args ...float64) float64 {
156 if len(args) < 3 {
157 return 0
158 }
159
160 t := args[0]
161 slotdur := args[1]
162 values := args[2:]
163
164 u, _ := math.Modf(t / slotdur)
165 n := len(values)
166 i := int(u) % n
167 if 0 <= i && i < n {
168 return values[i]
169 }
170 return 0
171 }
172
173 // timeval requires at least 2 args, the first 2 being the current time and
174 // the total looping-period, respectively: these 2 are followed by pairs of
175 // numbers, each consisting of a timestamp and a matching value, in order
176 func timeval(args ...float64) float64 {
177 if len(args) < 2 {
178 return 0
179 }
180
181 t := args[0]
182 period := args[1]
183 u, _ := math.Modf(t / period)
184
185 // find the first value whose periodic timestamp is due
186 for rest := args[2:]; len(rest) >= 2; rest = rest[2:] {
187 if u >= rest[0]/period {
188 return rest[1]
189 }
190 }
191 return 0
192 }
File: ./zcat/zcat.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package zcat
26
27 import (
28 "compress/gzip"
29 "io"
30 "os"
31 )
32
33 const info = `
34 zcat [options...] [files...]
35
36 Concatenate gzip-decompressed files/data to the standard output. To put it in
37 other words: all inputs are expected to be gzip-compressed, while the output
38 is decompressed/normal.
39
40 Options
41
42 --help show this help message
43 `
44
45 func Main() {
46 args := os.Args[1:]
47 for len(args) > 0 {
48 switch args[0] {
49 case `--help`:
50 os.Stderr.WriteString(info[1:])
51 return
52 }
53
54 break
55 }
56
57 if len(args) > 0 && args[0] == `--` {
58 args = args[1:]
59 }
60
61 for _, path := range args {
62 if err := handleFile(os.Stdout, path); err != nil && err != io.EOF {
63 os.Stderr.WriteString(`zcat: `)
64 os.Stderr.WriteString(err.Error())
65 os.Stderr.WriteString("\n")
66 os.Exit(1)
67 return
68 }
69 }
70
71 if len(args) == 0 {
72 if err := zcat(os.Stdout, os.Stdin); err != nil && err != io.EOF {
73 os.Stderr.WriteString(`zcat: `)
74 os.Stderr.WriteString(err.Error())
75 os.Stderr.WriteString("\n")
76 os.Exit(1)
77 return
78 }
79 }
80 }
81
82 func handleFile(w io.Writer, path string) error {
83 f, err := os.Open(path)
84 if err != nil {
85 return err
86 }
87 defer f.Close()
88 return zcat(w, f)
89 }
90
91 func zcat(w io.Writer, r io.Reader) error {
92 r, err := gzip.NewReader(r)
93 if err != nil {
94 return err
95 }
96 return cat(w, r)
97 }
98
99 func cat(w io.Writer, r io.Reader) error {
100 var buf [32 * 1024]byte
101
102 for {
103 got, err := r.Read(buf[:])
104 if err == io.EOF {
105 if got > 0 {
106 w.Write(buf[:got])
107 }
108 break
109 }
110
111 if err != nil {
112 return err
113 }
114
115 if _, err := w.Write(buf[:got]); err != nil {
116 return io.EOF
117 }
118 }
119
120 return nil
121 }
File: ./zj/zj.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package zj
26
27 import (
28 "bufio"
29 "encoding/json"
30 "errors"
31 "io"
32 "os"
33 "strconv"
34 "strings"
35 "unicode/utf8"
36 )
37
38 const info = `
39 zj [keys/indices...]
40
41 Zoom Json digs into subsets of the JSON data read from the standard input.
42 `
43
44 // sets keeps track of 2 sets: one for integer indices, the other for string
45 // keys; the sets are reused across recursive calls of func `zoom` to save
46 // on memory/allocations
47 type sets struct {
48 indices map[int]struct{}
49 keys map[string]struct{}
50 }
51
52 // dictionary is a map which also remembers the order of its keys
53 type dictionary struct {
54 Keys []string
55 Map map[string]any
56 }
57
58 func Main() {
59 args := os.Args[1:]
60
61 if len(args) > 0 {
62 switch args[0] {
63 case `-h`, `--h`, `-help`, `--help`:
64 os.Stdout.WriteString(info[1:])
65 return
66 }
67 }
68
69 if len(args) > 0 && args[0] == `--` {
70 args = args[1:]
71 }
72
73 data, err := load(os.Stdin)
74 if err != nil {
75 os.Stderr.WriteString(`zj: `)
76 os.Stderr.WriteString(err.Error())
77 os.Stderr.WriteString("\n")
78 os.Exit(1)
79 return
80 }
81
82 var avoid sets
83 avoid.indices = make(map[int]struct{})
84 avoid.keys = make(map[string]struct{})
85
86 data, err = zoom(data, args, &avoid)
87 if err != nil && err != io.EOF {
88 os.Stderr.WriteString(`zj: `)
89 os.Stderr.WriteString(err.Error())
90 os.Stderr.WriteString("\n")
91 os.Exit(1)
92 return
93 }
94
95 if err := json0(os.Stdout, data); err != nil && err != io.EOF {
96 os.Stderr.WriteString(`zj: `)
97 os.Stderr.WriteString(err.Error())
98 os.Stderr.WriteString("\n")
99 os.Exit(1)
100 return
101 }
102 }
103
104 func load(r io.Reader) (any, error) {
105 // dec := json.NewDecoder(r)
106 dec := json.NewDecoder(bufio.NewReaderSize(r, 32*1024))
107 // avoid parsing numbers, so unusually-long numbers are kept verbatim,
108 // even if JSON parsers aren't required to guarantee such input-fidelity
109 // for numbers
110 dec.UseNumber()
111
112 t, err := dec.Token()
113 if err == io.EOF {
114 return nil, errors.New(`input has no JSON values`)
115 }
116
117 data, err := loadToken(dec, t)
118 if err != nil {
119 return data, err
120 }
121
122 _, err = dec.Token()
123 if err == io.EOF {
124 // input is over, so it's a success
125 return data, nil
126 }
127
128 if err == nil {
129 // a successful `read` is a failure, as it means there are
130 // trailing JSON tokens
131 return data, errors.New(`unexpected trailing data`)
132 }
133
134 // any other error
135 return data, err
136 }
137
138 // loadToken handles recursion for func load
139 func loadToken(dec *json.Decoder, t json.Token) (any, error) {
140 switch t := t.(type) {
141 case json.Delim:
142 switch t {
143 case json.Delim('['):
144 return loadArray(dec)
145 case json.Delim('{'):
146 return loadObject(dec)
147 default:
148 return nil, errors.New(`unsupported JSON syntax ` + string(t))
149 }
150
151 case nil, bool, json.Number, string:
152 return t, nil
153
154 default:
155 // return nil, fmt.Errorf(`unsupported token type %T`, t)
156 return nil, errors.New(`invalid JSON token`)
157 }
158 }
159
160 // loadArray handles arrays for func loadToken
161 func loadArray(dec *json.Decoder) ([]any, error) {
162 var items []any
163
164 for i := 0; true; i++ {
165 t, err := dec.Token()
166 if err == io.EOF {
167 return items, errors.New(`end of JSON before array was closed`)
168 }
169 if err != nil {
170 return items, err
171 }
172
173 if t == json.Delim(']') {
174 return items, nil
175 }
176
177 v, err := loadToken(dec, t)
178 if err != nil {
179 return items, err
180 }
181 items = append(items, v)
182 }
183
184 // make the compiler happy
185 return items, nil
186 }
187
188 // loadObject handles objects for func loadToken
189 func loadObject(dec *json.Decoder) (dictionary, error) {
190 var items dictionary
191
192 for i := 0; true; i++ {
193 t, err := dec.Token()
194 if err == io.EOF {
195 return items, errors.New(`end of JSON before object was closed`)
196 }
197 if err != nil {
198 return items, err
199 }
200
201 if t == json.Delim('}') {
202 return items, nil
203 }
204
205 k, ok := t.(string)
206 if !ok {
207 return items, errors.New(`expected a string for a key-value pair`)
208 }
209
210 t, err = dec.Token()
211 if err == io.EOF {
212 return items, errors.New(`expected a value for a key-value pair`)
213 }
214
215 v, err := loadToken(dec, t)
216 if err != nil {
217 return items, err
218 }
219
220 if i == 0 {
221 items.Map = make(map[string]any)
222 }
223 if _, ok := items.Map[k]; !ok {
224 items.Keys = append(items.Keys, k)
225 }
226 items.Map[k] = v
227 }
228
229 // make the compiler happy
230 return items, nil
231 }
232
233 func zoom(data any, keys []string, avoid *sets) (any, error) {
234 for i, k := range keys {
235 switch v := data.(type) {
236 case nil:
237 return v, errors.New(`too many keys: can't zoom a null value`)
238
239 case bool:
240 return v, errors.New(`too many keys: can't zoom a boolean value`)
241
242 case json.Number:
243 return v, errors.New(`too many keys: can't zoom a number`)
244
245 case string:
246 v, err := zoomString(v, k)
247 if err != nil {
248 return v, err
249 }
250 data = v
251
252 case []any:
253 switch k {
254 case `.`:
255 return loopZoomArray(v, keys[i+1:], avoid)
256 case `+`:
257 return pickArrayItems(v, keys[i+1:])
258 case `-`:
259 clear(avoid.indices)
260 appendIndices(avoid.indices, v, keys[i+1:])
261 return dropArrayItems(v, avoid.indices)
262 }
263
264 res, err := zoomArray(v, k)
265 if err != nil {
266 return data, err
267 }
268 data = res
269
270 case dictionary:
271 if v, ok := v.Map[k]; ok {
272 data = v
273 continue
274 }
275
276 switch k {
277 case `+`:
278 return pickObjectItems(v, keys[i+1:])
279 case `-`:
280 clear(avoid.keys)
281 for _, k := range keys[i+1:] {
282 avoid.keys[k] = struct{}{}
283 }
284 return dropObjectItems(v, avoid.keys)
285 }
286
287 if _, v, ok := matchObjectKey(v, k); ok {
288 data = v
289 } else {
290 data = nil
291 }
292
293 default:
294 return v, errors.New(`too many keys: can't zoom basic values`)
295 }
296 }
297 return data, nil
298 }
299
300 func zoomArray(items []any, k string) (any, error) {
301 // trim leading spaces
302 for len(k) > 0 && k[0] == ' ' {
303 k = k[1:]
304 }
305
306 // trim trailing spaces
307 for len(k) > 0 && k[len(k)-1] == ' ' {
308 k = k[:len(k)-1]
309 }
310
311 if i, j, ok := tryArraySlice(items, k); ok {
312 if j >= len(items) {
313 j = len(items)
314 }
315 if i < 0 || j < 0 || i > j {
316 return []any(nil), nil
317 }
318 return items[i:j], nil
319 }
320
321 i, err := strconv.ParseInt(k, 10, 64)
322 if err != nil {
323 return nil, nil
324 }
325
326 if i < 0 {
327 i += int64(len(items))
328 }
329
330 if 0 <= i && i < int64(len(items)) {
331 return items[i], nil
332 }
333 return nil, nil
334 }
335
336 func tryArraySlice(items []any, k string) (i int, j int, ok bool) {
337 if k == `` {
338 return 0, 0, false
339 }
340
341 colon := strings.IndexByte(k, ':')
342 if colon < 0 {
343 if dots := indexPair(k, '.', '.'); dots >= 0 {
344 return tryIncArraySlice(items, k, dots)
345 }
346
347 return 0, 0, false
348 }
349
350 // handle omitted/implied starting 0
351 if colon == 0 {
352 j, err := strconv.ParseInt(k[colon+1:], 10, 64)
353 if err != nil {
354 return 0, 0, false
355 }
356
357 if j < 0 {
358 j += int64(len(items))
359 }
360
361 return 0, int(j), true
362 }
363
364 // handle omitted/implied until the end
365 if colon == len(k)-1 {
366 i, err := strconv.ParseInt(k[:colon], 10, 64)
367 if err != nil {
368 return 0, 0, false
369 }
370
371 if i < 0 {
372 i += int64(len(items))
373 }
374
375 return int(i), len(items), true
376 }
377
378 start, err := strconv.ParseInt(k[:colon], 10, 64)
379 if err != nil {
380 return 0, 0, false
381 }
382
383 if start < 0 {
384 start += int64(len(items))
385 }
386
387 end, err := strconv.ParseInt(k[colon+1:], 10, 64)
388 if err != nil {
389 return 0, 0, false
390 }
391
392 if end < 0 {
393 end += int64(len(items))
394 }
395
396 return int(start), int(end), ok
397 }
398
399 func tryIncArraySlice(items []any, k string, dots int) (i int, j int, ok bool) {
400 if k == `` {
401 return 0, 0, false
402 }
403
404 if dots < 0 {
405 return 0, 0, false
406 }
407
408 // handle omitted/implied starting 0
409 if dots == 0 {
410 j, err := strconv.ParseInt(k[dots+2:], 10, 64)
411 if err != nil {
412 return 0, 0, false
413 }
414
415 if j < 0 {
416 j += int64(len(items))
417 }
418 if j >= 0 {
419 j++
420 }
421
422 return 0, int(j), true
423 }
424
425 // handle omitted/implied until the end
426 if dots == len(k)-1 {
427 i, err := strconv.ParseInt(k[:dots], 10, 64)
428 if err != nil {
429 return 0, 0, false
430 }
431
432 if i < 0 {
433 i += int64(len(items))
434 }
435
436 return int(i), len(items), true
437 }
438
439 start, err := strconv.ParseInt(k[:dots], 10, 64)
440 if err != nil {
441 return 0, 0, false
442 }
443
444 if start < 0 {
445 start += int64(len(items))
446 }
447
448 end, err := strconv.ParseInt(k[dots+2:], 10, 64)
449 if err != nil {
450 return 0, 0, false
451 }
452
453 if end < 0 {
454 end += int64(len(items))
455 }
456 if end >= 0 {
457 end++
458 }
459 return int(start), int(end), ok
460 }
461
462 func matchObjectKey(items dictionary, k string) (match string, v any, ok bool) {
463 // first, try direct key lookup
464 if v, ok := items.Map[k]; ok {
465 return k, v, true
466 }
467
468 // second, try case-insensitive key lookup
469 for s := range items.Map {
470 if strings.EqualFold(k, s) {
471 return s, items.Map[s], true
472 }
473 }
474
475 // finally, try integer/index lookup
476 i, err := strconv.ParseInt(k, 10, 64)
477 if err != nil {
478 return ``, nil, false
479 }
480
481 if i < 0 {
482 i += int64(len(items.Keys))
483 }
484
485 if 0 <= i && i < int64(len(items.Keys)) {
486 k := items.Keys[i]
487 return k, items.Map[k], true
488 }
489
490 // nothing worked
491 return ``, nil, false
492 }
493
494 func zoomString(s string, k string) (string, error) {
495 if i, j, ok := tryRuneSlice(s, k); ok {
496 if i > j {
497 return ``, nil
498 }
499 return sliceRunes(s, i, j), nil
500 }
501
502 i, err := strconv.ParseInt(k, 10, 64)
503 if err != nil {
504 return ``, err
505 }
506
507 // don't bother looping when the index given is obviously out of bounds
508 if int(i) >= len(s) || int(-i) > len(s) {
509 return ``, nil
510 }
511
512 if i < 0 {
513 // shrink string backward from the end
514 for len(s) > 0 && i < 0 {
515 _, size := utf8.DecodeLastRuneInString(s)
516 s = s[:len(s)-size]
517 i++
518 }
519
520 if len(s) > 0 && i == 0 {
521 _, size := utf8.DecodeLastRuneInString(s)
522 return s[len(s)-size:], nil
523 }
524 return ``, nil
525 }
526
527 // shrink string forward from the start
528 for len(s) > 0 && i > 0 {
529 _, size := utf8.DecodeRuneInString(s)
530 s = s[size:]
531 i--
532 }
533
534 if len(s) > 0 && i == 0 {
535 _, size := utf8.DecodeRuneInString(s)
536 return s[:size], nil
537 }
538 return ``, nil
539 }
540
541 func tryRuneSlice(s string, k string) (i int, j int, ok bool) {
542 if k == `` {
543 return 0, 0, false
544 }
545
546 colon := strings.IndexByte(k, ':')
547 if colon < 0 {
548 return 0, 0, false
549 }
550
551 // handle omitted/implied starting 0
552 if colon == 0 {
553 j, err := strconv.ParseInt(k[colon+1:], 10, 64)
554 if err != nil {
555 return 0, 0, false
556 }
557
558 return 0, int(j), true
559 }
560
561 // handle omitted/implied until the end
562 if colon == len(k)-1 {
563 i, err := strconv.ParseInt(k[:colon], 10, 64)
564 if err != nil {
565 return 0, 0, false
566 }
567
568 return int(i), len(s), true
569 }
570
571 start, err := strconv.ParseInt(k[:colon], 10, 64)
572 if err != nil {
573 return 0, 0, false
574 }
575
576 end, err := strconv.ParseInt(k[colon+1:], 10, 64)
577 if err != nil {
578 return 0, 0, false
579 }
580
581 return int(start), int(end), ok
582 }
583
584 func sliceRunes(s string, i int, j int) string {
585 if i >= j {
586 return ``
587 }
588
589 // to do: backward-indexing
590 if i < 0 || j < 0 {
591 return ``
592 }
593
594 // don't bother looping when the index given is obviously out of bounds
595 if int(i) >= len(s) || int(-i) > len(s) {
596 return ``
597 }
598
599 // skip leading runes, according to the first index
600 for len(s) > 0 && i > 0 {
601 _, size := utf8.DecodeRuneInString(s)
602 s = s[size:]
603 i--
604 }
605
606 if len(s) == 0 {
607 return ``
608 }
609
610 end := 0
611 rest := s
612 for len(rest) > 0 && j > 0 {
613 _, size := utf8.DecodeRuneInString(rest)
614 rest = rest[size:]
615 end += size
616 j--
617 }
618
619 if len(s) > 0 && j == 0 {
620 return s[:end]
621 }
622 return ``
623 }
624
625 func json0(w io.Writer, data any) error {
626 bw := bufio.NewWriterSize(w, 32*1024)
627 defer bw.Flush()
628
629 err := writeValue(bw, data)
630 bw.WriteByte('\n')
631
632 if err == io.EOF {
633 return nil
634 }
635 return err
636 }
637
638 func loopZoomArray(items []any, keys []string, avoid *sets) (any, error) {
639 res := items[:0]
640 for _, v := range items {
641 v, err := zoom(v, keys, avoid)
642 if err != nil {
643 return res, err
644 }
645 res = append(res, v)
646 }
647 return res, nil
648 }
649
650 func pickArrayItems(items []any, keys []string) (any, error) {
651 res := items[:0]
652 for _, k := range keys {
653 v, err := zoomArray(items, k)
654 if err != nil {
655 return res, err
656 }
657 res = append(res, v)
658 }
659 return res, nil
660 }
661
662 func dropArrayItems(items []any, avoid map[int]struct{}) (any, error) {
663 res := items[:0]
664 for i, v := range items {
665 if _, ok := avoid[i]; ok {
666 continue
667 }
668 res = append(res, v)
669 }
670 return res, nil
671 }
672
673 func pickObjectItems(items dictionary, keys []string) (any, error) {
674 var res dictionary
675 res.Keys = items.Keys[:0]
676 res.Map = items.Map
677
678 for _, k := range keys {
679 match, _, ok := matchObjectKey(items, k)
680 if !ok {
681 continue
682 }
683
684 if _, ok := res.Map[match]; !ok {
685 res.Keys = append(res.Keys, match)
686 }
687 }
688
689 return res, nil
690 }
691
692 func dropObjectItems(items dictionary, avoid map[string]struct{}) (any, error) {
693 var res dictionary
694 res.Keys = items.Keys[:0]
695 res.Map = items.Map
696
697 for _, k := range items.Keys {
698 if hasFold(avoid, k) {
699 continue
700 }
701
702 if _, ok := res.Map[k]; !ok {
703 res.Keys = append(res.Keys, k)
704 }
705 }
706
707 return res, nil
708 }
709
710 func hasFold(avoid map[string]struct{}, s string) bool {
711 for v := range avoid {
712 if v == s || strings.EqualFold(v, s) {
713 return true
714 }
715 }
716 return false
717 }
718
719 func appendIndices(dest map[int]struct{}, items []any, keys []string) {
720 for _, k := range keys {
721 i, err := strconv.ParseInt(k, 10, 64)
722 if err != nil {
723 continue
724 }
725
726 if i < 0 {
727 i += int64(len(items))
728 }
729
730 if 0 <= i && i < int64(len(items)) {
731 dest[int(i)] = struct{}{}
732 }
733 }
734 }
735
736 func writeValue(w *bufio.Writer, data any) error {
737 switch data := data.(type) {
738 case nil:
739 return writeKeyword(w, `null`)
740 case bool:
741 if data {
742 return writeKeyword(w, `true`)
743 }
744 return writeKeyword(w, `false`)
745 case json.Number:
746 if _, err := w.WriteString(data.String()); err != nil {
747 return io.EOF
748 }
749 return nil
750 case string:
751 return writeEscapedString(w, data)
752 case []any:
753 return writeArray(w, data)
754 case dictionary:
755 return writeObject(w, data)
756 default:
757 return errors.New(`invalid JSON value`)
758 }
759 }
760
761 func writeByte(w *bufio.Writer, b byte) error {
762 err := w.WriteByte(b)
763 if err != nil {
764 return io.EOF
765 }
766 return nil
767 }
768
769 func writeKeyword(w *bufio.Writer, s string) error {
770 if _, err := w.WriteString(s); err == nil {
771 return nil
772 }
773 return io.EOF
774 }
775
776 func writeEscapedString(w *bufio.Writer, s string) error {
777 if !needsEscaping(s) {
778 w.WriteByte('"')
779 w.WriteString(s)
780 return writeByte(w, '"')
781 }
782
783 w.WriteByte('"')
784
785 for _, r := range s {
786 if ' ' <= r && r <= '~' && r != '\\' && r != '"' {
787 w.WriteRune(r)
788 continue
789 }
790
791 switch r {
792 case '\\':
793 w.WriteString(`\\`)
794 case '"':
795 w.WriteString(`\"`)
796 default:
797 writeEscapedHex(w, r)
798 }
799 }
800
801 return writeByte(w, '"')
802 }
803
804 func writeEscapedHex(w *bufio.Writer, r rune) error {
805 w.WriteByte('\\')
806 w.WriteByte('u')
807 writeHex(w, byte(r>>24))
808 writeHex(w, byte(r>>16))
809 writeHex(w, byte(r>>8))
810 writeHex(w, byte(r))
811 return nil
812 }
813
814 // writeHex is faster than calling fmt.Fprintf(w, `%04x`, b)
815 func writeHex(w *bufio.Writer, b byte) {
816 const hexDigits = `0123456789abcdef`
817 w.WriteByte(hexDigits[b>>4])
818 w.WriteByte(hexDigits[b&0x0f])
819 }
820
821 func needsEscaping(s string) bool {
822 for i := range s {
823 if b := s[i]; ' ' <= b && b <= '~' && b != '\\' && b != '"' {
824 continue
825 }
826 return true
827 }
828
829 return false
830 }
831
832 func writeArray(w *bufio.Writer, items []any) error {
833 w.WriteByte('[')
834 for i, v := range items {
835 if i > 0 {
836 if err := writeByte(w, ','); err != nil {
837 return err
838 }
839 }
840 if err := writeValue(w, v); err != nil {
841 return err
842 }
843 }
844 return writeByte(w, ']')
845 }
846
847 func writeObject(w *bufio.Writer, items dictionary) error {
848 w.WriteByte('{')
849 for i, k := range items.Keys {
850 if i > 0 {
851 if err := writeByte(w, ','); err != nil {
852 return err
853 }
854 }
855 writeEscapedString(w, k)
856 w.WriteByte(':')
857 if err := writeValue(w, items.Map[k]); err != nil {
858 return err
859 }
860 }
861 return writeByte(w, '}')
862 }
863
864 func indexPair(s string, x byte, y byte) int {
865 var cur, prev byte
866
867 for i := range s {
868 cur = s[i]
869 if prev == x && cur == y && i > 0 {
870 return i
871 }
872 prev = cur
873 }
874
875 return -1
876 }
File: ./zj/zj_test.go
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2026 pacman64
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights to
9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 of the Software, and to permit persons to whom the Software is furnished to do
11 so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 package zj
26
27 import (
28 "io"
29 "strings"
30 "testing"
31 )
32
33 func TestZoomJson(t *testing.T) {
34 tests := map[string]struct {
35 input string
36 expected string
37 zoom string
38 }{
39 `no-zoom number`: {`123.456`, `123.456`, ``},
40 }
41
42 for name, tc := range tests {
43 t.Run(name, func(t *testing.T) {
44 data, err := load(strings.NewReader(tc.input))
45 if err != nil {
46 t.Error(err)
47 return
48 }
49
50 var avoid sets
51 avoid.indices = make(map[int]struct{})
52 avoid.keys = make(map[string]struct{})
53
54 var keys []string
55 if tc.zoom != `` {
56 keys = strings.Split(tc.zoom, ` `)
57 }
58
59 data, err = zoom(data, keys, &avoid)
60 if err != nil {
61 t.Error(err)
62 return
63 }
64
65 var sb strings.Builder
66 err = json0(&sb, data)
67 if err != nil && err != io.EOF {
68 t.Error(err)
69 return
70 }
71
72 got := sb.String()
73 got, _ = strings.CutSuffix(got, "\n")
74 if got != tc.expected {
75 t.Errorf(`expected %q but got %q instead`, tc.expected, got)
76 return
77 }
78 })
79 }
80 }