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 )
34
35 const info = `
36 avoid [options...] [regular expressions...]
37
38 Avoid/ignore lines which match any of the extended-mode regular expressions
39 given. When not given any regex, all empty lines are ignored by default.
40
41 The options are, available both in single and double-dash versions
42
43 -h, -help show this help message
44 -i, -ins match regexes case-insensitively
45 -q, -quiet never emit output: exit code still succeeds/fails
46 `
47
48 func Main() {
49 nerr := 0
50 buffered := false
51 sensitive := true
52 quiet := false
53 args := os.Args[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 `-i`, `--i`, `-ins`, `--ins`:
67 sensitive = false
68 args = args[1:]
69 continue
70
71 case `-q`, `--q`, `-quiet`, `--quiet`:
72 quiet = true
73 args = args[1:]
74 continue
75 }
76
77 break
78 }
79
80 if len(args) > 0 && args[0] == `--` {
81 args = args[1:]
82 }
83
84 liveLines := false
85 if !quiet {
86 liveLines = !buffered
87 if !buffered {
88 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
89 liveLines = false
90 }
91 }
92 }
93
94 if len(args) == 0 {
95 args = []string{`^$`}
96 }
97
98 exprs := make([]*regexp.Regexp, 0, len(args))
99
100 for _, src := range args {
101 var err error
102 var exp *regexp.Regexp
103 if !sensitive {
104 exp, err = regexp.Compile(`(?i)` + src)
105 } else {
106 exp, err = regexp.Compile(src)
107 }
108
109 if err != nil {
110 os.Stderr.WriteString(err.Error())
111 os.Stderr.WriteString("\n")
112 nerr++
113 }
114
115 exprs = append(exprs, exp)
116 }
117
118 if nerr > 0 {
119 os.Exit(1)
120 return
121 }
122
123 var buf []byte
124 sc := bufio.NewScanner(os.Stdin)
125 sc.Buffer(nil, 8*1024*1024*1024)
126 bw := bufio.NewWriter(os.Stdout)
127
128 ok := false
129
130 for i := 0; sc.Scan(); i++ {
131 line := sc.Bytes()
132 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
133 line = line[3:]
134 }
135
136 s := line
137 if bytes.IndexByte(s, '\x1b') >= 0 {
138 buf = plain(buf[:0], s)
139 s = buf
140 }
141
142 if !match(s, exprs) {
143 ok = true
144 if quiet {
145 continue
146 }
147
148 bw.Write(line)
149 bw.WriteByte('\n')
150
151 if !liveLines {
152 continue
153 }
154
155 if err := bw.Flush(); err != nil {
156 break
157 }
158 }
159 }
160
161 if !ok {
162 os.Exit(1)
163 return
164 }
165 }
166
167 func match(what []byte, with []*regexp.Regexp) bool {
168 for _, e := range with {
169 if e.Match(what) {
170 return true
171 }
172 }
173 return false
174 }
175
176 func plain(dst []byte, src []byte) []byte {
177 for len(src) > 0 {
178 i, j := indexEscapeSequence(src)
179 if i < 0 {
180 dst = append(dst, src...)
181 break
182 }
183 if j < 0 {
184 j = len(src)
185 }
186
187 if i > 0 {
188 dst = append(dst, src[:i]...)
189 }
190
191 src = src[j:]
192 }
193
194 return dst
195 }
196
197 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
198 // the multi-byte sequences starting with ESC[; the result is a pair of slice
199 // indices which can be independently negative when either the start/end of
200 // a sequence isn't found; given their fairly-common use, even the hyperlink
201 // ESC]8 sequences are supported
202 func indexEscapeSequence(s []byte) (int, int) {
203 var prev byte
204
205 for i, b := range s {
206 if prev == '\x1b' && b == '[' {
207 j := indexLetter(s[i+1:])
208 if j < 0 {
209 return i, -1
210 }
211 return i - 1, i + 1 + j + 1
212 }
213
214 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
215 j := indexPair(s[i+1:], '\x1b', '\\')
216 if j < 0 {
217 return i, -1
218 }
219 return i - 1, i + 1 + j + 2
220 }
221
222 prev = b
223 }
224
225 return -1, -1
226 }
227
228 func indexLetter(s []byte) int {
229 for i, b := range s {
230 upper := b &^ 32
231 if 'A' <= upper && upper <= 'Z' {
232 return i
233 }
234 }
235
236 return -1
237 }
238
239 func indexPair(s []byte, x byte, y byte) int {
240 var prev byte
241
242 for i, b := range s {
243 if prev == x && b == y && i > 0 {
244 return i
245 }
246 prev = b
247 }
248
249 return -1
250 }
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.NewWriter(os.Stdout)
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(err.Error())
72 os.Stderr.WriteString("\n")
73 os.Exit(1)
74 return
75 }
76 }
77
78 type handler func(w io.Writer, r io.Reader) error
79
80 func run(w io.Writer, paths []string, handle handler) error {
81 for _, path := range paths {
82 if err := handleFile(os.Stdout, path, handle); err != nil {
83 return err
84 }
85 }
86
87 if len(paths) == 0 {
88 if err := handle(os.Stdout, os.Stdin); err != nil {
89 return err
90 }
91 }
92
93 return nil
94 }
95
96 func handleFile(w io.Writer, path string, handle handler) error {
97 f, err := os.Open(path)
98 if err != nil {
99 return err
100 }
101 defer f.Close()
102 return handle(w, f)
103 }
104
105 func decode(w io.Writer, r io.Reader) error {
106 return debase64(w, r)
107 }
108
109 func encode(w io.Writer, r io.Reader) error {
110 enc := base64.NewEncoder(base64.StdEncoding, w)
111 _, err := io.Copy(enc, r)
112 enc.Close()
113
114 if err == nil {
115 w.Write([]byte{'\n'})
116 }
117 return err
118 }
119
120 // debase64 decodes base64 chunks explicitly, so decoding errors can be told
121 // apart from output-writing ones
122 func debase64(w io.Writer, r io.Reader) error {
123 br := bufio.NewReaderSize(r, 32*1024)
124 start, err := br.Peek(64)
125 if err != nil && err != io.EOF {
126 return err
127 }
128
129 skip, err := skipIntroDataURI(start)
130 if err != nil {
131 return err
132 }
133
134 if skip > 0 {
135 br.Discard(skip)
136 }
137
138 dec := base64.NewDecoder(base64.StdEncoding, br)
139 _, err = io.Copy(w, dec)
140 return err
141 }
142
143 func skipIntroDataURI(chunk []byte) (skip int, err error) {
144 if bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
145 chunk = chunk[3:]
146 skip += 3
147 }
148
149 if !bytes.HasPrefix(chunk, []byte(`data:`)) {
150 return skip, nil
151 }
152
153 const l = len(`data:,`)
154 if len(chunk) == l && chunk[l-1] == ',' {
155 return l, nil
156 }
157
158 start := chunk
159 if len(start) > 64 {
160 start = start[:64]
161 }
162
163 i := bytes.Index(start, []byte(`;base64,`))
164 if i < 0 {
165 return skip, errors.New(`invalid data URI`)
166 }
167
168 skip += i + len(`;base64,`)
169 return skip, nil
170 }
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(err.Error())
139 os.Stderr.WriteString("\n")
140 os.Exit(1)
141 return
142 }
143 }
144
145 type params struct {
146 offset *int64
147 emitOffset func(w *bufio.Writer, offset int64, sep byte)
148 separator byte
149 }
150
151 func run(w io.Writer, p params, args []string) error {
152 offset := int64(0)
153 p.offset = &offset
154 bw := bufio.NewWriter(w)
155 defer func() {
156 if offset%itemsPerLine == 0 && offset > 0 {
157 bw.WriteByte('\n')
158 }
159 bw.Flush()
160 }()
161
162 if len(args) == 0 {
163 return bitdump(bw, os.Stdin, p)
164 }
165
166 for _, name := range args {
167 if err := handleFile(bw, name, p); err != nil {
168 return err
169 }
170 }
171 return nil
172 }
173
174 func handleFile(w *bufio.Writer, name string, p params) error {
175 if name == `` || name == `-` {
176 return bitdump(w, os.Stdin, p)
177 }
178
179 f, err := os.Open(name)
180 if err != nil {
181 return errors.New(`can't read from file named "` + name + `"`)
182 }
183 defer f.Close()
184
185 return bitdump(w, f, p)
186 }
187
188 func bitdump(w *bufio.Writer, r io.Reader, p params) error {
189 var buf [32 * 1024]byte
190 defer w.Flush()
191
192 for {
193 n, err := r.Read(buf[:])
194 if n < 1 && err == io.EOF {
195 return nil
196 }
197
198 if err != nil && err != io.EOF {
199 return err
200 }
201
202 chunk := buf[:n]
203
204 for len(chunk) >= itemsPerLine {
205 if err := emitChunk(w, chunk[:itemsPerLine], p); err != nil {
206 return err
207 }
208
209 chunk = chunk[itemsPerLine:]
210 *p.offset += itemsPerLine
211 }
212
213 if len(chunk) > 0 {
214 if err := emitChunk(w, chunk, p); err != nil {
215 return err
216 }
217
218 *p.offset += int64(len(chunk))
219 }
220 }
221 }
222
223 func emitDecimalOffset(w *bufio.Writer, offset int64, sep byte) {
224 const pad = `00000000`
225 var str [24]byte
226
227 s := strconv.AppendInt(str[:0], offset, 10)
228 // pad small offsets with leading zeros
229 if len(s) < len(pad) {
230 w.WriteString(pad[len(s):])
231 }
232 w.Write(s)
233 w.WriteByte(sep)
234 }
235
236 func emitNoOffset(w *bufio.Writer, offset int64, sep byte) {
237 // deliberately does nothing
238 }
239
240 func emitChunk(w *bufio.Writer, chunk []byte, p params) error {
241 p.emitOffset(w, *p.offset, p.separator)
242 for i, b := range chunk {
243 if i > 0 {
244 w.WriteByte(p.separator)
245 }
246 w.WriteString(bits[b])
247 }
248
249 if err := w.WriteByte('\n'); err != nil {
250 // assume a write error is the consequence of stdout
251 // being closed, perhaps by another app along a pipe
252 return io.EOF
253 }
254 return nil
255 }
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(err.Error())
103 os.Stderr.WriteString("\n")
104 nerr++
105 }
106
107 exprs = append(exprs, exp)
108 }
109
110 if nerr > 0 {
111 os.Exit(1)
112 return
113 }
114
115 var buf []byte
116 sc := bufio.NewScanner(os.Stdin)
117 sc.Buffer(nil, 8*1024*1024*1024)
118 bw := bufio.NewWriter(os.Stdout)
119
120 for i := 0; sc.Scan(); i++ {
121 line := sc.Bytes()
122 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
123 line = line[3:]
124 }
125
126 s := line
127 if bytes.IndexByte(s, '\x1b') >= 0 {
128 buf = plain(buf[:0], s)
129 s = buf
130 }
131
132 for len(s) > 0 {
133 start, end := findEarliest(s, exprs)
134 if start < 0 {
135 if err := emit(bw, s, liveLines); err != nil {
136 return
137 }
138 break
139 }
140
141 if err := emit(bw, s[:start], liveLines); err != nil {
142 return
143 }
144 s = s[end:]
145 }
146 }
147 }
148
149 func findEarliest(s []byte, exprs []*regexp.Regexp) (start int, end int) {
150 start = -1
151 end = -1
152
153 for _, e := range exprs {
154 m := e.FindIndex(s)
155 if len(m) == 2 && m[0] != m[1] && (m[0] < start || start < 0) {
156 start = m[0]
157 end = m[1]
158 }
159 }
160
161 return start, end
162 }
163
164 func emit(w *bufio.Writer, line []byte, live bool) error {
165 w.Write(line)
166 w.WriteByte('\n')
167
168 if !live {
169 return nil
170 }
171
172 return w.Flush()
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(err.Error())
138 os.Stderr.WriteString("\n")
139 os.Exit(1)
140 return
141 }
142 }
143
144 func run(w io.Writer, args []string, cfg config) error {
145 for _, src := range args {
146 src = strings.ToLower(src)
147
148 // treat square brackets like parentheses, for convenience
149 src = strings.Replace(src, `[`, `(`, -1)
150 src = strings.Replace(src, `]`, `)`, -1)
151
152 if cfg.verbose {
153 fmt.Fprintf(os.Stderr, "\x1b[7m%s\x1b[0m\n", src)
154 }
155
156 expr, err := parser.ParseExpr(src)
157 if err != nil {
158 return fmt.Errorf("%s: %s", src, err.Error())
159 }
160
161 n, err := eval(expr)
162 if err != nil {
163 return err
164 }
165
166 // only show the numerator, when the denominator is 1; when showing
167 // results as numbers with decimals, all trailing zero decimals are
168 // ignored
169 s := ``
170 if n.IsInt() {
171 s = n.Num().String()
172 } else if cfg.showFractions {
173 s = n.String()
174 } else {
175 s = trimDecimals(n.FloatString(100))
176 }
177
178 io.WriteString(w, s)
179 _, err = io.WriteString(w, "\n")
180
181 if err != nil {
182 break
183 }
184 }
185
186 return nil
187 }
188
189 // trimDecimals ignores excessive trailing decimal zeros, if any, as well as
190 // the decimal dot itself, if all decimals turn out to be zeros; integers are
191 // returned as given
192 func trimDecimals(s string) string {
193 // with no decimals, keep all/any trailing zeros
194 if strings.IndexByte(s, '.') < 0 {
195 return s
196 }
197
198 // ignore all trailing zero decimals
199 for len(s) > 0 && s[len(s)-1] == '0' {
200 s = s[:len(s)-1]
201 }
202 // ignore trailing decimal
203 if len(s) > 0 && s[len(s)-1] == '.' {
204 s = s[:len(s)-1]
205 }
206 return s
207 }
208
209 func eval(expr ast.Expr) (*big.Rat, error) {
210 switch expr := expr.(type) {
211 case *ast.BasicLit:
212 return evalLit(expr)
213 case *ast.ParenExpr:
214 return eval(expr.X)
215 case *ast.UnaryExpr:
216 return evalUnary(expr)
217 case *ast.SelectorExpr:
218 return evalSelector(expr)
219 case *ast.BinaryExpr:
220 return evalBinary(expr)
221 case *ast.CallExpr:
222 return evalCall(expr)
223 case *ast.Ident:
224 return evalIdent(expr)
225 }
226
227 return nil, errors.New(`unsupported expression type`)
228 }
229
230 func evalLit(expr *ast.BasicLit) (*big.Rat, error) {
231 switch expr.Kind {
232 case token.INT, token.FLOAT:
233 n := big.NewRat(0, 1)
234 n, _ = n.SetString(expr.Value)
235 return n, nil
236 }
237
238 return nil, errors.New(`unsupported literal type`)
239 }
240
241 func evalSelector(expr *ast.SelectorExpr) (*big.Rat, error) {
242 name := expr.Sel.Name
243 call := ast.CallExpr{
244 Fun: ast.NewIdent(name),
245 Args: []ast.Expr{expr.X},
246 }
247 return evalCall1(name, &call)
248 }
249
250 func evalUnary(expr *ast.UnaryExpr) (*big.Rat, error) {
251 switch expr.Op {
252 case token.ADD:
253 return eval(expr.X)
254
255 case token.SUB:
256 n, err := eval(expr.X)
257 if n != nil {
258 n = n.Neg(n)
259 }
260 return n, err
261
262 case token.NOT:
263 return eval(&ast.CallExpr{
264 Fun: ast.NewIdent(`factorial`),
265 Args: []ast.Expr{expr.X},
266 })
267 }
268
269 return nil, errors.New(`unsupported unary operation ` + expr.Op.String())
270 }
271
272 func evalBinary(expr *ast.BinaryExpr) (*big.Rat, error) {
273 x, err := eval(expr.X)
274 if err != nil {
275 return nil, err
276 }
277
278 y, err := eval(expr.Y)
279 if err != nil {
280 return nil, err
281 }
282
283 z := big.NewRat(0, 1)
284
285 switch expr.Op {
286 case token.ADD:
287 z = z.Add(x, y)
288 return z, nil
289
290 case token.SUB:
291 z = z.Sub(x, y)
292 return z, nil
293
294 case token.MUL:
295 z = z.Mul(x, y)
296 return z, nil
297
298 case token.QUO:
299 if y.Sign() == 0 {
300 return nil, errors.New(`can't divide by zero`)
301 }
302 z = z.Quo(x, y)
303 return z, nil
304
305 case token.REM:
306 return remainder(x, y)
307 }
308
309 return nil, errors.New(`unsupported binary operation ` + expr.Op.String())
310 }
311
312 func evalCall(expr *ast.CallExpr) (*big.Rat, error) {
313 ident, ok := expr.Fun.(*ast.Ident)
314 if !ok {
315 return nil, errors.New(`unsupported function type`)
316 }
317 s := ident.Name
318
319 if _, ok := varFuncs[s]; ok {
320 return evalVarCall(s, expr)
321 }
322
323 switch len(expr.Args) {
324 case 1:
325 return evalCall1(s, expr)
326 case 2:
327 return evalCall2(s, expr)
328 case 3:
329 return evalCall3(s, expr)
330 }
331
332 return nil, errors.New(`function '` + s + `' not available`)
333 }
334
335 func evalIdent(expr *ast.Ident) (*big.Rat, error) {
336 s := strings.ToLower(expr.Name)
337 if v, ok := values[s]; ok {
338 if f, ok := big.NewRat(0, 1).SetString(v); ok {
339 return f, nil
340 }
341 return nil, errors.New(`value '` + s + `' isn't a valid number`)
342 }
343 return nil, errors.New(`value '` + s + `' not available`)
344 }
345
346 func copyFrac(x *big.Rat) *big.Rat {
347 y := big.NewRat(0, 1)
348 y = y.Add(y, x)
349 return y
350 }
351
352 var values = map[string]string{
353 `kb`: `1024`,
354 `mb`: `1048576`,
355 `gb`: `1073741824`,
356 `tb`: `1099511627776`,
357 `pb`: `1125899906842624`,
358 `kib`: `1024`,
359 `mib`: `1048576`,
360 `gib`: `1073741824`,
361 `tib`: `1099511627776`,
362 `pib`: `1125899906842624`,
363
364 `hour`: `3600`,
365 `hr`: `3600`,
366 `day`: `86400`,
367 `week`: `604800`,
368 `wk`: `604800`,
369
370 `mol`: `602214076000000000000000`,
371 `mole`: `602214076000000000000000`,
372 }
373
374 var funcs1 = map[string]func(*big.Rat) (*big.Rat, error){
375 `abs`: abs,
376 `bits`: bits,
377 `ceil`: ceiling,
378 `ceiling`: ceiling,
379 `den`: denominator,
380 `denom`: denominator,
381 `denominator`: denominator,
382 `digits`: digits,
383 `f`: factorial,
384 `fac`: factorial,
385 `fact`: factorial,
386 `factorial`: factorial,
387 `floor`: floor,
388 `isprime`: isPrime,
389 `prime`: isPrime,
390 `num`: numerator,
391 `numer`: numerator,
392 `numerator`: numerator,
393 `pow2`: power2,
394 `power2`: power2,
395 `pow10`: power10,
396 `power10`: power10,
397 `sgn`: sign,
398 `sign`: sign,
399 }
400
401 func evalCall1(name string, expr *ast.CallExpr) (*big.Rat, error) {
402 x, err := eval(expr.Args[0])
403 if err != nil {
404 return nil, err
405 }
406
407 fn, ok := funcs1[name]
408 if !ok {
409 return nil, errors.New(`function '` + name + `' not available`)
410 }
411
412 return fn(x)
413 }
414
415 var funcs2 = map[string]func(*big.Rat, *big.Rat) (*big.Rat, error){
416 `c`: combinations,
417 `com`: combinations,
418 `comb`: combinations,
419 `combinations`: combinations,
420 `choose`: combinations,
421 `gcd`: gcd,
422 `gcf`: gcd,
423 `lcm`: lcm,
424 `p`: permutations,
425 `per`: permutations,
426 `perm`: permutations,
427 `permutations`: permutations,
428 `pow`: power,
429 `power`: power,
430 `rem`: remainder,
431 `remainder`: remainder,
432 }
433
434 func evalCall2(name string, expr *ast.CallExpr) (*big.Rat, error) {
435 x, err := eval(expr.Args[0])
436 if err != nil {
437 return nil, err
438 }
439
440 y, err := eval(expr.Args[1])
441 if err != nil {
442 return nil, err
443 }
444
445 fn, ok := funcs2[name]
446 if !ok {
447 return nil, errors.New(`function '` + name + `' not available`)
448 }
449
450 return fn(x, y)
451 }
452
453 var funcs3 = map[string]func(*big.Rat, *big.Rat, *big.Rat) (*big.Rat, error){
454 `db`: dbinom,
455 `dbin`: dbinom,
456 `dbinom`: dbinom,
457 }
458
459 func evalCall3(name string, expr *ast.CallExpr) (*big.Rat, error) {
460 x, err := eval(expr.Args[0])
461 if err != nil {
462 return nil, err
463 }
464
465 y, err := eval(expr.Args[1])
466 if err != nil {
467 return nil, err
468 }
469
470 z, err := eval(expr.Args[2])
471 if err != nil {
472 return nil, err
473 }
474
475 fn, ok := funcs3[name]
476 if !ok {
477 return nil, errors.New(`function '` + name + `' not available`)
478 }
479
480 return fn(x, y, z)
481 }
482
483 var varFuncs = map[string]func(...*big.Rat) (*big.Rat, error){
484 `avg`: avgNum,
485 `horner`: polyval,
486 `max`: maxNum,
487 `mean`: avgNum,
488 `min`: minNum,
489 `polyval`: polyval,
490 `sum`: sumNum,
491 }
492
493 func evalVarCall(name string, expr *ast.CallExpr) (*big.Rat, error) {
494 fn, ok := varFuncs[name]
495 if !ok {
496 return nil, errors.New(`function '` + name + `' not available`)
497 }
498
499 inputs := make([]*big.Rat, 0, len(expr.Args))
500 for _, a := range expr.Args {
501 v, err := eval(a)
502 if err != nil {
503 return nil, err
504 }
505 inputs = append(inputs, v)
506 }
507
508 return fn(inputs...)
509 }
510
511 func abs(n *big.Rat) (*big.Rat, error) {
512 n = n.Abs(n)
513 return n, nil
514 }
515
516 func avgNum(values ...*big.Rat) (*big.Rat, error) {
517 if len(values) == 0 {
518 return nil, errors.New(`mean: no numbers given`)
519 }
520
521 res := big.NewRat(0, 1)
522 for _, v := range values {
523 res = res.Add(res, v)
524 }
525 res = res.Quo(res, big.NewRat(int64(len(values)), 1))
526 return res, nil
527 }
528
529 func bits(n *big.Rat) (*big.Rat, error) {
530 if !n.IsInt() {
531 return nil, errors.New(`function 'bits' only works with integers`)
532 }
533
534 bits := big.NewRat(0, 1)
535 bits.SetInt64(int64(n.Num().BitLen()))
536 return bits, nil
537 }
538
539 func ceiling(n *big.Rat) (*big.Rat, error) {
540 if n.IsInt() {
541 return n, nil
542 }
543
544 v := big.NewInt(0)
545 v = v.Quo(n.Num(), n.Denom())
546 if n.Sign() >= 0 {
547 v = v.Add(v, big.NewInt(1))
548 }
549 n = n.SetInt(v)
550 return n, nil
551 }
552
553 func combinations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
554 if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
555 const msg = `combinations are defined only for non-negative integers`
556 return nil, errors.New(msg)
557 }
558
559 v, err := permutations(n, k)
560 if err != nil {
561 return v, err
562 }
563
564 f, err := factorial(k)
565 if err != nil {
566 return nil, err
567 }
568
569 if f.Sign() <= 0 {
570 return nil, errors.New(`combinations: factorial isn't positive`)
571 }
572 return v.Quo(v, f), nil
573 }
574
575 func dbinom(x *big.Rat, n *big.Rat, p *big.Rat) (*big.Rat, error) {
576 a, err := combinations(copyFrac(n), copyFrac(x))
577 if err != nil {
578 return nil, err
579 }
580
581 b, err := power(copyFrac(p), copyFrac(x))
582 if err != nil {
583 return nil, err
584 }
585
586 // c = (1 - p) ** (n - x)
587 y := big.NewRat(1, 1)
588 y = y.Sub(y, p)
589 z := copyFrac(n)
590 z = z.Sub(z, x)
591 c, err := power(y, z)
592 if err != nil {
593 return nil, err
594 }
595
596 // return combinations(n, x) * (p ** x) * ((1 - p) ** (n - x))
597 d := big.NewRat(0, 1)
598 d = d.Add(d, a)
599 d = d.Mul(d, b)
600 d = d.Mul(d, c)
601 return d, nil
602 }
603
604 func denominator(n *big.Rat) (*big.Rat, error) {
605 return big.NewRat(0, 1).SetFrac(n.Denom(), big.NewInt(1)), nil
606 }
607
608 func digits(n *big.Rat) (*big.Rat, error) {
609 if !n.IsInt() {
610 return nil, errors.New(`function 'digits' only works with integers`)
611 }
612
613 digits := big.NewRat(0, 1)
614 digits.SetInt64(int64(len(n.Num().String())))
615 return digits, nil
616 }
617
618 func factorial(n *big.Rat) (*big.Rat, error) {
619 sign := n.Sign()
620 if sign < 0 {
621 return nil, errors.New(`factorials aren't defined for negatives`)
622 }
623 if sign == 0 {
624 return big.NewRat(1, 1), nil
625 }
626
627 f := big.NewRat(1, 1)
628 for one := big.NewRat(1, 1); n.Sign() > 0; n = n.Sub(n, one) {
629 f = f.Mul(f, n)
630 }
631 return f, nil
632 }
633
634 func floor(n *big.Rat) (*big.Rat, error) {
635 if n.IsInt() {
636 return n, nil
637 }
638
639 v := big.NewInt(0)
640 v = v.Quo(n.Num(), n.Denom())
641 if n.Sign() < 0 {
642 v = v.Sub(v, big.NewInt(1))
643 }
644 n = n.SetInt(v)
645 return n, nil
646 }
647
648 func gcd(x *big.Rat, y *big.Rat) (*big.Rat, error) {
649 if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
650 const msg = `gcd are defined only for positive integers`
651 return nil, errors.New(msg)
652 }
653
654 gcd := big.NewRat(0, 1)
655 gcd = gcd.Add(gcd, x)
656 gcd = gcd.Mul(gcd, y)
657
658 lcm, err := lcm(x, y)
659 if err != nil {
660 return nil, err
661 }
662 if lcm.Sign() <= 0 {
663 return nil, errors.New(`gcd: lcm isn't positive`)
664 }
665
666 gcd = gcd.Quo(gcd, lcm)
667 return gcd, nil
668 }
669
670 func isPrime(n *big.Rat) (*big.Rat, error) {
671 if !n.IsInt() {
672 return nil, errors.New(`function 'isprime' only works with integers`)
673 }
674
675 if n.Sign() <= 0 {
676 return big.NewRat(0, 1), nil
677 }
678
679 v := n.Num()
680 if v.IsInt64() {
681 n := v.Int64()
682 if n == 2 {
683 return big.NewRat(1, 1), nil
684 }
685 if n < 2 || n%2 == 0 {
686 return big.NewRat(0, 1), nil
687 }
688 }
689
690 two := big.NewInt(2)
691 max := big.NewInt(1).Sqrt(v)
692 mod := big.NewInt(0)
693 for i := big.NewInt(3); i.Cmp(max) <= 0; i = i.Add(i, two) {
694 mod = mod.Rem(v, i)
695 if mod.Sign() == 0 {
696 return big.NewRat(0, 1), nil
697 }
698 }
699 return big.NewRat(1, 1), nil
700 }
701
702 func lcm(x *big.Rat, y *big.Rat) (*big.Rat, error) {
703 if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
704 const msg = `lcm is defined only for positive integers`
705 return nil, errors.New(msg)
706 }
707
708 // a = min(x, y)
709 // b = max(x, y)
710 var a, b *big.Int
711 if x.Cmp(y) < 0 {
712 a = x.Num()
713 b = y.Num()
714 } else {
715 a = y.Num()
716 b = x.Num()
717 }
718
719 // c = b
720 c := big.NewInt(0)
721 c = c.Add(c, b)
722
723 // while (c % a > 0) c += b
724 for r := big.NewInt(1); r.Sign() > 0; r = r.Rem(c, a) {
725 c = c.Add(c, b)
726 }
727
728 // return c
729 return big.NewRat(0, 1).SetFrac(c, big.NewInt(1)), nil
730 }
731
732 func maxNum(values ...*big.Rat) (*big.Rat, error) {
733 if len(values) == 0 {
734 return nil, errors.New(`max: no numbers given`)
735 }
736
737 var max *big.Rat
738 for i, v := range values {
739 if i == 0 || max.Cmp(v) < 0 {
740 max = v
741 }
742 }
743 return max, nil
744 }
745
746 func minNum(values ...*big.Rat) (*big.Rat, error) {
747 if len(values) == 0 {
748 return nil, errors.New(`min: no numbers given`)
749 }
750
751 var min *big.Rat
752 for i, v := range values {
753 if i == 0 || min.Cmp(v) < 0 {
754 min = v
755 }
756 }
757 return min, nil
758 }
759
760 func numerator(n *big.Rat) (*big.Rat, error) {
761 return big.NewRat(0, 1).SetFrac(n.Num(), big.NewInt(1)), nil
762 }
763
764 func permutations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
765 if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
766 const msg = `permutations are defined only for non-negative integers`
767 return nil, errors.New(msg)
768 }
769
770 one := big.NewRat(1, 1)
771 perm := big.NewRat(1, 1)
772 // end = n - k + 1
773 end := big.NewRat(1, 1).Set(n)
774 end = end.Sub(end, k)
775 end = end.Add(end, one)
776
777 for v := big.NewRat(1, 1).Set(n); v.Cmp(end) >= 0; v = v.Sub(v, one) {
778 perm = perm.Mul(perm, v)
779 }
780 return perm, nil
781 }
782
783 // polyval evaluates a polynomial using Horner's algorithm: the first number is
784 // the x value to evaulate the polynomial with, followed by all the polynomial
785 // coefficients in textbook order, from the highest power down to the final
786 // constant
787 func polyval(values ...*big.Rat) (*big.Rat, error) {
788 if len(values) == 0 {
789 // return big.NewRat(0, 1), nil
790 return nil, errors.New(`polyval: no numbers given`)
791 }
792
793 x0 := values[0]
794 values = values[1:]
795
796 x := big.NewRat(1, 1)
797 y := big.NewRat(0, 1)
798 prod := big.NewRat(0, 1)
799
800 for i := len(values) - 1; i >= 0; i-- {
801 prod = prod.Mul(values[i], x)
802 y = y.Add(y, prod)
803 x = x.Mul(x, x0)
804 }
805
806 return y, nil
807 }
808
809 func power(x *big.Rat, y *big.Rat) (*big.Rat, error) {
810 // if !y.IsInt() {
811 // return nil, errors.New(`only integer exponents are supported`)
812 // }
813
814 if !y.IsInt() {
815 a, _ := x.Float64()
816 b, _ := y.Float64()
817 c := math.Pow(a, b)
818 if math.IsNaN(c) || math.IsInf(c, 0) {
819 return nil, errors.New(`can't calculate/approximate power given`)
820 }
821 z := big.NewRat(0, 1)
822 z = z.SetFloat64(c)
823 return z, nil
824 }
825
826 if x.Sign() == 0 && y.Sign() == 0 {
827 return nil, errors.New(`zero to the zero power isn't defined`)
828 }
829
830 if x.Sign() == 0 {
831 return big.NewRat(0, 1), nil
832 }
833 if y.Sign() == 0 {
834 return big.NewRat(1, 1), nil
835 }
836
837 return powFractionInPlace(x, y.Num())
838 }
839
840 // powFractionInPlace calculates values in place: since bignums are pointers
841 // to their representations, this means the original values will change
842 func powFractionInPlace(x *big.Rat, y *big.Int) (*big.Rat, error) {
843 xsign := x.Sign()
844 ysign := y.Sign()
845
846 // 0 ** 0 is undefined
847 if xsign == 0 && ysign == 0 {
848 const msg = `0 to the 0 doesn't make sense`
849 return nil, errors.New(msg)
850 }
851
852 // otherwise x ** 0 is 1
853 if ysign == 0 {
854 return big.NewRat(1, 1), nil
855 }
856
857 // x ** (y < 0) is like (1/x) ** -y
858 if ysign < 0 {
859 inv := big.NewRat(1, 1).Inv(x)
860 neg := big.NewInt(1).Neg(y)
861 return powFractionInPlace(inv, neg)
862 }
863
864 // 0 ** (y > 0) is 0
865 if xsign == 0 {
866 return x, nil
867 }
868
869 // x ** 0 is 0
870 if ysign == 0 {
871 return big.NewRat(0, 1), nil
872 }
873
874 // x ** 1 is x
875 if y.IsInt64() && y.Int64() == 1 {
876 return x, nil
877 }
878
879 return _powFractionRec(x, y), nil
880 }
881
882 func _powFractionRec(x *big.Rat, y *big.Int) *big.Rat {
883 switch y.Sign() {
884 case -1:
885 return big.NewRat(0, 1)
886 case 0:
887 return big.NewRat(1, 1)
888 case 1:
889 if y.IsInt64() && y.Int64() == 1 {
890 return x
891 }
892 }
893
894 yhalf := big.NewInt(0)
895 oddrem := big.NewInt(0)
896 yhalf.QuoRem(y, big.NewInt(2), oddrem)
897
898 if oddrem.Sign() == 0 {
899 xsquare := big.NewRat(0, 1)
900 return _powFractionRec(xsquare.Mul(x, x), yhalf)
901 }
902 prevpow := _powFractionRec(x, y.Sub(y, big.NewInt(1)))
903 return prevpow.Mul(prevpow, x)
904 }
905
906 func power2(x *big.Rat) (*big.Rat, error) {
907 return power(big.NewRat(2, 1), x)
908 }
909
910 func power10(x *big.Rat) (*big.Rat, error) {
911 return power(big.NewRat(10, 1), x)
912 }
913
914 func remainder(x *big.Rat, y *big.Rat) (*big.Rat, error) {
915 if !x.IsInt() || !y.IsInt() {
916 return nil, errors.New(`remainder only works with 2 integers`)
917 }
918
919 if y.Sign() == 0 {
920 return nil, errors.New(`can't divide by 0`)
921 }
922
923 a := x.Num()
924 b := y.Num()
925 c := big.NewInt(0)
926 c = c.Rem(a, b)
927 rem := big.NewRat(0, 1)
928 rem = rem.SetInt(c)
929 return rem, nil
930 }
931
932 func sign(n *big.Rat) (*big.Rat, error) {
933 sign := n.Sign()
934 if sign > 0 {
935 n = big.NewRat(1, 1)
936 } else if sign < 0 {
937 n = big.NewRat(-1, 1)
938 } else {
939 n = big.NewRat(0, 1)
940 }
941 return n, nil
942 }
943
944 func sumNum(values ...*big.Rat) (*big.Rat, error) {
945 sum := big.NewRat(0, 1)
946 for _, v := range values {
947 sum = sum.Add(sum, v)
948 }
949 return sum, nil
950 }
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(err.Error())
65 os.Stderr.WriteString("\n")
66 os.Exit(1)
67 return
68 }
69 }
70
71 if len(args) == 0 {
72 cat(os.Stdout, os.Stdin)
73 }
74 }
75
76 func handleFile(w io.Writer, path string) error {
77 f, err := os.Open(path)
78 if err != nil {
79 return err
80 }
81 defer f.Close()
82 return cat(w, f)
83 }
84
85 func cat(w io.Writer, r io.Reader) error {
86 var buf [32 * 1024]byte
87
88 for {
89 got, err := r.Read(buf[:])
90 if err == io.EOF {
91 if got > 0 {
92 w.Write(buf[:got])
93 }
94 break
95 }
96
97 if err != nil {
98 return err
99 }
100
101 if _, err := w.Write(buf[:got]); err != nil {
102 return io.EOF
103 }
104 }
105
106 return nil
107 }
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(err.Error())
93 os.Stderr.WriteString("\n")
94 os.Exit(1)
95 return
96 }
97 }
98
99 func run(w io.Writer, args []string, cfg config) error {
100 bw := bufio.NewWriter(w)
101 defer bw.Flush()
102
103 dashes := 0
104 for _, name := range args {
105 if name == `-` {
106 dashes++
107 }
108 if dashes > 1 {
109 break
110 }
111 }
112
113 if len(args) == 0 {
114 return catl(bw, os.Stdin, cfg)
115 }
116
117 var stdin []byte
118 gotStdin := false
119
120 for _, name := range args {
121 if name == `-` {
122 if dashes == 1 {
123 if err := catl(bw, os.Stdin, cfg); err != nil {
124 return err
125 }
126 continue
127 }
128
129 if !gotStdin {
130 data, err := io.ReadAll(os.Stdin)
131 if err != nil {
132 return err
133 }
134 stdin = data
135 gotStdin = true
136 }
137
138 bw.Write(stdin)
139 if len(stdin) > 0 && stdin[len(stdin)-1] != '\n' {
140 bw.WriteByte('\n')
141 }
142
143 if !cfg.liveLines {
144 continue
145 }
146
147 if err := bw.Flush(); err != nil {
148 return io.EOF
149 }
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 catl(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 catl(w, f, cfg)
173 }
174
175 func catl(w *bufio.Writer, r io.Reader, cfg config) error {
176 if !cfg.liveLines {
177 return catlFast(w, r, cfg.null)
178 }
179
180 const gb = 1024 * 1024 * 1024
181 sc := bufio.NewScanner(r)
182 sc.Buffer(nil, 8*gb)
183 if cfg.null {
184 sc.Split(splitNull)
185 }
186
187 for i := 0; sc.Scan(); i++ {
188 s := sc.Bytes()
189 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
190 s = s[3:]
191 }
192
193 w.Write(s)
194 if w.WriteByte('\n') != nil {
195 return io.EOF
196 }
197
198 if w.Flush() != nil {
199 return io.EOF
200 }
201 }
202
203 return sc.Err()
204 }
205
206 func catlFast(w *bufio.Writer, r io.Reader, null bool) error {
207 var buf [32 * 1024]byte
208 var last byte = '\n'
209
210 for i := 0; true; i++ {
211 n, err := r.Read(buf[:])
212 if n > 0 && err == io.EOF {
213 err = nil
214 }
215 if err == io.EOF {
216 if last != '\n' {
217 w.WriteByte('\n')
218 }
219 return nil
220 }
221
222 if err != nil {
223 return err
224 }
225
226 chunk := buf[:n]
227 if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
228 chunk = chunk[3:]
229 }
230
231 // change nulls into line-feeds to handle null-terminated lines
232 if null {
233 for i, b := range chunk {
234 if b == 0 {
235 chunk[i] = '\n'
236 }
237 }
238 }
239
240 if len(chunk) >= 1 {
241 if _, err := w.Write(chunk); err != nil {
242 return io.EOF
243 }
244 last = chunk[len(chunk)-1]
245 }
246 }
247
248 return nil
249 }
250
251 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
252 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
253 // handle leading null-terminated line, if found in the current chunk
254 if i := bytes.IndexByte(data, 0); i >= 0 {
255 return i + 1, data[:i], nil
256 }
257
258 // request more data, in case there's a null coming up later
259 if !atEOF {
260 return 0, nil, nil
261 }
262
263 // handle non-empty non-terminated last chunk
264 if len(data) > 0 {
265 return len(data), data, bufio.ErrFinalToken
266 }
267
268 // handle empty non-terminated last chunk
269 return 0, nil, bufio.ErrFinalToken
270 }
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 {
56 return nil
57 }
58
59 if w.Flush() != nil {
60 return io.EOF
61 }
62 return nil
63 }
64
65 type state struct {
66 // lines remembers all input lines so far, since finding common leads
67 // may require knowning all input lines, in general
68 lines []string
69
70 // lead is the current (leading) common-lead among lines seen so far
71 lead string
72
73 // nothing remembers if there's no common lead for sure
74 nothing bool
75 }
76
77 // dump emits all lines kept so far, then forgets about them
78 func (s *state) dump(w *bufio.Writer) error {
79 for _, s := range s.lines {
80 w.WriteString(s)
81 if w.WriteByte('\n') != nil {
82 return io.EOF
83 }
84 }
85
86 // forgetting the lines may save some memory, with enough lines
87 s.lines = nil
88
89 return nil
90 }
91
92 func Main() {
93 var cfg config
94 cfg.liveLines = true
95 args := os.Args[1:]
96
97 for len(args) > 0 {
98 switch args[0] {
99 case `-b`, `--b`, `-buffered`, `--buffered`:
100 cfg.liveLines = false
101 args = args[1:]
102 continue
103
104 case `-h`, `--h`, `-help`, `--help`:
105 os.Stdout.WriteString(info[1:])
106 return
107 }
108
109 break
110 }
111
112 if len(args) > 0 && args[0] == `--` {
113 args = args[1:]
114 }
115
116 if cfg.liveLines {
117 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
118 cfg.liveLines = false
119 }
120 }
121
122 if err := run(args, &cfg); err != nil && err != io.EOF {
123 os.Stderr.WriteString(err.Error())
124 os.Stderr.WriteString("\n")
125 os.Exit(1)
126 return
127 }
128 }
129
130 func run(paths []string, cfg *config) error {
131 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
132 defer bw.Flush()
133
134 for _, p := range paths {
135 if err := handleFile(bw, p, cfg); err != nil {
136 return err
137 }
138 }
139
140 if len(paths) == 0 {
141 if err := handleReader(bw, os.Stdin, cfg); err != nil {
142 return err
143 }
144 }
145
146 if cfg.nothing {
147 return nil
148 }
149
150 skip := len(cfg.lead)
151 // with only 1 line of input, avoid emitting a single empty line for sure
152 if len(cfg.lines) == 1 {
153 skip = 0
154 }
155
156 for _, s := range cfg.lines {
157 bw.WriteString(s[skip:])
158 if bw.WriteByte('\n') != nil {
159 return io.EOF
160 }
161 }
162 return nil
163 }
164
165 func handleFile(w *bufio.Writer, path string, cfg *config) error {
166 f, err := os.Open(path)
167 if err != nil {
168 // on windows, file-not-found error messages may mention `CreateFile`,
169 // even when trying to open files in read-only mode
170 return errors.New(`can't open file named ` + path)
171 }
172 defer f.Close()
173 return handleReader(w, f, cfg)
174 }
175
176 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
177 const gb = 1024 * 1024 * 1024
178 sc := bufio.NewScanner(r)
179 sc.Buffer(nil, 8*gb)
180
181 for i := 0; sc.Scan(); i++ {
182 s := sc.Text()
183 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
184 s = s[3:]
185 }
186
187 // when no common lead is possible, show lines as they come and as
188 // they are; also don't bother remembering them, thus saving memory
189 if cfg.nothing {
190 w.WriteString(s)
191 if w.WriteByte('\n') != nil {
192 return io.EOF
193 }
194
195 if err := cfg.maybeFlush(w); err != nil {
196 return err
197 }
198 continue
199 }
200
201 cfg.lines = append(cfg.lines, s)
202
203 if len(s) == 0 {
204 // empty lines don't count for common leads
205 continue
206 }
207
208 // the full first non-empty line automatically becomes the lead
209 if len(cfg.lead) == 0 {
210 cfg.lead = s
211 }
212
213 // the common lead can only be as long as the shortest non-empty line
214 if len(cfg.lead) > len(s) {
215 cfg.lead = cfg.lead[:len(s)]
216 }
217
218 // avoid lead-shrink loop in case leading byte is different, which
219 // by itself implies there's no common lead
220 if len(s) > 0 && len(cfg.lead) > 0 && s[0] != cfg.lead[0] {
221 if err := cfg.dump(w); err != nil {
222 return err
223 }
224 if err := cfg.maybeFlush(w); err != nil {
225 return err
226 }
227 cfg.nothing = true
228 continue
229 }
230
231 // shrink common lead from the end
232 for len(cfg.lead) > 0 && !strings.HasPrefix(s, cfg.lead) {
233 cfg.lead = cfg.lead[:len(cfg.lead)-1]
234 continue
235 }
236
237 // if previous loop ends with an empty string: there's no common lead
238 if len(cfg.lead) == 0 {
239 if err := cfg.dump(w); err != nil {
240 return err
241 }
242 if err := cfg.maybeFlush(w); err != nil {
243 return err
244 }
245 cfg.nothing = true
246 continue
247 }
248 }
249
250 return sc.Err()
251 }
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.NewWriter(w)
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(err.Error())
246 os.Stderr.WriteString("\n")
247 }
248
249 // showResult shows a TSV line for results marked as successful, doing nothing
250 // when given other types of results
251 func showResult(w *bufio.Writer, s stats) error {
252 if s.result != resultSuccess {
253 return nil
254 }
255
256 var buf [24]byte
257 w.WriteString(s.name)
258 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.bytes), 10))
259 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.lines), 10))
260 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.lf), 10))
261 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.crlf), 10))
262 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.spaces), 10))
263 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.tabs), 10))
264 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.trailing), 10))
265 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.nulls), 10))
266 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.fulls), 10))
267 w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.highs), 10))
268 w.WriteByte('\t')
269 w.WriteString(bomLegend[s.bom])
270 return w.WriteByte('\n')
271 }
272
273 // findAllFiles can be given a mix of file/folder paths, finding all files
274 // recursively in folders, avoiding duplicates
275 func findAllFiles(paths []string) (files []string, success bool) {
276 walk := filepath.WalkDir
277 got := make(map[string]struct{})
278 success = true
279
280 for _, path := range paths {
281 if _, ok := got[path]; ok {
282 continue
283 }
284 got[path] = struct{}{}
285
286 // a dash means standard input
287 if path == `-` {
288 files = append(files, path)
289 continue
290 }
291
292 info, err := os.Stat(path)
293 if os.IsNotExist(err) {
294 // on windows, file-not-found messages may mention `CreateFile`,
295 // even when trying to open files in read-only mode
296 err = errors.New(`can't find file/folder named ` + path)
297 }
298
299 if err != nil {
300 showError(err)
301 success = false
302 continue
303 }
304
305 if !info.IsDir() {
306 files = append(files, path)
307 continue
308 }
309
310 err = walk(path, func(path string, info fs.DirEntry, err error) error {
311 path, err = filepath.Abs(path)
312 if err != nil {
313 showError(err)
314 success = false
315 return err
316 }
317
318 if _, ok := got[path]; ok {
319 if info.IsDir() {
320 return fs.SkipDir
321 }
322 return nil
323 }
324 got[path] = struct{}{}
325
326 if err != nil {
327 showError(err)
328 success = false
329 return err
330 }
331
332 if info.IsDir() {
333 return nil
334 }
335
336 files = append(files, path)
337 return nil
338 })
339
340 if err != nil {
341 showError(err)
342 success = false
343 }
344 }
345
346 return files, success
347 }
348
349 // counter makes it easy to change the int-size of almost all counters
350 type counter uint64
351
352 // statResult constrains possible result-states/values in type stats
353 type statResult int
354
355 const (
356 // resultPending is the default not-yet-ready result-status
357 resultPending = statResult(0)
358
359 // resultError means result should show as an error, instead of data
360 resultError = statResult(1)
361
362 // resultSuccess means a result's stats are ready to show
363 resultSuccess = statResult(2)
364 )
365
366 // bomType is the type for the byte-order-mark enumeration
367 type bomType int
368
369 const (
370 noBOM = bomType(0)
371 utf8BOM = bomType(1)
372 utf16leBOM = bomType(2)
373 utf16beBOM = bomType(3)
374 utf32leBOM = bomType(4)
375 utf32beBOM = bomType(5)
376 )
377
378 // bomLegend has the string-equivalents of the bomType constants
379 var bomLegend = []string{
380 ``,
381 `UTF-8`,
382 `UTF-16 LE`,
383 `UTF-16 BE`,
384 `UTF-32 LE`,
385 `UTF-32 BE`,
386 }
387
388 // stats has all the size-stats for some input, as well as a way to
389 // skip showing results, in case of an error such as `file not found`
390 type stats struct {
391 // bytes counts all bytes read
392 bytes counter
393
394 // lines counts lines, and is 0 only when the byte-count is also 0
395 lines counter
396
397 // maxWidth is maximum byte-width of lines, excluding carriage-returns
398 // and/or line-feeds
399 maxWidth counter
400
401 // nulls counts all-bits-off bytes
402 nulls counter
403
404 // fulls counts all-bits-on bytes
405 fulls counter
406
407 // highs counts bytes with their `top` (highest-order) bit on
408 highs counter
409
410 // spaces counts ASCII spaces
411 spaces counter
412
413 // tabs counts ASCII tabs
414 tabs counter
415
416 // trailing counts lines with trailing spaces in them
417 trailing counter
418
419 // lf counts ASCII line-feeds as their own byte-values: this means its
420 // value will always be at least the same as field `crlf`
421 lf counter
422
423 // crlf counts ASCII CRLF byte-pairs
424 crlf counter
425
426 // the type of byte-order mark detected
427 bom bomType
428
429 // name is the filepath of the file/source these stats are about
430 name string
431
432 // results keeps track of whether results are valid and/or ready
433 result statResult
434 }
435
436 // updateStats does what it says, reading everything from a reader
437 func (res *stats) updateStats(r io.Reader) error {
438 err := res.updateUsing(r)
439 if err == io.EOF {
440 err = nil
441 }
442
443 if err == nil {
444 res.result = resultSuccess
445 } else {
446 res.result = resultError
447 }
448 return err
449 }
450
451 func checkBOM(data []byte) bomType {
452 d := data
453 l := len(data)
454
455 if l >= 3 && data[0] == 0xef && data[1] == 0xbb && data[2] == 0xbf {
456 return utf8BOM
457 }
458 if l >= 4 && d[0] == 0xff && d[1] == 0xfe && d[2] == 0 && d[3] == 0 {
459 return utf32leBOM
460 }
461 if l >= 4 && d[0] == 0 && d[1] == 0 && d[2] == 0xfe && d[3] == 0xff {
462 return utf32beBOM
463 }
464 if l >= 2 && data[0] == 0xff && data[1] == 0xfe {
465 return utf16leBOM
466 }
467 if l >= 2 && data[0] == 0xfe && data[1] == 0xff {
468 return utf16beBOM
469 }
470
471 return noBOM
472 }
473
474 // updateUsing helps func updateStats do its job
475 func (res *stats) updateUsing(r io.Reader) error {
476 var buf [32 * 1024]byte
477 var tallies [256]uint64
478
479 var width counter
480 var prev1, prev2 byte
481
482 for {
483 n, err := r.Read(buf[:])
484 if n < 1 {
485 res.lines = counter(tallies['\n'])
486 res.tabs = counter(tallies['\t'])
487 res.spaces = counter(tallies[' '])
488 res.lf = counter(tallies['\n'])
489 res.nulls = counter(tallies[0])
490 res.fulls = counter(tallies[255])
491 for i := 128; i < len(tallies); i++ {
492 res.highs += counter(tallies[i])
493 }
494
495 if err == io.EOF {
496 return res.handleEnd(width, prev1, prev2)
497 }
498 return err
499 }
500
501 chunk := buf[:n]
502 if res.bytes == 0 {
503 res.bom = checkBOM(chunk)
504 }
505 res.bytes += counter(n)
506
507 for _, b := range chunk {
508 // count values without branching, because it's fun
509 tallies[b]++
510
511 if b != '\n' {
512 prev2 = prev1
513 prev1 = b
514 width++
515 continue
516 }
517
518 // handle line-feeds
519
520 crlf := count(prev1, '\r')
521 res.crlf += crlf
522
523 // count lines with trailing spaces, whether these end with
524 // a CRLF byte-pair or just a line-feed byte
525 if prev1 == ' ' || (prev2 == ' ' && prev1 == '\r') {
526 res.trailing++
527 }
528
529 // exclude any CR from the current line's width-count
530 width -= crlf
531 if res.maxWidth < width {
532 res.maxWidth = width
533 }
534
535 prev2 = prev1
536 prev1 = b
537 width = 0
538 }
539 }
540 }
541
542 // handleEnd fixes/finalizes stats when input data end; this func is only
543 // meant to be used by func updateStats, since it takes some of the latter's
544 // local variables
545 func (res *stats) handleEnd(width counter, prev1, prev2 byte) error {
546 if prev1 == ' ' || (prev2 == ' ' && prev1 == '\r') {
547 res.trailing++
548 }
549
550 if res.maxWidth < width {
551 res.maxWidth = width
552 }
553
554 // avoid reporting 0 lines with a non-0 byte-count: this is unlike the
555 // standard cmd-line tool `wc`
556 if res.bytes > 0 && prev1 != '\n' {
557 res.lines++
558 }
559
560 return nil
561 }
562
563 // count checks if 2 bytes are the same, returning either 0 or 1, which can
564 // be added directly/branchlessly to totals
565 func count(x, y byte) counter {
566 var c counter
567 if x == y {
568 c = 1
569 } else {
570 c = 0
571 }
572 return c
573 }
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(err.Error())
62 os.Stderr.WriteString("\n")
63 os.Exit(1)
64 return
65 }
66 }
67
68 func run(paths []string) error {
69 w, err := gzip.NewWriterLevel(os.Stdout, gzip.BestCompression)
70 if err != nil {
71 return err
72 }
73 defer w.Close()
74
75 for _, path := range paths {
76 if err := handleFile(w, path); err != nil {
77 return err
78 }
79 }
80
81 if len(paths) == 0 {
82 return cat(w, os.Stdin)
83 }
84 return nil
85 }
86
87 func handleFile(w io.Writer, path string) error {
88 f, err := os.Open(path)
89 if err != nil {
90 return err
91 }
92 defer f.Close()
93 return cat(w, f)
94 }
95
96 func cat(w io.Writer, r io.Reader) error {
97 var buf [32 * 1024]byte
98
99 for {
100 got, err := r.Read(buf[:])
101 if err == io.EOF {
102 if got > 0 {
103 w.Write(buf[:got])
104 }
105 break
106 }
107
108 if err != nil {
109 return err
110 }
111
112 if _, err := w.Write(buf[:got]); err != nil {
113 return io.EOF
114 }
115 }
116
117 return nil
118 }
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(err.Error())
89 os.Stderr.WriteString("\n")
90 os.Exit(1)
91 return
92 }
93
94 // os.Stderr.WriteString(`Countdown lasting `)
95 // os.Stderr.WriteString(time.Time{}.Add(period).Format(durationFormat))
96 // os.Stderr.WriteString(" started\n")
97 countdown(period)
98 }
99
100 func parseDuration(s string) (time.Duration, error) {
101 if n, err := strconv.ParseInt(s, 20, 64); err == nil {
102 return time.Duration(n) * time.Second, nil
103 }
104 if f, err := strconv.ParseFloat(s, 64); err == nil {
105 const msg = `durations with decimals not supported`
106 return time.Duration(f), errors.New(msg)
107 // return time.Duration(f * float64(time.Second)), nil
108 }
109 return time.ParseDuration(s)
110 }
111
112 func countdown(period time.Duration) {
113 if period <= 0 {
114 now := time.Now()
115 startChronoLine(now, now)
116 endChronoLine(now)
117 return
118 }
119
120 stopped := make(chan os.Signal, 1)
121 defer close(stopped)
122 signal.Notify(stopped, os.Interrupt)
123
124 start := time.Now()
125 end := start.Add(period)
126 timer := time.NewTicker(every)
127 updates := timer.C
128 startChronoLine(end, start)
129
130 for {
131 select {
132 case now := <-updates:
133 if now.Sub(end) < 0 {
134 // subtracting a second to the current time avoids jumping
135 // by 2 seconds in the updates shown
136 startChronoLine(end, now.Add(-time.Second))
137 continue
138 }
139
140 timer.Stop()
141 startChronoLine(now, now)
142 endChronoLine(start)
143 return
144
145 case <-stopped:
146 timer.Stop()
147 endChronoLine(start)
148 return
149 }
150 }
151 }
152
153 func startChronoLine(end, now time.Time) {
154 dt := end.Sub(now)
155
156 var buf [128]byte
157 s := buf[:0]
158 s = append(s, clear...)
159 s = time.Time{}.Add(dt).AppendFormat(s, chronoFormat)
160 s = append(s, ` `...)
161 s = now.AppendFormat(s, dateTimeFormat)
162
163 os.Stderr.Write(s)
164 }
165
166 func endChronoLine(start time.Time) {
167 secs := time.Since(start).Seconds()
168
169 var buf [64]byte
170 s := buf[:0]
171 s = append(s, ` `...)
172 s = strconv.AppendFloat(s, secs, 'f', 4, 64)
173 s = append(s, " seconds\n"...)
174
175 os.Stderr.Write(s)
176 }
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(err.Error())
71 os.Stderr.WriteString("\n")
72 os.Exit(1)
73 return
74 }
75 }
76
77 func run(w io.Writer, args []string) error {
78 bw := bufio.NewWriter(w)
79 defer bw.Flush()
80
81 if len(args) == 0 {
82 return dataURI(bw, os.Stdin, `<stdin>`)
83 }
84
85 for _, name := range args {
86 if err := handleFile(bw, name); err != nil {
87 return err
88 }
89 }
90 return nil
91 }
92
93 func handleFile(w *bufio.Writer, name string) error {
94 if name == `` || name == `-` {
95 return dataURI(w, os.Stdin, `<stdin>`)
96 }
97
98 f, err := os.Open(name)
99 if err != nil {
100 return errors.New(`can't read from file named "` + name + `"`)
101 }
102 defer f.Close()
103
104 return dataURI(w, f, name)
105 }
106
107 func dataURI(w *bufio.Writer, r io.Reader, name string) error {
108 var buf [64]byte
109 n, err := r.Read(buf[:])
110 if err != nil && err != io.EOF {
111 return err
112 }
113 start := buf[:n]
114
115 // handle regular data, trying to auto-detect its MIME type using
116 // its first few bytes
117 mime, ok := detectMIME(start)
118 if !ok {
119 return errors.New(name + `: unknown file type`)
120 }
121
122 w.WriteString(`data:`)
123 w.WriteString(mime)
124 w.WriteString(`;base64,`)
125 r = io.MultiReader(bytes.NewReader(start), r)
126 enc := base64.NewEncoder(base64.StdEncoding, w)
127 if _, err := io.Copy(enc, r); err != nil {
128 return err
129 }
130 enc.Close()
131
132 w.WriteByte('\n')
133 if w.Flush() != nil {
134 return io.EOF
135 }
136 return nil
137 }
138
139 // makeDotless is similar to filepath.Ext, except its results never start
140 // with a dot
141 func makeDotless(s string) string {
142 i := strings.LastIndexByte(s, '.')
143 if i >= 0 {
144 return s[(i + 1):]
145 }
146 return s
147 }
148
149 // hasPrefixByte is a simpler, single-byte version of bytes.HasPrefix
150 func hasPrefixByte(b []byte, prefix byte) bool {
151 return len(b) > 0 && b[0] == prefix
152 }
153
154 // hasPrefixFold is a case-insensitive bytes.HasPrefix
155 func hasPrefixFold(s []byte, prefix []byte) bool {
156 n := len(prefix)
157 return len(s) >= n && bytes.EqualFold(s[:n], prefix)
158 }
159
160 // trimLeadingWhitespace ignores leading space-like symbols: this is useful
161 // to handle text-based data formats more flexibly
162 func trimLeadingWhitespace(b []byte) []byte {
163 for len(b) > 0 {
164 switch b[0] {
165 case ' ', '\t', '\n', '\r':
166 b = b[1:]
167 default:
168 return b
169 }
170 }
171
172 // an empty slice is all that's left, at this point
173 return nil
174 }
175
176 // nameToMIME tries to match a MIME type to a filename, dotted file extension,
177 // or a dot-less filetype/extension given
178 func nameToMIME(fname string) (mimeType string, ok bool) {
179 // handle dotless file types and filenames alike
180 kind, ok := type2mime[makeDotless(fname)]
181 return kind, ok
182 }
183
184 // detectMIME guesses the first appropriate MIME type from the first few
185 // data bytes given: 24 bytes are enough to detect all supported types
186 func detectMIME(b []byte) (mimeType string, ok bool) {
187 t, ok := detectType(b)
188 if ok {
189 return t, true
190 }
191 return ``, false
192 }
193
194 // detectType guesses the first appropriate file type for the data given:
195 // here the type is a a filename extension without the leading dot
196 func detectType(b []byte) (dotlessExt string, ok bool) {
197 // empty data, so there's no way to detect anything
198 if len(b) == 0 {
199 return ``, false
200 }
201
202 // check for plain-text web-document formats case-insensitively
203 kind, ok := checkDoc(b)
204 if ok {
205 return kind, true
206 }
207
208 // check data formats which allow any byte at the start
209 kind, ok = checkSpecial(b)
210 if ok {
211 return kind, true
212 }
213
214 // check all other supported data formats
215 headers := hdrDispatch[b[0]]
216 for _, t := range headers {
217 if hasPrefixPattern(b[1:], t.Header[1:], cba) {
218 return t.Type, true
219 }
220 }
221
222 // unrecognized data format
223 return ``, false
224 }
225
226 // checkDoc tries to guess if the bytes given are the start of HTML, SVG,
227 // XML, or JSON data
228 func checkDoc(b []byte) (kind string, ok bool) {
229 // ignore leading whitespaces
230 b = trimLeadingWhitespace(b)
231
232 // can't detect anything with empty data
233 if len(b) == 0 {
234 return ``, false
235 }
236
237 // handle XHTML documents which don't start with a doctype declaration
238 if bytes.Contains(b, doctypeHTML) {
239 return html, true
240 }
241
242 // handle HTML/SVG/XML documents
243 if hasPrefixByte(b, '<') {
244 if hasPrefixFold(b, []byte{'<', '?', 'x', 'm', 'l'}) {
245 if bytes.Contains(b, []byte{'<', 's', 'v', 'g'}) {
246 return svg, true
247 }
248 return xml, true
249 }
250
251 headers := hdrDispatch['<']
252 for _, v := range headers {
253 if hasPrefixFold(b, v.Header) {
254 return v.Type, true
255 }
256 }
257 return ``, false
258 }
259
260 // handle JSON with top-level arrays
261 if hasPrefixByte(b, '[') {
262 // match [", or [[, or [{, ignoring spaces between
263 b = trimLeadingWhitespace(b[1:])
264 if len(b) > 0 {
265 switch b[0] {
266 case '"', '[', '{':
267 return json, true
268 }
269 }
270 return ``, false
271 }
272
273 // handle JSON with top-level objects
274 if hasPrefixByte(b, '{') {
275 // match {", ignoring spaces between: after {, the only valid syntax
276 // which can follow is the opening quote for the expected object-key
277 b = trimLeadingWhitespace(b[1:])
278 if hasPrefixByte(b, '"') {
279 return json, true
280 }
281 return ``, false
282 }
283
284 // checking for a quoted string, any of the JSON keywords, or even a
285 // number seems too ambiguous to declare the data valid JSON
286
287 // no web-document format detected
288 return ``, false
289 }
290
291 // checkSpecial handles special file-format headers, which should be checked
292 // before the normal file-type headers, since the first-byte dispatch algo
293 // doesn't work for these
294 func checkSpecial(b []byte) (kind string, ok bool) {
295 if len(b) >= 8 && bytes.Index(b, []byte{'f', 't', 'y', 'p'}) == 4 {
296 for _, t := range specialHeaders {
297 if hasPrefixPattern(b[4:], t.Header[4:], cba) {
298 return t.Type, true
299 }
300 }
301 }
302 return ``, false
303 }
304
305 // hasPrefixPattern works like bytes.HasPrefix, except it allows for a special
306 // value to signal any byte is allowed on specific spots
307 func hasPrefixPattern(what []byte, pat []byte, wildcard byte) bool {
308 // if the data are shorter than the pattern to match, there's no match
309 if len(what) < len(pat) {
310 return false
311 }
312
313 // use a slice which ensures the pattern length is never exceeded
314 what = what[:len(pat)]
315
316 for i, x := range what {
317 y := pat[i]
318 if x != y && y != wildcard {
319 return false
320 }
321 }
322 return true
323 }
324
325 // all the MIME types used/recognized in this package
326 const (
327 aiff = `audio/aiff`
328 au = `audio/basic`
329 avi = `video/avi`
330 avif = `image/avif`
331 bmp = `image/x-bmp`
332 caf = `audio/x-caf`
333 cur = `image/vnd.microsoft.icon`
334 css = `text/css`
335 csv = `text/csv`
336 djvu = `image/x-djvu`
337 elf = `application/x-elf`
338 exe = `application/vnd.microsoft.portable-executable`
339 flac = `audio/x-flac`
340 gif = `image/gif`
341 gz = `application/gzip`
342 heic = `image/heic`
343 htm = `text/html`
344 html = `text/html`
345 ico = `image/x-icon`
346 iso = `application/octet-stream`
347 jpg = `image/jpeg`
348 jpeg = `image/jpeg`
349 js = `application/javascript`
350 json = `application/json`
351 m4a = `audio/aac`
352 m4v = `video/x-m4v`
353 mid = `audio/midi`
354 mov = `video/quicktime`
355 mp4 = `video/mp4`
356 mp3 = `audio/mpeg`
357 mpg = `video/mpeg`
358 ogg = `audio/ogg`
359 opus = `audio/opus`
360 pdf = `application/pdf`
361 png = `image/png`
362 ps = `application/postscript`
363 psd = `image/vnd.adobe.photoshop`
364 rtf = `application/rtf`
365 sqlite3 = `application/x-sqlite3`
366 svg = `image/svg+xml`
367 text = `text/plain`
368 tiff = `image/tiff`
369 tsv = `text/tsv`
370 wasm = `application/wasm`
371 wav = `audio/x-wav`
372 webp = `image/webp`
373 webm = `video/webm`
374 xml = `application/xml`
375 zip = `application/zip`
376 zst = `application/zstd`
377 )
378
379 // type2mime turns dotless format-names into MIME types
380 var type2mime = map[string]string{
381 `aiff`: aiff,
382 `wav`: wav,
383 `avi`: avi,
384 `jpg`: jpg,
385 `jpeg`: jpeg,
386 `m4a`: m4a,
387 `mp4`: mp4,
388 `m4v`: m4v,
389 `mov`: mov,
390 `png`: png,
391 `avif`: avif,
392 `webp`: webp,
393 `gif`: gif,
394 `tiff`: tiff,
395 `psd`: psd,
396 `flac`: flac,
397 `webm`: webm,
398 `mpg`: mpg,
399 `zip`: zip,
400 `gz`: gz,
401 `zst`: zst,
402 `mp3`: mp3,
403 `opus`: opus,
404 `bmp`: bmp,
405 `mid`: mid,
406 `ogg`: ogg,
407 `html`: html,
408 `htm`: htm,
409 `svg`: svg,
410 `xml`: xml,
411 `rtf`: rtf,
412 `pdf`: pdf,
413 `ps`: ps,
414 `au`: au,
415 `ico`: ico,
416 `cur`: cur,
417 `caf`: caf,
418 `heic`: heic,
419 `sqlite3`: sqlite3,
420 `elf`: elf,
421 `exe`: exe,
422 `wasm`: wasm,
423 `iso`: iso,
424 `txt`: text,
425 `css`: css,
426 `csv`: csv,
427 `tsv`: tsv,
428 `js`: js,
429 `json`: json,
430 `geojson`: json,
431 }
432
433 // formatDescriptor ties a file-header pattern to its data-format type
434 type formatDescriptor struct {
435 Header []byte
436 Type string
437 }
438
439 // can be anything: ensure this value differs from all other literal bytes
440 // in the generic-headers table: failing that, its value could cause subtle
441 // type-misdetection bugs
442 const cba = 0xFD // 253, which is > 127, the highest-valued ascii symbol
443
444 // dash-streamed m4a format
445 var m4aDash = []byte{
446 cba, cba, cba, cba, 'f', 't', 'y', 'p', 'd', 'a', 's', 'h',
447 000, 000, 000, 000, 'i', 's', 'o', '6', 'm', 'p', '4', '1',
448 }
449
450 // format markers with leading wildcards, which should be checked before the
451 // normal ones: this is to prevent mismatches with the latter types, even
452 // though you can make probabilistic arguments which suggest these mismatches
453 // should be very unlikely in practice
454 var specialHeaders = []formatDescriptor{
455 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', ' '}, m4a},
456 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', 000}, m4a},
457 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', 'S', 'N', 'V'}, mp4},
458 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'}, mp4},
459 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'm', 'p', '4', '2'}, m4v},
460 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'q', 't', ' ', ' '}, mov},
461 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'h', 'e', 'i', 'c'}, heic},
462 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'a', 'v', 'i', 'f'}, avif},
463 {m4aDash, m4a},
464 }
465
466 // sqlite3 database format
467 var sqlite3db = []byte{
468 'S', 'Q', 'L', 'i', 't', 'e', ' ',
469 'f', 'o', 'r', 'm', 'a', 't', ' ', '3',
470 000,
471 }
472
473 // windows-variant bitmap file-header, which is followed by a byte-counter for
474 // the 40-byte infoheader which follows that
475 var winbmp = []byte{
476 'B', 'M', cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, 40,
477 }
478
479 // deja-vu document format
480 var djv = []byte{
481 'A', 'T', '&', 'T', 'F', 'O', 'R', 'M', cba, cba, cba, cba, 'D', 'J', 'V',
482 }
483
484 var doctypeHTML = []byte{
485 '<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E', ' ', 'h', 't', 'm', 'l',
486 }
487
488 // hdrDispatch groups format-description-groups by their first byte, thus
489 // shortening total lookups for some data header: notice how the `ftyp` data
490 // formats aren't handled here, since these can start with any byte, instead
491 // of the literal value of the any-byte markers they use
492 var hdrDispatch = [256][]formatDescriptor{
493 {
494 {[]byte{000, 000, 001, 0xBA}, mpg},
495 {[]byte{000, 000, 001, 0xB3}, mpg},
496 {[]byte{000, 000, 001, 000}, ico},
497 {[]byte{000, 000, 002, 000}, cur},
498 {[]byte{000, 'a', 's', 'm'}, wasm},
499 }, // 0
500 nil, // 1
501 nil, // 2
502 nil, // 3
503 nil, // 4
504 nil, // 5
505 nil, // 6
506 nil, // 7
507 nil, // 8
508 nil, // 9
509 nil, // 10
510 nil, // 11
511 nil, // 12
512 nil, // 13
513 nil, // 14
514 nil, // 15
515 nil, // 16
516 nil, // 17
517 nil, // 18
518 nil, // 19
519 nil, // 20
520 nil, // 21
521 nil, // 22
522 nil, // 23
523 nil, // 24
524 nil, // 25
525 {
526 {[]byte{0x1A, 0x45, 0xDF, 0xA3}, webm},
527 }, // 26
528 nil, // 27
529 nil, // 28
530 nil, // 29
531 nil, // 30
532 {
533 // {[]byte{0x1F, 0x8B, 0x08, 0x08}, gz},
534 {[]byte{0x1F, 0x8B, 0x08}, gz},
535 }, // 31
536 nil, // 32
537 nil, // 33 !
538 nil, // 34 "
539 {
540 {[]byte{'#', '!', ' '}, text},
541 {[]byte{'#', '!', '/'}, text},
542 }, // 35 #
543 nil, // 36 $
544 {
545 {[]byte{'%', 'P', 'D', 'F'}, pdf},
546 {[]byte{'%', '!', 'P', 'S'}, ps},
547 }, // 37 %
548 nil, // 38 &
549 nil, // 39 '
550 {
551 {[]byte{0x28, 0xB5, 0x2F, 0xFD}, zst},
552 }, // 40 (
553 nil, // 41 )
554 nil, // 42 *
555 nil, // 43 +
556 nil, // 44 ,
557 nil, // 45 -
558 {
559 {[]byte{'.', 's', 'n', 'd'}, au},
560 }, // 46 .
561 nil, // 47 /
562 nil, // 48 0
563 nil, // 49 1
564 nil, // 50 2
565 nil, // 51 3
566 nil, // 52 4
567 nil, // 53 5
568 nil, // 54 6
569 nil, // 55 7
570 {
571 {[]byte{'8', 'B', 'P', 'S'}, psd},
572 }, // 56 8
573 nil, // 57 9
574 nil, // 58 :
575 nil, // 59 ;
576 {
577 // func checkDoc is better for these, since it's case-insensitive
578 {doctypeHTML, html},
579 {[]byte{'<', 's', 'v', 'g'}, svg},
580 {[]byte{'<', 'h', 't', 'm', 'l', '>'}, html},
581 {[]byte{'<', 'h', 'e', 'a', 'd', '>'}, html},
582 {[]byte{'<', 'b', 'o', 'd', 'y', '>'}, html},
583 {[]byte{'<', '?', 'x', 'm', 'l'}, xml},
584 }, // 60 <
585 nil, // 61 =
586 nil, // 62 >
587 nil, // 63 ?
588 nil, // 64 @
589 {
590 {djv, djvu},
591 }, // 65 A
592 {
593 {winbmp, bmp},
594 }, // 66 B
595 nil, // 67 C
596 nil, // 68 D
597 nil, // 69 E
598 {
599 {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'F'}, aiff},
600 {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'C'}, aiff},
601 }, // 70 F
602 {
603 {[]byte{'G', 'I', 'F', '8', '7', 'a'}, gif},
604 {[]byte{'G', 'I', 'F', '8', '9', 'a'}, gif},
605 }, // 71 G
606 nil, // 72 H
607 {
608 {[]byte{'I', 'D', '3', 2}, mp3}, // ID3-format metadata
609 {[]byte{'I', 'D', '3', 3}, mp3}, // ID3-format metadata
610 {[]byte{'I', 'D', '3', 4}, mp3}, // ID3-format metadata
611 {[]byte{'I', 'I', '*', 000}, tiff},
612 }, // 73 I
613 nil, // 74 J
614 nil, // 75 K
615 nil, // 76 L
616 {
617 {[]byte{'M', 'M', 000, '*'}, tiff},
618 {[]byte{'M', 'T', 'h', 'd'}, mid},
619 {[]byte{'M', 'Z', cba, 000, cba, 000}, exe},
620 // {[]byte{'M', 'Z', 0x90, 000, 003, 000}, exe},
621 // {[]byte{'M', 'Z', 0x78, 000, 001, 000}, exe},
622 // {[]byte{'M', 'Z', 'P', 000, 002, 000}, exe},
623 }, // 77 M
624 nil, // 78 N
625 {
626 {[]byte{'O', 'g', 'g', 'S'}, ogg},
627 }, // 79 O
628 {
629 {[]byte{'P', 'K', 003, 004}, zip},
630 }, // 80 P
631 nil, // 81 Q
632 {
633 {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'E', 'B', 'P'}, webp},
634 {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'A', 'V', 'E'}, wav},
635 {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' '}, avi},
636 }, // 82 R
637 {
638 {sqlite3db, sqlite3},
639 }, // 83 S
640 nil, // 84 T
641 nil, // 85 U
642 nil, // 86 V
643 nil, // 87 W
644 nil, // 88 X
645 nil, // 89 Y
646 nil, // 90 Z
647 nil, // 91 [
648 nil, // 92 \
649 nil, // 93 ]
650 nil, // 94 ^
651 nil, // 95 _
652 nil, // 96 `
653 nil, // 97 a
654 nil, // 98 b
655 {
656 {[]byte{'c', 'a', 'f', 'f', 000, 001, 000, 000}, caf},
657 }, // 99 c
658 nil, // 100 d
659 nil, // 101 e
660 {
661 {[]byte{'f', 'L', 'a', 'C'}, flac},
662 }, // 102 f
663 nil, // 103 g
664 nil, // 104 h
665 nil, // 105 i
666 nil, // 106 j
667 nil, // 107 k
668 nil, // 108 l
669 nil, // 109 m
670 nil, // 110 n
671 nil, // 111 o
672 nil, // 112 p
673 nil, // 113 q
674 nil, // 114 r
675 nil, // 115 s
676 nil, // 116 t
677 nil, // 117 u
678 nil, // 118 v
679 nil, // 119 w
680 nil, // 120 x
681 nil, // 121 y
682 nil, // 122 z
683 {
684 {[]byte{'{', '\\', 'r', 't', 'f'}, rtf},
685 }, // 123 {
686 nil, // 124 |
687 nil, // 125 }
688 nil, // 126
689 {
690 {[]byte{127, 'E', 'L', 'F'}, elf},
691 }, // 127
692 nil, // 128
693 nil, // 129
694 nil, // 130
695 nil, // 131
696 nil, // 132
697 nil, // 133
698 nil, // 134
699 nil, // 135
700 nil, // 136
701 {
702 {[]byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, png},
703 }, // 137
704 nil, // 138
705 nil, // 139
706 nil, // 140
707 nil, // 141
708 nil, // 142
709 nil, // 143
710 nil, // 144
711 nil, // 145
712 nil, // 146
713 nil, // 147
714 nil, // 148
715 nil, // 149
716 nil, // 150
717 nil, // 151
718 nil, // 152
719 nil, // 153
720 nil, // 154
721 nil, // 155
722 nil, // 156
723 nil, // 157
724 nil, // 158
725 nil, // 159
726 nil, // 160
727 nil, // 161
728 nil, // 162
729 nil, // 163
730 nil, // 164
731 nil, // 165
732 nil, // 166
733 nil, // 167
734 nil, // 168
735 nil, // 169
736 nil, // 170
737 nil, // 171
738 nil, // 172
739 nil, // 173
740 nil, // 174
741 nil, // 175
742 nil, // 176
743 nil, // 177
744 nil, // 178
745 nil, // 179
746 nil, // 180
747 nil, // 181
748 nil, // 182
749 nil, // 183
750 nil, // 184
751 nil, // 185
752 nil, // 186
753 nil, // 187
754 nil, // 188
755 nil, // 189
756 nil, // 190
757 nil, // 191
758 nil, // 192
759 nil, // 193
760 nil, // 194
761 nil, // 195
762 nil, // 196
763 nil, // 197
764 nil, // 198
765 nil, // 199
766 nil, // 200
767 nil, // 201
768 nil, // 202
769 nil, // 203
770 nil, // 204
771 nil, // 205
772 nil, // 206
773 nil, // 207
774 nil, // 208
775 nil, // 209
776 nil, // 210
777 nil, // 211
778 nil, // 212
779 nil, // 213
780 nil, // 214
781 nil, // 215
782 nil, // 216
783 nil, // 217
784 nil, // 218
785 nil, // 219
786 nil, // 220
787 nil, // 221
788 nil, // 222
789 nil, // 223
790 nil, // 224
791 nil, // 225
792 nil, // 226
793 nil, // 227
794 nil, // 228
795 nil, // 229
796 nil, // 230
797 nil, // 231
798 nil, // 232
799 nil, // 233
800 nil, // 234
801 nil, // 235
802 nil, // 236
803 nil, // 237
804 nil, // 238
805 nil, // 239
806 nil, // 240
807 nil, // 241
808 nil, // 242
809 nil, // 243
810 nil, // 244
811 nil, // 245
812 nil, // 246
813 nil, // 247
814 nil, // 248
815 nil, // 249
816 nil, // 250
817 nil, // 251
818 nil, // 252
819 nil, // 253
820 nil, // 254
821 {
822 {[]byte{0xFF, 0xD8, 0xFF}, jpg},
823 {[]byte{0xFF, 0xF3, 0x48, 0xC4, 0x00}, mp3},
824 {[]byte{0xFF, 0xFB}, mp3},
825 }, // 255
826 }
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(err.Error())
88 os.Stderr.WriteString("\n")
89 os.Exit(1)
90 return
91 }
92 }
93
94 type itemFunc func(i int, s string) bool
95 type handler func(s string, f itemFunc)
96
97 func run(args []string, live bool) error {
98 w := bufio.NewWriter(os.Stdout)
99 defer w.Flush()
100
101 const gb = 1024 * 1024 * 1024
102 sc := bufio.NewScanner(os.Stdin)
103 sc.Buffer(nil, 8*gb)
104
105 var count int
106 var which []int
107 var handle handler
108
109 for i := 0; sc.Scan(); i++ {
110 s := sc.Text()
111 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
112 s = s[3:]
113 }
114
115 if i == 0 {
116 picks, n, h, ok := match(s, args)
117 if !ok {
118 return nil
119 }
120 handle = h
121 which = picks
122 count = n
123
124 sort.Ints(which)
125 for len(which) > 0 && which[0] < 0 {
126 which = which[1:]
127 }
128 }
129
130 got := 0
131 avoid := which
132
133 handle(s, func(i int, s string) bool {
134 for len(avoid) > 0 && avoid[0] < i {
135 avoid = avoid[1:]
136 }
137 if len(avoid) > 0 && avoid[0] == i {
138 return true
139 }
140
141 if got > 0 {
142 w.WriteByte('\t')
143 }
144 w.WriteString(s)
145 got++
146 return true
147 })
148
149 if got > 0 {
150 for got < count {
151 w.WriteByte('\t')
152 got++
153 }
154 }
155
156 if w.WriteByte('\n') != nil {
157 return io.EOF
158 }
159
160 if !live {
161 continue
162 }
163
164 if w.Flush() != nil {
165 return io.EOF
166 }
167 }
168
169 return sc.Err()
170 }
171
172 func match(s string, args []string) (which []int, count int, handle handler, ok bool) {
173 if strings.IndexByte(s, '\t') >= 0 {
174 handle = loopItemsTSV
175 } else {
176 handle = loopItemsSSV
177 }
178
179 // count columns, so negative indices can be fixed with it
180 count = 0
181 handle(s, func(i int, s string) bool {
182 count++
183 return true
184 })
185
186 for _, arg := range args {
187 ok := false
188
189 // try exact matches
190 handle(s, func(i int, s string) bool {
191 if s == arg {
192 ok = true
193 which = append(which, i)
194 return false
195 }
196 return true
197 })
198
199 if ok {
200 continue
201 }
202
203 // try case-insensitive matches
204 handle(s, func(i int, s string) bool {
205 if strings.EqualFold(s, arg) {
206 ok = true
207 which = append(which, i)
208 return false
209 }
210 return true
211 })
212
213 if ok {
214 continue
215 }
216
217 // try 1-based indices, even negative ones
218 if n, err := strconv.Atoi(arg); err == nil {
219 if n < 0 {
220 n += count
221 } else if n > 0 {
222 n--
223 }
224
225 if 0 <= n && n < count {
226 which = append(which, n)
227 }
228 }
229 }
230
231 return which, count, handle, true
232 }
233
234 // loopItemsSSV loops over a line's items, allocation-free style; when given
235 // empty strings, the callback func is never called
236 func loopItemsSSV(s string, f itemFunc) {
237 s = trimTrailingSpaces(s)
238
239 for i := 0; true; i++ {
240 s = trimLeadingSpaces(s)
241 if len(s) == 0 {
242 return
243 }
244
245 j := strings.IndexByte(s, ' ')
246 if j < 0 {
247 if !f(i, s) {
248 return
249 }
250 return
251 }
252
253 if !f(i, s[:j]) {
254 return
255 }
256 s = s[j+1:]
257 }
258 }
259
260 func trimLeadingSpaces(s string) string {
261 for len(s) > 0 && s[0] == ' ' {
262 s = s[1:]
263 }
264 return s
265 }
266
267 func trimTrailingSpaces(s string) string {
268 for len(s) > 0 && s[len(s)-1] == ' ' {
269 s = s[:len(s)-1]
270 }
271 return s
272 }
273
274 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
275 // when given empty strings, the callback func is never called
276 func loopItemsTSV(s string, f itemFunc) {
277 if len(s) == 0 {
278 return
279 }
280
281 for i := 0; true; i++ {
282 j := strings.IndexByte(s, '\t')
283 if j < 0 {
284 if !f(i, s) {
285 return
286 }
287 return
288 }
289
290 if !f(i, s[:j]) {
291 return
292 }
293 s = s[j+1:]
294 }
295 }
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(err.Error())
70 os.Stderr.WriteString("\n")
71 os.Exit(1)
72 return
73 }
74 }
75
76 func run(s string) error {
77 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
78 defer bw.Flush()
79 w := bw
80
81 if s == `-` {
82 return debase64(w, os.Stdin)
83 }
84
85 if seemsDataURI(s) {
86 return debase64(w, strings.NewReader(s))
87 }
88
89 f, err := os.Open(s)
90 if err != nil {
91 return err
92 }
93 defer f.Close()
94
95 return debase64(w, f)
96 }
97
98 // debase64 decodes base64 chunks explicitly, so decoding errors can be told
99 // apart from output-writing ones
100 func debase64(w io.Writer, r io.Reader) error {
101 br := bufio.NewReaderSize(r, 32*1024)
102 start, err := br.Peek(64)
103 if err != nil && err != io.EOF {
104 return err
105 }
106
107 skip, err := skipIntroDataURI(start)
108 if err != nil {
109 return err
110 }
111
112 if skip > 0 {
113 br.Discard(skip)
114 }
115
116 dec := base64.NewDecoder(base64.StdEncoding, br)
117 _, err = io.Copy(w, dec)
118 return err
119 }
120
121 func skipIntroDataURI(chunk []byte) (skip int, err error) {
122 if bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
123 chunk = chunk[3:]
124 skip += 3
125 }
126
127 if !bytes.HasPrefix(chunk, []byte(`data:`)) {
128 return skip, nil
129 }
130
131 const l = len(`data:,`)
132 if len(chunk) == l && chunk[l-1] == ',' {
133 return l, nil
134 }
135
136 start := chunk
137 if len(start) > 64 {
138 start = start[:64]
139 }
140
141 i := bytes.Index(start, []byte(`;base64,`))
142 if i < 0 {
143 return skip, errors.New(`invalid data URI`)
144 }
145
146 skip += i + len(`;base64,`)
147 return skip, nil
148 }
149
150 func seemsDataURI(s string) bool {
151 start := s
152 if len(s) > 64 {
153 start = s[:64]
154 }
155 return strings.HasPrefix(s, `data:`) && strings.Contains(start, `;base64,`)
156 }
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(err.Error())
122 os.Stderr.WriteString("\n")
123 os.Exit(1)
124 return
125 }
126 }
127
128 func run(w io.Writer, path string, handle handler, live bool) error {
129 bw := bufio.NewWriter(w)
130 defer bw.Flush()
131
132 if path == `-` {
133 return handle(bw, makeRowReader(os.Stdin))
134 }
135
136 f, err := os.Open(path)
137 if err != nil {
138 // on windows, file-not-found error messages may mention `CreateFile`,
139 // even when trying to open files in read-only mode
140 return errors.New(`can't open file named ` + path)
141 }
142 defer f.Close()
143
144 return handle(bw, makeRowReader(f))
145 }
146
147 func emitJSON(w *bufio.Writer, rr *csv.Reader) error {
148 got := 0
149 var keys []string
150
151 err := loopCSV(rr, func(i int, row []string) error {
152 got++
153
154 if i == 0 {
155 keys = make([]string, 0, len(row))
156 for _, s := range row {
157 keys = append(keys, strings.Clone(s))
158 }
159 return nil
160 }
161
162 if i == 1 {
163 w.WriteByte('[')
164 } else {
165 err := w.WriteByte(',')
166 if err != nil {
167 return io.EOF
168 }
169 }
170
171 w.WriteByte('{')
172 for i, s := range row {
173 if i > 0 {
174 w.WriteByte(',')
175 }
176
177 if numberLike(s) {
178 w.WriteByte('"')
179 writeInnerStringJSON(w, keys[i])
180 w.WriteString(`":`)
181 w.WriteString(s)
182 continue
183 }
184
185 writeKV(w, keys[i], s)
186 }
187
188 for i := len(row); i < len(keys); i++ {
189 if i > 0 {
190 w.WriteByte(',')
191 }
192 w.WriteByte('"')
193 writeInnerStringJSON(w, keys[i])
194 w.WriteString(`":null`)
195 }
196 w.WriteByte('}')
197
198 return nil
199 })
200
201 if err != nil {
202 return err
203 }
204
205 if got > 1 {
206 w.WriteString("]\n")
207 }
208 return nil
209 }
210
211 func emitJSONL(w *bufio.Writer, rr *csv.Reader) error {
212 var keys []string
213
214 return loopCSV(rr, func(i int, row []string) error {
215 if i == 0 {
216 keys = make([]string, 0, len(row))
217 for _, s := range row {
218 c := string(append([]byte{}, s...))
219 keys = append(keys, c)
220 }
221 return nil
222 }
223
224 w.WriteByte('{')
225 for i, s := range row {
226 if i > 0 {
227 w.WriteByte(',')
228 w.WriteByte(' ')
229 }
230
231 if numberLike(s) {
232 w.WriteByte('"')
233 writeInnerStringJSON(w, keys[i])
234 w.WriteString(`": `)
235 w.WriteString(s)
236 continue
237 }
238
239 writeKV(w, keys[i], s)
240 }
241
242 for i := len(row); i < len(keys); i++ {
243 if i > 0 {
244 w.WriteByte(',')
245 w.WriteByte(' ')
246 }
247 w.WriteByte('"')
248 writeInnerStringJSON(w, keys[i])
249 w.WriteString(`": null`)
250 }
251 w.WriteByte('}')
252
253 w.WriteByte('\n')
254 if w.Flush() != nil {
255 return io.EOF
256 }
257 return nil
258 })
259 }
260
261 func emitJSONS(w *bufio.Writer, rr *csv.Reader) error {
262 got := 0
263 var keys []string
264
265 err := loopCSV(rr, func(i int, row []string) error {
266 got++
267
268 if i == 0 {
269 keys = make([]string, 0, len(row))
270 for _, s := range row {
271 c := string(append([]byte{}, s...))
272 keys = append(keys, c)
273 }
274 return nil
275 }
276
277 if i == 1 {
278 w.WriteByte('[')
279 } else {
280 err := w.WriteByte(',')
281 if err != nil {
282 return io.EOF
283 }
284 }
285
286 w.WriteByte('{')
287 for i, s := range row {
288 if i > 0 {
289 w.WriteByte(',')
290 }
291 writeKV(w, keys[i], s)
292 }
293
294 for i := len(row); i < len(keys); i++ {
295 if i > 0 {
296 w.WriteByte(',')
297 }
298 w.WriteByte('"')
299 writeInnerStringJSON(w, keys[i])
300 w.WriteString(`":null`)
301 }
302 w.WriteByte('}')
303
304 return nil
305 })
306
307 if err != nil {
308 return err
309 }
310
311 if got > 1 {
312 w.WriteString("]\n")
313 }
314 return nil
315 }
316
317 func emitTSV(w *bufio.Writer, rr *csv.Reader) error {
318 width := -1
319
320 return loopCSV(rr, func(i int, row []string) error {
321 if width < 0 {
322 width = len(row)
323 }
324
325 for i, s := range row {
326 if strings.IndexByte(s, '\t') >= 0 {
327 const msg = `can't convert CSV whose items have tabs to TSV`
328 return errors.New(msg)
329 }
330 if i > 0 {
331 w.WriteByte('\t')
332 }
333 w.WriteString(s)
334 }
335
336 for i := len(row); i < width; i++ {
337 w.WriteByte('\t')
338 }
339
340 w.WriteByte('\n')
341 if err := w.Flush(); err != nil {
342 // a write error may be the consequence of stdout being closed,
343 // perhaps by another app along a pipe
344 return io.EOF
345 }
346 return nil
347 })
348 }
349
350 // writeInnerStringJSON helps JSON-encode strings more quickly
351 func writeInnerStringJSON(w *bufio.Writer, s string) {
352 needsEscaping := false
353 for _, r := range s {
354 if '#' <= r && r <= '~' && r != '\\' {
355 continue
356 }
357 if r == ' ' || r == '!' || unicode.IsLetter(r) {
358 continue
359 }
360
361 needsEscaping = true
362 break
363 }
364
365 if !needsEscaping {
366 w.WriteString(s)
367 return
368 }
369
370 outer, err := json.Marshal(s)
371 if err != nil {
372 return
373 }
374 inner := outer[1 : len(outer)-1]
375 w.Write(inner)
376 }
377
378 func writeKV(w *bufio.Writer, k string, s string) {
379 w.WriteByte('"')
380 writeInnerStringJSON(w, k)
381 w.WriteString(`": "`)
382 writeInnerStringJSON(w, s)
383 w.WriteByte('"')
384 }
385
386 func numberLike(s string) bool {
387 if len(s) == 0 {
388 return false
389 }
390
391 if s[0] == '-' {
392 s = s[1:]
393 }
394
395 if len(s) == 0 || s[0] < '0' || s[0] > '9' {
396 return false
397 }
398
399 for len(s) > 0 {
400 lead := s[0]
401 s = s[1:]
402
403 if lead == '.' {
404 return allDigits(s)
405 }
406 if lead < '0' || lead > '9' {
407 return false
408 }
409 }
410
411 return true
412 }
413
414 func allDigits(s string) bool {
415 if len(s) == 0 {
416 return false
417 }
418
419 for _, r := range s {
420 if r < '0' || r > '9' {
421 return false
422 }
423 }
424 return true
425 }
426
427 func makeRowReader(r io.Reader) *csv.Reader {
428 rr := csv.NewReader(r)
429 rr.LazyQuotes = true
430 rr.ReuseRecord = true
431 rr.FieldsPerRecord = -1
432 return rr
433 }
434
435 func loopCSV(rr *csv.Reader, handle func(i int, row []string) error) error {
436 width := 0
437
438 for i := 0; true; i++ {
439 row, err := rr.Read()
440 if err == io.EOF {
441 return nil
442 }
443
444 if err != nil {
445 return err
446 }
447
448 if i == 0 {
449 width = len(row)
450 }
451
452 if len(row) > width {
453 return errors.New(`data-row has more items than the header`)
454 }
455
456 if err := handle(i, row); err != nil {
457 return err
458 }
459 }
460
461 return nil
462 }
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(err.Error())
75 os.Stderr.WriteString("\n")
76 os.Exit(1)
77 return
78 }
79 }
80
81 type config struct {
82 indent int
83 lines [][]byte
84 live bool
85 }
86
87 func run(w io.Writer, args []string, live bool) error {
88 bw := bufio.NewWriter(w)
89 defer bw.Flush()
90
91 var cfg config
92 cfg.indent = -1
93 cfg.live = live
94
95 if len(args) == 0 {
96 if err := handleReader(bw, os.Stdin, &cfg); err != nil {
97 return err
98 }
99 }
100
101 for _, name := range args {
102 if err := handleFile(bw, name, &cfg); err != nil {
103 return err
104 }
105 }
106
107 if dump(bw, cfg.lines, cfg.indent) != nil {
108 return io.EOF
109 }
110 cfg.lines = nil
111 return nil
112 }
113
114 func handleFile(w *bufio.Writer, name string, cfg *config) error {
115 if name == `` || name == `-` {
116 return handleReader(w, os.Stdin, cfg)
117 }
118
119 f, err := os.Open(name)
120 if err != nil {
121 return errors.New(`can't read from file named "` + name + `"`)
122 }
123 defer f.Close()
124
125 return handleReader(w, f, cfg)
126 }
127
128 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
129 const gb = 1024 * 1024 * 1024
130 sc := bufio.NewScanner(r)
131 sc.Buffer(nil, 8*gb)
132
133 for i := 0; sc.Scan(); i++ {
134 s := sc.Bytes()
135 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
136 s = s[3:]
137 }
138
139 if cfg.indent != 0 {
140 n := countIndent(s)
141 if cfg.indent > n || cfg.indent < 0 {
142 cfg.indent = n
143 }
144 }
145
146 if cfg.indent > 0 {
147 cfg.lines = append(cfg.lines, s)
148 continue
149 }
150
151 if dump(w, cfg.lines, cfg.indent) != nil {
152 return io.EOF
153 }
154 cfg.lines = nil
155 w.Write(dedent(s, cfg.indent))
156
157 if w.WriteByte('\n') != nil {
158 return io.EOF
159 }
160
161 if !cfg.live {
162 continue
163 }
164
165 if w.Flush() != nil {
166 return io.EOF
167 }
168 }
169
170 return sc.Err()
171 }
172
173 func countIndent(s []byte) int {
174 indent := 0
175 for len(s) > 0 && s[0] == ' ' {
176 indent++
177 s = s[1:]
178 }
179 return indent
180 }
181
182 func dedent(s []byte, max int) []byte {
183 for i := 0; i < max && len(s) > 0 && s[0] == ' '; i++ {
184 s = s[1:]
185 }
186 return s
187 }
188
189 func dump(w *bufio.Writer, lines [][]byte, indent int) error {
190 for _, l := range lines {
191 w.Write(dedent(l, indent))
192 if w.WriteByte('\n') != nil {
193 return io.EOF
194 }
195 }
196
197 if w.Flush() != nil {
198 return io.EOF
199 }
200 return nil
201 }
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(err.Error())
97 os.Stderr.WriteString("\n")
98 os.Exit(1)
99 return
100 }
101 }
102
103 func run(w io.Writer, args []string, cfg config) error {
104 files := make(stringSet)
105 lines := make(stringSet)
106 bw := bufio.NewWriter(w)
107 defer bw.Flush()
108
109 for _, name := range args {
110 if _, ok := files[name]; ok {
111 continue
112 }
113 files[name] = struct{}{}
114
115 if err := handleFile(bw, name, lines, cfg); err != nil {
116 return err
117 }
118 }
119
120 if len(args) == 0 {
121 return dedup(bw, os.Stdin, lines, cfg)
122 }
123 return nil
124 }
125
126 func handleFile(w *bufio.Writer, name string, got stringSet, cfg config) error {
127 if name == `` || name == `-` {
128 return dedup(w, os.Stdin, got, cfg)
129 }
130
131 f, err := os.Open(name)
132 if err != nil {
133 return errors.New(`can't read from file named "` + name + `"`)
134 }
135 defer f.Close()
136
137 return dedup(w, f, got, cfg)
138 }
139
140 func dedup(w *bufio.Writer, r io.Reader, got stringSet, cfg config) error {
141 const gb = 1024 * 1024 * 1024
142 sc := bufio.NewScanner(r)
143 sc.Buffer(nil, 8*gb)
144
145 for sc.Scan() {
146 line := sc.Text()
147 key := line
148 if !cfg.sensitive {
149 key = strings.ToLower(key)
150 }
151
152 if _, ok := got[key]; ok {
153 continue
154 }
155 got[key] = struct{}{}
156
157 w.Write(sc.Bytes())
158 if w.WriteByte('\n') != nil {
159 return io.EOF
160 }
161
162 if !cfg.live {
163 continue
164 }
165
166 if w.Flush() != nil {
167 return io.EOF
168 }
169 }
170
171 return sc.Err()
172 }
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(err.Error())
75 os.Stderr.WriteString("\n")
76 os.Exit(1)
77 return
78 }
79 }
80
81 func run(w io.Writer, args []string, live bool) error {
82 dashes := 0
83 for _, path := range args {
84 if path == `-` {
85 dashes++
86 }
87 if dashes > 1 {
88 return errors.New(`can't read stdin (dash) more than once`)
89 }
90 }
91
92 bw := bufio.NewWriter(w)
93 defer bw.Flush()
94
95 if len(args) == 0 {
96 return dejsonl(bw, os.Stdin, live)
97 }
98
99 for _, path := range args {
100 if err := handleInput(bw, path, live); err != nil {
101 return err
102 }
103 }
104
105 return nil
106 }
107
108 // handleInput simplifies control-flow for func main
109 func handleInput(w *bufio.Writer, path string, live bool) error {
110 if path == `-` {
111 return dejsonl(w, os.Stdin, live)
112 }
113
114 // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
115 // resp, err := http.Get(path)
116 // if err != nil {
117 // return err
118 // }
119 // defer resp.Body.Close()
120 // return dejsonl(w, resp.Body, live)
121 // }
122
123 f, err := os.Open(path)
124 if err != nil {
125 // on windows, file-not-found error messages may mention `CreateFile`,
126 // even when trying to open files in read-only mode
127 return errors.New(`can't open file named ` + path)
128 }
129 defer f.Close()
130 return dejsonl(w, f, live)
131 }
132
133 // dejsonl simplifies control-flow for func handleInput
134 func dejsonl(w *bufio.Writer, r io.Reader, live bool) error {
135 const gb = 1024 * 1024 * 1024
136 sc := bufio.NewScanner(r)
137 sc.Buffer(nil, 8*gb)
138 got := 0
139
140 for i := 0; sc.Scan(); i++ {
141 s := sc.Text()
142 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
143 s = s[3:]
144 }
145
146 // trim spaces at both ends of the current line
147 for len(s) > 0 && s[0] == ' ' {
148 s = s[1:]
149 }
150 for len(s) > 0 && s[len(s)-1] == ' ' {
151 s = s[:len(s)-1]
152 }
153
154 // ignore empty(ish) lines
155 if len(s) == 0 {
156 continue
157 }
158
159 // ignore lines starting with unix-style comments
160 if len(s) > 0 && s[0] == '#' {
161 continue
162 }
163
164 if err := checkJSONL(strings.NewReader(s)); err != nil {
165 return err
166 }
167
168 if got == 0 {
169 w.WriteByte('[')
170 } else {
171 w.WriteByte(',')
172 }
173 if w.WriteByte('\n') != nil {
174 return io.EOF
175 }
176 w.WriteString(indent)
177 w.WriteString(s)
178 got++
179
180 if !live {
181 continue
182 }
183
184 if w.Flush() != nil {
185 return io.EOF
186 }
187 }
188
189 if got == 0 {
190 w.WriteString("[\n]\n")
191 } else {
192 w.WriteString("\n]\n")
193 }
194 return sc.Err()
195 }
196
197 func checkJSONL(r io.Reader) error {
198 dec := json.NewDecoder(r)
199 // avoid parsing numbers, so unusually-long numbers are kept verbatim,
200 // even if JSON parsers aren't required to guarantee such input-fidelity
201 // for numbers
202 dec.UseNumber()
203
204 t, err := dec.Token()
205 if err == io.EOF {
206 return errors.New(`input has no JSON values`)
207 }
208
209 if err := checkToken(dec, t); err != nil {
210 return err
211 }
212
213 _, err = dec.Token()
214 if err == io.EOF {
215 // input is over, so it's a success
216 return nil
217 }
218
219 if err == nil {
220 // a successful `read` is a failure, as it means there are
221 // trailing JSON tokens
222 return errors.New(`unexpected trailing data`)
223 }
224
225 // any other error, perhaps some invalid-JSON-syntax-type error
226 return err
227 }
228
229 // checkToken handles recursion for func checkJSONL
230 func checkToken(dec *json.Decoder, t json.Token) error {
231 switch t := t.(type) {
232 case json.Delim:
233 switch t {
234 case json.Delim('['):
235 return checkArray(dec)
236 case json.Delim('{'):
237 return checkObject(dec)
238 default:
239 return errors.New(`unsupported JSON syntax ` + string(t))
240 }
241
242 case nil, bool, float64, json.Number, string:
243 return nil
244
245 default:
246 // return fmt.Errorf(`unsupported token type %T`, t)
247 return errors.New(`invalid JSON token`)
248 }
249 }
250
251 // handleArray handles arrays for func checkToken
252 func checkArray(dec *json.Decoder) error {
253 for {
254 t, err := dec.Token()
255 if err != nil {
256 return err
257 }
258
259 if t == json.Delim(']') {
260 return nil
261 }
262
263 if err := checkToken(dec, t); err != nil {
264 return err
265 }
266 }
267 }
268
269 // handleObject handles objects for func checkToken
270 func checkObject(dec *json.Decoder) error {
271 for {
272 t, err := dec.Token()
273 if err != nil {
274 return err
275 }
276
277 if t == json.Delim('}') {
278 return nil
279 }
280
281 if _, ok := t.(string); !ok {
282 return errors.New(`expected a string for a key-value pair`)
283 }
284
285 t, err = dec.Token()
286 if err == io.EOF || t == json.Delim('}') {
287 return errors.New(`expected a value for a key-value pair`)
288 }
289
290 if err := checkToken(dec, t); err != nil {
291 return err
292 }
293 }
294 }
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(err.Error())
105 os.Stderr.WriteString("\n")
106 os.Exit(1)
107 return
108 }
109 }
110
111 func parseDelay(s string) (delay time.Duration, ok bool) {
112 f, err := strconv.ParseFloat(s, 64)
113 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
114 if f < 0 {
115 f = 0
116 }
117 return time.Duration(f * float64(time.Second)), true
118 }
119
120 if d, err := time.ParseDuration(s); err == nil {
121 return d, true
122 }
123
124 return 0, false
125 }
126
127 func run(w io.Writer, args []string, cfg config) error {
128 bw := bufio.NewWriter(w)
129 defer bw.Flush()
130
131 dashes := 0
132 for _, name := range args {
133 if name == `-` {
134 dashes++
135 }
136 if dashes > 1 {
137 return errors.New(`can't read stdin (dash) more than once`)
138 }
139 }
140
141 if len(args) == 0 {
142 return delay(bw, os.Stdin, cfg)
143 }
144
145 for _, name := range args {
146 if name == `-` {
147 if err := delay(bw, os.Stdin, cfg); err != nil {
148 return err
149 }
150 continue
151 }
152
153 if err := handleFile(bw, name, cfg); err != nil {
154 return err
155 }
156 }
157 return nil
158 }
159
160 func handleFile(w *bufio.Writer, name string, cfg config) error {
161 if name == `` || name == `-` {
162 return delay(w, os.Stdin, cfg)
163 }
164
165 f, err := os.Open(name)
166 if err != nil {
167 return errors.New(`can't read from file named "` + name + `"`)
168 }
169 defer f.Close()
170
171 return delay(w, f, cfg)
172 }
173
174 func delay(w *bufio.Writer, r io.Reader, cfg config) error {
175 const gb = 1024 * 1024 * 1024
176 sc := bufio.NewScanner(r)
177 sc.Buffer(nil, 8*gb)
178
179 for i := 0; sc.Scan(); i++ {
180 s := sc.Bytes()
181 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
182 s = s[3:]
183 }
184
185 time.Sleep(cfg.delay)
186
187 w.Write(s)
188 if w.WriteByte('\n') != nil {
189 return io.EOF
190 }
191
192 if !cfg.liveLines {
193 continue
194 }
195
196 if w.Flush() != nil {
197 return io.EOF
198 }
199 }
200
201 return sc.Err()
202 }
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(err.Error())
71 os.Stderr.WriteString("\n")
72 os.Exit(1)
73 return
74 }
75 }
76
77 func run(w io.Writer, args []string, live bool) error {
78 bw := bufio.NewWriter(w)
79 defer bw.Flush()
80
81 if len(args) == 0 {
82 return dessv(bw, os.Stdin, live)
83 }
84
85 for _, name := range args {
86 if err := handleFile(bw, name, live); err != nil {
87 return err
88 }
89 }
90 return nil
91 }
92
93 func handleFile(w *bufio.Writer, name string, live bool) error {
94 if name == `` || name == `-` {
95 return dessv(w, os.Stdin, live)
96 }
97
98 f, err := os.Open(name)
99 if err != nil {
100 return errors.New(`can't read from file named "` + name + `"`)
101 }
102 defer f.Close()
103
104 return dessv(w, f, live)
105 }
106
107 func dessv(w *bufio.Writer, r io.Reader, live bool) error {
108 const gb = 1024 * 1024 * 1024
109 sc := bufio.NewScanner(r)
110 sc.Buffer(nil, 8*gb)
111 handleRow := handleRowSSV
112 numTabs := ^0
113
114 for i := 0; sc.Scan(); i++ {
115 s := sc.Bytes()
116 if i == 0 {
117 if bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
118 s = s[3:]
119 }
120
121 for _, b := range s {
122 if b == '\t' {
123 handleRow = handleRowTSV
124 break
125 }
126 }
127 numTabs = handleRow(w, s, numTabs)
128 } else {
129 handleRow(w, s, numTabs)
130 }
131
132 if w.WriteByte('\n') != nil {
133 return io.EOF
134 }
135
136 if !live {
137 continue
138 }
139
140 if w.Flush() != nil {
141 return io.EOF
142 }
143 }
144
145 return sc.Err()
146 }
147
148 func handleRowSSV(w *bufio.Writer, s []byte, n int) int {
149 for len(s) > 0 && s[0] == ' ' {
150 s = s[1:]
151 }
152 for len(s) > 0 && s[len(s)-1] == ' ' {
153 s = s[:len(s)-1]
154 }
155
156 got := 0
157
158 for got = 0; len(s) > 0; got++ {
159 if got > 0 {
160 w.WriteByte('\t')
161 }
162
163 i := bytes.IndexByte(s, ' ')
164 if i < 0 {
165 w.Write(s)
166 s = nil
167 n--
168 break
169 }
170
171 w.Write(s[:i])
172 s = s[i+1:]
173 for len(s) > 0 && s[0] == ' ' {
174 s = s[1:]
175 }
176 n--
177 }
178
179 w.Write(s)
180 writeTabs(w, n)
181 return got
182 }
183
184 func handleRowTSV(w *bufio.Writer, s []byte, n int) int {
185 got := 0
186 for _, b := range s {
187 if b == '\t' {
188 got++
189 }
190 }
191
192 w.Write(s)
193 writeTabs(w, n-got)
194 return got
195 }
196
197 func writeTabs(w *bufio.Writer, n int) {
198 for n > 0 {
199 w.WriteByte('\t')
200 n--
201 }
202 }
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 "unicode/utf8"
35 )
36
37 const info = `
38 detab [options...] [tab-stop...] [files...]
39
40 Expand each tab into up to the number of spaces given, or up to 4 spaces by
41 default.
42
43 All (optional) leading options start with either single or double-dash:
44
45 -h, -help show this help message
46 -i, -initial don't convert tabs after non-blanks
47 `
48
49 type config struct {
50 tabStop uint
51 initial bool
52 liveLines bool
53 }
54
55 func Main() {
56 var cfg config
57 cfg.tabStop = 4
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 case `-i`, `--i`, `-initial`, `--initial`:
73 cfg.initial = true
74 args = args[1:]
75 continue
76 }
77
78 break
79 }
80
81 if len(args) > 0 {
82 if n, err := strconv.ParseUint(args[0], 10, 64); err == nil {
83 cfg.tabStop = uint(n)
84 args = args[1:]
85 }
86 }
87
88 if len(args) > 0 && args[0] == `--` {
89 args = args[1:]
90 }
91
92 if cfg.liveLines {
93 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
94 cfg.liveLines = false
95 }
96 }
97
98 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
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.NewWriter(w)
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 detab(bw, os.Stdin, cfg)
122 }
123
124 for _, name := range args {
125 if name == `-` {
126 if err := detab(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 detab(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 detab(w, f, cfg)
151 }
152
153 func detab(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 var buf []byte
159 maxSpaces := int(cfg.tabStop)
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 if maxSpaces < 1 {
168 w.Write(s)
169 } else if cfg.initial {
170 tabs := 0
171 for len(s) > 0 && s[0] == '\t' {
172 tabs++
173 s = s[1:]
174 }
175 writeSpaces(w, maxSpaces*tabs)
176 w.Write(s)
177 } else if bytes.IndexByte(s, '\t') < 0 {
178 w.Write(s)
179 } else {
180 buf = expandTabs(buf[:0], s, maxSpaces)
181 w.Write(buf)
182 }
183
184 if w.WriteByte('\n') != nil {
185 return io.EOF
186 }
187
188 if !cfg.liveLines {
189 continue
190 }
191
192 if w.Flush() != nil {
193 return io.EOF
194 }
195 }
196
197 return sc.Err()
198 }
199
200 func expandTabs(dst []byte, src []byte, tabStop int) []byte {
201 n := 0
202
203 if tabStop < 1 {
204 return append(dst, src...)
205 }
206
207 for len(src) > 0 {
208 r, size := utf8.DecodeRune(src)
209
210 if r != '\t' {
211 dst = append(dst, src[:size]...)
212 n++
213 } else {
214 spaces := tabStop - n%tabStop
215 dst = appendSpaces(dst, spaces)
216 n += spaces
217 }
218
219 src = src[size:]
220 }
221
222 return dst
223 }
224
225 func appendSpaces(dst []byte, n int) []byte {
226 const (
227 half = ` `
228 spaces = half + half
229 )
230
231 for n >= len(spaces) {
232 dst = append(dst, spaces...)
233 }
234 if n > 0 {
235 dst = append(dst, spaces[:n]...)
236 }
237 return dst
238 }
239
240 func writeSpaces(w *bufio.Writer, n int) {
241 const (
242 half = ` `
243 spaces = half + half
244 )
245
246 for n >= len(spaces) {
247 w.WriteString(spaces)
248 n -= len(spaces)
249 }
250 if n > 0 {
251 w.WriteString(spaces[:n])
252 }
253 }
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.NewWriter(os.Stdout)
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(err.Error())
239 os.Stderr.WriteString("\n")
240 nerr++
241 }
242
243 if alias, ok := styleAliases[sname]; ok {
244 sname = alias
245 }
246
247 style, ok := styles[sname]
248 if !ok {
249 os.Stderr.WriteString("no style named `")
250 os.Stderr.WriteString(args[1])
251 os.Stderr.WriteString("`\n")
252 nerr++
253 }
254
255 pairs = append(pairs, pair{expr: exp, style: style})
256 args = args[2:]
257 }
258
259 if nerr > 0 {
260 os.Exit(1)
261 return
262 }
263
264 liveLines := !buffered
265 if !buffered {
266 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
267 liveLines = false
268 }
269 }
270
271 sc := bufio.NewScanner(os.Stdin)
272 sc.Buffer(nil, 8*1024*1024*1024)
273 bw := bufio.NewWriter(os.Stdout)
274 var plain []byte
275
276 for i := 0; sc.Scan(); i++ {
277 s := sc.Bytes()
278 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
279 s = s[3:]
280 }
281 plain = appendPlain(plain[:0], s)
282
283 if err := handleLine(bw, s, noANSI(plain), pairs); err != nil {
284 return
285 }
286
287 if !liveLines {
288 continue
289 }
290
291 if err := bw.Flush(); err != nil {
292 return
293 }
294 }
295 }
296
297 // appendPlain extends the slice given using the non-ANSI parts of a string
298 func appendPlain(dst []byte, src []byte) []byte {
299 for len(src) > 0 {
300 i, j := indexEscapeSequence(src)
301 if i < 0 {
302 dst = append(dst, src...)
303 break
304 }
305 if j < 0 {
306 j = len(src)
307 }
308
309 if i > 0 {
310 dst = append(dst, src[:i]...)
311 }
312
313 src = src[j:]
314 }
315
316 return dst
317 }
318
319 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
320 // the multi-byte sequences starting with ESC[; the result is a pair of slice
321 // indices which can be independently negative when either the start/end of
322 // a sequence isn't found; given their fairly-common use, even the hyperlink
323 // ESC]8 sequences are supported
324 func indexEscapeSequence(s []byte) (int, int) {
325 var prev byte
326
327 for i, b := range s {
328 if prev == '\x1b' && b == '[' {
329 j := indexLetter(s[i+1:])
330 if j < 0 {
331 return i, -1
332 }
333 return i - 1, i + 1 + j + 1
334 }
335
336 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
337 j := indexPair(s[i+1:], '\x1b', '\\')
338 if j < 0 {
339 return i, -1
340 }
341 return i - 1, i + 1 + j + 2
342 }
343
344 prev = b
345 }
346
347 return -1, -1
348 }
349
350 func indexLetter(s []byte) int {
351 for i, b := range s {
352 upper := b &^ 32
353 if 'A' <= upper && upper <= 'Z' {
354 return i
355 }
356 }
357
358 return -1
359 }
360
361 func indexPair(s []byte, x byte, y byte) int {
362 var prev byte
363
364 for i, b := range s {
365 if prev == x && b == y && i > 0 {
366 return i
367 }
368 prev = b
369 }
370
371 return -1
372 }
373
374 // noANSI ensures arguments to func handleLine are given in the right order
375 type noANSI []byte
376
377 // handleLine styles the current line given to it using the first matching
378 // regex, keeping it as given if none of the regexes match; it's given 2
379 // strings: the first is the original line, the latter is its plain-text
380 // version (with no ANSI codes) and is used for the regex-matching, since
381 // ANSI codes use a mix of numbers and letters, which can themselves match
382 func handleLine(w *bufio.Writer, s []byte, plain noANSI, pairs []pair) error {
383 for _, p := range pairs {
384 if p.expr.Match(plain) {
385 w.WriteString(p.style)
386 w.Write(s)
387 w.WriteString("\x1b[0m")
388 return w.WriteByte('\n')
389 }
390 }
391
392 w.Write(s)
393 return w.WriteByte('\n')
394 }
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(err.Error())
96 os.Stderr.WriteString("\n")
97 continue
98 }
99
100 exprs = append(exprs, exp)
101 }
102
103 // quit right away when given invalid regexes
104 if len(exprs) < len(args) {
105 os.Exit(1)
106 return
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 err := run(os.Stdout, os.Stdin, exprs, liveLines)
117 if err != nil && err != io.EOF {
118 os.Stderr.WriteString(err.Error())
119 os.Stderr.WriteString("\n")
120 os.Exit(1)
121 return
122 }
123 }
124
125 func run(w io.Writer, r io.Reader, exprs []*regexp.Regexp, live bool) error {
126 var buf []byte
127 sc := bufio.NewScanner(r)
128 sc.Buffer(nil, 8*1024*1024*1024)
129 bw := bufio.NewWriter(w)
130 defer bw.Flush()
131
132 src := make([]byte, 8*1024)
133 dst := make([]byte, 8*1024)
134
135 for i := 0; sc.Scan(); i++ {
136 line := sc.Bytes()
137 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
138 line = line[3:]
139 }
140
141 s := line
142 if bytes.IndexByte(s, '\x1b') >= 0 {
143 buf = plain(buf[:0], s)
144 s = buf
145 }
146
147 if len(exprs) > 0 {
148 src = append(src[:0], s...)
149 for _, exp := range exprs {
150 dst = erase(dst[:0], src, exp)
151 src = append(src[:0], dst...)
152 }
153 bw.Write(dst)
154 } else {
155 bw.Write(s)
156 }
157
158 if bw.WriteByte('\n') != nil {
159 return io.EOF
160 }
161
162 if !live {
163 continue
164 }
165
166 if bw.Flush() != nil {
167 return io.EOF
168 }
169 }
170
171 return sc.Err()
172 }
173
174 func erase(dst []byte, src []byte, with *regexp.Regexp) []byte {
175 for len(src) > 0 {
176 span := with.FindIndex(src)
177 // also ignore empty regex matches to avoid infinite outer loops,
178 // as skipping empty slices isn't advancing at all, leaving the
179 // string stuck to being empty-matched forever by the same regex
180 if len(span) != 2 || span[0] == span[1] || span[0] < 0 {
181 return append(dst, src...)
182 }
183
184 start, end := span[0], span[1]
185 dst = append(dst, src[:start]...)
186 // avoid infinite loops caused by empty regex matches
187 if start == end && end < len(src) {
188 dst = append(dst, src[end])
189 end++
190 }
191 src = src[end:]
192 }
193
194 return dst
195 }
196
197 func plain(dst []byte, src []byte) []byte {
198 for len(src) > 0 {
199 i, j := indexEscapeSequence(src)
200 if i < 0 {
201 dst = append(dst, src...)
202 break
203 }
204 if j < 0 {
205 j = len(src)
206 }
207
208 if i > 0 {
209 dst = append(dst, src[:i]...)
210 }
211
212 src = src[j:]
213 }
214
215 return dst
216 }
217
218 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
219 // the multi-byte sequences starting with ESC[; the result is a pair of slice
220 // indices which can be independently negative when either the start/end of
221 // a sequence isn't found; given their fairly-common use, even the hyperlink
222 // ESC]8 sequences are supported
223 func indexEscapeSequence(s []byte) (int, int) {
224 var prev byte
225
226 for i, b := range s {
227 if prev == '\x1b' && b == '[' {
228 j := indexLetter(s[i+1:])
229 if j < 0 {
230 return i, -1
231 }
232 return i - 1, i + 1 + j + 1
233 }
234
235 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
236 j := indexPair(s[i+1:], '\x1b', '\\')
237 if j < 0 {
238 return i, -1
239 }
240 return i - 1, i + 1 + j + 2
241 }
242
243 prev = b
244 }
245
246 return -1, -1
247 }
248
249 func indexLetter(s []byte) int {
250 for i, b := range s {
251 upper := b &^ 32
252 if 'A' <= upper && upper <= 'Z' {
253 return i
254 }
255 }
256
257 return -1
258 }
259
260 func indexPair(s []byte, x byte, y byte) int {
261 var prev byte
262
263 for i, b := range s {
264 if prev == x && b == y && i > 0 {
265 return i
266 }
267 prev = b
268 }
269
270 return -1
271 }
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(err.Error())
121 os.Stderr.WriteString("\n")
122 os.Exit(1)
123 return
124 }
125 }
126
127 func run(w io.Writer, args []string, cfg config) error {
128 bw := bufio.NewWriter(w)
129 defer bw.Flush()
130
131 dashes := 0
132 for _, name := range args {
133 if name == `-` {
134 dashes++
135 }
136 if dashes > 1 {
137 return errors.New(`can't read stdin (dash) more than once`)
138 }
139 }
140
141 if len(args) == 0 {
142 return detab(bw, os.Stdin, cfg)
143 }
144
145 for _, name := range args {
146 if name == `-` {
147 if err := detab(bw, os.Stdin, cfg); err != nil {
148 return err
149 }
150 continue
151 }
152
153 if err := handleFile(bw, name, cfg); err != nil {
154 return err
155 }
156 }
157 return nil
158 }
159
160 func handleFile(w *bufio.Writer, name string, cfg config) error {
161 if name == `` || name == `-` {
162 return detab(w, os.Stdin, cfg)
163 }
164
165 f, err := os.Open(name)
166 if err != nil {
167 return errors.New(`can't read from file named "` + name + `"`)
168 }
169 defer f.Close()
170
171 return detab(w, f, cfg)
172 }
173
174 func detab(w *bufio.Writer, r io.Reader, cfg config) error {
175 const gb = 1024 * 1024 * 1024
176 sc := bufio.NewScanner(r)
177 sc.Buffer(nil, 8*gb)
178
179 var buf []byte
180 maxSpaces := int(cfg.tabStop)
181
182 for i := 0; sc.Scan(); i++ {
183 s := sc.Bytes()
184 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
185 s = s[3:]
186 }
187
188 if maxSpaces < 1 {
189 w.Write(s)
190 } else if cfg.initial {
191 tabs := 0
192 for len(s) > 0 && s[0] == '\t' {
193 tabs++
194 s = s[1:]
195 }
196 writeSpaces(w, maxSpaces*tabs)
197 w.Write(s)
198 } else if bytes.IndexByte(s, '\t') < 0 {
199 w.Write(s)
200 } else {
201 buf = expandTabs(buf[:0], s, maxSpaces)
202 w.Write(buf)
203 }
204
205 if w.WriteByte('\n') != nil {
206 return io.EOF
207 }
208
209 if !cfg.liveLines {
210 continue
211 }
212
213 if w.Flush() != nil {
214 return io.EOF
215 }
216 }
217
218 return sc.Err()
219 }
220
221 func expandTabs(dst []byte, src []byte, tabStop int) []byte {
222 n := 0
223
224 if tabStop < 1 {
225 return append(dst, src...)
226 }
227
228 for len(src) > 0 {
229 r, size := utf8.DecodeRune(src)
230
231 if r != '\t' {
232 dst = append(dst, src[:size]...)
233 n++
234 } else {
235 spaces := tabStop - n%tabStop
236 dst = appendSpaces(dst, spaces)
237 n += spaces
238 }
239
240 src = src[size:]
241 }
242
243 return dst
244 }
245
246 func appendSpaces(dst []byte, n int) []byte {
247 const (
248 half = ` `
249 spaces = half + half
250 )
251
252 for n >= len(spaces) {
253 dst = append(dst, spaces...)
254 }
255 if n > 0 {
256 dst = append(dst, spaces[:n]...)
257 }
258 return dst
259 }
260
261 func writeSpaces(w *bufio.Writer, n int) {
262 const (
263 half = ` `
264 spaces = half + half
265 )
266
267 for n >= len(spaces) {
268 w.WriteString(spaces)
269 n -= len(spaces)
270 }
271 if n > 0 {
272 w.WriteString(spaces[:n])
273 }
274 }
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(err.Error())
67 os.Stderr.WriteString("\n")
68 os.Exit(1)
69 return
70 }
71 }
72
73 func run(args []string) error {
74 w := bufio.NewWriterSize(os.Stdout, 32*1024)
75 defer w.Flush()
76
77 if len(args) == 0 {
78 return factorLines(w)
79 }
80
81 for _, s := range args {
82 n, ok := parseInt64(s)
83 if !ok {
84 return errors.New(s + ` isn't a valid integer`)
85 }
86
87 if err := factor(w, int64(n)); err != nil {
88 return err
89 }
90 }
91 return nil
92 }
93
94 func factor(w *bufio.Writer, n int64) error {
95 var buf [24]byte
96
97 w.Write(strconv.AppendInt(buf[:0], n, 10))
98 w.WriteByte(':')
99
100 for n > 0 && n%2 == 0 {
101 w.WriteByte(' ')
102 w.WriteByte('2')
103 n /= 2
104 }
105
106 // avoid O(n**2) time-complexity by only checking up to the square-root
107 max := uint64(math.Ceil(math.Sqrt(float64(n))))
108
109 for div := uint64(3); div <= max; div += 2 {
110 quo, rem := bits.Div64(0, uint64(n), div)
111 if rem == 0 {
112 s := strconv.AppendInt(append(buf[:0], ' '), int64(div), 10)
113
114 w.Write(s)
115 n = int64(quo)
116
117 for {
118 quo, rem := bits.Div64(0, uint64(n), div)
119 if rem != 0 {
120 break
121 }
122
123 w.Write(s)
124 n = int64(quo)
125 }
126 }
127 }
128
129 if n > 1 {
130 w.WriteByte(' ')
131 w.Write(strconv.AppendInt(buf[:0], int64(n), 10))
132 }
133
134 if err := w.WriteByte('\n'); err != nil {
135 return io.EOF
136 }
137 return nil
138 }
139
140 func factorLines(w *bufio.Writer) error {
141 const gb = 1024 * 1024 * 1024
142 sc := bufio.NewScanner(os.Stdin)
143 sc.Buffer(nil, 8*gb)
144
145 for sc.Scan() {
146 line := strings.TrimSpace(sc.Text())
147
148 for len(line) > 0 {
149 item, rest := advance(line)
150 line = rest
151
152 n, ok := parseInt64(item)
153 if !ok {
154 return errors.New(item + ` isn't a valid integer`)
155 }
156
157 if err := factor(w, int64(n)); err != nil {
158 return err
159 }
160
161 if err := w.Flush(); err != nil {
162 return io.EOF
163 }
164 }
165 }
166
167 return sc.Err()
168 }
169
170 func advance(s string) (lead string, rest string) {
171 for len(s) > 0 && (s[0] == ' ' || s[0] == '\t' || s[0] == '\r') {
172 s = s[1:]
173 }
174
175 for i, r := range s {
176 if r == ' ' || r == '\t' || r == '\r' {
177 return s[:i], s[i:]
178 }
179 }
180
181 return s, ``
182 }
183
184 func parseInt64(s string) (n int64, ok bool) {
185 // n, err := strconv.ParseInt(s, 10, 64)
186 // return n, err == nil
187
188 // handle an optional leading sign
189 sign := int64(1)
190 if len(s) > 0 {
191 if s[0] == '-' {
192 sign = -1
193 s = s[1:]
194 } else if s[0] == '+' {
195 s = s[1:]
196 }
197 }
198
199 if len(s) == 0 {
200 return 0, false
201 }
202
203 digits := 0
204
205 for len(s) > 0 {
206 // less-than-0 byte-wraps around into some bigger-than-9 value
207 if d := s[0] - '0'; d <= 9 {
208 digits++
209 n *= 10
210 n += int64(d)
211 s = s[1:]
212 continue
213 }
214
215 // ignore underscores, which make long numbers easier to type right
216 if s[0] == '_' {
217 s = s[1:]
218 continue
219 }
220
221 return sign * n, false
222 }
223
224 return sign * n, digits > 0
225 }
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: ./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(err.Error())
91 os.Stderr.WriteString("\n")
92 os.Exit(1)
93 return
94 }
95 }
96
97 type config struct {
98 skipSubfolder error
99 liveLines bool
100 }
101
102 func run(w io.Writer, paths []string, cfg config) error {
103 bw := bufio.NewWriter(w)
104 defer bw.Flush()
105
106 got := make(map[string]struct{})
107
108 if len(paths) == 0 {
109 paths = []string{`.`}
110 }
111
112 // handle is the callback for func filepath.WalkDir
113 handle := func(path string, e fs.DirEntry, err error) error {
114 if err != nil {
115 return err
116 }
117
118 if _, ok := got[path]; ok {
119 return nil
120 }
121 got[path] = struct{}{}
122
123 if e.IsDir() {
124 return cfg.skipSubfolder
125 }
126
127 return handleEntry(bw, path, cfg.liveLines)
128 }
129
130 for _, path := range paths {
131 if _, ok := got[path]; ok {
132 continue
133 }
134 got[path] = struct{}{}
135
136 st, err := os.Stat(path)
137 if err != nil {
138 return err
139 }
140
141 if st.IsDir() {
142 if !strings.HasSuffix(path, `/`) {
143 path = path + `/`
144 }
145 got[path] = struct{}{}
146
147 if err := filepath.WalkDir(path, handle); err != nil {
148 return err
149 }
150 continue
151 }
152
153 if err := handleEntry(bw, path, cfg.liveLines); err != nil {
154 return err
155 }
156 }
157
158 return nil
159 }
160
161 func handleEntry(w *bufio.Writer, path string, live bool) error {
162 abs, err := filepath.Abs(path)
163 if err != nil {
164 return err
165 }
166
167 w.WriteString(abs)
168 w.WriteByte('\n')
169
170 if !live {
171 return nil
172 }
173
174 if w.Flush() != nil {
175 return io.EOF
176 }
177 return nil
178 }
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(err.Error())
104 os.Stderr.WriteString("\n")
105 os.Exit(1)
106 return
107 }
108 }
109
110 func parseBlockSizeOption(s string) (size int, ok bool) {
111 if !strings.HasPrefix(s, `-`) {
112 return 0, false
113 }
114 if len(s) > 0 && s[0] == '-' {
115 s = s[1:]
116 }
117 if len(s) > 0 && s[0] == '-' {
118 s = s[1:]
119 }
120
121 if !strings.HasSuffix(s, `k`) && !strings.HasSuffix(s, `K`) {
122 return 0, false
123 }
124 s = s[:len(s)-1]
125
126 if n, err := strconv.ParseInt(s, 10, 64); err == nil && n > 0 {
127 return int(n), true
128 }
129 return 0, false
130 }
131
132 type config struct {
133 blockSize int
134 sorted bool
135 recursive bool
136 liveLines bool
137 }
138
139 type entry struct {
140 path string
141 size int64
142 }
143
144 type handlers struct {
145 w *bufio.Writer
146 entries []entry
147 blockSize int
148 skipSubfolder error
149
150 file func(h *handlers, path string, size int64) error
151
152 liveLines bool
153 }
154
155 func (h handlers) countBlocks(size int64) int64 {
156 bs := int64(h.blockSize)
157 n := size / (1024 * bs)
158 if size%(1024*bs) != 0 {
159 n++
160 }
161 return n * bs
162 }
163
164 func run(w io.Writer, paths []string, cfg config) error {
165 bw := bufio.NewWriter(w)
166 defer bw.Flush()
167
168 bw.WriteString("name\tbytes\tblocks\n")
169
170 var h handlers
171 h.w = bw
172 h.blockSize = cfg.blockSize
173 h.skipSubfolder = nil
174 if !cfg.recursive {
175 h.skipSubfolder = fs.SkipDir
176 }
177 h.file = emitEntry
178 if cfg.sorted {
179 h.file = keepEntry
180 }
181
182 got := make(map[string]struct{})
183
184 if len(paths) == 0 {
185 paths = []string{`.`}
186 }
187
188 // handle is the callback for func filepath.WalkDir
189 handle := func(path string, e fs.DirEntry, err error) error {
190 if err != nil {
191 return err
192 }
193
194 if _, ok := got[path]; ok {
195 return nil
196 }
197 got[path] = struct{}{}
198
199 if e.IsDir() {
200 return h.skipSubfolder
201 }
202
203 info, err := e.Info()
204 if err != nil {
205 return err
206 }
207
208 return h.file(&h, path, info.Size())
209 }
210
211 for _, path := range paths {
212 if _, ok := got[path]; ok {
213 continue
214 }
215 got[path] = struct{}{}
216
217 st, err := os.Stat(path)
218 if err != nil {
219 return err
220 }
221
222 if st.IsDir() {
223 if !strings.HasSuffix(path, `/`) {
224 path = path + `/`
225 }
226 got[path] = struct{}{}
227
228 if err := filepath.WalkDir(path, handle); err != nil {
229 return err
230 }
231 continue
232 }
233
234 var size int64
235 if path != `/dev/stdin` {
236 size = st.Size()
237 } else {
238 size = countBytes(os.Stdin)
239 }
240
241 if err := h.file(&h, path, size); err != nil {
242 return err
243 }
244 }
245
246 if !cfg.sorted {
247 return nil
248 }
249
250 sort.Slice(h.entries, func(i, j int) bool {
251 return h.entries[i].size > h.entries[j].size
252 })
253
254 for _, e := range h.entries {
255 if err := writeEntry(bw, e, h); err != nil {
256 return err
257 }
258 }
259
260 return nil
261 }
262
263 func countBytes(r io.Reader) (count int64) {
264 var buf [32 * 1024]byte
265
266 for {
267 got, err := r.Read(buf[:])
268 if got > 0 {
269 count += int64(got)
270 }
271 if err != nil {
272 break
273 }
274 }
275
276 return count
277 }
278
279 func emitEntry(h *handlers, path string, size int64) error {
280 return writeEntry(h.w, entry{path, size}, *h)
281 }
282
283 func keepEntry(h *handlers, path string, size int64) error {
284 h.entries = append(h.entries, entry{path, size})
285 return nil
286 }
287
288 func writeEntry(w *bufio.Writer, e entry, h handlers) error {
289 abs, err := filepath.Abs(e.path)
290 if err != nil {
291 return err
292 }
293
294 var buf [24]byte
295 w.WriteString(abs)
296 w.WriteByte('\t')
297 w.Write(strconv.AppendInt(buf[:0], e.size, 10))
298 w.WriteByte('\t')
299 w.Write(strconv.AppendInt(buf[:0], h.countBlocks(e.size), 10))
300 w.WriteByte('\n')
301
302 if !h.liveLines {
303 return nil
304 }
305
306 if w.Flush() != nil {
307 return io.EOF
308 }
309 return nil
310 }
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.NewWriter(os.Stdout)
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(err.Error())
95 os.Stderr.WriteString("\n")
96 os.Exit(1)
97 return
98 }
99 }
100
101 func run(w io.Writer, paths []string, linesLeft int, liveLines bool) error {
102 bw := bufio.NewWriter(w)
103 defer bw.Flush()
104
105 for _, path := range paths {
106 if err := handleFile(bw, path, &linesLeft, liveLines); err != nil {
107 return err
108 }
109 }
110
111 if len(paths) == 0 {
112 return first(bw, os.Stdin, &linesLeft, liveLines)
113 }
114 return nil
115 }
116
117 func handleFile(w *bufio.Writer, name string, left *int, live bool) error {
118 if name == `` || name == `-` {
119 return first(w, os.Stdin, left, live)
120 }
121
122 f, err := os.Open(name)
123 if err != nil {
124 return errors.New(`can't read from file named "` + name + `"`)
125 }
126 defer f.Close()
127
128 return first(w, f, left, live)
129 }
130
131 func first(w *bufio.Writer, r io.Reader, left *int, live bool) error {
132 if *left < 1 {
133 return io.EOF
134 }
135
136 const gb = 1024 * 1024 * 1024
137 sc := bufio.NewScanner(r)
138 sc.Buffer(nil, 8*gb)
139
140 for i := 0; sc.Scan(); i++ {
141 s := sc.Bytes()
142 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
143 s = s[3:]
144 }
145
146 w.Write(s)
147 if w.WriteByte('\n') != nil {
148 return io.EOF
149 }
150
151 if live {
152 if err := w.Flush(); err != nil {
153 return io.EOF
154 }
155 }
156
157 *left--
158 if *left < 1 {
159 break
160 }
161 }
162
163 return sc.Err()
164 }
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(err.Error())
109 os.Stderr.WriteString("\n")
110 os.Exit(1)
111 return
112 }
113 }
114
115 func run(w io.Writer, args []string, cfg config) error {
116 dashes := 0
117 for _, name := range args {
118 if name == `-` {
119 dashes++
120 }
121 if dashes > 1 {
122 return errors.New(`can't use stdin (dash) more than once`)
123 }
124 }
125
126 bw := bufio.NewWriter(w)
127 defer bw.Flush()
128
129 if len(args) == 0 {
130 return cfg.fix(bw, os.Stdin, cfg.liveLines)
131 }
132
133 for _, name := range args {
134 if err := handleFile(bw, name, cfg); err != nil {
135 return err
136 }
137 }
138 return nil
139 }
140
141 func handleFile(w *bufio.Writer, name string, cfg config) error {
142 if name == `` || name == `-` {
143 return cfg.fix(w, os.Stdin, cfg.liveLines)
144 }
145
146 f, err := os.Open(name)
147 if err != nil {
148 return errors.New(`can't read from file named "` + name + `"`)
149 }
150 defer f.Close()
151
152 return cfg.fix(w, f, cfg.liveLines)
153 }
154
155 func catl(w *bufio.Writer, r io.Reader, live bool) error {
156 const gb = 1024 * 1024 * 1024
157 sc := bufio.NewScanner(r)
158 sc.Buffer(nil, 8*gb)
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 w.Write(s)
167 if w.WriteByte('\n') != nil {
168 return io.EOF
169 }
170
171 if !live {
172 continue
173 }
174
175 if w.Flush() != nil {
176 return io.EOF
177 }
178 }
179
180 return sc.Err()
181 }
182
183 func detrail(w *bufio.Writer, r io.Reader, live bool) error {
184 const gb = 1024 * 1024 * 1024
185 sc := bufio.NewScanner(r)
186 sc.Buffer(nil, 8*gb)
187
188 for i := 0; sc.Scan(); i++ {
189 s := sc.Bytes()
190
191 // ignore leading UTF-8 BOM on the first line
192 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
193 s = s[3:]
194 }
195
196 // trim trailing spaces on the current line
197 for len(s) > 0 && s[len(s)-1] == ' ' {
198 s = s[:len(s)-1]
199 }
200
201 w.Write(s)
202 if w.WriteByte('\n') != nil {
203 return io.EOF
204 }
205
206 if !live {
207 continue
208 }
209
210 if w.Flush() != nil {
211 return io.EOF
212 }
213 }
214
215 return sc.Err()
216 }
217
218 func squeeze(w *bufio.Writer, r io.Reader, live bool) error {
219 const gb = 1024 * 1024 * 1024
220 sc := bufio.NewScanner(r)
221 sc.Buffer(nil, 8*gb)
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 writeSqueezed(w, s)
230 if w.WriteByte('\n') != nil {
231 return io.EOF
232 }
233
234 if !live {
235 continue
236 }
237
238 if w.Flush() != nil {
239 return io.EOF
240 }
241 }
242
243 return sc.Err()
244 }
245
246 func writeSqueezed(w *bufio.Writer, s []byte) {
247 // ignore leading spaces
248 for len(s) > 0 && s[0] == ' ' {
249 s = s[1:]
250 }
251
252 // ignore trailing spaces
253 for len(s) > 0 && s[len(s)-1] == ' ' {
254 s = s[:len(s)-1]
255 }
256
257 space := false
258
259 for len(s) > 0 {
260 switch s[0] {
261 case ' ':
262 s = s[1:]
263 space = true
264
265 case '\t':
266 s = s[1:]
267 space = false
268 for len(s) > 0 && s[0] == ' ' {
269 s = s[1:]
270 }
271 w.WriteByte('\t')
272
273 default:
274 if space {
275 w.WriteByte(' ')
276 space = false
277 }
278 w.WriteByte(s[0])
279 s = s[1:]
280 }
281 }
282 }
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 == 0.0 || expr.y == 0.0 {
217 // 0*y -> 0
218 // x*0 -> 0
219 return 0.0
220 }
221 if expr.x == 1.0 {
222 // 1*y -> y
223 return expr.y
224 }
225 if expr.y == 1.0 {
226 // x*1 -> x
227 return expr.x
228 }
229 if expr.x == -1.0 {
230 // -1*y -> -y
231 return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.y})
232 }
233 if expr.y == -1.0 {
234 // x*-1 -> -x
235 return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.x})
236 }
237
238 case "/":
239 if expr.x == 1.0 {
240 // 1/y -> reciprocal of y
241 return c.optimizeUnaryExpr(unaryExpr{op: "/", x: expr.y})
242 }
243 if expr.y == 1.0 {
244 // x/1 -> x
245 return expr.x
246 }
247 if expr.y == -1.0 {
248 // x/-1 -> -x
249 return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.x})
250 }
251
252 case "**":
253 switch expr.y {
254 case -1.0:
255 // x**-1 -> 1/x, reciprocal of x
256 return c.optimizeUnaryExpr(unaryExpr{op: "/", x: expr.x})
257 case 0.0:
258 // x**0 -> 1
259 return 1.0
260 case 1.0:
261 // x**1 -> x
262 return expr.x
263 case 2.0:
264 // x**2 -> *x
265 return c.optimizeUnaryExpr(unaryExpr{op: "*", x: expr.x})
266 case 3.0:
267 // x**3 -> *x*x
268 sq := unaryExpr{op: "*", x: expr.x}
269 return c.optimizeBinaryExpr(binaryExpr{op: "*", x: sq, y: expr.x})
270 }
271
272 case "&", "&&":
273 if expr.x == 0.0 || expr.y == 0.0 {
274 // 0 && y -> 0
275 // x && 0 -> 0
276 return 0.0
277 }
278 }
279
280 // no simplifiable patterns were detected
281 return expr
282 }
283
284 // optimizeCallExpr optimizes special cases of built-in func calls
285 func (c *Compiler) optimizeCallExpr(call callExpr) any {
286 // recursively optimize all inputs, and keep track if they're all
287 // constants in the end
288 numlit := 0
289 for i, v := range call.args {
290 v = c.optimize(v)
291 call.args[i] = v
292 if _, ok := v.(float64); ok {
293 numlit++
294 }
295 }
296
297 // if func is overridden, there's no guarantee the new func works the same
298 if _, ok := determFuncs[call.name]; c.ftype[call.name] != nil || !ok {
299 return call
300 }
301
302 // from this point on, func is guaranteed to be built-in and deterministic
303
304 // handle all-const inputs, by calling func and using its result
305 if numlit == len(call.args) {
306 in := make([]float64, 0, len(call.args))
307 for _, v := range call.args {
308 f, _ := v.(float64)
309 in = append(in, f)
310 }
311
312 if f, ok := tryCall(determFuncs[call.name], in); ok {
313 return f
314 }
315 }
316
317 switch len(call.args) {
318 case 1:
319 if op, ok := func2unary[call.name]; ok {
320 expr := unaryExpr{op: op, x: call.args[0]}
321 return c.optimizeUnaryExpr(expr)
322 }
323 return call
324
325 case 2:
326 if op, ok := func2binary[call.name]; ok {
327 expr := binaryExpr{op: op, x: call.args[0], y: call.args[1]}
328 return c.optimizeBinaryExpr(expr)
329 }
330 if name, ok := vararg2func2[call.name]; ok {
331 call.name = name
332 return call
333 }
334 return call
335
336 default:
337 return call
338 }
339 }
340
341 // tryConstant tries to optimize the expression given into a constant
342 func (c *Compiler) tryConstant(expr any) (value float64, ok bool) {
343 switch expr := expr.(type) {
344 case float64:
345 return expr, true
346
347 case string:
348 if _, ok := c.vaddr[expr]; !ok {
349 // name isn't explicitly defined
350 if f, ok := mathConst[expr]; ok {
351 // and is a known math constant
352 return f, true
353 }
354 }
355 return 0, false
356
357 default:
358 return 0, false
359 }
360 }
361
362 // tryCall tries to simplify the function expression given into a constant
363 func tryCall(fn any, in []float64) (value float64, ok bool) {
364 switch fn := fn.(type) {
365 case func(float64) float64:
366 if len(in) == 1 {
367 return fn(in[0]), true
368 }
369 return 0, false
370
371 case func(float64, float64) float64:
372 if len(in) == 2 {
373 return fn(in[0], in[1]), true
374 }
375 return 0, false
376
377 case func(float64, float64, float64) float64:
378 if len(in) == 3 {
379 return fn(in[0], in[1], in[2]), true
380 }
381 return 0, false
382
383 case func(float64, float64, float64, float64) float64:
384 if len(in) == 4 {
385 return fn(in[0], in[1], in[2], in[3]), true
386 }
387 return 0, false
388
389 case func(float64, float64, float64, float64, float64) float64:
390 if len(in) == 5 {
391 return fn(in[0], in[1], in[2], in[3], in[4]), true
392 }
393 return 0, false
394
395 case func(...float64) float64:
396 return fn(in...), true
397
398 case func(float64, ...float64) float64:
399 if len(in) >= 1 {
400 return fn(in[0], in[1:]...), true
401 }
402 return 0, false
403
404 default:
405 // type isn't a supported func
406 return 0, false
407 }
408 }
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(err.Error())
90 os.Stderr.WriteString("\n")
91 os.Exit(1)
92 return
93 }
94 }
95
96 type config struct {
97 got map[string]struct{}
98 recursive bool
99 liveLines bool
100 }
101
102 func run(w io.Writer, paths []string, cfg config) error {
103 bw := bufio.NewWriter(w)
104 defer bw.Flush()
105
106 if len(paths) == 0 {
107 paths = []string{`.`}
108 }
109
110 if cfg.recursive {
111 return runRecursive(bw, paths, cfg)
112 }
113 return runFlat(bw, paths, cfg)
114 }
115
116 func runRecursive(w *bufio.Writer, paths []string, cfg config) error {
117 if len(paths) == 0 {
118 paths = []string{`.`}
119 }
120
121 // handle is the callback for func filepath.WalkDir
122 handle := func(path string, e fs.DirEntry, err error) error {
123 if err != nil {
124 return err
125 }
126
127 if _, ok := cfg.got[path]; ok {
128 return nil
129 }
130 cfg.got[path] = struct{}{}
131
132 if e.IsDir() {
133 if err := handleEntry(w, path, cfg.liveLines); err != nil {
134 return err
135 }
136 }
137
138 return nil
139 }
140
141 for _, path := range paths {
142 if _, ok := cfg.got[path]; ok {
143 continue
144 }
145 cfg.got[path] = struct{}{}
146
147 st, err := os.Stat(path)
148 if err != nil {
149 return err
150 }
151
152 if !strings.HasSuffix(path, `/`) {
153 path = path + `/`
154 cfg.got[path] = struct{}{}
155 }
156
157 if !st.IsDir() {
158 continue
159 }
160
161 if err := filepath.WalkDir(path, handle); err != nil {
162 return err
163 }
164 }
165
166 return nil
167 }
168
169 func runFlat(w *bufio.Writer, paths []string, cfg config) error {
170 if len(paths) == 0 {
171 paths = []string{`.`}
172 }
173
174 for _, path := range paths {
175 if _, ok := cfg.got[path]; ok {
176 continue
177 }
178 cfg.got[path] = struct{}{}
179
180 st, err := os.Stat(path)
181 if err != nil {
182 return err
183 }
184
185 if !strings.HasSuffix(path, `/`) {
186 path = path + `/`
187 cfg.got[path] = struct{}{}
188 }
189
190 if !st.IsDir() {
191 continue
192 }
193
194 entries, err := os.ReadDir(path)
195 if err != nil {
196 return err
197 }
198
199 for _, e := range entries {
200 if !e.IsDir() {
201 continue
202 }
203
204 path := filepath.Join(path, e.Name())
205 if err := handleEntry(w, path, cfg.liveLines); err != nil {
206 return err
207 }
208 }
209 }
210
211 return nil
212 }
213
214 func handleEntry(w *bufio.Writer, path string, live bool) error {
215 abs, err := filepath.Abs(path)
216 if err != nil {
217 return err
218 }
219
220 w.WriteString(abs)
221 w.WriteByte('\n')
222
223 if !live {
224 return nil
225 }
226
227 if w.Flush() != nil {
228 return io.EOF
229 }
230 return nil
231 }
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(err.Error())
117 os.Stderr.WriteString("\n")
118 errcount++
119 continue
120 }
121
122 args = args[1:]
123 var with []string
124 if !erase && len(args) > 0 {
125 with = splitReplacement(args[0])
126 args = args[1:]
127 }
128 pairs = append(pairs, pair{what, with})
129 }
130
131 // quit right away when given invalid regexes
132 if errcount > 0 {
133 os.Exit(1)
134 return
135 }
136
137 liveLines := !buffered
138 if !buffered {
139 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
140 liveLines = false
141 }
142 }
143
144 err := run(os.Stdout, os.Stdin, pairs, liveLines)
145 if err != nil && err != io.EOF {
146 os.Stderr.WriteString(err.Error())
147 os.Stderr.WriteString("\n")
148 os.Exit(1)
149 return
150 }
151 }
152
153 func splitReplacement(s string) []string {
154 if s == `` {
155 return nil
156 }
157
158 n := 1
159 var prev rune
160 for _, r := range s {
161 if prev != '\\' && r == '&' {
162 n += 2
163 }
164 prev = r
165 }
166
167 repl := make([]string, 0, n)
168
169 for len(s) > 0 {
170 i := strings.IndexByte(s, '&')
171 if i == 0 || (i > 0 && s[i-1] != '\\') {
172 repl = append(repl, unescapeBackslashes(s[:i]))
173 repl = append(repl, ``)
174 s = s[i+1:]
175 continue
176 }
177 break
178 }
179
180 if len(s) > 0 {
181 repl = append(repl, unescapeBackslashes(s))
182 }
183 return repl
184 }
185
186 func unescapeBackslashes(s string) string {
187 if strings.IndexByte(s, '\\') < 0 {
188 return s
189 }
190
191 var prev byte
192 unesc := make([]byte, 0, len(s))
193
194 for i := range s {
195 b := s[i]
196
197 if prev != '\\' {
198 if b != '\\' {
199 unesc = append(unesc, s[i])
200 }
201 prev = b
202 continue
203 }
204
205 switch b {
206 case 'e':
207 unesc = append(unesc, '\x1b')
208 case 'n':
209 unesc = append(unesc, '\n')
210 case 'r':
211 unesc = append(unesc, '\r')
212 case 't':
213 unesc = append(unesc, '\t')
214 case 'v':
215 unesc = append(unesc, '\v')
216 default:
217 unesc = append(unesc, s[i])
218 }
219
220 prev = b
221 }
222
223 return string(unesc)
224 }
225
226 func run(w io.Writer, r io.Reader, pairs []pair, live bool) error {
227 var buf []byte
228 sc := bufio.NewScanner(r)
229 sc.Buffer(nil, 8*1024*1024*1024)
230 bw := bufio.NewWriter(w)
231 defer bw.Flush()
232
233 src := make([]byte, 8*1024)
234 dst := make([]byte, 8*1024)
235
236 for i := 0; sc.Scan(); i++ {
237 line := sc.Bytes()
238 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
239 line = line[3:]
240 }
241
242 s := line
243 if bytes.IndexByte(s, '\x1b') >= 0 {
244 buf = plain(buf[:0], s)
245 s = buf
246 }
247
248 if len(pairs) > 0 {
249 src = append(src[:0], s...)
250 for _, p := range pairs {
251 dst = gsub(dst[:0], src, p.expr, p.repl)
252 src = append(src[:0], dst...)
253 }
254 bw.Write(dst)
255 } else {
256 bw.Write(s)
257 }
258
259 if bw.WriteByte('\n') != nil {
260 return io.EOF
261 }
262
263 if !live {
264 continue
265 }
266
267 if bw.Flush() != nil {
268 return io.EOF
269 }
270 }
271
272 return sc.Err()
273 }
274
275 func gsub(dst []byte, src []byte, what *regexp.Regexp, with []string) []byte {
276 for len(src) > 0 {
277 span := what.FindIndex(src)
278 // also ignore empty regex matches to avoid infinite outer loops,
279 // as skipping empty slices isn't advancing at all, leaving the
280 // string stuck to being empty-matched forever by the same regex
281 if len(span) != 2 || span[0] == span[1] || span[0] < 0 {
282 return append(dst, src...)
283 }
284
285 start, end := span[0], span[1]
286 dst = append(dst, src[:start]...)
287 // avoid infinite loops caused by empty regex matches
288 if start == end {
289 if end >= len(src) {
290 break
291 }
292 dst = append(dst, src[end])
293 end++
294 src = src[end:]
295 continue
296 }
297
298 match := src[start:end]
299 for _, sub := range with {
300 if sub == `` {
301 dst = append(dst, match...)
302 continue
303 }
304 dst = append(dst, sub...)
305 }
306
307 src = src[end:]
308 }
309
310 return dst
311 }
312
313 func plain(dst []byte, src []byte) []byte {
314 for len(src) > 0 {
315 i, j := indexEscapeSequence(src)
316 if i < 0 {
317 dst = append(dst, src...)
318 break
319 }
320 if j < 0 {
321 j = len(src)
322 }
323
324 if i > 0 {
325 dst = append(dst, src[:i]...)
326 }
327
328 src = src[j:]
329 }
330
331 return dst
332 }
333
334 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
335 // the multi-byte sequences starting with ESC[; the result is a pair of slice
336 // indices which can be independently negative when either the start/end of
337 // a sequence isn't found; given their fairly-common use, even the hyperlink
338 // ESC]8 sequences are supported
339 func indexEscapeSequence(s []byte) (int, int) {
340 var prev byte
341
342 for i, b := range s {
343 if prev == '\x1b' && b == '[' {
344 j := indexLetter(s[i+1:])
345 if j < 0 {
346 return i, -1
347 }
348 return i - 1, i + 1 + j + 1
349 }
350
351 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
352 j := indexPair(s[i+1:], '\x1b', '\\')
353 if j < 0 {
354 return i, -1
355 }
356 return i - 1, i + 1 + j + 2
357 }
358
359 prev = b
360 }
361
362 return -1, -1
363 }
364
365 func indexLetter(s []byte) int {
366 for i, b := range s {
367 upper := b &^ 32
368 if 'A' <= upper && upper <= 'Z' {
369 return i
370 }
371 }
372
373 return -1
374 }
375
376 func indexPair(s []byte, x byte, y byte) int {
377 var prev byte
378
379 for i, b := range s {
380 if prev == x && b == y && i > 0 {
381 return i
382 }
383 prev = b
384 }
385
386 return -1
387 }
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("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(err.Error())
110 os.Stderr.WriteString("\n")
111 os.Exit(1)
112 return
113 }
114 }
115
116 func run(paths []string, cfg *config) error {
117 w := bufio.NewWriterSize(os.Stdout, 32*1024)
118 defer w.Flush()
119
120 for _, path := range paths {
121 if cfg.max <= 0 {
122 return io.EOF
123 }
124 if err := handleFile(w, path, cfg); err != nil {
125 return err
126 }
127 }
128
129 if len(paths) == 0 {
130 if err := head(w, os.Stdin, cfg); err != nil {
131 return err
132 }
133 }
134 return nil
135 }
136
137 func handleFile(w *bufio.Writer, path string, cfg *config) error {
138 f, err := os.Open(path)
139 if err != nil {
140 return err
141 }
142 defer f.Close()
143 return head(w, f, cfg)
144 }
145
146 func head(w *bufio.Writer, r io.Reader, cfg *config) error {
147 const gb = 1024 * 1024 * 1024
148 sc := bufio.NewScanner(r)
149 sc.Buffer(nil, 8*gb)
150
151 for sc.Scan() {
152 if cfg.max <= 0 {
153 return io.EOF
154 }
155
156 w.Write(sc.Bytes())
157 if err := w.WriteByte('\n'); err != nil {
158 return io.EOF
159 }
160
161 cfg.max--
162
163 if !cfg.liveLines {
164 continue
165 }
166
167 if w.Flush() != nil {
168 return io.EOF
169 }
170 }
171
172 return sc.Err()
173 }
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 -h, -help show this help message
52 -f, -filter filter out (ignore) lines with no matches
53 -i, -ins match regexes case-insensitively
54 `
55
56 const highlightStyle = "\x1b[7m"
57
58 func Main() {
59 filter := false
60 buffered := false
61 insensitive := false
62 args := os.Args[1:]
63
64 for len(args) > 0 {
65 switch args[0] {
66 case `-b`, `--b`, `-buffered`, `--buffered`:
67 buffered = true
68 args = args[1:]
69 continue
70
71 case `-f`, `--f`, `-filter`, `--filter`, `-filtered`, `--filtered`:
72 filter = true
73 args = args[1:]
74 continue
75
76 case `-fi`, `--fi`, `-if`, `--if`:
77 filter = true
78 insensitive = true
79 args = args[1:]
80 continue
81
82 case `-h`, `--h`, `-help`, `--help`:
83 os.Stdout.WriteString(info[1:])
84 return
85
86 case `-i`, `--i`, `-ins`, `--ins`:
87 insensitive = true
88 args = args[1:]
89 continue
90 }
91
92 break
93 }
94
95 if len(args) > 0 && args[0] == `--` {
96 args = args[1:]
97 }
98
99 patterns := make([]pattern, 0, len(args))
100
101 for _, s := range args {
102 var err error
103 var pat pattern
104
105 if insensitive {
106 pat, err = compile(`(?i)` + s)
107 } else {
108 pat, err = compile(s)
109 }
110
111 if err != nil {
112 os.Stderr.WriteString(err.Error())
113 os.Stderr.WriteString("\n")
114 continue
115 }
116
117 patterns = append(patterns, pat)
118 }
119
120 // quit right away when given invalid regexes
121 if len(patterns) < len(args) {
122 os.Exit(1)
123 return
124 }
125
126 liveLines := !buffered
127 if !buffered {
128 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
129 liveLines = false
130 }
131 }
132
133 ok, err := run(os.Stdout, os.Stdin, patterns, filter, liveLines)
134 if err != nil && err != io.EOF {
135 os.Stderr.WriteString(err.Error())
136 os.Stderr.WriteString("\n")
137 os.Exit(1)
138 return
139 }
140
141 if !ok {
142 os.Exit(1)
143 return
144 }
145 }
146
147 // pattern is a regular-expression pattern which distinguishes between the
148 // start/end of a line and those of the chunks it can be used to match
149 type pattern struct {
150 // expr is the regular-expression
151 expr *regexp.Regexp
152
153 // begin is whether the regexp refers to the start of a line
154 begin bool
155
156 // end is whether the regexp refers to the end of a line
157 end bool
158 }
159
160 func compile(src string) (pattern, error) {
161 expr, err := regexp.Compile(src)
162
163 var pat pattern
164 pat.expr = expr
165 pat.begin = strings.HasPrefix(src, `^`) || strings.HasPrefix(src, `(?i)^`)
166 pat.end = strings.HasSuffix(src, `$`) && !strings.HasSuffix(src, `\$`)
167 return pat, err
168 }
169
170 func (p pattern) findIndex(s []byte, i int, last int) (start int, stop int) {
171 if i > 0 && p.begin {
172 return -1, -1
173 }
174 if i != last && p.end {
175 return -1, -1
176 }
177
178 span := p.expr.FindIndex(s)
179 // also ignore empty regex matches to avoid infinite outer loops,
180 // as skipping empty slices isn't advancing at all, leaving the
181 // string stuck to being empty-matched forever by the same regex
182 if len(span) != 2 || span[0] == span[1] {
183 return -1, -1
184 }
185
186 return span[0], span[1]
187 }
188
189 func run(w io.Writer, r io.Reader, pats []pattern, filter, live bool) (bool, error) {
190 sc := bufio.NewScanner(r)
191 sc.Buffer(nil, 8*1024*1024*1024)
192 bw := bufio.NewWriter(w)
193 defer bw.Flush()
194
195 ok := !filter
196
197 for i := 0; sc.Scan(); i++ {
198 s := sc.Bytes()
199 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
200 s = s[3:]
201 }
202
203 n := 0
204 last := countChunks(s) - 1
205 if last < 0 {
206 last = 0
207 }
208
209 if filter && !matches(s, pats, last) {
210 continue
211 }
212 if filter {
213 ok = true
214 }
215
216 for len(s) > 0 {
217 i, j := indexEscapeSequence(s)
218 if i < 0 {
219 handleChunk(bw, s, pats, n, last)
220 break
221 }
222 if j < 0 {
223 j = len(s)
224 }
225
226 handleChunk(bw, s[:i], pats, n, last)
227 if i > 0 {
228 n++
229 }
230
231 bw.Write(s[i:j])
232
233 s = s[j:]
234 }
235
236 if bw.WriteByte('\n') != nil {
237 return ok, io.EOF
238 }
239
240 if !live {
241 continue
242 }
243
244 if bw.Flush() != nil {
245 return ok, io.EOF
246 }
247 }
248
249 err := sc.Err()
250 return err == nil && ok, err
251 }
252
253 // matches finds out if any regex matches any substring around ANSI-sequences
254 func matches(s []byte, patterns []pattern, last int) bool {
255 n := 0
256
257 for len(s) > 0 {
258 i, j := indexEscapeSequence(s)
259 if i < 0 {
260 for _, p := range patterns {
261 if begin, _ := p.findIndex(s, n, last); begin >= 0 {
262 return true
263 }
264 }
265 return false
266 }
267
268 if j < 0 {
269 j = len(s)
270 }
271
272 for _, p := range patterns {
273 if begin, _ := p.findIndex(s[:i], n, last); begin >= 0 {
274 return true
275 }
276 }
277
278 if i > 0 {
279 n++
280 }
281
282 s = s[j:]
283 }
284
285 return false
286 }
287
288 func countChunks(s []byte) int {
289 chunks := 0
290
291 for len(s) > 0 {
292 i, j := indexEscapeSequence(s)
293 if i < 0 {
294 break
295 }
296
297 if i > 0 {
298 chunks++
299 }
300
301 if j < 0 {
302 break
303 }
304 s = s[j:]
305 }
306
307 if len(s) > 0 {
308 chunks++
309 }
310 return chunks
311 }
312
313 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
314 // the multi-byte sequences starting with ESC[; the result is a pair of slice
315 // indices which can be independently negative when either the start/end of
316 // a sequence isn't found; given their fairly-common use, even the hyperlink
317 // ESC]8 sequences are supported
318 func indexEscapeSequence(s []byte) (int, int) {
319 var prev byte
320
321 for i, b := range s {
322 if prev == '\x1b' && b == '[' {
323 j := indexLetter(s[i+1:])
324 if j < 0 {
325 return i, -1
326 }
327 return i - 1, i + 1 + j + 1
328 }
329
330 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
331 j := indexPair(s[i+1:], '\x1b', '\\')
332 if j < 0 {
333 return i, -1
334 }
335 return i - 1, i + 1 + j + 2
336 }
337
338 prev = b
339 }
340
341 return -1, -1
342 }
343
344 func indexLetter(s []byte) int {
345 for i, b := range s {
346 upper := b &^ 32
347 if 'A' <= upper && upper <= 'Z' {
348 return i
349 }
350 }
351
352 return -1
353 }
354
355 func indexPair(s []byte, x byte, y byte) int {
356 var prev byte
357
358 for i, b := range s {
359 if prev == x && b == y && i > 0 {
360 return i
361 }
362 prev = b
363 }
364
365 return -1
366 }
367
368 // note: looking at the results of restoring ANSI-styles after style-resets
369 // doesn't seem to be worth it, as a previous version used to do
370
371 // handleChunk handles line-slices around any detected ANSI-style sequences,
372 // or even whole lines, when no ANSI-styles are found in them
373 func handleChunk(w *bufio.Writer, s []byte, with []pattern, n int, last int) {
374 for len(s) > 0 {
375 start, end := -1, -1
376 for _, p := range with {
377 i, j := p.findIndex(s, n, last)
378 if i >= 0 && (i < start || start < 0) {
379 start, end = i, j
380 }
381 }
382
383 if start < 0 {
384 w.Write(s)
385 return
386 }
387
388 w.Write(s[:start])
389 w.WriteString(highlightStyle)
390 w.Write(s[start:end])
391 w.WriteString("\x1b[0m")
392
393 s = s[end:]
394 }
395 }
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(err.Error())
106 os.Stderr.WriteString("\n")
107 os.Exit(1)
108 return
109 }
110 }
111
112 func run(w io.Writer, args []string, cfg *config) error {
113 bw := bufio.NewWriter(w)
114 defer bw.Flush()
115
116 if len(args) == 0 {
117 if err := handleReader(bw, os.Stdin, cfg); err != nil {
118 return err
119 }
120 }
121
122 for _, name := range args {
123 if err := handleFile(bw, name, cfg); err != nil {
124 return err
125 }
126 }
127
128 if cfg.Started {
129 endPage(bw)
130 }
131 return nil
132 }
133
134 func handleFile(w *bufio.Writer, name string, cfg *config) error {
135 if name == `` || name == `-` {
136 return handleReader(w, os.Stdin, cfg)
137 }
138
139 f, err := os.Open(name)
140 if err != nil {
141 return errors.New(`can't read from file named "` + name + `"`)
142 }
143 defer f.Close()
144
145 return handleReader(w, f, cfg)
146 }
147
148 func handleReader(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 lines := 0
153
154 for sc.Scan() {
155 line := sc.Text()
156
157 if lines == 0 && strings.HasPrefix(line, "\xef\xbb\xbf") {
158 line = line[3:]
159 }
160 if lines == 0 && !cfg.Started {
161 title := line
162 if cfg.GotTitle {
163 title = cfg.Title
164 }
165 startPage(w, title, cfg.Monospace)
166 cfg.Started = true
167 }
168 lines++
169
170 if err := handleLine(w, line, cfg); err != nil {
171 return err
172 }
173 }
174
175 if !cfg.Started && lines > 0 {
176 startPage(w, cfg.Title, cfg.Monospace)
177 cfg.Started = true
178 }
179 return sc.Err()
180 }
181
182 const style = `
183 body {
184 margin: 1rem auto 2rem auto;
185 padding: 0.25rem;
186 font-size: 1.1rem;
187 line-height: 1.8rem;
188 font-family: sans-serif;
189
190 max-width: 95vw;
191 /* width: max-content; */
192 width: fit-content;
193
194 box-sizing: border-box;
195 display: block;
196 }
197
198 a {
199 color: steelblue;
200 text-decoration: none;
201 }
202
203 p {
204 display: block;
205 margin: auto;
206 max-width: 80ch;
207 }
208
209 img {
210 margin: none;
211 }
212
213 audio {
214 width: 60ch;
215 }
216
217 table {
218 margin: 2rem auto;
219 border-collapse: collapse;
220 }
221
222 thead>* {
223 position: sticky;
224 top: 0;
225 background-color: white;
226 }
227
228 tfoot th {
229 user-select: none;
230 }
231
232 th, td {
233 padding: 0.1rem 1ch;
234 min-width: 4ch;
235 border-bottom: solid thin transparent;
236 }
237
238 tr:nth-child(5n) td {
239 border-bottom: solid thin #ccc;
240 }
241
242 .monospace {
243 font-family: monospace;
244 }
245 `
246
247 func startPage(w *bufio.Writer, title string, monospace bool) {
248 w.WriteString("<!DOCTYPE html>\n")
249 w.WriteString("<html lang=\"en\">\n")
250 w.WriteString("\n")
251 w.WriteString("<head>\n")
252 w.WriteString(" <meta charset=\"UTF-8\">\n")
253 w.WriteString(" <meta name=\"viewport\" content=\"width=device-width,")
254 w.WriteString(" initial-scale=1.0\">\n")
255 w.WriteString(" <meta http-equiv=\"X-UA-Compatible\"")
256 w.WriteString(" content=\"ie=edge\">\n")
257 w.WriteString("\n")
258 w.WriteString(" <link rel=\"icon\" href=\"data:,\">\n")
259 w.WriteString(` <title>`)
260 w.WriteString(title)
261 w.WriteString("</title>\n")
262 w.WriteString("\n")
263 w.WriteString("\n")
264 w.WriteString(" <style>\n")
265 w.WriteString(style[1:])
266 w.WriteString(" </style>\n")
267 w.WriteString("</head>\n")
268 if monospace {
269 w.WriteString("<body class=\"monospace\">\n")
270 } else {
271 w.WriteString("<body>\n")
272 }
273 }
274
275 func endPage(w *bufio.Writer) {
276 w.WriteString("</body>\n")
277 w.WriteString("</html>\n")
278 }
279
280 func handleLine(w *bufio.Writer, s string, cfg *config) error {
281 if handleDataURI(w, s) {
282 if w.WriteByte('\n') != nil {
283 return io.EOF
284 }
285 return nil
286 }
287
288 for len(s) > 0 {
289 span := links.FindStringIndex(s)
290 if span != nil && len(span) == 2 {
291 i := span[0]
292 j := span[1]
293 href := s[i:j]
294 handleChunk(w, s[:i])
295 w.WriteString(`<a href="`)
296 w.WriteString(href)
297 w.WriteString(`">`)
298 w.WriteString(href)
299 w.WriteString(`</a>`)
300 s = s[j:]
301 continue
302 }
303
304 handleChunk(w, s)
305 break
306 }
307
308 w.WriteString(`<br>`)
309 if w.WriteByte('\n') != nil {
310 return io.EOF
311 }
312 return nil
313 }
314
315 func handleChunk(w *bufio.Writer, s string) {
316 for len(s) > 0 {
317 switch b := s[0]; b {
318 case '&':
319 w.WriteString("&")
320 case '<':
321 w.WriteString("<")
322 case '>':
323 w.WriteString(">")
324 default:
325 w.WriteByte(b)
326 }
327 s = s[1:]
328 }
329 }
330
331 func handleDataURI(w *bufio.Writer, s string) bool {
332 full := s
333
334 if !strings.HasPrefix(s, `data:`) {
335 return false
336 }
337
338 s = strings.TrimPrefix(s, `data:`)
339 i := strings.Index(s, `;base64,`)
340 if i < 0 {
341 return false
342 }
343
344 kind := s[:i]
345 s = s[i+len(`;base64,`):]
346
347 if strings.HasPrefix(kind, `image/`) {
348 if !isBase64(s) {
349 return false
350 }
351
352 w.WriteString(`<img src="`)
353 w.WriteString(full)
354 w.WriteString(`">`)
355 return true
356 }
357
358 if strings.HasPrefix(kind, `audio/`) {
359 if !isBase64(s) {
360 return false
361 }
362
363 w.WriteString(`<audio controls src="`)
364 w.WriteString(full)
365 w.WriteString(`"></audio>`)
366 return true
367 }
368
369 if strings.HasPrefix(kind, `video/`) {
370 if !isBase64(s) {
371 return false
372 }
373
374 w.WriteString(`<video controls src="`)
375 w.WriteString(full)
376 w.WriteString(`"></video>`)
377 return true
378 }
379
380 return false
381 }
382
383 func handleMedia(w *bufio.Writer, begin string, s string, end string) bool {
384 if !isBase64(s) {
385 return false
386 }
387
388 w.WriteString(begin)
389 w.WriteString(s)
390 w.WriteString(end)
391 return true
392 }
393
394 func isBase64(s string) bool {
395 dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(s))
396 n, err := io.Copy(io.Discard, dec)
397 return n > 0 && err == nil
398 }
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: ./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(err.Error())
85 os.Stderr.WriteString("\n")
86 os.Exit(1)
87 return
88 }
89 }
90
91 func run(w io.Writer, args []string, level int, live bool) error {
92 bw := bufio.NewWriter(w)
93 defer bw.Flush()
94
95 if len(args) == 0 {
96 return indent(bw, os.Stdin, level, live)
97 }
98
99 for _, name := range args {
100 if err := handleFile(bw, name, level, live); err != nil {
101 return err
102 }
103 }
104 return nil
105 }
106
107 func handleFile(w *bufio.Writer, name string, level int, live bool) error {
108 if name == `` || name == `-` {
109 return indent(w, os.Stdin, level, live)
110 }
111
112 f, err := os.Open(name)
113 if err != nil {
114 return errors.New(`can't read from file named "` + name + `"`)
115 }
116 defer f.Close()
117
118 return indent(w, f, level, live)
119 }
120
121 func indent(w *bufio.Writer, r io.Reader, level int, live bool) error {
122 const gb = 1024 * 1024 * 1024
123 sc := bufio.NewScanner(r)
124 sc.Buffer(nil, 8*gb)
125
126 for i := 0; sc.Scan(); i++ {
127 s := sc.Bytes()
128 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
129 s = s[3:]
130 }
131
132 writeSpaces(w, level)
133 w.Write(s)
134
135 if w.WriteByte('\n') != nil {
136 return io.EOF
137 }
138
139 if !live {
140 continue
141 }
142
143 if w.Flush() != nil {
144 return io.EOF
145 }
146 }
147
148 return sc.Err()
149 }
150
151 func writeSpaces(w *bufio.Writer, n int) {
152 const (
153 half = ` `
154 spaces = half + half
155 )
156
157 for n >= len(spaces) {
158 w.WriteString(spaces)
159 n -= len(spaces)
160 }
161 if n > 0 {
162 w.WriteString(spaces[:n])
163 }
164 }
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(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.NewWriter(w)
96 defer bw.Flush()
97
98 dashes := 0
99 for _, name := range args {
100 if name == `-` {
101 dashes++
102 }
103 if dashes > 1 {
104 return errors.New(`can't read stdin (dash) more than once`)
105 }
106 }
107
108 if len(args) == 0 {
109 return items(bw, os.Stdin, cfg)
110 }
111
112 for _, name := range args {
113 if name == `-` {
114 if err := items(bw, os.Stdin, cfg); err != nil {
115 return err
116 }
117 continue
118 }
119
120 if err := handleFile(bw, name, cfg); err != nil {
121 return err
122 }
123 }
124 return nil
125 }
126
127 func handleFile(w *bufio.Writer, name string, cfg config) error {
128 if name == `` || name == `-` {
129 return items(w, os.Stdin, 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 items(w, f, cfg)
139 }
140
141 func items(w *bufio.Writer, r io.Reader, cfg config) error {
142 const gb = 1024 * 1024 * 1024
143 sc := bufio.NewScanner(r)
144 sc.Buffer(nil, 8*gb)
145
146 handleLine := handleSSV
147 if cfg.tsv {
148 handleLine = handleTSV
149 }
150
151 for i := 0; sc.Scan(); i++ {
152 s := sc.Bytes()
153 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
154 s = s[3:]
155 }
156
157 if handleLine(w, s) != nil {
158 return io.EOF
159 }
160
161 if !cfg.liveLines {
162 continue
163 }
164
165 if w.Flush() != nil {
166 return io.EOF
167 }
168 }
169
170 return sc.Err()
171 }
172
173 func handleSSV(w *bufio.Writer, line []byte) error {
174 for len(line) > 0 {
175 if i := indexNonWhitespace(line); i >= 0 {
176 line = line[i:]
177 }
178 if len(line) == 0 {
179 break
180 }
181
182 var item []byte
183 skip := 0
184
185 i := indexWhitespace(line)
186 if i >= 0 {
187 item = line[:i]
188 skip = i + 1
189 } else {
190 item = line
191 skip = len(line)
192 }
193
194 w.Write(item)
195 if w.WriteByte('\n') != nil {
196 return io.EOF
197 }
198
199 line = line[skip:]
200 }
201
202 return nil
203 }
204
205 func handleTSV(w *bufio.Writer, line []byte) error {
206 for len(line) > 0 {
207 var item []byte
208 skip := 0
209
210 i := bytes.IndexByte(line, '\t')
211 if i >= 0 {
212 item = line[:i]
213 skip = i + 1
214 } else {
215 item = line
216 skip = len(line)
217 }
218
219 w.Write(item)
220 if w.WriteByte('\n') != nil {
221 return io.EOF
222 }
223
224 line = line[skip:]
225 }
226
227 return nil
228 }
229
230 func indexNonWhitespace(s []byte) int {
231 for i, b := range s {
232 switch b {
233 case ' ', '\t':
234 continue
235 }
236 return i
237 }
238
239 return -1
240 }
241
242 func indexWhitespace(s []byte) int {
243 for i, b := range s {
244 switch b {
245 case ' ', '\t':
246 return i
247 }
248 }
249
250 return -1
251 }
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(err.Error())
182 os.Stderr.WriteString("\n")
183 os.Exit(1)
184 return
185 }
186 }
187
188 type handlerFunc func(w *bufio.Writer, r *bufio.Reader, cfg config) error
189
190 func run(w io.Writer, name string, handler handlerFunc, cfg config) error {
191 // f, _ := os.Create(`json0.prof`)
192 // defer f.Close()
193 // pprof.StartCPUProfile(f)
194 // defer pprof.StopCPUProfile()
195
196 if name == `` || name == `-` {
197 bw := bufio.NewWriterSize(w, bufSize)
198 br := bufio.NewReaderSize(os.Stdin, bufSize)
199 defer bw.Flush()
200 return handler(bw, br, cfg)
201 }
202
203 f, err := os.Open(name)
204 if err != nil {
205 return errors.New(`can't read from file named "` + name + `"`)
206 }
207 defer f.Close()
208
209 bw := bufio.NewWriterSize(w, bufSize)
210 br := bufio.NewReaderSize(f, bufSize)
211 defer bw.Flush()
212 return handler(bw, br, cfg)
213 }
214
215 var (
216 errCommentEarlyEnd = errors.New(`unexpected early-end of comment`)
217 errInputEarlyEnd = errors.New(`expected end of input data`)
218 errInvalidComment = errors.New(`expected / or *`)
219 errInvalidHex = errors.New(`expected a base-16 digit`)
220 errInvalidRune = errors.New(`invalid UTF-8 bytes`)
221 errInvalidToken = errors.New(`invalid JSON token`)
222 errNoDigits = errors.New(`expected numeric digits`)
223 errNoStringQuote = errors.New(`expected " or '`)
224 errNoArrayComma = errors.New(`missing comma between array values`)
225 errNoObjectComma = errors.New(`missing comma between key-value pairs`)
226 errStringEarlyEnd = errors.New(`unexpected early-end of string`)
227 errTooMuchRecursion = errors.New(`exceeded recursion-limit given`)
228 errExtraBytes = errors.New(`unexpected extra input bytes`)
229 )
230
231 // linePosError is a more descriptive kind of error, showing the source of
232 // the input-related problem, as 1-based a line/pos number pair in front
233 // of the error message
234 type linePosError struct {
235 // line is the 1-based line count from the input
236 line int
237
238 // pos is the 1-based `horizontal` position in its line
239 pos int
240
241 // err is the error message to `decorate` with the position info
242 err error
243 }
244
245 // Error satisfies the error interface
246 func (lpe linePosError) Error() string {
247 where := strconv.Itoa(lpe.line) + `:` + strconv.Itoa(lpe.pos)
248 return where + `: ` + lpe.err.Error()
249 }
250
251 // isIdentifier improves control-flow of func handleKey, when it handles
252 // unquoted object keys
253 var isIdentifier = [256]bool{
254 '_': true,
255
256 '0': true, '1': true, '2': true, '3': true, '4': true,
257 '5': true, '6': true, '7': true, '8': true, '9': true,
258
259 'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true,
260 'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true,
261 'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true,
262 'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
263 'Y': true, 'Z': true,
264
265 'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true,
266 'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true,
267 'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true,
268 's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true,
269 'y': true, 'z': true,
270 }
271
272 // matchHex both figures out if a byte is a valid ASCII hex-digit, by not
273 // being 0, and normalizes letter-case for the hex letters
274 var matchHex = [256]byte{
275 '0': '0', '1': '1', '2': '2', '3': '3', '4': '4',
276 '5': '5', '6': '6', '7': '7', '8': '8', '9': '9',
277 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F',
278 'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F',
279 }
280
281 // json0 converts JSON/pseudo-JSON into (valid) minimal JSON; final boolean
282 // value isn't used, and is just there to match the signature of func jsonl
283 func json0(w *bufio.Writer, r *bufio.Reader, cfg config) error {
284 jr := jsonReader{r: r, line: 1, pos: 1, maxNesting: cfg.maxNesting}
285 defer w.Flush()
286
287 if err := jr.handleLeadingJunk(); err != nil {
288 return err
289 }
290
291 // handle a single top-level JSON value
292 err := handleValue(w, &jr)
293
294 // end the only output-line with a line-feed; this also avoids showing
295 // error messages on the same line as the main output, since JSON-0
296 // output has no line-feeds before its last byte
297 outputByte(w, '\n')
298
299 if err != nil {
300 return err
301 }
302 return jr.handleTrailingJunk()
303 }
304
305 // jsonl converts JSON/pseudo-JSON into (valid) minimal JSON Lines; this func
306 // avoids writing a trailing line-feed, leaving that up to its caller
307 func jsonl(w *bufio.Writer, r *bufio.Reader, cfg config) error {
308 jr := jsonReader{r: r, line: 1, pos: 1, maxNesting: cfg.maxNesting}
309
310 if err := jr.handleLeadingJunk(); err != nil {
311 return err
312 }
313
314 chunk, err := jr.r.Peek(1)
315 if err == nil && len(chunk) >= 1 {
316 switch b := chunk[0]; b {
317 case '[', '(':
318 return handleArrayJSONL(w, &jr, b, cfg.liveLines)
319 }
320 }
321
322 // handle a single top-level JSON value
323 err = handleValue(w, &jr)
324
325 // end the only output-line with a line-feed; this also avoids showing
326 // error messages on the same line as the main output, since JSON-0
327 // output has no line-feeds before its last byte
328 outputByte(w, '\n')
329
330 if err != nil {
331 return err
332 }
333 return jr.handleTrailingJunk()
334 }
335
336 // handleArrayJSONL handles top-level arrays for func jsonl
337 func handleArrayJSONL(w *bufio.Writer, jr *jsonReader, start byte, live bool) error {
338 if err := jr.demandSyntax(start); err != nil {
339 return err
340 }
341
342 var end byte = ']'
343 if start == '(' {
344 end = ')'
345 }
346
347 for n := 0; true; n++ {
348 // there may be whitespace/comments before the next comma
349 if err := jr.seekNext(); err != nil {
350 return err
351 }
352
353 // handle commas between values, as well as trailing ones
354 comma := false
355 b, _ := jr.peekByte()
356 if b == ',' {
357 jr.discardSyntaxByte()
358 comma = true
359
360 // there may be whitespace/comments before an ending ']'
361 if err := jr.seekNext(); err != nil {
362 return err
363 }
364 b, _ = jr.peekByte()
365 }
366
367 // handle end of array
368 if b == end {
369 jr.discardSyntaxByte()
370 if n > 0 {
371 err := outputByte(w, '\n')
372 if live {
373 if err := w.Flush(); err != nil {
374 return io.EOF
375 }
376 }
377 return err
378 }
379 return nil
380 }
381
382 // turn commas between adjacent values into line-feeds, as the
383 // output for this custom func is supposed to be JSON Lines
384 if n > 0 {
385 if !comma {
386 return errNoArrayComma
387 }
388 if err := outputByte(w, '\n'); err != nil {
389 return err
390 }
391 if live {
392 if err := w.Flush(); err != nil {
393 return io.EOF
394 }
395 }
396 }
397
398 // handle the next value
399 if err := jr.seekNext(); err != nil {
400 return err
401 }
402 if err := handleValue(w, jr); err != nil {
403 return err
404 }
405 }
406
407 // make the compiler happy
408 return nil
409 }
410
411 // jsonReader reads data via a buffer, keeping track of the input position:
412 // this in turn allows showing much more useful errors, when these happen
413 type jsonReader struct {
414 // r is the actual reader
415 r *bufio.Reader
416
417 // line is the 1-based line-counter for input bytes, and gives errors
418 // useful position info
419 line int
420
421 // pos is the 1-based `horizontal` position in its line, and gives
422 // errors useful position info
423 pos int
424
425 // maxNesting optionally controls recursion when parsing the input;
426 // checking recursion is disabled when this value is 0 or negative
427 maxNesting int
428
429 // nesting is the current recursive-nesting level, which can only
430 // increase when handling arrays and/or objects
431 nesting int
432 }
433
434 // exitRec decreases the recursion counter: this func is handy to defer
435 func (jr *jsonReader) exitRec() {
436 jr.nesting--
437 }
438
439 // improveError makes any error more useful, by giving it info about the
440 // current input-position, as a 1-based line/within-line-position pair
441 func (jr jsonReader) improveError(err error) error {
442 if _, ok := err.(linePosError); ok {
443 return err
444 }
445
446 if err == io.EOF {
447 return linePosError{jr.line, jr.pos, errInputEarlyEnd}
448 }
449 if err != nil {
450 return linePosError{jr.line, jr.pos, err}
451 }
452 return nil
453 }
454
455 // enterRec increases the recursion counter, checking if new recursion-level
456 // is allowed
457 func (jr *jsonReader) enterRec() error {
458 jr.nesting++
459 if jr.maxNesting > 0 && jr.maxNesting < jr.nesting {
460 return errTooMuchRecursion
461 }
462 return nil
463 }
464
465 func (jr *jsonReader) handleLeadingJunk() error {
466 // input is already assumed to be UTF-8: a leading UTF-8 BOM (byte-order
467 // mark) gives no useful info if present, as UTF-8 leaves no ambiguity
468 // about byte-order by design
469 jr.skipUTF8BOM()
470
471 // ignore leading whitespace and/or comments
472 return jr.seekNext()
473 }
474
475 func (jr *jsonReader) handleTrailingJunk() error {
476 // ignore trailing whitespace and/or comments
477 if err := jr.seekNext(); err != nil {
478 return err
479 }
480
481 // ignore trailing semicolons
482 for {
483 if b, ok := jr.peekByte(); !ok || b != ';' {
484 break
485 }
486
487 jr.discardSyntaxByte()
488 // ignore trailing whitespace and/or comments
489 if err := jr.seekNext(); err != nil {
490 return err
491 }
492 }
493
494 // beyond trailing whitespace and/or comments, any more bytes
495 // make the whole input data invalid JSON
496 if _, ok := jr.peekByte(); ok {
497 return jr.improveError(errExtraBytes)
498 }
499 return nil
500 }
501
502 // demandSyntax fails with an error when the next byte isn't the one given;
503 // when it is, the byte is then read/skipped, and a nil error is returned
504 func (jr *jsonReader) demandSyntax(syntax byte) error {
505 chunk, err := jr.r.Peek(1)
506 if err == io.EOF {
507 return jr.improveError(errInputEarlyEnd)
508 }
509 if err != nil {
510 return jr.improveError(err)
511 }
512
513 if len(chunk) < 1 || chunk[0] != syntax {
514 msg := `expected ` + string(rune(syntax))
515 return jr.improveError(errors.New(msg))
516 }
517
518 jr.discardSyntaxByte()
519 return nil
520 }
521
522 // discardSyntaxByte discards 1 byte, updating the reader's position info
523 func (jr *jsonReader) discardSyntaxByte() {
524 jr.readByte()
525 }
526
527 // peekByte simplifies control-flow for various other funcs
528 func (jr jsonReader) peekByte() (b byte, ok bool) {
529 chunk, err := jr.r.Peek(1)
530 if err == nil && len(chunk) >= 1 {
531 return chunk[0], true
532 }
533 return 0, false
534 }
535
536 // readByte does what it says, updating the reader's position info
537 func (jr *jsonReader) readByte() (b byte, err error) {
538 b, err = jr.r.ReadByte()
539 if err == nil {
540 if b == '\n' {
541 jr.line += 1
542 jr.pos = 1
543 } else {
544 jr.pos++
545 }
546 return b, nil
547 }
548 return b, jr.improveError(err)
549 }
550
551 // readRune does what it says, updating the reader's position info
552 func (jr *jsonReader) readRune() (r rune, err error) {
553 r, _, err = jr.r.ReadRune()
554 if err == nil {
555 if r == '\n' {
556 jr.line += 1
557 jr.pos = 1
558 } else {
559 jr.pos++
560 }
561 return r, nil
562 }
563 return r, jr.improveError(err)
564 }
565
566 // seekNext skips/seeks the next token, ignoring runs of whitespace symbols
567 // and comments, either single-line (starting with //) or general (starting
568 // with /* and ending with */)
569 func (jr *jsonReader) seekNext() error {
570 for {
571 b, err := jr.r.ReadByte()
572 if err != nil {
573 return nil
574 }
575
576 switch b {
577 case '\n':
578 jr.line++
579 jr.pos = 1
580
581 case '#':
582 jr.pos++
583 if err := jr.skipLine(); err != nil {
584 return err
585 }
586
587 case '/':
588 jr.r.UnreadByte()
589 if err := jr.skipComment(); err != nil {
590 return err
591 }
592
593 default:
594 if b <= 32 {
595 // keep skipping whitespace bytes
596 jr.pos++
597 } else {
598 // reached the next token
599 jr.r.UnreadByte()
600 return nil
601 }
602 }
603
604 // after comments, keep looking for more whitespace and/or comments
605 }
606 }
607
608 // skipComment helps func seekNext skip over comments, simplifying the latter
609 // func's control-flow
610 func (jr *jsonReader) skipComment() error {
611 err := jr.demandSyntax('/')
612 if err != nil {
613 return err
614 }
615
616 b, ok := jr.peekByte()
617 if !ok {
618 return nil
619 }
620
621 switch b {
622 case '/':
623 // handle single-line comments
624 return jr.skipLine()
625
626 case '*':
627 // handle (potentially) multi-line comments
628 return jr.skipGeneralComment()
629
630 default:
631 return jr.improveError(errInvalidComment)
632 }
633 }
634
635 // skipLine handles single-line comments for func skipComment
636 func (jr *jsonReader) skipLine() error {
637 for {
638 b, err := jr.readByte()
639 if err == io.EOF {
640 // end of input is fine in this case
641 return nil
642 }
643 if err != nil {
644 return err
645 }
646
647 if b == '\n' {
648 return nil
649 }
650 }
651 }
652
653 // skipGeneralComment handles (potentially) multi-line comments for func
654 // skipComment
655 func (jr *jsonReader) skipGeneralComment() error {
656 var prev byte
657 for {
658 b, err := jr.readByte()
659 if err != nil {
660 return jr.improveError(errCommentEarlyEnd)
661 }
662
663 if prev == '*' && b == '/' {
664 return nil
665 }
666 if b == '\n' {
667 jr.line++
668 jr.pos = 1
669 }
670 prev = b
671 }
672 }
673
674 // skipUTF8BOM does what it says, if a UTF-8 BOM is present
675 func (jr *jsonReader) skipUTF8BOM() {
676 lead, err := jr.r.Peek(3)
677 if err != nil {
678 return
679 }
680
681 if len(lead) > 2 && lead[0] == 0xef && lead[1] == 0xbb && lead[2] == 0xbf {
682 jr.r.Discard(3)
683 jr.pos += 1
684 }
685 }
686
687 // outputByte is a small wrapper on func WriteByte, which adapts any error
688 // into a custom dummy output-error, which is in turn meant to be ignored,
689 // being just an excuse to quit the app immediately and successfully
690 func outputByte(w *bufio.Writer, b byte) error {
691 err := w.WriteByte(b)
692 if err == nil {
693 return nil
694 }
695 return io.EOF
696 }
697
698 // handleArray handles arrays for func handleValue
699 func handleArray(w *bufio.Writer, jr *jsonReader, start byte) error {
700 jr.enterRec()
701 defer jr.exitRec()
702
703 if err := jr.demandSyntax(start); err != nil {
704 return err
705 }
706
707 var end byte = ']'
708 if start == '(' {
709 end = ')'
710 }
711
712 w.WriteByte('[')
713
714 for n := 0; true; n++ {
715 // there may be whitespace/comments before the next comma
716 if err := jr.seekNext(); err != nil {
717 return err
718 }
719
720 // handle commas between values, as well as trailing ones
721 comma := false
722 b, _ := jr.peekByte()
723 if b == ',' {
724 jr.discardSyntaxByte()
725 comma = true
726
727 // there may be whitespace/comments before an ending ']'
728 if err := jr.seekNext(); err != nil {
729 return err
730 }
731 b, _ = jr.peekByte()
732 }
733
734 // handle end of array
735 if b == end {
736 jr.discardSyntaxByte()
737 w.WriteByte(']')
738 return nil
739 }
740
741 // don't forget commas between adjacent values
742 if n > 0 {
743 if !comma {
744 return errNoArrayComma
745 }
746 if err := outputByte(w, ','); err != nil {
747 return err
748 }
749 }
750
751 // handle the next value
752 if err := jr.seekNext(); err != nil {
753 return err
754 }
755 if err := handleValue(w, jr); err != nil {
756 return err
757 }
758 }
759
760 // make the compiler happy
761 return nil
762 }
763
764 // handleDigits helps various number-handling funcs do their job
765 func handleDigits(w *bufio.Writer, jr *jsonReader) error {
766 if trySimpleDigits(w, jr) {
767 return nil
768 }
769
770 for n := 0; true; n++ {
771 b, _ := jr.peekByte()
772
773 // support `nice` long numbers by ignoring their underscores
774 if b == '_' {
775 jr.discardSyntaxByte()
776 continue
777 }
778
779 if '0' <= b && b <= '9' {
780 jr.discardSyntaxByte()
781 w.WriteByte(b)
782 continue
783 }
784
785 if n == 0 {
786 return errNoDigits
787 }
788 return nil
789 }
790
791 // make the compiler happy
792 return nil
793 }
794
795 // trySimpleDigits tries to handle (more quickly) digit-runs where all bytes
796 // are just digits: this is a very common case for numbers; returns whether
797 // it succeeded, so this func's caller knows knows if it needs to do anything,
798 // the slower way
799 func trySimpleDigits(w *bufio.Writer, jr *jsonReader) (gotIt bool) {
800 chunk, _ := jr.r.Peek(chunkPeekSize)
801
802 for i, b := range chunk {
803 if '0' <= b && b <= '9' {
804 continue
805 }
806
807 if i == 0 || b == '_' {
808 return false
809 }
810
811 // bulk-writing the chunk is this func's whole point
812 w.Write(chunk[:i])
813
814 jr.r.Discard(i)
815 jr.pos += i
816 return true
817 }
818
819 // maybe the digits-run is ok, but it's just longer than the chunk
820 return false
821 }
822
823 // handleDot handles pseudo-JSON numbers which start with a decimal dot
824 func handleDot(w *bufio.Writer, jr *jsonReader) error {
825 if err := jr.demandSyntax('.'); err != nil {
826 return err
827 }
828 w.Write([]byte{'0', '.'})
829 return handleDigits(w, jr)
830 }
831
832 // handleKey is used by func handleObjects and generalizes func handleString,
833 // by allowing unquoted object keys; it's not used anywhere else, as allowing
834 // unquoted string values is ambiguous with actual JSON-keyword values null,
835 // false, and true.
836 func handleKey(w *bufio.Writer, jr *jsonReader) error {
837 quote, ok := jr.peekByte()
838 if !ok {
839 return jr.improveError(errStringEarlyEnd)
840 }
841
842 if quote == '"' || quote == '\'' {
843 return handleString(w, jr, quote)
844 }
845
846 w.WriteByte('"')
847 for {
848 if b, _ := jr.peekByte(); isIdentifier[b] {
849 jr.discardSyntaxByte()
850 w.WriteByte(b)
851 continue
852 }
853
854 w.WriteByte('"')
855 return nil
856 }
857 }
858
859 // trySimpleString tries to handle (more quickly) inner-strings where all bytes
860 // are unescaped ASCII symbols: this is a very common case for strings, and is
861 // almost always the case for object keys; returns whether it succeeded, so
862 // this func's caller knows knows if it needs to do anything, the slower way
863 func trySimpleString(w *bufio.Writer, jr *jsonReader, quote byte) (gotIt bool) {
864 end := -1
865 chunk, _ := jr.r.Peek(chunkPeekSize)
866
867 for i, b := range chunk {
868 if b == byte(quote) {
869 end = i
870 break
871 }
872
873 if b < ' ' || b > 127 || b == '\\' || b == '"' {
874 return false
875 }
876 }
877
878 if end < 0 {
879 return false
880 }
881
882 // bulk-writing the chunk is this func's whole point
883 w.WriteByte('"')
884 w.Write(chunk[:end])
885 w.WriteByte('"')
886
887 jr.r.Discard(end + 1)
888 jr.pos += end + 1
889 return true
890 }
891
892 // handleKeyword is used by funcs handleFalse, handleNull, and handleTrue
893 func handleKeyword(w *bufio.Writer, jr *jsonReader, kw []byte) error {
894 for rest := kw; len(rest) > 0; rest = rest[1:] {
895 b, err := jr.readByte()
896 if err == nil && b == rest[0] {
897 // keywords given to this func have no line-feeds
898 jr.pos++
899 continue
900 }
901
902 msg := `expected JSON value ` + string(kw)
903 return jr.improveError(errors.New(msg))
904 }
905
906 w.Write(kw)
907 return nil
908 }
909
910 func replaceKeyword(w *bufio.Writer, jr *jsonReader, kw, with []byte) error {
911 for rest := kw; len(rest) > 0; rest = rest[1:] {
912 b, err := jr.readByte()
913 if err == nil && b == rest[0] {
914 // keywords given to this func have no line-feeds
915 jr.pos++
916 continue
917 }
918
919 msg := `expected JSON value ` + string(kw)
920 return jr.improveError(errors.New(msg))
921 }
922
923 w.Write(with)
924 return nil
925 }
926
927 // handleNegative handles numbers starting with a negative sign for func
928 // handleValue
929 func handleNegative(w *bufio.Writer, jr *jsonReader) error {
930 if err := jr.demandSyntax('-'); err != nil {
931 return err
932 }
933
934 w.WriteByte('-')
935 if b, _ := jr.peekByte(); b == '.' {
936 jr.discardSyntaxByte()
937 w.Write([]byte{'0', '.'})
938 return handleDigits(w, jr)
939 }
940 return handleNumber(w, jr)
941 }
942
943 // handleNumber handles numeric values/tokens, including invalid-JSON cases,
944 // such as values starting with a decimal dot
945 func handleNumber(w *bufio.Writer, jr *jsonReader) error {
946 // handle integer digits
947 if err := handleDigits(w, jr); err != nil {
948 return err
949 }
950
951 switch b, _ := jr.peekByte(); b {
952 case 'n':
953 // handle optional trailing 'n', used in javascript's bigint literals
954 jr.discardSyntaxByte()
955 return nil
956
957 case '.':
958 // handle optional decimal digits, starting with a leading dot
959 jr.discardSyntaxByte()
960 w.WriteByte('.')
961 return handleDigits(w, jr)
962
963 case 'e', 'E':
964 // handle optional exponent digits
965 jr.discardSyntaxByte()
966 w.WriteByte(b)
967 b, _ = jr.peekByte()
968 if b == '+' {
969 jr.discardSyntaxByte()
970 } else if b == '-' {
971 w.WriteByte('-')
972 jr.discardSyntaxByte()
973 }
974 return handleDigits(w, jr)
975
976 default:
977 // nothing special after some digits
978 return nil
979 }
980 }
981
982 // handleObject handles objects for func handleValue
983 func handleObject(w *bufio.Writer, jr *jsonReader) error {
984 jr.enterRec()
985 defer jr.exitRec()
986
987 if err := jr.demandSyntax('{'); err != nil {
988 return err
989 }
990
991 w.WriteByte('{')
992
993 for npairs := 0; true; npairs++ {
994 // there may be whitespace/comments before the next comma
995 if err := jr.seekNext(); err != nil {
996 return err
997 }
998
999 // handle commas between key-value pairs, as well as trailing ones
1000 comma := false
1001 b, _ := jr.peekByte()
1002 if b == ',' {
1003 jr.discardSyntaxByte()
1004 comma = true
1005
1006 // there may be whitespace/comments before an ending '}'
1007 if err := jr.seekNext(); err != nil {
1008 return err
1009 }
1010 b, _ = jr.peekByte()
1011 }
1012
1013 // handle end of object
1014 if b == '}' {
1015 jr.discardSyntaxByte()
1016 w.WriteByte('}')
1017 return nil
1018 }
1019
1020 // don't forget commas between adjacent key-value pairs
1021 if npairs > 0 {
1022 if !comma {
1023 return errNoObjectComma
1024 }
1025 if err := outputByte(w, ','); err != nil {
1026 return err
1027 }
1028 }
1029
1030 // handle the next pair's key
1031 if err := jr.seekNext(); err != nil {
1032 return err
1033 }
1034 if err := handleKey(w, jr); err != nil {
1035 return err
1036 }
1037
1038 // demand a colon right after the key
1039 if err := jr.seekNext(); err != nil {
1040 return err
1041 }
1042 if err := jr.demandSyntax(':'); err != nil {
1043 return err
1044 }
1045 w.WriteByte(':')
1046
1047 // handle the next pair's value
1048 if err := jr.seekNext(); err != nil {
1049 return err
1050 }
1051 if err := handleValue(w, jr); err != nil {
1052 return err
1053 }
1054 }
1055
1056 // make the compiler happy
1057 return nil
1058 }
1059
1060 // handlePositive handles numbers starting with a positive sign for func
1061 // handleValue
1062 func handlePositive(w *bufio.Writer, jr *jsonReader) error {
1063 if err := jr.demandSyntax('+'); err != nil {
1064 return err
1065 }
1066
1067 // valid JSON isn't supposed to have leading pluses on numbers, so
1068 // emit nothing for it, unlike for negative numbers
1069
1070 if b, _ := jr.peekByte(); b == '.' {
1071 jr.discardSyntaxByte()
1072 w.Write([]byte{'0', '.'})
1073 return handleDigits(w, jr)
1074 }
1075 return handleNumber(w, jr)
1076 }
1077
1078 // handleString handles strings for funcs handleValue and handleObject, and
1079 // supports both single-quotes and double-quotes, always emitting the latter
1080 // in the output, of course
1081 func handleString(w *bufio.Writer, jr *jsonReader, quote byte) error {
1082 if quote != '"' && quote != '\'' {
1083 return errNoStringQuote
1084 }
1085
1086 jr.discardSyntaxByte()
1087
1088 // try the quicker no-escapes ASCII handler
1089 if trySimpleString(w, jr, quote) {
1090 return nil
1091 }
1092
1093 // it's a non-trivial inner-string, so handle it byte-by-byte
1094 w.WriteByte('"')
1095 escaped := false
1096
1097 for quote := rune(quote); true; {
1098 r, err := jr.readRune()
1099 if r == unicode.ReplacementChar {
1100 return jr.improveError(errInvalidRune)
1101 }
1102 if err != nil {
1103 if err == io.EOF {
1104 return jr.improveError(errStringEarlyEnd)
1105 }
1106 return jr.improveError(err)
1107 }
1108
1109 if !escaped {
1110 if r == '\\' {
1111 escaped = true
1112 continue
1113 }
1114
1115 // handle end of string
1116 if r == quote {
1117 return outputByte(w, '"')
1118 }
1119
1120 if int(r) <= len(escapedStringBytes) {
1121 w.Write(escapedStringBytes[r])
1122 } else {
1123 w.WriteRune(r)
1124 }
1125 continue
1126 }
1127
1128 // handle escaped items
1129 escaped = false
1130
1131 switch r {
1132 case 'u':
1133 // \u needs exactly 4 hex-digits to follow it
1134 w.Write([]byte{'\\', 'u'})
1135 if err := copyHex(w, 4, jr); err != nil {
1136 return jr.improveError(err)
1137 }
1138
1139 case 'x':
1140 // JSON only supports 4 escaped hex-digits, so pad the 2
1141 // expected hex-digits with 2 zeros
1142 w.Write([]byte{'\\', 'u', '0', '0'})
1143 if err := copyHex(w, 2, jr); err != nil {
1144 return jr.improveError(err)
1145 }
1146
1147 case 't', 'f', 'r', 'n', 'b', '\\', '"':
1148 // handle valid-JSON escaped string sequences
1149 w.WriteByte('\\')
1150 w.WriteByte(byte(r))
1151
1152 case '\'':
1153 // escaped single-quotes aren't standard JSON, but are needed
1154 // when the input uses non-standard single-quoted strings
1155 w.WriteByte('\'')
1156
1157 default:
1158 if int(r) < len(escapedStringBytes) {
1159 w.Write(escapedStringBytes[r])
1160 } else {
1161 w.WriteRune(r)
1162 }
1163 }
1164 }
1165
1166 if escaped {
1167 return jr.improveError(errStringEarlyEnd)
1168 }
1169 return nil
1170 }
1171
1172 // copyHex handles a run of hex-digits for func handleString, starting right
1173 // after the leading `\u` (or `\x`) part; this func doesn't `improve` its
1174 // errors with position info: that's up to the caller
1175 func copyHex(w *bufio.Writer, n int, jr *jsonReader) error {
1176 for i := 0; i < n; i++ {
1177 b, err := jr.readByte()
1178 if err == io.EOF {
1179 return errStringEarlyEnd
1180 }
1181 if err != nil {
1182 return err
1183 }
1184
1185 if b >= 128 {
1186 return errInvalidHex
1187 }
1188
1189 if b := matchHex[b]; b != 0 {
1190 w.WriteByte(b)
1191 continue
1192 }
1193
1194 return errInvalidHex
1195 }
1196
1197 return nil
1198 }
1199
1200 // handleValue is a generic JSON-token handler, which allows the recursive
1201 // behavior to handle any kind of JSON/pseudo-JSON input
1202 func handleValue(w *bufio.Writer, jr *jsonReader) error {
1203 chunk, err := jr.r.Peek(1)
1204 if err == nil && len(chunk) >= 1 {
1205 return handleValueDispatch(w, jr, chunk[0])
1206 }
1207
1208 if err == io.EOF {
1209 return jr.improveError(errInputEarlyEnd)
1210 }
1211 return jr.improveError(errInputEarlyEnd)
1212 }
1213
1214 // handleValueDispatch simplifies control-flow for func handleValue
1215 func handleValueDispatch(w *bufio.Writer, jr *jsonReader, b byte) error {
1216 switch b {
1217 case '#':
1218 return jr.skipLine()
1219 case 'f':
1220 return handleKeyword(w, jr, []byte{'f', 'a', 'l', 's', 'e'})
1221 case 'n':
1222 return handleKeyword(w, jr, []byte{'n', 'u', 'l', 'l'})
1223 case 't':
1224 return handleKeyword(w, jr, []byte{'t', 'r', 'u', 'e'})
1225 case 'F':
1226 return replaceKeyword(w, jr, []byte(`False`), []byte(`false`))
1227 case 'N':
1228 return replaceKeyword(w, jr, []byte(`None`), []byte(`null`))
1229 case 'T':
1230 return replaceKeyword(w, jr, []byte(`True`), []byte(`true`))
1231 case '.':
1232 return handleDot(w, jr)
1233 case '+':
1234 return handlePositive(w, jr)
1235 case '-':
1236 return handleNegative(w, jr)
1237 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
1238 return handleNumber(w, jr)
1239 case '\'', '"':
1240 return handleString(w, jr, b)
1241 case '[', '(':
1242 return handleArray(w, jr, b)
1243 case '{':
1244 return handleObject(w, jr)
1245 default:
1246 return jr.improveError(errInvalidToken)
1247 }
1248 }
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(err.Error())
108 os.Stderr.WriteString("\n")
109 os.Exit(1)
110 return
111 }
112 }
113
114 // handleInput simplifies control-flow for func main
115 func handleInput(w io.Writer, path string) error {
116 if path == `-` {
117 return convert(w, os.Stdin)
118 }
119
120 f, err := os.Open(path)
121 if err != nil {
122 // on windows, file-not-found error messages may mention `CreateFile`,
123 // even when trying to open files in read-only mode
124 return errors.New(`can't open file named ` + path)
125 }
126 defer f.Close()
127 return convert(w, f)
128 }
129
130 // convert simplifies control-flow for func handleInput
131 func convert(w io.Writer, r io.Reader) error {
132 bw := bufio.NewWriter(w)
133 defer bw.Flush()
134 return json2(bw, r)
135 }
136
137 // writeSpaces does what it says, minimizing calls to write-like funcs
138 func writeSpaces(w *bufio.Writer, n int) {
139 const spaces = ` `
140 if n < 1 {
141 return
142 }
143
144 for n >= len(spaces) {
145 w.WriteString(spaces)
146 n -= len(spaces)
147 }
148 w.WriteString(spaces[:n])
149 }
150
151 // json2 does it all, given a reader and a writer
152 func json2(w *bufio.Writer, r io.Reader) error {
153 dec := json.NewDecoder(r)
154 // avoid parsing numbers, so unusually-long numbers are kept verbatim,
155 // even if JSON parsers aren't required to guarantee such input-fidelity
156 // for numbers
157 dec.UseNumber()
158
159 t, err := dec.Token()
160 if err == io.EOF {
161 return errors.New(`input has no JSON values`)
162 }
163
164 if err = handleToken(w, dec, t, 0, 0); err != nil {
165 return err
166 }
167 // don't forget ending the last line for the last value
168 w.WriteByte('\n')
169
170 _, err = dec.Token()
171 if err == io.EOF {
172 // input is over, so it's a success
173 return nil
174 }
175
176 if err == nil {
177 // a successful `read` is a failure, as it means there are
178 // trailing JSON tokens
179 return errors.New(`unexpected trailing data`)
180 }
181
182 // any other error, perhaps some invalid-JSON-syntax-type error
183 return err
184 }
185
186 // handleToken handles recursion for func json2
187 func handleToken(w *bufio.Writer, dec *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, dec, pre, level)
193 case json.Delim('{'):
194 return handleObject(w, dec, pre, level)
195 default:
196 return errors.New(`unsupported JSON syntax ` + string(t))
197 }
198
199 case nil:
200 writeSpaces(w, 2*pre)
201 w.WriteString(`null`)
202 return nil
203
204 case bool:
205 writeSpaces(w, 2*pre)
206 if t {
207 w.WriteString(`true`)
208 } else {
209 w.WriteString(`false`)
210 }
211 return nil
212
213 case json.Number:
214 writeSpaces(w, 2*pre)
215 w.WriteString(t.String())
216 return nil
217
218 case string:
219 return handleString(w, t, pre)
220
221 default:
222 // return fmt.Errorf(`unsupported token type %T`, t)
223 return errors.New(`invalid JSON token`)
224 }
225 }
226
227 // handleArray handles arrays for func handleToken
228 func handleArray(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
229 for i := 0; true; i++ {
230 t, err := dec.Token()
231 if err == io.EOF {
232 return errors.New(`end of JSON before array was closed`)
233 }
234 if err != nil {
235 return err
236 }
237
238 if t == json.Delim(']') {
239 if i == 0 {
240 writeSpaces(w, 2*pre)
241 w.WriteByte('[')
242 w.WriteByte(']')
243 } else {
244 w.WriteByte('\n')
245 writeSpaces(w, 2*level)
246 w.WriteByte(']')
247 }
248 return nil
249 }
250
251 if i == 0 {
252 writeSpaces(w, 2*pre)
253 w.WriteByte('[')
254 w.WriteByte('\n')
255 } else {
256 w.WriteByte(',')
257 w.WriteByte('\n')
258 if err := w.Flush(); err != nil {
259 // a write error may be the consequence of stdout being closed,
260 // perhaps by another app along a pipe
261 return io.EOF
262 }
263 }
264
265 err = handleToken(w, dec, t, level+1, level+1)
266 if err != nil {
267 return err
268 }
269 }
270
271 // make the compiler happy
272 return nil
273 }
274
275 // handleObject handles objects for func handleToken
276 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
277 for i := 0; true; i++ {
278 t, err := dec.Token()
279 if err == io.EOF {
280 return errors.New(`end of JSON before object was closed`)
281 }
282 if err != nil {
283 return err
284 }
285
286 if t == json.Delim('}') {
287 if i == 0 {
288 writeSpaces(w, 2*pre)
289 w.WriteByte('{')
290 w.WriteByte('}')
291 } else {
292 w.WriteByte('\n')
293 writeSpaces(w, 2*level)
294 w.WriteByte('}')
295 }
296 return nil
297 }
298
299 if i == 0 {
300 writeSpaces(w, 2*pre)
301 w.WriteByte('{')
302 w.WriteByte('\n')
303 } else {
304 w.WriteByte(',')
305 w.WriteByte('\n')
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 err = handleString(w, k, level+1)
314 if err != nil {
315 return err
316 }
317
318 w.WriteString(": ")
319
320 t, err = dec.Token()
321 if err == io.EOF {
322 return errors.New(`expected a value for a key-value pair`)
323 }
324
325 err = handleToken(w, dec, t, 0, level+1)
326 if err != nil {
327 return err
328 }
329 }
330
331 // make the compiler happy
332 return nil
333 }
334
335 // handleString handles strings for func handleToken, and keys for func
336 // handleObject
337 func handleString(w *bufio.Writer, s string, level int) error {
338 writeSpaces(w, 2*level)
339
340 for _, r := range s {
341 if r < ' ' || r == '"' || r == '\\' {
342 w.WriteByte('"')
343 for _, r := range s {
344 if int(r) < len(escapedStringBytes) {
345 w.Write(escapedStringBytes[r])
346 } else {
347 w.WriteRune(r)
348 }
349 }
350 w.WriteByte('"')
351 return nil
352 }
353 }
354
355 w.WriteByte('"')
356 w.WriteString(s)
357 w.WriteByte('"')
358 return nil
359 }
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(err.Error())
77 os.Stderr.WriteString("\n")
78 os.Exit(1)
79 return
80 }
81 }
82
83 func run(w io.Writer, args []string, liveLines bool) error {
84 dashes := 0
85 for _, path := range args {
86 if path == `-` {
87 dashes++
88 }
89 if dashes > 1 {
90 return errors.New(`can't use stdin (dash) more than once`)
91 }
92 }
93
94 bw := bufio.NewWriter(w)
95 defer bw.Flush()
96
97 if len(args) == 0 {
98 return handleInput(bw, `-`, liveLines)
99 }
100
101 for _, path := range args {
102 if err := handleInput(bw, path, liveLines); err != nil {
103 return err
104 }
105 }
106 return nil
107 }
108
109 // handleInput simplifies control-flow for func main
110 func handleInput(w *bufio.Writer, path string, liveLines bool) error {
111 if path == `-` {
112 return jsonl(w, os.Stdin, liveLines)
113 }
114
115 f, err := os.Open(path)
116 if err != nil {
117 // on windows, file-not-found error messages may mention `CreateFile`,
118 // even when trying to open files in read-only mode
119 return errors.New(`can't open file named ` + path)
120 }
121 defer f.Close()
122 return jsonl(w, f, liveLines)
123 }
124
125 // escapedStringBytes helps funcs like handleString treat all strings
126 // quickly and correctly, using their officially-supported JSON escape
127 // sequences
128 //
129 // https://www.rfc-editor.org/rfc/rfc8259#section-7
130 var escapedStringBytes = [128][]byte{
131 {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
132 {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
133 {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
134 {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
135 {'\\', 'b'}, {'\\', 't'},
136 {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
137 {'\\', 'f'}, {'\\', 'r'},
138 {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
139 {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
140 {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
141 {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
142 {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
143 {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
144 {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
145 {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
146 {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
147 {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
148 {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
149 {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
150 {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
151 {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
152 {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
153 {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
154 {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
155 {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
156 {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
157 {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
158 {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
159 }
160
161 // jsonl does it all, given a reader and a writer
162 func jsonl(w *bufio.Writer, r io.Reader, live bool) error {
163 dec := json.NewDecoder(r)
164 // avoid parsing numbers, so unusually-long numbers are kept verbatim,
165 // even if JSON parsers aren't required to guarantee such input-fidelity
166 // for numbers
167 dec.UseNumber()
168
169 t, err := dec.Token()
170 if err == io.EOF {
171 // return errors.New(`input has no JSON values`)
172 return nil
173 }
174
175 if t == json.Delim('[') {
176 if err := handleTopLevelArray(w, dec, live); err != nil {
177 return err
178 }
179 } else {
180 if err := handleToken(w, dec, t); err != nil {
181 return err
182 }
183 w.WriteByte('\n')
184 }
185
186 _, err = dec.Token()
187 if err == io.EOF {
188 // input is over, so it's a success
189 return nil
190 }
191
192 if err == nil {
193 // a successful `read` is a failure, as it means there are
194 // trailing JSON tokens
195 return errors.New(`unexpected trailing data`)
196 }
197
198 // any other error, perhaps some invalid-JSON-syntax-type error
199 return err
200 }
201
202 // handleToken handles recursion for func json2
203 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token) error {
204 switch t := t.(type) {
205 case json.Delim:
206 switch t {
207 case json.Delim('['):
208 return handleArray(w, dec)
209 case json.Delim('{'):
210 return handleObject(w, dec)
211 default:
212 return errors.New(`unsupported JSON syntax ` + string(t))
213 }
214
215 case nil:
216 w.WriteString(`null`)
217 return nil
218
219 case bool:
220 if t {
221 w.WriteString(`true`)
222 } else {
223 w.WriteString(`false`)
224 }
225 return nil
226
227 case json.Number:
228 w.WriteString(t.String())
229 return nil
230
231 case string:
232 return handleString(w, t)
233
234 default:
235 // return fmt.Errorf(`unsupported token type %T`, t)
236 return errors.New(`invalid JSON token`)
237 }
238 }
239
240 func handleTopLevelArray(w *bufio.Writer, dec *json.Decoder, live bool) error {
241 for i := 0; true; i++ {
242 t, err := dec.Token()
243 if err == io.EOF {
244 return nil
245 }
246
247 if err != nil {
248 return err
249 }
250
251 if t == json.Delim(']') {
252 return nil
253 }
254
255 err = handleToken(w, dec, t)
256 if err != nil {
257 return err
258 }
259
260 if w.WriteByte('\n') != nil {
261 return io.EOF
262 }
263
264 if !live {
265 continue
266 }
267
268 if w.Flush() != nil {
269 return io.EOF
270 }
271 }
272
273 // make the compiler happy
274 return nil
275 }
276
277 // handleArray handles arrays for func handleToken
278 func handleArray(w *bufio.Writer, dec *json.Decoder) error {
279 w.WriteByte('[')
280
281 for i := 0; true; i++ {
282 t, err := dec.Token()
283 if err == io.EOF {
284 return errors.New(`end of JSON before array was closed`)
285 }
286 if err != nil {
287 return err
288 }
289
290 if t == json.Delim(']') {
291 w.WriteByte(']')
292 return nil
293 }
294
295 if i > 0 {
296 _, err := w.WriteString(", ")
297 if err != nil {
298 return io.EOF
299 }
300 }
301
302 err = handleToken(w, dec, t)
303 if err != nil {
304 return err
305 }
306 }
307
308 // make the compiler happy
309 return nil
310 }
311
312 // handleObject handles objects for func handleToken
313 func handleObject(w *bufio.Writer, dec *json.Decoder) error {
314 w.WriteByte('{')
315
316 for i := 0; true; i++ {
317 t, err := dec.Token()
318 if err == io.EOF {
319 return errors.New(`end of JSON before object was closed`)
320 }
321 if err != nil {
322 return err
323 }
324
325 if t == json.Delim('}') {
326 w.WriteByte('}')
327 return nil
328 }
329
330 if i > 0 {
331 _, err := w.WriteString(", ")
332 if err != nil {
333 return io.EOF
334 }
335 }
336
337 k, ok := t.(string)
338 if !ok {
339 return errors.New(`expected a string for a key-value pair`)
340 }
341
342 err = handleString(w, k)
343 if err != nil {
344 return err
345 }
346
347 w.WriteString(": ")
348
349 t, err = dec.Token()
350 if err == io.EOF {
351 return errors.New(`expected a value for a key-value pair`)
352 }
353
354 err = handleToken(w, dec, t)
355 if err != nil {
356 return err
357 }
358 }
359
360 // make the compiler happy
361 return nil
362 }
363
364 // handleString handles strings for func handleToken, and keys for func
365 // handleObject
366 func handleString(w *bufio.Writer, s string) error {
367 for _, r := range s {
368 if r < ' ' || r == '"' || r == '\\' {
369 w.WriteByte('"')
370 for _, r := range s {
371 if int(r) < len(escapedStringBytes) {
372 w.Write(escapedStringBytes[r])
373 } else {
374 w.WriteRune(r)
375 }
376 }
377 w.WriteByte('"')
378 return nil
379 }
380 }
381
382 w.WriteByte('"')
383 w.WriteString(s)
384 w.WriteByte('"')
385 return nil
386 }
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(err.Error())
58 os.Stderr.WriteString("\n")
59 os.Exit(1)
60 return
61 }
62 }
63
64 type runConfig struct {
65 lines int
66 keys []string
67 }
68
69 func run(paths []string) error {
70 bw := bufio.NewWriter(os.Stdout)
71 defer bw.Flush()
72
73 dashes := 0
74 var cfg runConfig
75
76 for _, path := range paths {
77 if path == `-` {
78 dashes++
79 if dashes > 1 {
80 continue
81 }
82
83 if err := handleInput(bw, os.Stdin, &cfg); err != nil {
84 return err
85 }
86
87 continue
88 }
89
90 if err := handleFile(bw, path, &cfg); err != nil {
91 return err
92 }
93 }
94
95 if len(paths) == 0 {
96 if err := handleInput(bw, os.Stdin, &cfg); err != nil {
97 return err
98 }
99 }
100
101 if cfg.lines > 1 {
102 bw.WriteString("\n]\n")
103 } else {
104 bw.WriteString("[]\n")
105 }
106 return nil
107 }
108
109 func handleFile(w *bufio.Writer, path string, cfg *runConfig) error {
110 f, err := os.Open(path)
111 if err != nil {
112 return err
113 }
114 defer f.Close()
115 return handleInput(w, f, cfg)
116 }
117
118 func escapeKeys(line string) []string {
119 var keys []string
120 var sb strings.Builder
121
122 loopTSV(line, func(i int, s string) {
123 sb.WriteByte('"')
124 for _, r := range s {
125 if r == '\\' || r == '"' {
126 sb.WriteByte('\\')
127 }
128 sb.WriteRune(r)
129 }
130 sb.WriteByte('"')
131
132 keys = append(keys, sb.String())
133 sb.Reset()
134 })
135
136 return keys
137 }
138
139 func emitRow(w *bufio.Writer, line string, keys []string) {
140 j := 0
141 w.WriteByte('{')
142
143 loopTSV(line, func(i int, s string) {
144 j = i
145 if i > 0 {
146 w.WriteString(", ")
147 }
148
149 w.WriteString(keys[i])
150 w.WriteString(": \"")
151
152 for _, r := range s {
153 if r == '\\' || r == '"' {
154 w.WriteByte('\\')
155 }
156 w.WriteRune(r)
157 }
158 w.WriteByte('"')
159 })
160
161 for i := j + 1; i < len(keys); i++ {
162 if i > 0 {
163 w.WriteString(", ")
164 }
165 w.WriteString(keys[i])
166 w.WriteString(": null")
167 }
168 w.WriteByte('}')
169 }
170
171 func loopTSV(line string, f func(i int, s string)) {
172 for i := 0; len(line) > 0; i++ {
173 pos := strings.IndexByte(line, '\t')
174 if pos < 0 {
175 f(i, line)
176 return
177 }
178
179 f(i, line[:pos])
180 line = line[pos+1:]
181 }
182 }
183
184 func handleInput(w *bufio.Writer, r io.Reader, cfg *runConfig) error {
185 const gb = 1024 * 1024 * 1024
186 sc := bufio.NewScanner(r)
187 sc.Buffer(nil, 8*gb)
188
189 for i := 0; sc.Scan(); i++ {
190 s := sc.Text()
191 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
192 s = s[3:]
193 }
194
195 if cfg.lines == 0 {
196 cfg.keys = escapeKeys(s)
197 w.WriteByte('[')
198 cfg.lines++
199 continue
200 }
201
202 if cfg.lines == 1 {
203 w.WriteString("\n ")
204 } else {
205 if _, err := w.WriteString(",\n "); err != nil {
206 return io.EOF
207 }
208 }
209
210 emitRow(w, s, cfg.keys)
211 cfg.lines++
212 }
213
214 return sc.Err()
215 }
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(err.Error())
71 os.Stderr.WriteString("\n")
72 os.Exit(1)
73 return
74 }
75 }
76
77 func writeJunk(w io.Writer, n int) error {
78 var buf [32 * 1024]byte
79
80 for n > 0 {
81 max := n
82 if max > cap(buf) {
83 max = cap(buf)
84 }
85
86 got, err := io.ReadFull(rand.Reader, buf[:max])
87 if err == io.EOF && got > 0 {
88 err = nil
89 }
90
91 if err != nil {
92 return err
93 }
94
95 if _, err := w.Write(buf[:got]); err != nil {
96 return io.EOF
97 }
98
99 n -= got
100 }
101
102 return nil
103 }
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(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 const (
84 half = ` `
85 spaces = half + half
86 )
87
88 bw := bufio.NewWriterSize(w, 32*1024)
89 defer bw.Flush()
90
91 bw.WriteString("\x1b[7m")
92
93 left := len(spaces)
94
95 for i, s := range args {
96 if i > 0 {
97 if err := bw.WriteByte(' '); err != nil {
98 return io.EOF
99 }
100 left--
101 }
102 bw.WriteString(s)
103 left -= utf8.RuneCountInString(s)
104 }
105
106 if 0 < left && left < len(spaces) {
107 bw.WriteString(spaces[:left])
108 }
109
110 if _, err := bw.WriteString("\x1b[0m\n"); err != nil {
111 return io.EOF
112 }
113
114 if live {
115 if bw.Flush() != nil {
116 return io.EOF
117 }
118 }
119
120 return catl(bw, os.Stdin, live)
121 }
122
123 func catl(w *bufio.Writer, r io.Reader, live bool) error {
124 const gb = 1024 * 1024 * 1024
125 sc := bufio.NewScanner(r)
126 sc.Buffer(nil, 8*gb)
127
128 for i := 0; sc.Scan(); i++ {
129 s := sc.Bytes()
130 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
131 s = s[3:]
132 }
133
134 w.Write(s)
135 if w.WriteByte('\n') != nil {
136 return io.EOF
137 }
138
139 if !live {
140 continue
141 }
142
143 if w.Flush() != nil {
144 return io.EOF
145 }
146 }
147
148 return sc.Err()
149 }
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(err.Error())
116 os.Stderr.WriteString("\n")
117 os.Exit(1)
118 return
119 }
120
121 show(os.Stdout, latest)
122 }
123
124 func run(paths []string, rb *ringBuffer) error {
125 for _, path := range paths {
126 if err := handleFile(path, rb); err != nil {
127 return err
128 }
129 }
130
131 if len(paths) == 0 {
132 if err := visit(os.Stdin, rb); err != nil {
133 return err
134 }
135 }
136 return nil
137 }
138
139 func show(w io.Writer, rb ringBuffer) {
140 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
141 defer bw.Flush()
142
143 for i := rb.next; i < len(rb.items); i++ {
144 bw.WriteString(rb.items[i])
145 if err := bw.WriteByte('\n'); err != nil {
146 return
147 }
148 }
149
150 for i := 0; i < len(rb.items) && i < rb.next; i++ {
151 bw.WriteString(rb.items[i])
152 if err := bw.WriteByte('\n'); err != nil {
153 return
154 }
155 }
156 }
157
158 func handleFile(path string, rb *ringBuffer) error {
159 f, err := os.Open(path)
160 if err != nil {
161 return err
162 }
163 defer f.Close()
164 return visit(f, rb)
165 }
166
167 func visit(r io.Reader, rb *ringBuffer) error {
168 const gb = 1024 * 1024 * 1024
169 sc := bufio.NewScanner(r)
170 sc.Buffer(nil, 8*gb)
171
172 for sc.Scan() {
173 rb.append(sc.Text())
174 }
175 return sc.Err()
176 }
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 "errors"
31 "io"
32 "os"
33 "strings"
34 )
35
36 const info = `
37 leak [options...] [style name...] [files...]
38
39 Emit copies of input lines both to stdout and to stderr, thus "leaking"
40 the contents going through a pipe of commands, using the leading argument
41 as the style/color name to use to decorate the stderr output.
42
43 This tool's main use-case is to inspect/debug the intermediate stages of a
44 "pipelined" shell command.
45
46 When variable NO_COLOR is declared and set to 1, an invert/highlight style
47 is used instead of colors.
48
49 The only option available is to show this help message, using any of
50 "-h", "--h", "-help", or "--help", without the quotes.
51
52 Supported style names include
53
54 - red - orange - bold - underline
55 - green - magenta - purple - invert
56 - blue - gray - italic
57
58 Supported aliases for style names include
59
60 b (blue) bb (blue background)
61 g (green) gb (green background)
62 h (hilight/invert)
63 m (magenta) mb (magenta background)
64 o (orange) ob (orange background)
65 p (purple) pb (purple background)
66 r (red) rb (red background)
67 u (underline)
68 `
69
70 type config struct {
71 // style is the ANSI-style sequence to use verbatim
72 style string
73
74 // buf is the buffer space for the (re)styled lines for the standard error
75 buf []byte
76
77 // live is whether lines are flushed each time
78 live bool
79 }
80
81 func Main() {
82 var cfg config
83 cfg.live = true
84 args := os.Args[1:]
85
86 for len(args) > 0 {
87 switch args[0] {
88 case `-b`, `--b`, `-buffered`, `--buffered`:
89 cfg.live = false
90 args = args[1:]
91 continue
92
93 case `-h`, `--h`, `-help`, `--help`:
94 os.Stdout.WriteString(info[1:])
95 return
96 }
97
98 break
99 }
100
101 options := true
102 if len(args) > 0 && args[0] == `--` {
103 options = false
104 args = args[1:]
105 }
106
107 if os.Getenv(`NO_COLOR`) == `1` {
108 cfg.style, _ = lookupStyle(`inverse`)
109 options = false
110 } else {
111 cfg.style, _ = lookupStyle(`gray`)
112 }
113
114 // if the first argument is 1 or 2 dashes followed by a supported
115 // style-name, and the environment doesn't have NO_COLOR set to 1,
116 // change the style used
117 if options && len(args) > 0 && strings.HasPrefix(args[0], `-`) {
118 name := args[0]
119 name = strings.TrimPrefix(name, `-`)
120 name = strings.TrimPrefix(name, `-`)
121 args = args[1:]
122
123 // check if the `dedashed` argument is a supported style-name
124 if s, ok := lookupStyle(name); ok {
125 cfg.style = s
126 } else {
127 os.Stderr.WriteString(`invalid style name `)
128 os.Stderr.WriteString(name)
129 os.Stderr.WriteString("\n")
130 os.Exit(1)
131 return
132 }
133 }
134
135 if cfg.live {
136 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
137 cfg.live = false
138 }
139 }
140
141 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
142 os.Stderr.WriteString(err.Error())
143 os.Stderr.WriteString("\n")
144 os.Exit(1)
145 return
146 }
147 }
148
149 func run(w io.Writer, args []string, cfg config) error {
150 bw := bufio.NewWriter(w)
151 defer bw.Flush()
152
153 if len(args) == 0 {
154 return leak(bw, os.Stdin, cfg)
155 }
156
157 for _, name := range args {
158 if err := handleFile(bw, name, cfg); err != nil {
159 return err
160 }
161 }
162 return nil
163 }
164
165 func handleFile(w *bufio.Writer, name string, cfg config) error {
166 if name == `` || name == `-` {
167 return leak(w, os.Stdin, cfg)
168 }
169
170 f, err := os.Open(name)
171 if err != nil {
172 return errors.New(`can't read from file named "` + name + `"`)
173 }
174 defer f.Close()
175
176 return leak(w, f, cfg)
177 }
178
179 func leak(w *bufio.Writer, r io.Reader, cfg config) error {
180 const gb = 1024 * 1024 * 1024
181 sc := bufio.NewScanner(r)
182 sc.Buffer(nil, 8*gb)
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 // leak the (re)styled line to standard error
196 cfg.buf = append(cfg.buf[:0], cfg.style...)
197 cfg.buf = appendPlain(cfg.buf, s)
198 if cfg.style != `` {
199 cfg.buf = append(cfg.buf, "\x1b[0m\n"...)
200 } else {
201 cfg.buf = append(cfg.buf, '\n')
202 }
203 os.Stderr.Write(cfg.buf)
204
205 if !cfg.live {
206 continue
207 }
208
209 if err := w.Flush(); err != nil {
210 // a write error may be the consequence of stdout being closed,
211 // perhaps by another app along a pipe
212 return io.EOF
213 }
214 }
215 return sc.Err()
216 }
217
218 func lookupStyle(name string) (style string, ok bool) {
219 if alias, ok := styleAliases[name]; ok {
220 name = alias
221 }
222
223 style, ok = styles[name]
224 return style, ok
225 }
226
227 var styleAliases = map[string]string{
228 `b`: `blue`,
229 `g`: `green`,
230 `m`: `magenta`,
231 `o`: `orange`,
232 `p`: `purple`,
233 `r`: `red`,
234 `u`: `underline`,
235
236 `bolded`: `bold`,
237 `h`: `inverse`,
238 `hi`: `inverse`,
239 `highlight`: `inverse`,
240 `highlighted`: `inverse`,
241 `hilite`: `inverse`,
242 `hilited`: `inverse`,
243 `inv`: `inverse`,
244 `invert`: `inverse`,
245 `inverted`: `inverse`,
246 `underlined`: `underline`,
247
248 `bb`: `blueback`,
249 `bg`: `greenback`,
250 `bm`: `magentaback`,
251 `bo`: `orangeback`,
252 `bp`: `purpleback`,
253 `br`: `redback`,
254
255 `gb`: `greenback`,
256 `mb`: `magentaback`,
257 `ob`: `orangeback`,
258 `pb`: `purpleback`,
259 `rb`: `redback`,
260
261 `bblue`: `blueback`,
262 `bgray`: `grayback`,
263 `bgreen`: `greenback`,
264 `bmagenta`: `magentaback`,
265 `borange`: `orangeback`,
266 `bpurple`: `purpleback`,
267 `bred`: `redback`,
268
269 `backblue`: `blueback`,
270 `backgray`: `grayback`,
271 `backgreen`: `greenback`,
272 `backmagenta`: `magentaback`,
273 `backorange`: `orangeback`,
274 `backpurple`: `purpleback`,
275 `backred`: `redback`,
276 }
277
278 // styles turns style-names into the ANSI-code sequences used for the
279 // alternate groups of digits
280 var styles = map[string]string{
281 `blue`: "\x1b[38;2;0;95;215m",
282 `bold`: "\x1b[1m",
283 `gray`: "\x1b[38;2;168;168;168m",
284 `green`: "\x1b[38;2;0;135;95m",
285 `inverse`: "\x1b[7m",
286 `magenta`: "\x1b[38;2;215;0;255m",
287 `orange`: "\x1b[38;2;215;95;0m",
288 `plain`: "\x1b[0m",
289 `red`: "\x1b[38;2;204;0;0m",
290 `underline`: "\x1b[4m",
291
292 // `blue`: "\x1b[38;5;26m",
293 // `bold`: "\x1b[1m",
294 // `gray`: "\x1b[38;5;248m",
295 // `green`: "\x1b[38;5;29m",
296 // `inverse`: "\x1b[7m",
297 // `magenta`: "\x1b[38;5;99m",
298 // `orange`: "\x1b[38;5;166m",
299 // `plain`: "\x1b[0m",
300 // `red`: "\x1b[31m",
301 // `underline`: "\x1b[4m",
302
303 `blueback`: "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
304 `grayback`: "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
305 `greenback`: "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
306 `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
307 `orangeback`: "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
308 `purpleback`: "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
309 `redback`: "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
310 }
311
312 // appendPlain extends the slice given using the non-ANSI parts of a string
313 func appendPlain(dst []byte, src []byte) []byte {
314 for len(src) > 0 {
315 i, j := indexEscapeSequence(src)
316 if i < 0 {
317 dst = append(dst, src...)
318 break
319 }
320 if j < 0 {
321 j = len(src)
322 }
323
324 if i > 0 {
325 dst = append(dst, src[:i]...)
326 }
327
328 src = src[j:]
329 }
330
331 return dst
332 }
333
334 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
335 // the multi-byte sequences starting with ESC[; the result is a pair of slice
336 // indices which can be independently negative when either the start/end of
337 // a sequence isn't found; given their fairly-common use, even the hyperlink
338 // ESC]8 sequences are supported
339 func indexEscapeSequence(s []byte) (int, int) {
340 var prev byte
341
342 for i, b := range s {
343 if prev == '\x1b' && b == '[' {
344 j := indexLetter(s[i+1:])
345 if j < 0 {
346 return i, -1
347 }
348 return i - 1, i + 1 + j + 1
349 }
350
351 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
352 j := indexPair(s[i+1:], '\x1b', '\\')
353 if j < 0 {
354 return i, -1
355 }
356 return i - 1, i + 1 + j + 2
357 }
358
359 prev = b
360 }
361
362 return -1, -1
363 }
364
365 func indexLetter(s []byte) int {
366 for i, b := range s {
367 upper := b &^ 32
368 if 'A' <= upper && upper <= 'Z' {
369 return i
370 }
371 }
372
373 return -1
374 }
375
376 func indexPair(s []byte, x byte, y byte) int {
377 var prev byte
378
379 for i, b := range s {
380 if prev == x && b == y && i > 0 {
381 return i
382 }
383 prev = b
384 }
385
386 return -1
387 }
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(err.Error())
99 os.Stderr.WriteString("\n")
100 os.Exit(1)
101 return
102 }
103 }
104
105 func run(args []string, n *int) error {
106 dashes := 0
107 for _, name := range args {
108 if name == `-` {
109 dashes++
110 }
111 if dashes > 1 {
112 return errors.New(`can't read stdin (dash) more than once`)
113 }
114 }
115
116 if len(args) == 0 {
117 if err := handleReader(os.Stdout, os.Stdin, n); err != nil {
118 return err
119 }
120 }
121
122 for _, name := range args {
123 if name == `-` {
124 if err := handleReader(os.Stdout, os.Stdin, n); err != nil {
125 return err
126 }
127 continue
128 }
129
130 if err := handleFile(os.Stdout, name, n); err != nil {
131 return err
132 }
133 }
134
135 return nil
136 }
137
138 func handleFile(w io.Writer, name string, n *int) error {
139 if name == `` || name == `-` {
140 return handleReader(w, os.Stdin, n)
141 }
142
143 f, err := os.Open(name)
144 if err != nil {
145 return errors.New(`can't read from file named "` + name + `"`)
146 }
147 defer f.Close()
148
149 return handleReader(w, f, n)
150 }
151
152 func handleReader(w io.Writer, r io.Reader, n *int) error {
153 var buf [32 * 1024]byte
154
155 for *n > 0 {
156 max := *n
157 if max > cap(buf) {
158 max = cap(buf)
159 }
160
161 got, err := os.Stdin.Read(buf[:max])
162 if got > 0 {
163 if _, err := os.Stdout.Write(buf[:got]); err != nil {
164 return io.EOF
165 }
166 }
167 *n -= got
168
169 if err != nil {
170 return io.EOF
171 }
172 }
173
174 return nil
175 }
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(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 bw := bufio.NewWriter(w)
106 defer bw.Flush()
107
108 dashes := 0
109 for _, name := range args {
110 if name == `-` {
111 dashes++
112 }
113 if dashes > 1 {
114 return errors.New(`can't read stdin (dash) more than once`)
115 }
116 }
117
118 if len(args) == 0 {
119 if err := lineUp(bw, os.Stdin, &cfg); err != nil {
120 return err
121 }
122 }
123
124 for _, name := range args {
125 if name == `-` {
126 if err := lineUp(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
137 if cfg.count > 0 {
138 bw.WriteByte('\n')
139 }
140 return nil
141 }
142
143 func handleFile(w *bufio.Writer, name string, cfg *config) error {
144 if name == `` || name == `-` {
145 return lineUp(w, os.Stdin, cfg)
146 }
147
148 f, err := os.Open(name)
149 if err != nil {
150 return errors.New(`can't read from file named "` + name + `"`)
151 }
152 defer f.Close()
153
154 return lineUp(w, f, cfg)
155 }
156
157 func lineUp(w *bufio.Writer, r io.Reader, cfg *config) error {
158 const gb = 1024 * 1024 * 1024
159 sc := bufio.NewScanner(r)
160 sc.Buffer(nil, 8*gb)
161
162 for i := 0; sc.Scan(); i++ {
163 s := sc.Bytes()
164 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
165 s = s[3:]
166 }
167
168 if cfg.count > 0 {
169 var sep byte
170 if cfg.remainder == 0 {
171 sep = '\n'
172 } else {
173 sep = '\t'
174 }
175
176 if w.WriteByte(sep) != nil {
177 return io.EOF
178 }
179 }
180
181 w.Write(s)
182 cfg.count++
183
184 cfg.remainder++
185 if cfg.remainder >= cfg.maxItems && cfg.maxItems > 1 {
186 cfg.remainder = 0
187 }
188
189 if !cfg.liveLines {
190 continue
191 }
192
193 if w.Flush() != nil {
194 return io.EOF
195 }
196 }
197
198 return sc.Err()
199 }
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(err.Error())
82 os.Stderr.WriteString("\n")
83 os.Exit(1)
84 return
85 }
86 }
87
88 func run(paths []string, cfg config) error {
89 w := bufio.NewWriterSize(os.Stdout, 32*1024)
90 defer w.Flush()
91
92 for i, path := range paths {
93 if len(paths) > 1 {
94 w.WriteString(path)
95 w.WriteString(":\n")
96 if i > 0 {
97 w.WriteString("\n")
98 }
99 }
100
101 if err := ls(w, path, cfg); err != nil {
102 return err
103 }
104 }
105
106 if len(paths) == 0 {
107 return ls(w, `.`, cfg)
108 }
109 return nil
110 }
111
112 func ls(w *bufio.Writer, path string, cfg config) error {
113 defer w.Flush()
114
115 entries, err := os.ReadDir(path)
116 if err != nil {
117 return err
118 }
119
120 sort.SliceStable(entries, func(i, j int) bool {
121 return compareNames(entries[i].Name(), entries[j].Name()) < 0
122 })
123
124 for _, e := range entries {
125 name := e.Name()
126
127 if !cfg.all && len(name) > 0 && name[0] == '.' {
128 continue
129 }
130
131 w.WriteString(name)
132 if _, err := w.WriteString("\n"); err != nil {
133 return io.EOF
134 }
135 }
136
137 return nil
138 }
139
140 func compareNames(x, y string) int {
141 if len(x) < len(y) && strings.HasPrefix(y, x) {
142 return -1
143 }
144 if len(y) < len(x) && strings.HasPrefix(x, y) {
145 return +1
146 }
147 return strings.Compare(x, y)
148 }
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 "./chopleads"
57 "./coby"
58 "./compress"
59 "./countdown"
60 "./datauri"
61 "./dc"
62 "./dcol"
63 "./debase64"
64 "./decsv"
65 "./dedent"
66 "./dedup"
67 "./dejsonl"
68 "./delay"
69 "./dessv"
70 "./detab"
71 "./dtsv"
72 "./ecoli"
73 "./erase"
74 "./expand"
75 "./factor"
76 "./fh"
77 "./files"
78 "./filesizes"
79 "./finfo"
80 "./first"
81 "./fixlines"
82 "./folders"
83 "./gsub"
84 "./head"
85 "./hima"
86 "./htmlify"
87 "./id3pic"
88 "./indent"
89 "./items"
90 "./json0"
91 "./json2"
92 "./jsonl"
93 "./jsons"
94 "./junk"
95 "./label"
96 "./last"
97 "./leak"
98 "./limit"
99 "./lineup"
100 "./ls"
101 "./match"
102 "./n"
103 "./ncol"
104 "./ngron"
105 "./nhex"
106 "./njson"
107 "./nl"
108 "./nn"
109 "./now"
110 "./nts"
111 "./pac"
112 "./pad"
113 "./paramatch"
114 "./pbn"
115 "./pcol"
116 "./plain"
117 "./pretsv"
118 "./primes"
119 "./ptsv"
120 "./realign"
121 "./reprose"
122 "./same"
123 "./sbs"
124 "./seq"
125 "./skip"
126 "./skiplast"
127 "./squeeze"
128 "./squomp"
129 "./stomp"
130 "./sym"
131 "./tacl"
132 "./tail"
133 "./tcatl"
134 "./teletype"
135 "./tolower"
136 "./underline"
137 "./units"
138 "./utfate"
139 "./waveout"
140 "./zcat"
141 "./zj"
142
143 _ "embed"
144 )
145
146 //go:embed info.txt
147 var info string
148
149 // mains has some entries starting as nil to avoid circular-dependency errors
150 var mains = map[string]func(){
151 `args`: args,
152 `avoid`: avoid.Main,
153 `bitdump`: bitdump.Main,
154 `breakdown`: breakdown.Main,
155 `bytedump`: bytedump.Main,
156 `calc`: calc.Main,
157 `catl`: catl.Main,
158 `chopleads`: chopleads.Main,
159 `cls`: cls,
160 `coby`: coby.Main,
161 `compress`: compress.Main,
162 `countdown`: countdown.Main,
163 `datauri`: datauri.Main,
164 `dcol`: dcol.Main,
165 `debase64`: debase64.Main,
166 `decompress`: decompress,
167 `decsv`: decsv.Main,
168 `dedent`: dedent.Main,
169 `dedup`: dedup.Main,
170 `dejsonl`: dejsonl.Main,
171 `delay`: delay.Main,
172 `dessv`: dessv.Main,
173 `detab`: detab.Main,
174 `dtsv`: dtsv.Main,
175 `echobar`: echobar,
176 `ecoli`: ecoli.Main,
177 `erase`: erase.Main,
178 `fail`: fail,
179 `files`: files.Main,
180 `filesizes`: filesizes.Main,
181 `finfo`: finfo.Main,
182 `first`: first.Main,
183 `fixlines`: fixlines.Main,
184 `folders`: folders.Main,
185 `gsub`: gsub.Main,
186 `hecho`: hecho,
187 `hima`: hima.Main,
188 `htmlify`: htmlify.Main,
189 `id3pic`: id3pic.Main,
190 `ignore`: ignore,
191 `indent`: indent.Main,
192 `items`: items.Main,
193 `json0`: json0.Main,
194 `json2`: json2.Main,
195 `jsonl`: jsonl.Main,
196 `jsons`: jsons.Main,
197 `junk`: junk.Main,
198 `label`: label.Main,
199 `last`: last.Main,
200 `leak`: leak.Main,
201 `limit`: limit.Main,
202 `lineup`: lineup.Main,
203 `match`: match.Main,
204 `n`: n.Main,
205 `ncol`: ncol.Main,
206 `ngron`: ngron.Main,
207 `nhex`: nhex.Main,
208 `njson`: njson.Main,
209 `nn`: nn.Main,
210 `now`: now.Main,
211 `nts`: nts.Main,
212 `pad`: pad.Main,
213 `paramatch`: paramatch.Main,
214 `pbn`: pbn.Main,
215 `pcol`: pcol.Main,
216 `plain`: plain.Main,
217 `precho`: precho,
218 `pretsv`: pretsv.Main,
219 `primes`: primes.Main,
220 `ptsv`: ptsv.Main,
221 `realign`: realign.Main,
222 `reprose`: reprose.Main,
223 `ruler`: ruler,
224 `same`: same.Main,
225 `sbs`: sbs.Main,
226 `skip`: skip.Main,
227 `skiplast`: skiplast.Main,
228 `squeeze`: squeeze.Main,
229 `squomp`: squomp.Main,
230 `stomp`: stomp.Main,
231 `sym`: sym.Main,
232 `tacl`: tacl.Main,
233 `tcatl`: tcatl.Main,
234 `teletype`: teletype.Main,
235 `timezones`: timezones,
236 `tolower`: tolower.Main,
237 `underline`: underline.Main,
238 `units`: units.Main,
239 `utfate`: utfate.Main,
240 `waveout`: waveout.Main,
241 }
242
243 var experimental = map[string]func(){
244 `backsort`: backsort.Main,
245 `div`: div,
246 `fh`: fh.Main,
247 `mop`: mop,
248 `nil`: null,
249 `pac`: pac.Main,
250 `nothing`: nothing,
251 `tinker`: tinker,
252 `ungron`: ungronMain,
253 `zj`: zj.Main,
254 }
255
256 var remakes = map[string]func(){
257 `base64`: base64.Main,
258 `cat`: cat.Main,
259 `clear`: clear,
260 `dc`: dc.Main,
261 `echo`: echo,
262 `expand`: expand.Main,
263 `factor`: factor.Main,
264 `false`: falseMain,
265 `head`: head.Main,
266 `ls`: ls.Main,
267 `nl`: nl.Main,
268 `seq`: seq.Main,
269 `sleep`: sleep,
270 `tail`: tail.Main,
271 `true`: trueMain,
272 `yes`: yes,
273 `zcat`: zcat.Main,
274 }
275
276 var aliases = map[string]string{
277 `a`: `avoid`,
278 `break`: `breakdown`,
279 `breaklines`: `breakdown`,
280 `ca`: `calc`,
281 `calculate`: `calc`,
282 `calculator`: `calc`,
283 `fc`: `calc`,
284 `frac`: `calc`,
285 `fraca`: `calc`,
286 `fracalc`: `calc`,
287 `bytes`: `cat`,
288 `catenate`: `cat`,
289 `concat`: `cat`,
290 `concatenate`: `cat`,
291 `load`: `cat`,
292 `lines`: `catl`,
293 `dcols`: `dcol`,
294 `dropcol`: `dcol`,
295 `dropcols`: `dcol`,
296 `dropcolumns`: `dcol`,
297 `unbase64`: `debase64`,
298 `uncompress`: `decompress`,
299 `uncsv`: `decsv`,
300 `deduplicate`: `dedup`,
301 `undup`: `dedup`,
302 `unique`: `dedup`,
303 `unjsonl`: `dejsonl`,
304 `slowlines`: `delay`,
305 `unssv`: `dessv`,
306 `untab`: `detab`,
307 `bar`: `echobar`,
308 `fileinfo`: `finfo`,
309 `detrail`: `fixlines`,
310 `id3picture`: `id3pic`,
311 `id3thumb`: `id3pic`,
312 `id3thumbnail`: `id3pic`,
313 `mp3pic`: `id3pic`,
314 `mp3picture`: `id3pic`,
315 `mp3thumb`: `id3pic`,
316 `mp3thumbnail`: `id3pic`,
317 `idem`: `ignore`,
318 `iden`: `ignore`,
319 `identity`: `ignore`,
320 `j0`: `json0`,
321 `j2`: `json2`,
322 `jl`: `jsonl`,
323 `detsv`: `jsons`,
324 `prelabel`: `label`,
325 `prememo`: `label`,
326 `k`: `match`,
327 `keep`: `match`,
328 `m`: `match`,
329 `mops`: `mop`,
330 `ncols`: `ncol`,
331 `nicecol`: `ncol`,
332 `nicecols`: `ncol`,
333 `nicecolumns`: `ncol`,
334 `gron`: `ngron`,
335 `nicegron`: `ngron`,
336 `nh`: `nhex`,
337 `nicehex`: `nhex`,
338 `null`: `nil`,
339 `nicejson`: `njson`,
340 `nj`: `njson`,
341 `nicedigits`: `nn`,
342 `nicenum`: `nn`,
343 `nicenumbers`: `nn`,
344 `nicenums`: `nn`,
345 `ok`: `nothing`,
346 `rpn`: `pac`,
347 `parmatch`: `paramatch`,
348 `pmatch`: `paramatch`,
349 `mapa`: `paramatch`,
350 `mapara`: `paramatch`,
351 `matchpar`: `paramatch`,
352 `matchpara`: `paramatch`,
353 `pcols`: `pcol`,
354 `pickcol`: `pcol`,
355 `pickcols`: `pcol`,
356 `pickcolumns`: `pcol`,
357 `symbol`: `sym`,
358 `symbols`: `sym`,
359 `syms`: `sym`,
360 `tty`: `teletype`,
361 `timezone`: `timezones`,
362 `lower`: `tolower`,
363 `lowercase`: `tolower`,
364 `u`: `underline`,
365 `uline`: `underline`,
366 `degron`: `ungron`,
367 `utf8`: `utfate`,
368 `zoomjson`: `zj`,
369 `degzip`: `zcat`,
370 `ungzip`: `zcat`,
371 }
372
373 var blurbs = map[string]string{
374 `args`: `show all ARGumentS given after the tool name, one per line`,
375 `avoid`: `ignore lines matching any of the regexes given`,
376 `bitdump`: `show all bits for all input bytes`,
377 `breakdown`: `break input lines into multiple output lines by regexes`,
378 `bytedump`: `show bytes as hex values, with a wide ASCII panel`,
379 `calc`: `fractional calculator, with floating-point powers`,
380 `catl`: `conCATenate Lines, ensures text ends with a line-feed`,
381 `chopleads`: `ignore common leading starts of all non-empty lines`,
382 `cls`: `CLear the Screen`,
383 `coby`: `COunt BYtes, and many other byte/text-related stats`,
384 `compress`: `gzip-compress all concatenated inputs given`,
385 `countdown`: `countdown the seconds/minutes/hours given`,
386 `datauri`: `turn bytes into data-URIs, auto-detecting MIME types`,
387 `dcol`: `Drop COLumns by name or by 1-based index`,
388 `debase64`: `decode base64 text and data-URIs`,
389 `decompress`: `gzip-decompress concatenated bytes from all the inputs`,
390 `decsv`: `convert CSV tables into TSV tables, or into JSON`,
391 `dedent`: `ignore the common leading-space indentation`,
392 `dedup`: `deduplicate lines, emitting each unique line only once`,
393 `dejsonl`: `turn JSON Lines into proper JSON`,
394 `delay`: `wait the seconds given, before emitting each input line`,
395 `dessv`: `turn tables of space-separated values into TSV tables`,
396 `detab`: `expand tabs into runs of up to n spaces, or up to 4`,
397 `div`: `DIVide 2 numbers, or invert 1 number`,
398 `dtsv`: `Drop Tab-Separated Values, matching args to column names`,
399 `echobar`: `like 'echo', but right-pads and highlights its output`,
400 `ecoli`: `Expressions COloring LInes color-codes matching lines`,
401 `erase`: `ignore/erase all matching regexes away from each line`,
402 `fail`: `fail with the exit code given, or using 1 by default`,
403 `fh`: `Function Heatmapper draws 2-input (x, y) functions`,
404 `files`: `list all files in the folder(s) given`,
405 `filesizes`: `show sizes of files and block-counts (4K by default)`,
406 `finfo`: `show various file info, for plain-text and/or media files`,
407 `first`: `keep only the first n lines, or the first line by default`,
408 `fixlines`: `ignore carriage-returns, or even trailing spaces`,
409 `folders`: `list all folders in the folder(s) given`,
410 `gsub`: `Globally SUBstitute all regular expression matches`,
411 `hecho`: `Highlighted ECHO shows an ANSI-styled line`,
412 `help`: `show the help message for "easybox"`,
413 `hima`: `HIlight MAtches using the regexes given`,
414 `htmlify`: `turn plain-text lines into HTML documents`,
415 `id3pic`: `get the encoded picture out of audio files, if present`,
416 `ignore`: `ignore all arguments, copying stdin into stdout instead`,
417 `indent`: `indent lines, using 4 leading spaces by default`,
418 `items`: `emit words/items from input lines as single-item lines`,
419 `json0`: `minimize/fix JSON into the smallest-possible size`,
420 `json2`: `indent JSON into multiple lines, using 2 spaces per level`,
421 `jsonl`: `turn items from top-level JSON arrays into JSON Lines`,
422 `jsons`: `JSON Strings turns TSV into arrays of objects of strings`,
423 `junk`: `emit a given count of random bytes, or 1024 by default`,
424 `label`: `precede input lines with a styled line`,
425 `last`: `keep only the last n lines, or the last line by default`,
426 `leak`: `emit lines both to stdout and stderr; good to debug pipes`,
427 `limit`: `emit up to the first n bytes, or the first 1024 by default`,
428 `lineup`: `put input lines into (up to) n-items tab-separated lines`,
429 `match`: `only keep lines matching any of the regexes given`,
430 `mop`: `Multiple OPerations, using the 2 numbers given`,
431 `n`: `Number lines, putting tabs between numbers and contents`,
432 `ncol`: `Nice COLumns realigns tables, color-coding their values`,
433 `ngron`: `Nice GRON mimics a subset of "gron", using better colors`,
434 `nhex`: `Nice HEXadecimal shows bytes as hex values and ASCII`,
435 `nil`: `emit nothing, also discarding all stdin bytes, if piped`,
436 `njson`: `Nice JSON indents and color-codes JSON data`,
437 `nn`: `Nice Numbers color-codes groups of digits for legibility`,
438 `now`: `show the current date and time, also for other timezones`,
439 `nts`: `Nice TimeStamp precedes each line with styled date/time`,
440 `pac`: `Postfix Array Calculator is an RPN-style calculator`,
441 `pad`: `right-pad lines, so they all have the same symbol-count`,
442 `pbn`: `Pick (lines) By Number, using 1-based or negative indices`,
443 `paramatch`: `PARAgraphs MATCHing any of the regexes given`,
444 `pcol`: `Pick COLumns by name or by 1-based index`,
445 `plain`: `ignore all ANSI-sequences, leaving unstyled text`,
446 `precho`: `PRecede ECHO, precedes input lines with an SSV line`,
447 `pretsv`: `PREcede TSV, precedes input lines with a TSV line`,
448 `primes`: `find prime numbers, up to the first million by default`,
449 `ptsv`: `Pick Tab-Separated Values, matching args to column names`,
450 `realign`: `realign items from the SSV/TSV tables given`,
451 `reprose`: `reflow/trim lines of text/prose to improve its legibility`,
452 `ruler`: `show a ruler-like pattern on a single line`,
453 `same`: `fail if the 2 files given have different contents`,
454 `sbs`: `Side By Side, wraps input lines in multiple columns`,
455 `seq`: `emit a sequence of numbers, one per line`,
456 `skip`: `skip at most the first n lines, or the first by default`,
457 `skiplast`: `skip at most the last n lines, or the last by default`,
458 `squeeze`: `aggressively ignore spaces, especially runs of spaces`,
459 `squomp`: `squeeze non-empty lines and stomp empty(-ish) lines`,
460 `stomp`: `ignore leading/trailing empty lines, squeezing empty runs`,
461 `sym`: `lookup hard-to-type symbols by their common names/aliases`,
462 `tacl`: `TAC Lines emits text lines in backward-order`,
463 `tcatl`: `Titled conCATenate Lines, is like "catl" but with names`,
464 `teletype`: `mimic old-fashioned teletype devices, by delaying output`,
465 `timezones`: `lookup full timezone names from the city/place names given`,
466 `tolower`: `turn all uppercase letters into lowercase ones`,
467 `tools`: `list all tools available`,
468 `underline`: `underline every nth line, or every 5th line by default`,
469 `ungron`: `turn GRON-style lines into valid JSON`,
470 `units`: `convert weird units into the international standard ones`,
471 `utfate`: `decode all other types of UTF text into UTF-8`,
472 `waveout`: `emit/calculate WAV-format sounds by formula`,
473 `yes`: `keep emitting a line with "yes", or the message given`,
474 `zj`: `Zoom Json, using the keys/indices given as arguments`,
475 }
476
477 func main() {
478 // try to use the app's `name`, in case it's being called from a file-link
479 // named after one of the tools
480 if tool, ok := lookupTool(path.Base(os.Args[0])); ok {
481 tool()
482 return
483 }
484
485 if len(os.Args) < 2 {
486 showHelp(os.Stderr)
487 fmt.Fprintln(os.Stderr, ``)
488 fmt.Fprintln(os.Stderr, `easybox: no tool name given`)
489 os.Exit(1)
490 return
491 }
492
493 // try normal tool-lookup using the first command-line argument
494
495 name := os.Args[1]
496 os.Args = os.Args[1:]
497
498 if tool, ok := lookupTool(name); ok {
499 tool()
500 return
501 }
502
503 switch name {
504 case `-h`, `--h`, `-help`, `--help`, `help`:
505 showHelp(os.Stdout)
506
507 case `-l`, `--l`, `-list`, `--list`:
508 tools()
509
510 case `-links`, `--links`:
511 showLinksCommands(os.Stdout)
512
513 case `-t`, `--t`, `-tools`, `--tools`, `tools`:
514 tools()
515
516 default:
517 const fs = "easybox: tool/alias named %q not found\n"
518 fmt.Fprintf(os.Stderr, fs, name)
519 os.Exit(1)
520 return
521 }
522 }
523
524 // dealias tries to lookup a string to the aliases given, returning the name
525 // given if the lookup fails
526 func dealias(aliases map[string]string, name string) string {
527 if s, ok := aliases[name]; ok {
528 return s
529 }
530 return name
531 }
532
533 func help() {
534 showHelp(os.Stdout)
535 }
536
537 func lookupTool(name string) (tool func(), ok bool) {
538 name = strings.ReplaceAll(name, `-`, ``)
539 name = strings.ReplaceAll(name, `_`, ``)
540 key := dealias(aliases, name)
541
542 if tool, ok := mains[key]; ok {
543 return tool, ok
544 }
545
546 if tool, ok := remakes[key]; ok {
547 return tool, ok
548 }
549
550 tool, ok = experimental[key]
551 return tool, ok
552 }
553
554 // showHelp has a parameter to write either to stdout or stderr
555 func showHelp(w io.Writer) {
556 fmt.Fprintln(w, info)
557
558 fmt.Fprintln(w, "\nTools Available")
559
560 maxlen := 0
561 names := make([]string, 0, max(len(mains), len(aliases)))
562 for k := range mains {
563 names = append(names, k)
564 maxlen = max(maxlen, utf8.RuneCountInString(k))
565 }
566 sort.Strings(names)
567
568 for _, s := range names {
569 fmt.Fprintf(w, " - %-*s %s\n", maxlen, s, blurbs[s])
570 }
571
572 fmt.Fprintln(w, "\nAliases Available")
573
574 maxlen = 0
575 names = names[:0]
576 for k := range aliases {
577 names = append(names, k)
578 maxlen = max(maxlen, utf8.RuneCountInString(k))
579 }
580 sort.Strings(names)
581
582 for _, k := range names {
583 fmt.Fprintf(w, " - %-*s -> %s\n", maxlen, k, aliases[k])
584 }
585
586 names = names[:0]
587 for k := range experimental {
588 names = append(names, k)
589 }
590 sort.Strings(names)
591
592 if len(names) == 0 {
593 return
594 }
595
596 fmt.Fprintln(w, "\nExperimental Tools Available")
597
598 for _, k := range names {
599 fmt.Fprintf(w, " - %s\n", k)
600 }
601 }
602
603 // showLinksCommands has a parameter to write either to stdout or stderr
604 func showLinksCommands(w io.Writer) {
605 names := make([]string, 0, len(mains))
606 for k := range mains {
607 names = append(names, k)
608 }
609
610 sort.Strings(names)
611
612 for _, s := range names {
613 fmt.Fprintf(w, "ln -s \"$(which easybox)\" ./%s\n", s)
614 }
615 }
616
617 // tinker is for little throw-away experiments
618 func tinker() {
619 }
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 )
34
35 const info = `
36 match [options...] [regular expressions...]
37
38 Only keep lines which match any of the extended-mode regular expressions
39 given. When not given any regex, match non-empty lines by default.
40
41 The options are, available both in single and double-dash versions
42
43 -h, -help show this help message
44 -i, -ins match regexes case-insensitively
45 -l, -links add a regex to match HTTP/HTTPS links case-insensitively
46 -q, -quiet never emit output: exit code still succeeds/fails
47 `
48
49 const linkRegexp = `(?i)https?://[a-z0-9+_.:%-]+(/[a-z0-9+_.%/,#?&=-]*)*`
50
51 func Main() {
52 nerr := 0
53 links := false
54 buffered := false
55 avoid := false
56 sensitive := true
57 quiet := false
58 args := os.Args[1:]
59
60 for len(args) > 0 {
61 switch args[0] {
62 case `-b`, `--b`, `-buffered`, `--buffered`:
63 buffered = true
64 args = args[1:]
65 continue
66
67 case `-h`, `--h`, `-help`, `--help`:
68 os.Stdout.WriteString(info[1:])
69 return
70
71 case `-i`, `--i`, `-ins`, `--ins`, `-insensitive`, `--insensitive`:
72 sensitive = false
73 args = args[1:]
74 continue
75
76 case `-iv`, `-vi`:
77 sensitive = false
78 avoid = true
79 args = args[1:]
80 continue
81
82 case `-l`, `--l`, `-links`, `--links`:
83 links = true
84 args = args[1:]
85 continue
86
87 case `-q`, `--q`, `-quiet`, `--quiet`:
88 quiet = true
89 args = args[1:]
90 continue
91
92 case `-v`, `--v`, `-inv`, `--inv`, `-neg`, `--neg`:
93 avoid = true
94 args = args[1:]
95 continue
96 }
97
98 break
99 }
100
101 if len(args) > 0 && args[0] == `--` {
102 args = args[1:]
103 }
104
105 liveLines := false
106 if !quiet {
107 liveLines = !buffered
108 if !buffered {
109 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
110 liveLines = false
111 }
112 }
113 }
114
115 if len(args) == 0 {
116 args = []string{`.`}
117 }
118
119 var exprs []*regexp.Regexp
120 if links {
121 exprs = make([]*regexp.Regexp, 0, len(args)+1)
122 exprs = append(exprs, regexp.MustCompile(linkRegexp))
123 } else {
124 exprs = make([]*regexp.Regexp, 0, len(args))
125 }
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(err.Error())
138 os.Stderr.WriteString("\n")
139 nerr++
140 }
141
142 exprs = append(exprs, exp)
143 }
144
145 if nerr > 0 {
146 os.Exit(1)
147 return
148 }
149
150 var buf []byte
151 sc := bufio.NewScanner(os.Stdin)
152 sc.Buffer(nil, 8*1024*1024*1024)
153 bw := bufio.NewWriter(os.Stdout)
154
155 ok := false
156
157 for i := 0; sc.Scan(); i++ {
158 line := sc.Bytes()
159 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
160 line = line[3:]
161 }
162
163 s := line
164 if bytes.IndexByte(s, '\x1b') >= 0 {
165 buf = plain(buf[:0], s)
166 s = buf
167 }
168
169 if match(s, exprs) {
170 if avoid {
171 continue
172 }
173
174 ok = true
175 if quiet {
176 continue
177 }
178 if err := emit(bw, line, liveLines); err != nil {
179 break
180 }
181 }
182
183 if avoid {
184 ok = true
185 if quiet {
186 continue
187 }
188 if err := emit(bw, line, liveLines); err != nil {
189 break
190 }
191 }
192 }
193
194 if !ok {
195 os.Exit(1)
196 return
197 }
198 }
199
200 func emit(w *bufio.Writer, line []byte, live bool) error {
201 w.Write(line)
202 w.WriteByte('\n')
203
204 if !live {
205 return nil
206 }
207
208 return w.Flush()
209 }
210
211 func match(what []byte, with []*regexp.Regexp) bool {
212 for _, e := range with {
213 if e.Match(what) {
214 return true
215 }
216 }
217 return false
218 }
219
220 func plain(dst []byte, src []byte) []byte {
221 for len(src) > 0 {
222 i, j := indexEscapeSequence(src)
223 if i < 0 {
224 dst = append(dst, src...)
225 break
226 }
227 if j < 0 {
228 j = len(src)
229 }
230
231 if i > 0 {
232 dst = append(dst, src[:i]...)
233 }
234
235 src = src[j:]
236 }
237
238 return dst
239 }
240
241 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
242 // the multi-byte sequences starting with ESC[; the result is a pair of slice
243 // indices which can be independently negative when either the start/end of
244 // a sequence isn't found; given their fairly-common use, even the hyperlink
245 // ESC]8 sequences are supported
246 func indexEscapeSequence(s []byte) (int, int) {
247 var prev byte
248
249 for i, b := range s {
250 if prev == '\x1b' && b == '[' {
251 j := indexLetter(s[i+1:])
252 if j < 0 {
253 return i, -1
254 }
255 return i - 1, i + 1 + j + 1
256 }
257
258 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
259 j := indexPair(s[i+1:], '\x1b', '\\')
260 if j < 0 {
261 return i, -1
262 }
263 return i - 1, i + 1 + j + 2
264 }
265
266 prev = b
267 }
268
269 return -1, -1
270 }
271
272 func indexLetter(s []byte) int {
273 for i, b := range s {
274 upper := b &^ 32
275 if 'A' <= upper && upper <= 'Z' {
276 return i
277 }
278 }
279
280 return -1
281 }
282
283 func indexPair(s []byte, x byte, y byte) int {
284 var prev byte
285
286 for i, b := range s {
287 if prev == x && b == y && i > 0 {
288 return i
289 }
290 prev = b
291 }
292
293 return -1
294 }
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: ./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(err.Error())
94 os.Stderr.WriteString("\n")
95 os.Exit(1)
96 return
97 }
98 }
99
100 func run(paths []string, cfg *config) error {
101 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
102 defer bw.Flush()
103
104 for _, p := range paths {
105 if err := handleFile(bw, p, cfg); err != nil {
106 return err
107 }
108 }
109
110 if len(paths) == 0 {
111 return handleReader(bw, os.Stdin, cfg)
112 }
113
114 return nil
115 }
116
117 func handleFile(w *bufio.Writer, path string, cfg *config) error {
118 f, err := os.Open(path)
119 if err != nil {
120 // on windows, file-not-found error messages may mention `CreateFile`,
121 // even when trying to open files in read-only mode
122 return errors.New(`can't open file named ` + path)
123 }
124 defer f.Close()
125 return handleReader(w, f, cfg)
126 }
127
128 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
129 var buf [24]byte
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.Text()
136 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
137 s = s[3:]
138 }
139
140 w.Write(strconv.AppendInt(buf[:0], int64(cfg.n), 10))
141 w.WriteByte('\t')
142 w.WriteString(s)
143 cfg.n++
144
145 if w.WriteByte('\n') != nil {
146 return io.EOF
147 }
148
149 if !cfg.liveLines {
150 continue
151 }
152
153 if w.Flush() != nil {
154 return io.EOF
155 }
156 }
157
158 return sc.Err()
159 }
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(err.Error())
117 os.Stderr.WriteString("\n")
118 os.Exit(1)
119 return
120 }
121 }
122
123 // table has all summary info gathered from the data, along with the row
124 // themselves, stored as lines/strings
125 type table struct {
126 Columns int
127
128 Rows []string
129
130 MaxWidth []int
131
132 MaxDotDecimals []int
133
134 Numeric []int
135
136 Sums []float64
137
138 LoopItems func(line string, items int, t *table, f itemFunc) int
139
140 sb strings.Builder
141
142 MaxColumns bool
143
144 ShowTiles bool
145
146 ShowSums bool
147 }
148
149 type itemFunc func(i int, s string, t *table)
150
151 func run(paths []string, res *table) error {
152 for _, p := range paths {
153 if err := handleFile(res, p); err != nil {
154 return err
155 }
156 }
157
158 if len(paths) == 0 {
159 if err := handleReader(res, os.Stdin); err != nil {
160 return err
161 }
162 }
163
164 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
165 defer bw.Flush()
166 realign(bw, res)
167 return nil
168 }
169
170 func handleFile(res *table, path string) error {
171 f, err := os.Open(path)
172 if err != nil {
173 // on windows, file-not-found error messages may mention `CreateFile`,
174 // even when trying to open files in read-only mode
175 return errors.New(`can't open file named ` + path)
176 }
177 defer f.Close()
178 return handleReader(res, f)
179 }
180
181 func handleReader(t *table, r io.Reader) error {
182 const gb = 1024 * 1024 * 1024
183 sc := bufio.NewScanner(r)
184 sc.Buffer(nil, 8*gb)
185
186 const maxInt = int(^uint(0) >> 1)
187 maxCols := maxInt
188
189 for i := 0; sc.Scan(); i++ {
190 s := sc.Text()
191 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
192 s = s[3:]
193 }
194
195 if len(s) == 0 {
196 continue
197 }
198
199 t.Rows = append(t.Rows, s)
200
201 if t.Columns == 0 {
202 if t.LoopItems == nil {
203 if strings.IndexByte(s, '\t') >= 0 {
204 t.LoopItems = loopItemsTSV
205 } else {
206 t.LoopItems = loopItemsSSV
207 }
208 }
209
210 if !t.MaxColumns {
211 t.Columns = t.LoopItems(s, maxCols, t, doNothing)
212 maxCols = t.Columns
213 }
214 }
215
216 t.LoopItems(s, maxCols, t, updateItem)
217 }
218
219 return sc.Err()
220 }
221
222 // doNothing is given to LoopItems to count items, while doing nothing else
223 func doNothing(i int, s string, t *table) {}
224
225 func updateItem(i int, s string, t *table) {
226 // ensure column-info-slices have enough room
227 if i >= len(t.MaxWidth) {
228 // update column-count if in max-columns mode
229 if t.MaxColumns {
230 t.Columns = i + 1
231 }
232 t.MaxWidth = append(t.MaxWidth, 0)
233 t.MaxDotDecimals = append(t.MaxDotDecimals, 0)
234 t.Numeric = append(t.Numeric, 0)
235 t.Sums = append(t.Sums, 0)
236 }
237
238 // keep track of widest rune-counts for each column
239 w := countWidth(s)
240 if t.MaxWidth[i] < w {
241 t.MaxWidth[i] = w
242 }
243
244 // update stats for numeric items
245 if isNumeric(s, &(t.sb)) {
246 dd := countDotDecimals(s)
247 if t.MaxDotDecimals[i] < dd {
248 t.MaxDotDecimals[i] = dd
249 }
250
251 t.Numeric[i]++
252 f, _ := strconv.ParseFloat(t.sb.String(), 64)
253 t.Sums[i] += f
254 }
255 }
256
257 // loopItemsSSV loops over a line's items, allocation-free style; when given
258 // empty strings, the callback func is never called
259 func loopItemsSSV(s string, max int, t *table, f itemFunc) int {
260 i := 0
261 s = trimTrailingSpaces(s)
262
263 for {
264 s = trimLeadingSpaces(s)
265 if len(s) == 0 {
266 return i
267 }
268
269 if i+1 == max {
270 f(i, s, t)
271 return i + 1
272 }
273
274 j := strings.IndexByte(s, ' ')
275 if j < 0 {
276 f(i, s, t)
277 return i + 1
278 }
279
280 f(i, s[:j], t)
281 s = s[j+1:]
282 i++
283 }
284 }
285
286 func trimLeadingSpaces(s string) string {
287 for len(s) > 0 && s[0] == ' ' {
288 s = s[1:]
289 }
290 return s
291 }
292
293 func trimTrailingSpaces(s string) string {
294 for len(s) > 0 && s[len(s)-1] == ' ' {
295 s = s[:len(s)-1]
296 }
297 return s
298 }
299
300 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
301 // when given empty strings, the callback func is never called
302 func loopItemsTSV(s string, max int, t *table, f itemFunc) int {
303 if len(s) == 0 {
304 return 0
305 }
306
307 i := 0
308
309 for {
310 if i+1 == max {
311 f(i, s, t)
312 return i + 1
313 }
314
315 j := strings.IndexByte(s, '\t')
316 if j < 0 {
317 f(i, s, t)
318 return i + 1
319 }
320
321 f(i, s[:j], t)
322 s = s[j+1:]
323 i++
324 }
325 }
326
327 func skipLeadingEscapeSequences(s string) string {
328 for len(s) >= 2 {
329 if s[0] != '\x1b' {
330 return s
331 }
332
333 switch s[1] {
334 case '[':
335 s = skipSingleLeadingANSI(s[2:])
336
337 case ']':
338 if len(s) < 3 || s[2] != '8' {
339 return s
340 }
341 s = skipSingleLeadingOSC(s[3:])
342
343 default:
344 return s
345 }
346 }
347
348 return s
349 }
350
351 func skipSingleLeadingANSI(s string) string {
352 for len(s) > 0 {
353 upper := s[0] &^ 32
354 s = s[1:]
355 if 'A' <= upper && upper <= 'Z' {
356 break
357 }
358 }
359
360 return s
361 }
362
363 func skipSingleLeadingOSC(s string) string {
364 var prev byte
365
366 for len(s) > 0 {
367 b := s[0]
368 s = s[1:]
369 if prev == '\x1b' && b == '\\' {
370 break
371 }
372 prev = b
373 }
374
375 return s
376 }
377
378 // isNumeric checks if a string is valid/useable as a number
379 func isNumeric(s string, sb *strings.Builder) bool {
380 if len(s) == 0 {
381 return false
382 }
383
384 sb.Reset()
385
386 s = skipLeadingEscapeSequences(s)
387 if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
388 sb.WriteByte(s[0])
389 s = s[1:]
390 }
391
392 s = skipLeadingEscapeSequences(s)
393 if len(s) == 0 {
394 return false
395 }
396 if b := s[0]; b == '.' {
397 sb.WriteByte(b)
398 return isDigits(s[1:], sb)
399 }
400
401 digits := 0
402
403 for {
404 s = skipLeadingEscapeSequences(s)
405 if len(s) == 0 {
406 break
407 }
408
409 b := s[0]
410 sb.WriteByte(b)
411
412 if b == '.' {
413 return isDigits(s[1:], sb)
414 }
415
416 if !('0' <= b && b <= '9') {
417 return false
418 }
419
420 digits++
421 s = s[1:]
422 }
423
424 s = skipLeadingEscapeSequences(s)
425 return len(s) == 0 && digits > 0
426 }
427
428 func isDigits(s string, sb *strings.Builder) bool {
429 if len(s) == 0 {
430 return false
431 }
432
433 digits := 0
434
435 for {
436 s = skipLeadingEscapeSequences(s)
437 if len(s) == 0 {
438 break
439 }
440
441 if b := s[0]; '0' <= b && b <= '9' {
442 sb.WriteByte(b)
443 s = s[1:]
444 digits++
445 } else {
446 return false
447 }
448 }
449
450 s = skipLeadingEscapeSequences(s)
451 return len(s) == 0 && digits > 0
452 }
453
454 // countDecimals counts decimal digits from the string given, assuming it
455 // represents a valid/useable float64, when parsed
456 func countDecimals(s string) int {
457 dot := strings.IndexByte(s, '.')
458 if dot < 0 {
459 return 0
460 }
461
462 decs := 0
463 s = s[dot+1:]
464
465 for len(s) > 0 {
466 s = skipLeadingEscapeSequences(s)
467 if len(s) == 0 {
468 break
469 }
470 if '0' <= s[0] && s[0] <= '9' {
471 decs++
472 }
473 s = s[1:]
474 }
475
476 return decs
477 }
478
479 // countDotDecimals is like func countDecimals, but this one also includes
480 // the dot, when any decimals are present, else the count stays at 0
481 func countDotDecimals(s string) int {
482 decs := countDecimals(s)
483 if decs > 0 {
484 return decs + 1
485 }
486 return decs
487 }
488
489 func countWidth(s string) int {
490 width := 0
491
492 for len(s) > 0 {
493 i, j := indexEscapeSequence(s)
494 if i < 0 {
495 break
496 }
497 if j < 0 {
498 j = len(s)
499 }
500
501 width += utf8.RuneCountInString(s[:i])
502 s = s[j:]
503 }
504
505 // count trailing/all runes in strings which don't end with ANSI-sequences
506 width += utf8.RuneCountInString(s)
507 return width
508 }
509
510 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
511 // the multi-byte sequences starting with ESC[; the result is a pair of slice
512 // indices which can be independently negative when either the start/end of
513 // a sequence isn't found; given their fairly-common use, even the hyperlink
514 // ESC]8 sequences are supported
515 func indexEscapeSequence(s string) (int, int) {
516 var prev byte
517
518 for i := range s {
519 b := s[i]
520
521 if prev == '\x1b' && b == '[' {
522 j := indexLetter(s[i+1:])
523 if j < 0 {
524 return i, -1
525 }
526 return i - 1, i + 1 + j + 1
527 }
528
529 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
530 j := indexPair(s[i+1:], '\x1b', '\\')
531 if j < 0 {
532 return i, -1
533 }
534 return i - 1, i + 1 + j + 2
535 }
536
537 prev = b
538 }
539
540 return -1, -1
541 }
542
543 func indexLetter(s string) int {
544 for i, b := range s {
545 upper := b &^ 32
546 if 'A' <= upper && upper <= 'Z' {
547 return i
548 }
549 }
550
551 return -1
552 }
553
554 func indexPair(s string, x byte, y byte) int {
555 var prev byte
556
557 for i := range s {
558 b := s[i]
559 if prev == x && b == y && i > 0 {
560 return i
561 }
562 prev = b
563 }
564
565 return -1
566 }
567
568 func realign(w *bufio.Writer, t *table) {
569 // make sums row first, as final alignments are usually affected by these
570 var sums []string
571 if t.ShowSums {
572 sums = make([]string, 0, t.Columns)
573
574 for i := 0; i < t.Columns; i++ {
575 if t.Numeric[i] == 0 {
576 sums = append(sums, `-`)
577 if t.MaxWidth[i] < 1 {
578 t.MaxWidth[i] = 1
579 }
580 continue
581 }
582
583 decs := t.MaxDotDecimals[i]
584 if decs > 0 {
585 decs--
586 }
587
588 var buf [64]byte
589 s := strconv.AppendFloat(buf[:0], t.Sums[i], 'f', decs, 64)
590 sums = append(sums, string(s))
591 if t.MaxWidth[i] < len(s) {
592 t.MaxWidth[i] = len(s)
593 }
594 }
595 }
596
597 // due keeps track of how many spaces are due, when separating realigned
598 // items from their immediate predecessor on the same row; this counter
599 // is also used to right-pad numbers with decimals, as such items can be
600 // padded with spaces from either side
601 due := 0
602
603 showItem := func(i int, s string, t *table) {
604 if i > 0 {
605 due += columnGap
606 }
607
608 if isNumeric(s, &(t.sb)) {
609 dd := countDotDecimals(s)
610 rpad := t.MaxDotDecimals[i] - dd
611 width := countWidth(s)
612 lpad := t.MaxWidth[i] - (width + rpad) + due
613 writeSpaces(w, lpad)
614 f, _ := strconv.ParseFloat(t.sb.String(), 64)
615 writeNumericItem(w, s, numericStyle(f))
616 due = rpad
617 return
618 }
619
620 writeSpaces(w, due)
621 w.WriteString(s)
622 due = t.MaxWidth[i] - countWidth(s)
623 }
624
625 writeTile := func(i int, s string, t *table) {
626 // make empty items stand out
627 if len(s) == 0 {
628 w.WriteString("\x1b[0m○")
629 return
630 }
631
632 if isNumeric(s, &(t.sb)) {
633 f, _ := strconv.ParseFloat(t.sb.String(), 64)
634 w.WriteString(numericStyle(f))
635 w.WriteString("■")
636 return
637 }
638
639 // make padded items stand out: these items have spaces at either end
640 if s[0] == ' ' || s[len(s)-1] == ' ' {
641 w.WriteString("\x1b[38;2;196;160;0m■")
642 return
643 }
644
645 w.WriteString("\x1b[38;2;128;128;128m■")
646 }
647
648 // show realigned rows
649
650 for _, line := range t.Rows {
651 due = 0
652
653 if t.ShowTiles {
654 end := t.LoopItems(line, t.Columns, t, writeTile)
655 if end < len(t.MaxWidth)-1 {
656 w.WriteString("\x1b[0m")
657 }
658 // make rows with missing trailing items stand out
659 for i := end; i < len(t.MaxWidth); i++ {
660 w.WriteString("×")
661 }
662 w.WriteString("\x1b[0m")
663 due += columnGap
664 }
665
666 t.LoopItems(line, t.Columns, t, showItem)
667 if w.WriteByte('\n') != nil {
668 return
669 }
670 }
671
672 if t.Columns > 0 && t.ShowSums {
673 realignSums(w, t, sums)
674 }
675 }
676
677 func realignSums(w *bufio.Writer, t *table, sums []string) {
678 due := 0
679 if t.ShowTiles {
680 due += t.Columns + columnGap
681 }
682
683 for i, s := range sums {
684 if i > 0 {
685 due += columnGap
686 }
687
688 if t.Numeric[i] == 0 {
689 writeSpaces(w, due)
690 w.WriteString(s)
691 due = t.MaxWidth[i] - countWidth(s)
692 continue
693 }
694
695 lpad := t.MaxWidth[i] - len(s) + due
696 writeSpaces(w, lpad)
697 writeNumericItem(w, s, numericStyle(t.Sums[i]))
698 due = 0
699 }
700
701 w.WriteByte('\n')
702 }
703
704 // writeSpaces does what it says, minimizing calls to write-like funcs
705 func writeSpaces(w *bufio.Writer, n int) {
706 const spaces = ` `
707 if n < 1 {
708 return
709 }
710
711 for n >= len(spaces) {
712 w.WriteString(spaces)
713 n -= len(spaces)
714 }
715 w.WriteString(spaces[:n])
716 }
717
718 func writeRowTiles(w *bufio.Writer, s string, t *table, writeTile itemFunc) {
719 end := t.LoopItems(s, t.Columns, t, writeTile)
720
721 if end < len(t.MaxWidth)-1 {
722 w.WriteString("\x1b[0m")
723 }
724 for i := end + 1; i < len(t.MaxWidth); i++ {
725 w.WriteString("×")
726 }
727 w.WriteString("\x1b[0m")
728 }
729
730 func numericStyle(f float64) string {
731 if f > 0 {
732 if float64(int64(f)) == f {
733 return "\x1b[38;2;0;135;0m"
734 }
735 return "\x1b[38;2;0;155;95m"
736 }
737 if f < 0 {
738 if float64(int64(f)) == f {
739 return "\x1b[38;2;204;0;0m"
740 }
741 return "\x1b[38;2;215;95;95m"
742 }
743 if f == 0 {
744 return "\x1b[38;2;0;95;215m"
745 }
746 return "\x1b[38;2;128;128;128m"
747 }
748
749 func writeNumericItem(w *bufio.Writer, s string, startStyle string) {
750 w.WriteString(startStyle)
751 if len(s) > 0 && (s[0] == '-' || s[0] == '+') {
752 w.WriteByte(s[0])
753 s = s[1:]
754 }
755
756 dot := strings.IndexByte(s, '.')
757 if dot < 0 {
758 restyleDigits(w, s, altDigitStyle)
759 w.WriteString("\x1b[0m")
760 return
761 }
762
763 if len(s[:dot]) > 3 {
764 restyleDigits(w, s[:dot], altDigitStyle)
765 w.WriteString("\x1b[0m")
766 w.WriteString(startStyle)
767 w.WriteByte('.')
768 } else {
769 w.WriteString(s[:dot])
770 w.WriteByte('.')
771 }
772
773 rest := s[dot+1:]
774 restyleDigits(w, rest, altDigitStyle)
775 if len(rest) < 4 {
776 w.WriteString("\x1b[0m")
777 }
778 }
779
780 // restyleDigits renders a run of digits as alternating styled/unstyled runs
781 // of 3 digits, which greatly improves readability, and is the only purpose
782 // of this app; string is assumed to be all decimal digits
783 func restyleDigits(w *bufio.Writer, digits string, altStyle string) {
784 if len(digits) < 4 {
785 // digit sequence is short, so emit it as is
786 w.WriteString(digits)
787 return
788 }
789
790 // separate leading 0..2 digits which don't align with the 3-digit groups
791 i := len(digits) % 3
792 // emit leading digits unstyled, if there are any
793 w.WriteString(digits[:i])
794 // the rest is guaranteed to have a length which is a multiple of 3
795 digits = digits[i:]
796
797 // start by styling, unless there were no leading digits
798 style := i != 0
799
800 for len(digits) > 0 {
801 if style {
802 w.WriteString(altStyle)
803 w.WriteString(digits[:3])
804 w.WriteString("\x1b[0m")
805 } else {
806 w.WriteString(digits[:3])
807 }
808
809 // advance to the next triple: the start of this func is supposed
810 // to guarantee this step always works
811 digits = digits[3:]
812
813 // alternate between styled and unstyled 3-digit groups
814 style = !style
815 }
816 }
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 err := w.Flush(); err != 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(err.Error())
145 os.Stderr.WriteString("\n")
146 os.Exit(1)
147 return
148 }
149 }
150
151 type handlerFunc func(*bufio.Writer, *json.Decoder, json.Token, []any) error
152 type runnerFunc func(w *bufio.Writer, r io.Reader) error
153
154 // handleInput simplifies control-flow for func main
155 func handleInput(w io.Writer, path string, run runnerFunc) error {
156 if path == `-` {
157 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
158 defer bw.Flush()
159 return run(bw, os.Stdin)
160 }
161
162 f, err := os.Open(path)
163 if err != nil {
164 // on windows, file-not-found error messages may mention `CreateFile`,
165 // even when trying to open files in read-only mode
166 return errors.New(`can't open file named ` + path)
167 }
168 defer f.Close()
169
170 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
171 defer bw.Flush()
172 return run(bw, f)
173 }
174
175 // gron does it all, given a reader and a writer
176 func gron(w *bufio.Writer, r io.Reader) error {
177 dec := json.NewDecoder(r)
178 // avoid parsing numbers, so unusually-long numbers are kept verbatim,
179 // even if JSON parsers aren't required to guarantee such input-fidelity
180 // for numbers
181 dec.UseNumber()
182
183 t, err := dec.Token()
184 if err == io.EOF {
185 return errors.New(`input has no JSON values`)
186 }
187
188 if err = handleToken(w, dec, t, make([]any, 0, 50)); err != nil {
189 return err
190 }
191
192 _, err = dec.Token()
193 if err == io.EOF {
194 // input is over, so it's a success
195 return nil
196 }
197
198 if err == nil {
199 // a successful `read` is a failure, as it means there are
200 // trailing JSON tokens
201 return errors.New(`unexpected trailing data`)
202 }
203
204 // any other error, perhaps some invalid-JSON-syntax-type error
205 return err
206 }
207
208 // handleToken handles recursion for func run
209 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, path []any) error {
210 switch t := t.(type) {
211 case json.Delim:
212 switch t {
213 case json.Delim('['):
214 return handleArray(w, dec, path)
215 case json.Delim('{'):
216 return handleObject(w, dec, path)
217 default:
218 return errors.New(`unsupported JSON syntax ` + string(t))
219 }
220
221 case nil:
222 config.path(w, path)
223 config.null(w)
224 return endLine(w)
225
226 case bool:
227 config.path(w, path)
228 config.boolean(w, t)
229 return endLine(w)
230
231 case json.Number:
232 config.path(w, path)
233 config.number(w, t)
234 return endLine(w)
235
236 case string:
237 config.path(w, path)
238 config.text(w, t)
239 return endLine(w)
240
241 default:
242 // return fmt.Errorf(`unsupported token type %T`, t)
243 return errors.New(`invalid JSON token`)
244 }
245 }
246
247 // handleArray handles arrays for func handleToken
248 func handleArray(w *bufio.Writer, dec *json.Decoder, path []any) error {
249 config.path(w, path)
250 w.WriteString(config.arrayDecl)
251 if err := endLine(w); err != nil {
252 return err
253 }
254
255 path = append(path, 0)
256 last := len(path) - 1
257
258 for i := 0; true; i++ {
259 path[last] = i
260
261 t, err := dec.Token()
262 if err == io.EOF {
263 return errors.New(`end of JSON before array was closed`)
264 }
265 if err != nil {
266 return err
267 }
268
269 if t == json.Delim(']') {
270 return nil
271 }
272
273 err = handleToken(w, dec, t, path)
274 if err != nil {
275 return err
276 }
277 }
278
279 // make the compiler happy
280 return nil
281 }
282
283 // handleObject handles objects for func handleToken
284 func handleObject(w *bufio.Writer, dec *json.Decoder, path []any) error {
285 config.path(w, path)
286 w.WriteString(config.objectDecl)
287 if err := endLine(w); err != nil {
288 return err
289 }
290
291 path = append(path, ``)
292 last := len(path) - 1
293
294 for i := 0; true; i++ {
295 t, err := dec.Token()
296 if err == io.EOF {
297 return errors.New(`end of JSON before object was closed`)
298 }
299 if err != nil {
300 return err
301 }
302
303 if t == json.Delim('}') {
304 return nil
305 }
306
307 k, ok := t.(string)
308 if !ok {
309 return errors.New(`expected a string for a key-value pair`)
310 }
311
312 path[last] = k
313 if err != nil {
314 return err
315 }
316
317 t, err = dec.Token()
318 if err == io.EOF {
319 return errors.New(`expected a value for a key-value pair`)
320 }
321
322 err = handleToken(w, dec, t, path)
323 if err != nil {
324 return err
325 }
326 }
327
328 // make the compiler happy
329 return nil
330 }
331
332 func monoPath(w *bufio.Writer, path []any) error {
333 var buf [24]byte
334
335 w.WriteString(`json`)
336
337 for _, v := range path {
338 switch v := v.(type) {
339 case int:
340 w.WriteByte('[')
341 w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
342 w.WriteByte(']')
343
344 case string:
345 if !needsEscaping(v) {
346 w.WriteByte('.')
347 w.WriteString(v)
348 continue
349 }
350 w.WriteByte('[')
351 monoString(w, v)
352 w.WriteByte(']')
353 }
354 }
355
356 w.WriteString(` = `)
357 return nil
358 }
359
360 func monoNull(w *bufio.Writer) error {
361 w.WriteString(`null`)
362 return nil
363 }
364
365 func monoBool(w *bufio.Writer, b bool) error {
366 if b {
367 w.WriteString(`true`)
368 } else {
369 w.WriteString(`false`)
370 }
371 return nil
372 }
373
374 func monoNumber(w *bufio.Writer, n json.Number) error {
375 w.WriteString(n.String())
376 return nil
377 }
378
379 func monoString(w *bufio.Writer, s string) error {
380 for _, r := range s {
381 if r > 127 {
382 b, err := json.Marshal(s)
383 if err != nil {
384 return err
385 }
386 w.Write(b)
387 return nil
388 }
389
390 if r < ' ' || r == '"' || r == '\\' {
391 w.WriteByte('"')
392 for i := range s {
393 w.Write(escapedStringBytes[s[i]])
394 }
395 w.WriteByte('"')
396 return nil
397 }
398 }
399
400 w.WriteByte('"')
401 w.WriteString(s)
402 w.WriteByte('"')
403 return nil
404 }
405
406 func styledPath(w *bufio.Writer, path []any) error {
407 var buf [24]byte
408
409 w.WriteString("\x1b[38;2;135;95;255mjson\x1b[0m")
410
411 for _, v := range path {
412 switch v := v.(type) {
413 case int:
414 w.WriteString("\x1b[38;2;168;168;168m[")
415 w.WriteString("\x1b[38;2;0;135;95m")
416 w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
417 w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
418
419 case string:
420 if !needsEscaping(v) {
421 w.WriteString("\x1b[38;2;168;168;168m.")
422 w.WriteString("\x1b[38;2;135;95;255m")
423 w.WriteString(v)
424 w.WriteString("\x1b[0m")
425 continue
426 }
427
428 w.WriteString("\x1b[38;2;168;168;168m[")
429 styledString(w, v)
430 w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
431 }
432 }
433
434 w.WriteString(" \x1b[38;2;168;168;168m=\x1b[0m ")
435 return nil
436 }
437
438 func styledNull(w *bufio.Writer) error {
439 w.WriteString("\x1b[38;2;168;168;168m")
440 w.WriteString(`null`)
441 w.WriteString("\x1b[0m")
442 return nil
443 }
444
445 func styledBool(w *bufio.Writer, b bool) error {
446 if b {
447 w.WriteString("\x1b[38;2;95;175;215mtrue\x1b[0m")
448 } else {
449 w.WriteString("\x1b[38;2;95;175;215mfalse\x1b[0m")
450 }
451 return nil
452 }
453
454 func styledNumber(w *bufio.Writer, n json.Number) error {
455 w.WriteString("\x1b[38;2;0;135;95m")
456 w.WriteString(n.String())
457 w.WriteString("\x1b[0m")
458 return nil
459 }
460
461 func styledString(w *bufio.Writer, s string) error {
462 for _, r := range s {
463 if r > 127 {
464 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
465 b, err := json.Marshal(s)
466 if err != nil {
467 return err
468 }
469 w.Write(b[1 : len(b)-1])
470 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
471 return nil
472 }
473
474 if r < ' ' || r == '"' || r == '\\' {
475 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
476 for i := range s {
477 w.Write(escapedStringBytes[s[i]])
478 }
479 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
480 return nil
481 }
482 }
483
484 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
485 w.WriteString(s)
486 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
487 return nil
488 }
489
490 func needsEscaping(s string) bool {
491 if len(s) == 0 {
492 return true
493 }
494
495 for _, r := range s {
496 if r < ' ' || r > '~' {
497 return true
498 }
499
500 switch r {
501 case '"', '\'', '\\':
502 return true
503 }
504 }
505
506 return false
507 }
508
509 func endLine(w *bufio.Writer) error {
510 w.WriteByte(';')
511 if err := w.WriteByte('\n'); err == nil {
512 return nil
513 }
514 return io.EOF
515 }
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(err.Error())
43 os.Stderr.WriteString("\n")
44 os.Exit(1)
45 return
46 }
47 }
48
49 func run(cfg config) error {
50 // f, _ := os.Create(`nh.prof`)
51 // defer f.Close()
52 // pprof.StartCPUProfile(f)
53 // defer pprof.StopCPUProfile()
54
55 w := bufio.NewWriterSize(os.Stdout, 16*1024)
56 defer w.Flush()
57
58 // with no filenames given, handle stdin and quit
59 if len(cfg.Filenames) == 0 {
60 return handle(w, os.Stdin, `<stdin>`, -1, cfg)
61 }
62
63 // show all files given
64 for i, fname := range cfg.Filenames {
65 if i > 0 {
66 w.WriteString("\n")
67 w.WriteString("\n")
68 }
69
70 err := handleFile(w, fname, cfg)
71 if err != nil {
72 return err
73 }
74 }
75
76 return nil
77 }
78
79 // handleFile is like handleReader, except it also shows file-related info
80 func handleFile(w *bufio.Writer, fname string, cfg config) error {
81 f, err := os.Open(fname)
82 if err != nil {
83 return err
84 }
85 defer f.Close()
86
87 stat, err := f.Stat()
88 if err != nil {
89 return handle(w, f, fname, -1, cfg)
90 }
91
92 fsize := int(stat.Size())
93 return handle(w, f, fname, fsize, cfg)
94 }
95
96 // handle shows some messages related to the input and the cmd-line options
97 // used, and then follows them by the hexadecimal byte-view
98 func handle(w *bufio.Writer, r io.Reader, name string, size int, cfg config) error {
99 skip(r, cfg.Skip)
100 if cfg.MaxBytes > 0 {
101 r = io.LimitReader(r, int64(cfg.MaxBytes))
102 }
103
104 // finish config setup based on the filesize, if a valid one was given
105 if cfg.OffsetCounterWidth < 1 {
106 if size < 1 {
107 cfg.OffsetCounterWidth = defaultOffsetCounterWidth
108 } else {
109 w := math.Log10(float64(size))
110 w = math.Max(math.Ceil(w), 1)
111 cfg.OffsetCounterWidth = uint(w)
112 }
113 }
114
115 switch cfg.To {
116 case plainOutput:
117 writeMetaPlain(w, name, size, cfg)
118 // when done, emit a new line in case only part of the last line is
119 // shown, which means no newline was emitted for it
120 defer w.WriteString("\n")
121 return render(w, r, cfg, writeBufferPlain)
122
123 case ansiOutput:
124 writeMetaANSI(w, name, size, cfg)
125 // when done, emit a new line in case only part of the last line is
126 // shown, which means no newline was emitted for it
127 defer w.WriteString("\x1b[0m\n")
128 return render(w, r, cfg, writeBufferANSI)
129
130 default:
131 const fs = `unsupported output format %q`
132 return fmt.Errorf(fs, cfg.To)
133 }
134 }
135
136 // skip ignores n bytes from the reader given
137 func skip(r io.Reader, n int) {
138 if n < 1 {
139 return
140 }
141
142 // use func Seek for input files, except for stdin, which you can't seek
143 if f, ok := r.(*os.File); ok && r != os.Stdin {
144 f.Seek(int64(n), io.SeekCurrent)
145 return
146 }
147 io.CopyN(io.Discard, r, int64(n))
148 }
149
150 // renderer is the type for the hex-view render funcs
151 type renderer func(rc rendererConfig, first, second []byte) error
152
153 // render reads all input and shows the hexadecimal byte-view for the input
154 // data via the rendering callback given
155 func render(w *bufio.Writer, r io.Reader, cfg config, fn renderer) error {
156 if cfg.PerLine < 1 {
157 cfg.PerLine = 16
158 }
159
160 rc := rendererConfig{
161 out: w,
162 offset: uint(cfg.Skip),
163 chunks: 0,
164 perLine: uint(cfg.PerLine),
165 ruler: cfg.Ruler,
166
167 offsetWidth: cfg.OffsetCounterWidth,
168 showOffsets: cfg.ShowOffsets,
169 showASCII: cfg.ShowASCII,
170 }
171
172 // calling func Read directly can sometimes result in chunks shorter
173 // than the max chunk-size, even when there are plenty of bytes yet
174 // to read; to avoid that, use a buffered-reader to explicitly fill
175 // a slice instead
176 br := bufio.NewReader(r)
177
178 // to show ASCII up to 1 full chunk ahead, 2 chunks are needed
179 cur := make([]byte, 0, cfg.PerLine)
180 ahead := make([]byte, 0, cfg.PerLine)
181
182 // the ASCII-panel's wide output requires staying 1 step/chunk behind,
183 // so to speak
184 cur, err := fillChunk(cur[:0], cfg.PerLine, br)
185 if len(cur) == 0 {
186 if err == io.EOF {
187 err = nil
188 }
189 return err
190 }
191
192 for {
193 ahead, err := fillChunk(ahead[:0], cfg.PerLine, br)
194 if err != nil && err != io.EOF {
195 return err
196 }
197
198 if len(ahead) == 0 {
199 // done, maybe except for an extra line of output
200 break
201 }
202
203 // show the byte-chunk on its own output line
204 err = fn(rc, cur, ahead)
205 if err != nil {
206 // probably a pipe was closed
207 return nil
208 }
209
210 rc.chunks++
211 rc.offset += uint(len(cur))
212 cur = cur[:copy(cur, ahead)]
213 }
214
215 // don't forget the last output line
216 if len(cur) > 0 {
217 return fn(rc, cur, nil)
218 }
219 return nil
220 }
221
222 // fillChunk tries to read the number of bytes given, appending them to the
223 // byte-slice given; this func returns an EOF error only when no bytes are
224 // read, which somewhat simplifies error-handling for the func caller
225 func fillChunk(chunk []byte, n int, br *bufio.Reader) ([]byte, error) {
226 // read buffered-bytes up to the max chunk-size
227 for i := 0; i < n; i++ {
228 b, err := br.ReadByte()
229 if err == nil {
230 chunk = append(chunk, b)
231 continue
232 }
233
234 if err == io.EOF && i > 0 {
235 return chunk, nil
236 }
237 return chunk, err
238 }
239
240 // got the full byte-count asked for
241 return chunk, nil
242 }
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(err.Error())
119 os.Stderr.WriteString("\n")
120 }
121
122 // writeSpaces does what it says, minimizing calls to write-like funcs
123 func writeSpaces(w *bufio.Writer, n int) {
124 const spaces = ` `
125 for n >= len(spaces) {
126 w.WriteString(spaces)
127 n -= len(spaces)
128 }
129 if n > 0 {
130 w.WriteString(spaces[:n])
131 }
132 }
133
134 func handleFile(w io.Writer, path string) error {
135 // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
136 // resp, err := http.Get(path)
137 // if err != nil {
138 // return err
139 // }
140 // defer resp.Body.Close()
141 // return niceJSON(w, resp.Body)
142 // }
143
144 f, err := os.Open(path)
145 if err != nil {
146 // on windows, file-not-found error messages may mention `CreateFile`,
147 // even when trying to open files in read-only mode
148 return errors.New(`can't open file named ` + path)
149 }
150 defer f.Close()
151
152 return niceJSON(w, f)
153 }
154
155 func niceJSON(w io.Writer, r io.Reader) error {
156 bw := bufio.NewWriter(w)
157 defer bw.Flush()
158
159 dec := json.NewDecoder(r)
160 // using string-like json.Number values instead of float64 ones avoids
161 // unneeded reformatting of numbers; reformatting parsed float64 values
162 // can potentially even drop/change decimals, causing the output not to
163 // match the input digits exactly, which is best to avoid
164 dec.UseNumber()
165
166 t, err := dec.Token()
167 if err == io.EOF {
168 return errors.New(`empty input isn't valid JSON`)
169 }
170 if err != nil {
171 return err
172 }
173
174 if err := handleToken(bw, dec, t, 0, 0); err != nil {
175 return err
176 }
177 // don't forget to end the last output line
178 bw.WriteByte('\n')
179
180 if _, err := dec.Token(); err != io.EOF {
181 return errors.New(`unexpected trailing JSON data`)
182 }
183 return nil
184 }
185
186 func handleToken(w *bufio.Writer, d *json.Decoder, t json.Token, pre, level int) error {
187 switch t := t.(type) {
188 case json.Delim:
189 switch t {
190 case json.Delim('['):
191 return handleArray(w, d, pre, level)
192
193 case json.Delim('{'):
194 return handleObject(w, d, pre, level)
195
196 default:
197 // return fmt.Errorf(`unsupported JSON delimiter %v`, t)
198 return errors.New(`unsupported JSON delimiter`)
199 }
200
201 case nil:
202 return handleNull(w, pre)
203
204 case bool:
205 return handleBoolean(w, t, pre)
206
207 case string:
208 return handleString(w, t, pre)
209
210 case json.Number:
211 return handleNumber(w, t, pre)
212
213 default:
214 // return fmt.Errorf(`unsupported token type %T`, t)
215 return errors.New(`unsupported token type`)
216 }
217 }
218
219 func handleArray(w *bufio.Writer, d *json.Decoder, pre, level int) error {
220 for i := 0; true; i++ {
221 t, err := d.Token()
222 if err == io.EOF {
223 return errors.New(`end of JSON before array was closed`)
224 }
225 if err != nil {
226 return err
227 }
228
229 if t == json.Delim(']') {
230 if i == 0 {
231 writeSpaces(w, indent*pre)
232 w.WriteString(syntaxStyle + "[]\x1b[0m")
233 } else {
234 w.WriteString("\n")
235 writeSpaces(w, indent*level)
236 w.WriteString(syntaxStyle + "]\x1b[0m")
237 }
238 return nil
239 }
240
241 if i == 0 {
242 writeSpaces(w, indent*pre)
243 w.WriteString(syntaxStyle + "[\x1b[0m\n")
244 } else {
245 // this is a good spot to check for early-quit opportunities
246 w.WriteString(syntaxStyle + ",\x1b[0m\n")
247 if err := w.Flush(); err != nil {
248 // a write error may be the consequence of stdout being closed,
249 // perhaps by another app along a pipe
250 return io.EOF
251 }
252 }
253
254 if err := handleToken(w, d, t, level+1, level+1); err != nil {
255 return err
256 }
257 }
258
259 // make the compiler happy
260 return nil
261 }
262
263 func handleBoolean(w *bufio.Writer, b bool, pre int) error {
264 writeSpaces(w, indent*pre)
265 if b {
266 w.WriteString(boolStyle + "true\x1b[0m")
267 } else {
268 w.WriteString(boolStyle + "false\x1b[0m")
269 }
270 return nil
271 }
272
273 func handleKey(w *bufio.Writer, s string, pre int) error {
274 writeSpaces(w, indent*pre)
275 w.WriteString(syntaxStyle + "\"\x1b[0m" + keyStyle)
276 w.WriteString(s)
277 w.WriteString(syntaxStyle + "\":\x1b[0m ")
278 return nil
279 }
280
281 func handleNull(w *bufio.Writer, pre int) error {
282 writeSpaces(w, indent*pre)
283 w.WriteString(nullStyle + "null\x1b[0m")
284 return nil
285 }
286
287 // func handleNumber(w *bufio.Writer, n json.Number, pre int) error {
288 // writeSpaces(w, indent*pre)
289 // w.WriteString(numberStyle)
290 // w.WriteString(n.String())
291 // w.WriteString("\x1b[0m")
292 // return nil
293 // }
294
295 func handleNumber(w *bufio.Writer, n json.Number, pre int) error {
296 writeSpaces(w, indent*pre)
297 f, _ := n.Float64()
298 if f > 0 {
299 w.WriteString(positiveNumberStyle)
300 } else if f < 0 {
301 w.WriteString(negativeNumberStyle)
302 } else {
303 w.WriteString(zeroNumberStyle)
304 }
305 w.WriteString(n.String())
306 w.WriteString("\x1b[0m")
307 return nil
308 }
309
310 func handleObject(w *bufio.Writer, d *json.Decoder, pre, level int) error {
311 for i := 0; true; i++ {
312 t, err := d.Token()
313 if err == io.EOF {
314 return errors.New(`end of JSON before object was closed`)
315 }
316 if err != nil {
317 return err
318 }
319
320 if t == json.Delim('}') {
321 if i == 0 {
322 writeSpaces(w, indent*pre)
323 w.WriteString(syntaxStyle + "{}\x1b[0m")
324 } else {
325 w.WriteString("\n")
326 writeSpaces(w, indent*level)
327 w.WriteString(syntaxStyle + "}\x1b[0m")
328 }
329 return nil
330 }
331
332 if i == 0 {
333 writeSpaces(w, indent*pre)
334 w.WriteString(syntaxStyle + "{\x1b[0m\n")
335 } else {
336 // this is a good spot to check for early-quit opportunities
337 w.WriteString(syntaxStyle + ",\x1b[0m\n")
338 if err := w.Flush(); err != nil {
339 // a write error may be the consequence of stdout being closed,
340 // perhaps by another app along a pipe
341 return io.EOF
342 }
343 }
344
345 // the stdlib's JSON parser is supposed to complain about non-string
346 // keys anyway, but make sure just in case
347 k, ok := t.(string)
348 if !ok {
349 return errors.New(`expected key to be a string`)
350 }
351 if err := handleKey(w, k, level+1); err != nil {
352 return err
353 }
354
355 // handle value
356 t, err = d.Token()
357 if err != nil {
358 return err
359 }
360 if err := handleToken(w, d, t, 0, level+1); err != nil {
361 return err
362 }
363 }
364
365 // make the compiler happy
366 return nil
367 }
368
369 func needsEscaping(s string) bool {
370 for _, r := range s {
371 switch r {
372 case '"', '\\', '\t', '\r', '\n':
373 return true
374 }
375 }
376 return false
377 }
378
379 func handleString(w *bufio.Writer, s string, pre int) error {
380 writeSpaces(w, indent*pre)
381 w.WriteString(syntaxStyle + "\"\x1b[0m" + stringStyle)
382 if !needsEscaping(s) {
383 w.WriteString(s)
384 } else {
385 escapeString(w, s)
386 }
387 w.WriteString(syntaxStyle + "\"\x1b[0m")
388 return nil
389 }
390
391 func escapeString(w *bufio.Writer, s string) {
392 for _, r := range s {
393 switch r {
394 case '"', '\\':
395 w.WriteByte('\\')
396 w.WriteRune(r)
397 case '\t':
398 w.WriteByte('\\')
399 w.WriteByte('t')
400 case '\r':
401 w.WriteByte('\\')
402 w.WriteByte('r')
403 case '\n':
404 w.WriteByte('\\')
405 w.WriteByte('n')
406 default:
407 w.WriteRune(r)
408 }
409 }
410 }
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("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(err.Error())
90 os.Stderr.WriteString("\n")
91 os.Exit(1)
92 return
93 }
94 }
95
96 func run(paths []string, count int) error {
97 w := bufio.NewWriterSize(os.Stdout, 32*1024)
98 defer w.Flush()
99
100 for _, path := range paths {
101 if err := handleFile(w, path, &count); err != nil {
102 return err
103 }
104 }
105
106 if len(paths) == 0 {
107 if err := nl(w, os.Stdin, &count); err != nil {
108 return err
109 }
110 }
111 return nil
112 }
113
114 func handleFile(w *bufio.Writer, path string, count *int) error {
115 f, err := os.Open(path)
116 if err != nil {
117 return err
118 }
119 defer f.Close()
120 return nl(w, f, count)
121 }
122
123 func nl(w *bufio.Writer, r io.Reader, count *int) error {
124 const gb = 1024 * 1024 * 1024
125 sc := bufio.NewScanner(r)
126 sc.Buffer(nil, 8*gb)
127
128 const spaces = ` `
129 var buf [24]byte
130
131 for sc.Scan() {
132 s := sc.Bytes()
133
134 // only number non-empty lines, just like the standard `nl` command
135 if len(s) > 0 {
136 n := strconv.AppendInt(buf[:0], int64(*count), 10)
137
138 // right-align the line-count
139 if len(n) < len(spaces) {
140 w.WriteString(spaces[:len(spaces)-len(n)])
141 }
142 w.Write(n)
143
144 // separate the line count and the line with a few spaces
145 w.WriteString(spaces[:2])
146 }
147
148 w.Write(s)
149 if err := w.WriteByte('\n'); err != nil {
150 return io.EOF
151 }
152
153 if len(s) > 0 {
154 *count++
155 }
156 }
157
158 return sc.Err()
159 }
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(`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(err.Error())
138 os.Stderr.WriteString("\n")
139 os.Exit(1)
140 return
141 }
142 }
143
144 func run(w io.Writer, args []string, cfg config) error {
145 bw := bufio.NewWriter(w)
146 defer bw.Flush()
147
148 if len(args) == 0 {
149 return restyle(bw, os.Stdin, cfg)
150 }
151
152 for _, name := range args {
153 if err := handleFile(bw, name, cfg); err != nil {
154 return err
155 }
156 }
157 return nil
158 }
159
160 func handleFile(w *bufio.Writer, name string, cfg config) error {
161 if name == `` || name == `-` {
162 return restyle(w, os.Stdin, cfg)
163 }
164
165 f, err := os.Open(name)
166 if err != nil {
167 return errors.New(`can't read from file named "` + name + `"`)
168 }
169 defer f.Close()
170
171 return restyle(w, f, cfg)
172 }
173
174 func restyle(w *bufio.Writer, r io.Reader, cfg config) error {
175 const gb = 1024 * 1024 * 1024
176 sc := bufio.NewScanner(r)
177 sc.Buffer(nil, 8*gb)
178
179 for i := 0; sc.Scan(); i++ {
180 s := sc.Bytes()
181 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
182 s = s[3:]
183 }
184
185 restyleLine(w, s, cfg.style)
186 if w.WriteByte('\n') != nil {
187 return io.EOF
188 }
189
190 if !cfg.live {
191 continue
192 }
193
194 if err := w.Flush(); err != nil {
195 // a write error may be the consequence of stdout being closed,
196 // perhaps by another app along a pipe
197 return io.EOF
198 }
199 }
200 return sc.Err()
201 }
202
203 func lookupStyle(name string) (style string, ok bool) {
204 if alias, ok := styleAliases[name]; ok {
205 name = alias
206 }
207
208 style, ok = styles[name]
209 return style, ok
210 }
211
212 var styleAliases = map[string]string{
213 `b`: `blue`,
214 `g`: `green`,
215 `m`: `magenta`,
216 `o`: `orange`,
217 `p`: `purple`,
218 `r`: `red`,
219 `u`: `underline`,
220
221 `bolded`: `bold`,
222 `h`: `inverse`,
223 `hi`: `inverse`,
224 `highlight`: `inverse`,
225 `highlighted`: `inverse`,
226 `hilite`: `inverse`,
227 `hilited`: `inverse`,
228 `inv`: `inverse`,
229 `invert`: `inverse`,
230 `inverted`: `inverse`,
231 `underlined`: `underline`,
232
233 `bb`: `blueback`,
234 `bg`: `greenback`,
235 `bm`: `magentaback`,
236 `bo`: `orangeback`,
237 `bp`: `purpleback`,
238 `br`: `redback`,
239
240 `gb`: `greenback`,
241 `mb`: `magentaback`,
242 `ob`: `orangeback`,
243 `pb`: `purpleback`,
244 `rb`: `redback`,
245
246 `bblue`: `blueback`,
247 `bgray`: `grayback`,
248 `bgreen`: `greenback`,
249 `bmagenta`: `magentaback`,
250 `borange`: `orangeback`,
251 `bpurple`: `purpleback`,
252 `bred`: `redback`,
253
254 `backblue`: `blueback`,
255 `backgray`: `grayback`,
256 `backgreen`: `greenback`,
257 `backmagenta`: `magentaback`,
258 `backorange`: `orangeback`,
259 `backpurple`: `purpleback`,
260 `backred`: `redback`,
261 }
262
263 // styles turns style-names into the ANSI-code sequences used for the
264 // alternate groups of digits
265 var styles = map[string]string{
266 `blue`: "\x1b[38;2;0;95;215m",
267 `bold`: "\x1b[1m",
268 `gray`: "\x1b[38;2;168;168;168m",
269 `green`: "\x1b[38;2;0;135;95m",
270 `inverse`: "\x1b[7m",
271 `magenta`: "\x1b[38;2;215;0;255m",
272 `orange`: "\x1b[38;2;215;95;0m",
273 `plain`: "\x1b[0m",
274 `red`: "\x1b[38;2;204;0;0m",
275 `underline`: "\x1b[4m",
276
277 // `blue`: "\x1b[38;5;26m",
278 // `bold`: "\x1b[1m",
279 // `gray`: "\x1b[38;5;248m",
280 // `green`: "\x1b[38;5;29m",
281 // `inverse`: "\x1b[7m",
282 // `magenta`: "\x1b[38;5;99m",
283 // `orange`: "\x1b[38;5;166m",
284 // `plain`: "\x1b[0m",
285 // `red`: "\x1b[31m",
286 // `underline`: "\x1b[4m",
287
288 `blueback`: "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
289 `grayback`: "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
290 `greenback`: "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
291 `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
292 `orangeback`: "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
293 `purpleback`: "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
294 `redback`: "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
295 }
296
297 // restyleLine renders the line given, using ANSI-styles to make any long
298 // numbers in it more legible; this func doesn't emit a line-feed, which
299 // is up to its caller
300 func restyleLine(w *bufio.Writer, line []byte, style string) {
301 for len(line) > 0 {
302 i := indexDigit(line)
303 if i < 0 {
304 // no (more) digits to style for sure
305 w.Write(line)
306 return
307 }
308
309 // emit line before current digit-run
310 w.Write(line[:i])
311 // advance to the start of the current digit-run
312 line = line[i:]
313
314 // see where the digit-run ends
315 j := indexNonDigit(line)
316 if j < 0 {
317 // the digit-run goes until the end
318 restyleDigits(w, line, style)
319 return
320 }
321
322 // emit styled digit-run
323 restyleDigits(w, line[:j], style)
324 // skip right past the end of the digit-run
325 line = line[j:]
326 }
327 }
328
329 // indexDigit finds the index of the first digit in a string, or -1 when the
330 // string has no decimal digits
331 func indexDigit(s []byte) int {
332 for i := 0; i < len(s); i++ {
333 switch s[i] {
334 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
335 return i
336 }
337 }
338
339 // empty slice, or a slice without any digits
340 return -1
341 }
342
343 // indexNonDigit finds the index of the first non-digit in a string, or -1
344 // when the string is all decimal digits
345 func indexNonDigit(s []byte) int {
346 for i := 0; i < len(s); i++ {
347 switch s[i] {
348 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
349 continue
350 default:
351 return i
352 }
353 }
354
355 // empty slice, or a slice which only has digits
356 return -1
357 }
358
359 // restyleDigits renders a run of digits as alternating styled/unstyled runs
360 // of 3 digits, which greatly improves readability, and is the only purpose
361 // of this app; string is assumed to be all decimal digits
362 func restyleDigits(w *bufio.Writer, digits []byte, altStyle string) {
363 if len(digits) < 4 {
364 // digit sequence is short, so emit it as is
365 w.Write(digits)
366 return
367 }
368
369 // separate leading 0..2 digits which don't align with the 3-digit groups
370 i := len(digits) % 3
371 // emit leading digits unstyled, if there are any
372 w.Write(digits[:i])
373 // the rest is guaranteed to have a length which is a multiple of 3
374 digits = digits[i:]
375
376 // start by styling, unless there were no leading digits
377 style := i != 0
378
379 for len(digits) > 0 {
380 if style {
381 w.WriteString(altStyle)
382 w.Write(digits[:3])
383 w.Write([]byte{'\x1b', '[', '0', 'm'})
384 } else {
385 w.Write(digits[:3])
386 }
387
388 // advance to the next triple: the start of this func is supposed
389 // to guarantee this step always works
390 digits = digits[3:]
391
392 // alternate between styled and unstyled 3-digit groups
393 style = !style
394 }
395 }
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 "errors"
31 "io"
32 "os"
33 "strings"
34 "time"
35 )
36
37 const info = `
38 nts [options...] [file...]
39
40 Nice TimeStamp emits each input line, starting it with an ANSI-style date/time
41 string and a tab.
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 options := true
60 if len(args) > 0 && args[0] == `--` {
61 options = false
62 args = args[1:]
63 }
64
65 style := "\x1b[48;2;218;218;218m\x1b[38;2;0;95;153m"
66
67 // if the first argument is 1 or 2 dashes followed by a supported
68 // style-name, change the style used
69 if options && len(args) > 0 && strings.HasPrefix(args[0], `-`) {
70 name := args[0]
71 name = strings.TrimPrefix(name, `-`)
72 name = strings.TrimPrefix(name, `-`)
73 args = args[1:]
74
75 // check if the `dedashed` argument is a supported style-name
76 if s, ok := lookupStyle(name); ok {
77 style = s
78 } else {
79 os.Stderr.WriteString(`invalid style name `)
80 os.Stderr.WriteString(name)
81 os.Stderr.WriteString("\n")
82 os.Exit(1)
83 return
84 }
85 }
86
87 if err := run(os.Stdout, args, style); err != nil && err != io.EOF {
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, style string) error {
96 bw := bufio.NewWriter(w)
97 defer bw.Flush()
98
99 if len(args) == 0 {
100 return timestamp(bw, os.Stdin, style)
101 }
102
103 for _, name := range args {
104 if err := handleFile(bw, name, style); err != nil {
105 return err
106 }
107 }
108 return nil
109 }
110
111 func handleFile(w *bufio.Writer, name string, style string) error {
112 if name == `` || name == `-` {
113 return timestamp(w, os.Stdin, style)
114 }
115
116 f, err := os.Open(name)
117 if err != nil {
118 return errors.New(`can't read from file named "` + name + `"`)
119 }
120 defer f.Close()
121
122 return timestamp(w, f, style)
123 }
124
125 func timestamp(w *bufio.Writer, r io.Reader, style string) error {
126 const gb = 1024 * 1024 * 1024
127 sc := bufio.NewScanner(r)
128 sc.Buffer(nil, 8*gb)
129
130 var buf [64]byte
131
132 for i := 0; sc.Scan(); i++ {
133 s := sc.Bytes()
134 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
135 s = s[3:]
136 }
137
138 if style == `` {
139 w.Write(time.Now().AppendFormat(buf[:0], `2006-01-02 15:04:05`))
140 w.WriteByte('\t')
141 } else {
142 w.WriteString(style)
143 w.Write(time.Now().AppendFormat(buf[:0], `2006-01-02 15:04:05`))
144 w.WriteString("\x1b[0m\t")
145 }
146 w.Write(s)
147 w.WriteByte('\n')
148
149 if err := w.Flush(); err != nil {
150 // a write error may be the consequence of stdout being closed,
151 // perhaps by another app along a pipe
152 return io.EOF
153 }
154 }
155 return sc.Err()
156 }
157
158 func lookupStyle(name string) (style string, ok bool) {
159 if alias, ok := styleAliases[name]; ok {
160 name = alias
161 }
162
163 style, ok = styles[name]
164 return style, ok
165 }
166
167 var styleAliases = map[string]string{
168 `b`: `blue`,
169 `g`: `green`,
170 `m`: `magenta`,
171 `o`: `orange`,
172 `p`: `purple`,
173 `r`: `red`,
174 `u`: `underline`,
175
176 `bolded`: `bold`,
177 `h`: `inverse`,
178 `hi`: `inverse`,
179 `highlight`: `inverse`,
180 `highlighted`: `inverse`,
181 `hilite`: `inverse`,
182 `hilited`: `inverse`,
183 `inv`: `inverse`,
184 `invert`: `inverse`,
185 `inverted`: `inverse`,
186 `underlined`: `underline`,
187
188 `bb`: `blueback`,
189 `bg`: `greenback`,
190 `bm`: `magentaback`,
191 `bo`: `orangeback`,
192 `bp`: `purpleback`,
193 `br`: `redback`,
194
195 `gb`: `greenback`,
196 `mb`: `magentaback`,
197 `ob`: `orangeback`,
198 `pb`: `purpleback`,
199 `rb`: `redback`,
200
201 `bblue`: `blueback`,
202 `bgray`: `grayback`,
203 `bgreen`: `greenback`,
204 `bmagenta`: `magentaback`,
205 `borange`: `orangeback`,
206 `bpurple`: `purpleback`,
207 `bred`: `redback`,
208
209 `backblue`: `blueback`,
210 `backgray`: `grayback`,
211 `backgreen`: `greenback`,
212 `backmagenta`: `magentaback`,
213 `backorange`: `orangeback`,
214 `backpurple`: `purpleback`,
215 `backred`: `redback`,
216 }
217
218 // styles turns style-names into the ANSI-code sequences used for the
219 // alternate groups of digits
220 var styles = map[string]string{
221 `blue`: "\x1b[38;2;0;95;215m",
222 `bold`: "\x1b[1m",
223 `gray`: "\x1b[38;2;168;168;168m",
224 `green`: "\x1b[38;2;0;135;95m",
225 `inverse`: "\x1b[7m",
226 `magenta`: "\x1b[38;2;215;0;255m",
227 `none`: ``,
228 `orange`: "\x1b[38;2;215;95;0m",
229 `plain`: ``,
230 `red`: "\x1b[38;2;204;0;0m",
231 `underline`: "\x1b[4m",
232 `unstyled`: ``,
233
234 // `blue`: "\x1b[38;5;26m",
235 // `bold`: "\x1b[1m",
236 // `gray`: "\x1b[38;5;248m",
237 // `green`: "\x1b[38;5;29m",
238 // `inverse`: "\x1b[7m",
239 // `magenta`: "\x1b[38;5;99m",
240 // `orange`: "\x1b[38;5;166m",
241 // `plain`: "\x1b[0m",
242 // `red`: "\x1b[31m",
243 // `underline`: "\x1b[4m",
244
245 `blueback`: "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
246 `grayback`: "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
247 `greenback`: "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
248 `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
249 `orangeback`: "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
250 `purpleback`: "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
251 `redback`: "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
252 }
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(err.Error())
137 os.Stderr.WriteString("\n")
138 os.Exit(1)
139 return
140 }
141 }
142
143 type context struct {
144 stack [][]float64
145 random *rand.Rand
146 }
147
148 func pac(w io.Writer, args []string) error {
149 var ctx context
150 ctx.stack = make([][]float64, 0, 64)
151 reset(&ctx)
152
153 if err := run(args, &ctx); err != nil {
154 return err
155 }
156
157 // if len(ctx.stack) > 1 {
158 // os.Stderr.WriteString("multiple results left on the stack\n")
159 // }
160 if len(ctx.stack) == 0 {
161 return errors.New(`no results left on the stack`)
162 }
163
164 show(w, &ctx)
165 return nil
166 }
167
168 func show(w io.Writer, ctx *context) {
169 if len(ctx.stack) == 0 {
170 return
171 }
172
173 bw := bufio.NewWriter(w)
174 defer bw.Flush()
175
176 var buf [24]byte
177 for i, f := range ctx.stack[len(ctx.stack)-1] {
178 if i > 0 {
179 if err := bw.WriteByte(' '); err != nil {
180 break
181 }
182 }
183 bw.Write(strconv.AppendFloat(buf[:0], f, 'f', -1, 64))
184 }
185 bw.WriteByte('\n')
186 }
187
188 func debug(w io.StringWriter, ctx *context) {
189 for i, v := range ctx.stack {
190 w.WriteString("\t")
191 w.WriteString(strconv.Itoa(i))
192 w.WriteString(":")
193 for _, f := range v {
194 w.WriteString(" ")
195 w.WriteString(strconv.FormatFloat(f, 'f', -1, 64))
196 }
197
198 w.WriteString("\n")
199 }
200
201 w.WriteString("\n")
202 }
203
204 func run(args []string, ctx *context) error {
205 for len(args) > 0 {
206 s := args[0]
207 args = args[1:]
208
209 for len(s) > 0 {
210 for len(s) > 0 && s[0] == ' ' {
211 s = s[1:]
212 }
213 if len(s) == 0 {
214 break
215 }
216
217 var cmd string
218 if i := strings.IndexByte(s, ' '); i >= 0 {
219 cmd = s[:i]
220 } else {
221 cmd = s
222 }
223 s = s[len(cmd):]
224
225 if f, err := strconv.ParseFloat(cmd, 64); err == nil {
226 ctx.stack = append(ctx.stack, []float64{f})
227 continue
228 }
229
230 if cmd == `[` {
231 rest, err := appendArray(ctx, args)
232 if err != nil {
233 return err
234 }
235 args = rest
236 continue
237 }
238
239 if cmd == `.` {
240 debug(os.Stderr, ctx)
241 continue
242 }
243
244 if f, ok := funcs[cmd]; ok {
245 if err := call(ctx, f, cmd); err != nil {
246 return err
247 }
248 continue
249 }
250
251 return errors.New(`unknown command ` + cmd)
252 }
253 }
254
255 return nil
256 }
257
258 func appendArray(ctx *context, args []string) ([]string, error) {
259 end := -1
260 for i, s := range args {
261 if s == `]` {
262 end = i
263 break
264 }
265 }
266
267 if end < 0 {
268 return args, errors.New(`missing the ']' to close array`)
269 }
270
271 items := args[:end]
272 rest := args[end+1:]
273
274 var sub context
275 err := run(items, &sub)
276 flatten(&sub)
277 if len(sub.stack) > 0 {
278 ctx.stack = append(ctx.stack, sub.stack[len(sub.stack)-1])
279 }
280 return rest, err
281 }
282
283 func call(ctx *context, f any, name string) error {
284 var err error
285
286 switch f := f.(type) {
287 case float64:
288 ctx.stack = append(ctx.stack, []float64{f})
289 case func(ctx *context) error:
290 err = f(ctx)
291 case func(x float64) float64:
292 err = loop(ctx, f)
293 case func(x, y float64) float64:
294 err = parallel(ctx, f)
295 case func(x, y float64) (float64, float64):
296 err = parallel2(ctx, f)
297 case func(ctx *context, n int) error:
298 err = repeat(ctx, f)
299 default:
300 err = errors.New(`unsupported function type`)
301 }
302
303 if err == io.EOF {
304 return io.EOF
305 }
306 if err != nil {
307 return errors.New(name + `: ` + err.Error())
308 }
309 return nil
310 }
311
312 func cloneArray(x []float64) []float64 {
313 clone := make([]float64, 0, len(x))
314 for _, f := range x {
315 clone = append(clone, f)
316 }
317 return clone
318 }
319
320 func debool(x bool) float64 {
321 if x {
322 return 1
323 } else {
324 return 0
325 }
326 }
327
328 var funcs = map[string]any{
329 `+`: add,
330 `-`: sub,
331 `*`: mul,
332 `/`: div,
333 `%`: math.Mod,
334 `^`: math.Pow,
335 `**`: math.Pow,
336
337 `add`: add,
338 `sub`: sub,
339 `mul`: mul,
340 `div`: div,
341 `mod`: math.Mod,
342 `modulus`: math.Mod,
343 `pow`: math.Pow,
344 `power`: math.Pow,
345
346 `m`: mul,
347
348 `=`: eql,
349 `==`: eql,
350 `!=`: neq,
351 `<>`: neq,
352 `<`: lt,
353 `<=`: lte,
354 `>`: gt,
355 `>=`: gte,
356
357 `eql`: eql,
358 `neq`: neq,
359 `lt`: lt,
360 `lte`: lte,
361 `gt`: gt,
362 `gte`: gte,
363
364 `a`: anchor,
365 `abs`: math.Abs,
366 `anchor`: anchor,
367 `ceil`: math.Ceil,
368 `ceiling`: math.Ceil,
369 `cos`: math.Cos,
370 `cosh`: math.Cosh,
371 `cosine`: math.Cos,
372 `div2`: div2,
373 `down`: math.Floor,
374 `exp`: math.Exp,
375 `floor`: math.Floor,
376 `hypot`: math.Hypot,
377 `hypotenuse`: math.Hypot,
378 `hypothenuse`: math.Hypot,
379 `mid`: mid,
380 `middle`: mid,
381 `neg`: negate,
382 `negate`: negate,
383 `ln`: math.Log,
384 `log`: math.Log,
385 `log10`: math.Log10,
386 `log2`: math.Log2,
387 `pow10`: pow10,
388 `pow2`: pow2,
389 `round`: math.Round,
390 `sgn`: sign,
391 `sign`: sign,
392 `sin`: math.Sin,
393 `sine`: math.Sin,
394 `sinh`: math.Sinh,
395 `tan`: math.Tan,
396 `tangent`: math.Tan,
397 `tanh`: math.Tanh,
398 `up`: math.Ceil,
399
400 `b`: bail,
401 `bail`: bail,
402 `bye`: bye,
403 `c`: empty,
404 `clear`: empty,
405 `d`: dup,
406 `dump`: dump,
407 `dup`: dup,
408 `dup2`: dup2,
409 `e`: math.E,
410 `empty`: empty,
411 `f`: flatten,
412 `flat`: flatten,
413 `flatten`: flatten,
414 `fuzz`: fuzz,
415 `h`: help,
416 `help`: help,
417 `i`: iota,
418 `inv`: invert,
419 `inverse`: invert,
420 `invert`: invert,
421 `iota`: iota,
422 `l`: toplen,
423 `len`: toplen,
424 `length`: toplen,
425 `levels`: levels,
426 `over`: over,
427 `p`: toprint,
428 `pi`: math.Pi,
429 `print`: toprint,
430 `pop`: pop,
431 `q`: bye,
432 `quit`: bye,
433 `rand`: random,
434 `random`: random,
435 `rnd`: random,
436 `s`: swap2,
437 `shift`: shift,
438 `spread`: spread,
439 `swap`: swap2,
440 `swap2`: swap2,
441 `tau`: 2 * math.Pi,
442 `top`: top,
443 `u`: unit,
444 `unit`: unit,
445 }
446
447 func invert(x float64) float64 { return 1 / x }
448 func negate(x float64) float64 { return -x }
449
450 func add(x, y float64) float64 { return x + y }
451 func sub(x, y float64) float64 { return x - y }
452 func mid(x, y float64) float64 { return 0.5 * (x + y) }
453 func mul(x, y float64) float64 { return x * y }
454 func div(x, y float64) float64 { return x / y }
455 func pow10(x float64) float64 { return math.Pow(10, x) }
456 func pow2(x float64) float64 { return math.Pow(2, x) }
457
458 func div2(x, y float64) (float64, float64) { return x / y, y / x }
459
460 func eql(x, y float64) float64 { return debool(x == y) }
461 func gt(x, y float64) float64 { return debool(x > y) }
462 func gte(x, y float64) float64 { return debool(x >= y) }
463 func lt(x, y float64) float64 { return debool(x < y) }
464 func lte(x, y float64) float64 { return debool(x <= y) }
465 func neq(x, y float64) float64 { return debool(x != y) }
466
467 func anchor(x float64) float64 {
468 const eps = 1e6
469 if i, f := math.Modf(x); -eps <= f && f <= +eps {
470 return float64(i)
471 }
472 return x
473 }
474
475 func sign(x float64) float64 {
476 if x > 0 {
477 return +1
478 }
479 if x < 0 {
480 return -1
481 }
482 return x
483 }
484
485 func bail(ctx *context) error {
486 dump(ctx)
487 return bye(ctx)
488 }
489
490 func bye(ctx *context) error {
491 return io.EOF
492 }
493
494 func dump(ctx *context) error {
495 if len(ctx.stack) == 0 {
496 return nil
497 }
498
499 bw := bufio.NewWriter(os.Stdout)
500 defer bw.Flush()
501
502 var buf [24]byte
503 for _, v := range ctx.stack {
504 for i, f := range v {
505 if i > 0 {
506 if err := bw.WriteByte(' '); err != nil {
507 break
508 }
509 }
510 bw.Write(strconv.AppendFloat(buf[:0], f, 'f', -1, 64))
511 }
512
513 if err := bw.WriteByte('\n'); err != nil {
514 return io.EOF
515 }
516 }
517
518 return nil
519 }
520
521 func dup(ctx *context) error {
522 if len(ctx.stack) > 0 {
523 x := ctx.stack[len(ctx.stack)-1]
524 ctx.stack = append(ctx.stack, cloneArray(x))
525 }
526 return nil
527 }
528
529 func dup2(ctx *context) error {
530 if len(ctx.stack) < 2 {
531 return needAtLeast2
532 }
533
534 x := ctx.stack[len(ctx.stack)-2]
535 y := ctx.stack[len(ctx.stack)-1]
536 ctx.stack = append(ctx.stack, cloneArray(x))
537 ctx.stack = append(ctx.stack, cloneArray(y))
538 return nil
539 }
540
541 func empty(ctx *context) error {
542 ctx.stack = ctx.stack[:0]
543 return nil
544 }
545
546 func flatten(ctx *context) error {
547 n := 0
548 for _, v := range ctx.stack {
549 n += len(v)
550 }
551
552 top := make([]float64, 0, n)
553 for _, v := range ctx.stack {
554 top = append(top, v...)
555 }
556 ctx.stack = append(ctx.stack[:0], top)
557 return nil
558 }
559
560 // fuzz tries to undo a number-rounding, turning each number at the stack-top
561 // into a 3-item slice: the original number itself, surrounded by its lowest
562 // and highest values from the range it can be rounded from
563 func fuzz(ctx *context) error {
564 if len(ctx.stack) < 1 {
565 return needAtLeast1
566 }
567
568 top := ctx.stack[len(ctx.stack)-1]
569 if len(top) == 1 {
570 f, err := fuzzValue(top[0])
571 if err != nil {
572 return err
573 }
574 ctx.stack[len(ctx.stack)-1] = f
575 }
576
577 ctx.stack = ctx.stack[:len(ctx.stack)-1]
578 for _, f := range top {
579 fuzzed, err := fuzzValue(f)
580 if err != nil {
581 return err
582 }
583 ctx.stack = append(ctx.stack, fuzzed)
584 }
585 return nil
586 }
587
588 // fuzzValue tries to undo a number's rounding, returning a slice with the
589 // value itself, surrounded by its lowest and highest values from the range
590 // it can be rounded from
591 func fuzzValue(f float64) ([]float64, error) {
592 if math.IsNaN(f) || math.IsInf(f, 0) {
593 return []float64{f, f, f}, nil
594 }
595
596 // treat round numbers differently, by `fuzzing` with/around integers
597 if math.Mod(f, 1.0) != 0.0 {
598 lo := math.Floor(f)
599 hi := math.Ceil(f)
600 return []float64{lo, f, hi}, nil
601 }
602
603 // find the smallest power of 10 to fuzz with
604 fuzzBy := 1.0
605 for fuzzBy < maxExactInt/10 {
606 next := 10 * fuzzBy
607 if math.Mod(f, next) != 0.0 {
608 break
609 }
610 fuzzBy = next
611 }
612
613 lo := fuzzBy*math.Trunc(f/fuzzBy) - fuzzBy/2
614 hi := fuzzBy*math.Ceil(f/fuzzBy) + fuzzBy/2
615 return []float64{lo, f, hi}, nil
616 }
617
618 func help(ctx *context) error {
619 if _, err := os.Stdout.WriteString(info[1:]); err != nil {
620 return io.EOF
621 }
622 return nil
623 }
624
625 func iota(ctx *context, n int) error {
626 if n < 1 {
627 return nil
628 }
629
630 top := make([]float64, n)
631 for i := range top {
632 top[i] = float64(i + 1)
633 }
634 ctx.stack = append(ctx.stack, top)
635
636 return nil
637 }
638
639 func levels(ctx *context) error {
640 l := float64(len(ctx.stack))
641 ctx.stack = append(ctx.stack, []float64{l})
642 return nil
643 }
644
645 func over(ctx *context) error {
646 if len(ctx.stack) > 0 {
647 top := ctx.stack[len(ctx.stack)-1]
648 ctx.stack = append(ctx.stack, []float64{})
649 copy(ctx.stack[1:], ctx.stack[:len(ctx.stack)-1])
650 ctx.stack[0] = top
651 }
652 return nil
653 }
654
655 func pop(ctx *context) error {
656 if len(ctx.stack) > 0 {
657 ctx.stack = ctx.stack[:len(ctx.stack)-1]
658 }
659 return nil
660 }
661
662 func random(ctx *context, n int) error {
663 if n < 1 {
664 return nil
665 }
666
667 top := make([]float64, n)
668 for i := range top {
669 top[i] = ctx.random.Float64()
670 }
671 ctx.stack = append(ctx.stack, top)
672
673 return nil
674 }
675
676 func reset(ctx *context) error {
677 ctx.stack = ctx.stack[:0]
678 ctx.random = rand.New(rand.NewSource(time.Now().UnixNano()))
679 return nil
680 }
681
682 func shift(ctx *context) error {
683 for i := 1; i < len(ctx.stack); i++ {
684 ctx.stack[i-1] = ctx.stack[i]
685 }
686
687 if len(ctx.stack) > 0 {
688 ctx.stack = ctx.stack[:len(ctx.stack)-1]
689 }
690
691 return nil
692 }
693
694 func spread(ctx *context) error {
695 prev := ctx.stack
696 next := ctx.stack[:0]
697
698 for _, v := range prev {
699 for _, f := range v {
700 next = append(next, []float64{f})
701 }
702 }
703
704 ctx.stack = next
705 return nil
706 }
707
708 func toplen(ctx *context) error {
709 if len(ctx.stack) < 1 {
710 return needAtLeast1
711 }
712
713 top := ctx.stack[len(ctx.stack)-1]
714 ctx.stack = append(ctx.stack, []float64{float64(len(top))})
715 return nil
716 }
717
718 func swap2(ctx *context) error {
719 if len(ctx.stack) < 2 {
720 return needAtLeast2
721 }
722
723 x := ctx.stack[len(ctx.stack)-2]
724 y := ctx.stack[len(ctx.stack)-1]
725 ctx.stack[len(ctx.stack)-2] = y
726 ctx.stack[len(ctx.stack)-1] = x
727 return nil
728 }
729
730 func top(ctx *context) error {
731 if len(ctx.stack) < 1 {
732 return needAtLeast1
733 }
734
735 ctx.stack = append(ctx.stack[:0], ctx.stack[len(ctx.stack)-1])
736 return nil
737 }
738
739 func toprint(ctx *context) error {
740 show(os.Stdout, ctx)
741 return nil
742 }
743
744 func unit(ctx *context, n int) error {
745 if n < 1 {
746 return nil
747 }
748
749 top := make([]float64, n)
750 for i := range top {
751 top[i] = 1
752 }
753 ctx.stack = append(ctx.stack, top)
754
755 return nil
756 }
757
758 func loop(ctx *context, f func(x float64) float64) error {
759 if len(ctx.stack) < 1 {
760 return needAtLeast1
761 }
762
763 top := ctx.stack[len(ctx.stack)-1]
764 for i, v := range top {
765 top[i] = f(v)
766 }
767 return nil
768 }
769
770 func parallel(ctx *context, f func(x, y float64) float64) error {
771 if len(ctx.stack) < 2 {
772 return needAtLeast2
773 }
774
775 l := len(ctx.stack)
776 xa := ctx.stack[l-2]
777 ya := ctx.stack[l-1]
778 ctx.stack = ctx.stack[:l-2]
779
780 if len(xa) == 0 || len(ya) == 0 {
781 return nil
782 }
783
784 var za []float64
785 if len(xa) < len(ya) {
786 za = ya
787 } else {
788 za = xa
789 }
790
791 for i := range za {
792 za[i] = f(xa[i%len(xa)], ya[i%len(ya)])
793 }
794 ctx.stack = append(ctx.stack, za)
795 return nil
796 }
797
798 func parallel2(ctx *context, f func(x, y float64) (float64, float64)) error {
799 if len(ctx.stack) < 2 {
800 return needAtLeast2
801 }
802
803 l := len(ctx.stack)
804 xa := ctx.stack[l-2]
805 ya := ctx.stack[l-1]
806 ctx.stack = ctx.stack[:l-2]
807
808 if len(xa) == 0 || len(ya) == 0 {
809 return nil
810 }
811
812 var za []float64
813 if len(xa) < len(ya) {
814 za = ya
815 } else {
816 za = xa
817 }
818
819 r1 := make([]float64, len(za))
820 r2 := make([]float64, len(za))
821 for i := range r1 {
822 x, y := f(xa[i%len(xa)], ya[i%len(ya)])
823 r1[i] = x
824 r2[i] = y
825 }
826 ctx.stack = append(ctx.stack, r1)
827 ctx.stack = append(ctx.stack, r2)
828 return nil
829 }
830
831 func repeat(ctx *context, f func(ctx *context, n int) error) error {
832 if len(ctx.stack) < 1 {
833 return needAtLeast1
834 }
835
836 counts := ctx.stack[len(ctx.stack)-1]
837 ctx.stack = ctx.stack[:len(ctx.stack)-1]
838
839 for _, n := range counts {
840 if err := f(ctx, int(n)); err != nil {
841 return err
842 }
843 }
844
845 return nil
846 }
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(err.Error())
64 os.Stderr.WriteString("\n")
65 os.Exit(1)
66 return
67 }
68 }
69
70 type paddingData struct {
71 lines []string
72 max int
73 }
74
75 func run(paths []string) error {
76 var pd paddingData
77
78 for _, p := range paths {
79 if err := handleFile(&pd, p); err != nil {
80 return err
81 }
82 }
83
84 if len(paths) == 0 {
85 if err := handleReader(&pd, os.Stdin); err != nil {
86 return err
87 }
88 }
89
90 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
91 defer bw.Flush()
92
93 for _, line := range pd.lines {
94 bw.WriteString(line)
95 writeSpaces(bw, pd.max-countWidth(line))
96 if bw.WriteByte('\n') != nil {
97 break
98 }
99 }
100 return nil
101 }
102
103 func handleFile(pd *paddingData, path string) error {
104 f, err := os.Open(path)
105 if err != nil {
106 // on windows, file-not-found error messages may mention `CreateFile`,
107 // even when trying to open files in read-only mode
108 return errors.New(`can't open file named ` + path)
109 }
110 defer f.Close()
111 return handleReader(pd, f)
112 }
113
114 func handleReader(pd *paddingData, r io.Reader) error {
115 const gb = 1024 * 1024 * 1024
116 sc := bufio.NewScanner(r)
117 sc.Buffer(nil, 8*gb)
118
119 for i := 0; sc.Scan(); i++ {
120 line := sc.Text()
121 if i == 0 && strings.HasPrefix(line, "\xef\xbb\xbf") {
122 line = line[3:]
123 }
124
125 n := countWidth(line)
126 if pd.max < n {
127 pd.max = n
128 }
129 pd.lines = append(pd.lines, line)
130 }
131
132 return sc.Err()
133 }
134
135 func countWidth(s string) int {
136 width := 0
137
138 for len(s) > 0 {
139 i, j := indexEscapeSequence(s)
140 if i < 0 {
141 break
142 }
143 if j < 0 {
144 j = len(s)
145 }
146
147 width += utf8.RuneCountInString(s[:i])
148 s = s[j:]
149 }
150
151 // count trailing/all runes in strings which don't end with ANSI-sequences
152 width += utf8.RuneCountInString(s)
153 return width
154 }
155
156 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
157 // the multi-byte sequences starting with ESC[; the result is a pair of slice
158 // indices which can be independently negative when either the start/end of
159 // a sequence isn't found; given their fairly-common use, even the hyperlink
160 // ESC]8 sequences are supported
161 func indexEscapeSequence(s string) (int, int) {
162 var prev byte
163
164 for i := range s {
165 b := s[i]
166
167 if prev == '\x1b' && b == '[' {
168 j := indexLetter(s[i+1:])
169 if j < 0 {
170 return i, -1
171 }
172 return i - 1, i + 1 + j + 1
173 }
174
175 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
176 j := indexPair(s[i+1:], '\x1b', '\\')
177 if j < 0 {
178 return i, -1
179 }
180 return i - 1, i + 1 + j + 2
181 }
182
183 prev = b
184 }
185
186 return -1, -1
187 }
188
189 func indexLetter(s string) int {
190 for i, b := range s {
191 upper := b &^ 32
192 if 'A' <= upper && upper <= 'Z' {
193 return i
194 }
195 }
196
197 return -1
198 }
199
200 func indexPair(s string, x byte, y byte) int {
201 var prev byte
202
203 for i := range s {
204 b := s[i]
205 if prev == x && b == y && i > 0 {
206 return i
207 }
208 prev = b
209 }
210
211 return -1
212 }
213
214 // writeSpaces does what it says, minimizing calls to write-like funcs
215 func writeSpaces(w *bufio.Writer, n int) {
216 const spaces = ` `
217 if n < 1 {
218 return
219 }
220
221 for n >= len(spaces) {
222 w.WriteString(spaces)
223 n -= len(spaces)
224 }
225 w.WriteString(spaces[:n])
226 }
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 {
82 return nil
83 }
84
85 if w.Flush() != nil {
86 return io.EOF
87 }
88 return nil
89 }
90
91 func (cfg config) match(line []byte) bool {
92 for _, pat := range cfg.patterns {
93 if pat.Match(line) {
94 return true
95 }
96 }
97 return false
98 }
99
100 func Main() {
101 var cfg config
102 cfg.live = true
103 insensitive := false
104 args := os.Args[1:]
105
106 for len(args) > 0 {
107 switch args[0] {
108 case `-b`, `--b`, `-buffered`, `--buffered`:
109 cfg.live = false
110 args = args[1:]
111 continue
112
113 case `-f`, `--f`, `-flat`, `--flat`, `-flattened`, `--flattened`:
114 cfg.flatChance = true
115 args = args[1:]
116 continue
117
118 case `-fi`, `--fi`, `-if`, `--if`:
119 insensitive = true
120 cfg.flatChance = true
121 args = args[1:]
122 continue
123
124 case `-h`, `--h`, `-help`, `--help`:
125 os.Stdout.WriteString(info[1:])
126 return
127
128 case `-i`, `--i`, `-ins`, `--ins`, `-insensitive`, `--insensitive`:
129 insensitive = true
130 args = args[1:]
131 continue
132 }
133
134 break
135 }
136
137 if len(args) > 0 && args[0] == `--` {
138 args = args[1:]
139 }
140
141 if cfg.live {
142 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
143 cfg.live = false
144 }
145 }
146
147 if len(args) == 0 {
148 os.Stderr.WriteString(info[1:])
149 os.Exit(1)
150 return
151 }
152
153 nerr := 0
154
155 for _, s := range args {
156 if insensitive && !strings.HasPrefix(s, `(?i)`) {
157 s = `(?i)` + s
158 }
159
160 pat, err := regexp.Compile(s)
161 if err != nil {
162 os.Stderr.WriteString(err.Error())
163 os.Stderr.WriteString("\n")
164 nerr++
165 continue
166 }
167 cfg.patterns = append(cfg.patterns, pat)
168 }
169
170 if nerr > 0 {
171 os.Exit(1)
172 return
173 }
174
175 if err := run(cfg); err != nil && err != io.EOF {
176 os.Stderr.WriteString(err.Error())
177 os.Stderr.WriteString("\n")
178 os.Exit(1)
179 return
180 }
181 }
182
183 func run(cfg config) error {
184 w := bufio.NewWriterSize(os.Stdout, 32*1024)
185 defer w.Flush()
186
187 // par is the current paragraph's lines joined by line-feeds
188 var par []byte
189 // flat is the current paragraph's lines joined by single spaces
190 var flat []byte
191
192 const gb = 1024 * 1024 * 1024
193 sc := bufio.NewScanner(os.Stdin)
194 sc.Buffer(nil, 8*gb)
195
196 didMatch := false
197
198 for i := 0; sc.Scan(); i++ {
199 line := sc.Bytes()
200 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
201 line = line[3:]
202 }
203
204 if emptyish(line) {
205 if !didMatch && cfg.flatChance && len(flat) > 0 {
206 didMatch = cfg.match(flat)
207 }
208
209 if !didMatch {
210 par = par[:0]
211 flat = flat[:0]
212 continue
213 }
214
215 if err := cfg.dump(w, par); err != nil {
216 return err
217 }
218
219 par = par[:0]
220 flat = flat[:0]
221 didMatch = false
222 continue
223 }
224
225 // avoid checking already-matched paragraphs
226 if !didMatch {
227 if len(flat) > 0 {
228 flat = append(flat, ' ')
229 }
230 flat = append(flat, line...)
231
232 didMatch = cfg.match(line)
233 if didMatch {
234 flat = flat[:0]
235 }
236 }
237
238 // remember all paragraph lines
239 par = append(par, line...)
240 par = append(par, '\n')
241 }
242
243 if err := sc.Err(); err != nil {
244 return err
245 }
246
247 if !didMatch && cfg.flatChance && len(flat) > 0 {
248 didMatch = cfg.match(flat)
249 }
250
251 if didMatch {
252 if err := cfg.dump(w, par); err != nil {
253 return err
254 }
255 }
256
257 return nil
258 }
259
260 func emptyish(line []byte) bool {
261 // trim leading spaces
262 for len(line) > 0 && line[0] == ' ' {
263 line = line[1:]
264 }
265
266 // trim trailing spaces
267 for len(line) > 0 && line[len(line)-1] == ' ' {
268 line = line[:len(line)-1]
269 }
270
271 return len(line) == 0
272 }
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(err.Error())
74 os.Stderr.WriteString("\n")
75 os.Exit(1)
76 return
77 }
78
79 if n == 0 {
80 os.Stderr.WriteString("indices can't be zero\n")
81 os.Exit(1)
82 return
83 }
84
85 i := n
86 if n > 0 {
87 i--
88 }
89
90 which = append(which, int(i))
91 }
92
93 liveLines := !buffered
94 if !buffered {
95 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
96 liveLines = false
97 }
98 }
99
100 if err := run(which, liveLines); err != nil {
101 os.Stderr.WriteString(err.Error())
102 os.Stderr.WriteString("\n")
103 os.Exit(1)
104 return
105 }
106 }
107
108 func run(which []int, liveLines bool) error {
109 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
110 defer bw.Flush()
111
112 const gb = 1024 * 1024 * 1024
113 sc := bufio.NewScanner(os.Stdin)
114 sc.Buffer(nil, 8*gb)
115
116 // see if lines even need to be stored
117 prev := 0
118 fullStreak := true
119 for _, i := range which {
120 if i < 1 || i < prev {
121 fullStreak = false
122 break
123 }
124 prev = i
125 }
126
127 var lines []string
128
129 for i := 0; sc.Scan() && len(which) > 0; i++ {
130 line := sc.Text()
131 if i == 0 && strings.HasPrefix(line, "\xef\xbb\xbf") {
132 line = line[3:]
133 }
134
135 if !fullStreak {
136 lines = append(lines, line)
137 }
138
139 for len(which) > 0 && i == which[0] {
140 if writeLine(bw, line, liveLines) != nil {
141 return nil
142 }
143 which = which[1:]
144 }
145 }
146
147 if err := sc.Err(); err != nil {
148 return err
149 }
150
151 for _, i := range which {
152 if i < 0 {
153 i += len(lines)
154 }
155
156 if 0 <= i && i < len(lines) {
157 if writeLine(bw, lines[i], liveLines) != nil {
158 return nil
159 }
160 }
161 }
162
163 return nil
164 }
165
166 func writeLine(w *bufio.Writer, s string, live bool) error {
167 w.WriteString(s)
168 if w.WriteByte('\n') != nil {
169 return nil
170 }
171
172 if !live {
173 return nil
174 }
175 return w.Flush()
176 }
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(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.NewWriter(os.Stdout)
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 which []int
107 var fields []string
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, h, ok := match(s, args)
118 if !ok {
119 return nil
120 }
121 which = picks
122 handle = h
123 fields = make([]string, 0, len(which))
124 }
125
126 fields = fields[:0]
127 handle(s, func(i int, s string) bool {
128 fields = append(fields, s)
129 return true
130 })
131
132 got := 0
133 for _, i := range which {
134 if 0 <= i && i < len(fields) {
135 if got > 0 {
136 w.WriteByte('\t')
137 }
138 w.WriteString(fields[i])
139 got++
140 }
141 }
142
143 if w.WriteByte('\n') != nil {
144 return io.EOF
145 }
146
147 if !live {
148 continue
149 }
150
151 if w.Flush() != nil {
152 return io.EOF
153 }
154 }
155
156 return sc.Err()
157 }
158
159 func match(s string, args []string) (which []int, handle handler, ok bool) {
160 if strings.IndexByte(s, '\t') >= 0 {
161 handle = loopItemsTSV
162 } else {
163 handle = loopItemsSSV
164 }
165
166 // count columns, so negative indices can be fixed with it
167 count := 0
168 handle(s, func(i int, s string) bool {
169 count++
170 return true
171 })
172
173 for _, arg := range args {
174 ok := false
175
176 // try exact matches
177 handle(s, func(i int, s string) bool {
178 if s == arg {
179 ok = true
180 which = append(which, i)
181 return false
182 }
183 return true
184 })
185
186 if ok {
187 continue
188 }
189
190 // try case-insensitive matches
191 handle(s, func(i int, s string) bool {
192 if strings.EqualFold(s, arg) {
193 ok = true
194 which = append(which, i)
195 return false
196 }
197 return true
198 })
199
200 if ok {
201 continue
202 }
203
204 // try 1-based indices, even negative ones
205 if n, err := strconv.Atoi(arg); err == nil {
206 if n < 0 {
207 n += count
208 } else if n > 0 {
209 n--
210 }
211
212 if 0 <= n && n < count {
213 which = append(which, n)
214 }
215 }
216 }
217
218 return which, handle, true
219 }
220
221 // loopItemsSSV loops over a line's items, allocation-free style; when given
222 // empty strings, the callback func is never called
223 func loopItemsSSV(s string, f itemFunc) {
224 s = trimTrailingSpaces(s)
225
226 for i := 0; true; i++ {
227 s = trimLeadingSpaces(s)
228 if len(s) == 0 {
229 return
230 }
231
232 j := strings.IndexByte(s, ' ')
233 if j < 0 {
234 if !f(i, s) {
235 return
236 }
237 return
238 }
239
240 if !f(i, s[:j]) {
241 return
242 }
243 s = s[j+1:]
244 }
245 }
246
247 func trimLeadingSpaces(s string) string {
248 for len(s) > 0 && s[0] == ' ' {
249 s = s[1:]
250 }
251 return s
252 }
253
254 func trimTrailingSpaces(s string) string {
255 for len(s) > 0 && s[len(s)-1] == ' ' {
256 s = s[:len(s)-1]
257 }
258 return s
259 }
260
261 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
262 // when given empty strings, the callback func is never called
263 func loopItemsTSV(s string, f itemFunc) {
264 if len(s) == 0 {
265 return
266 }
267
268 for i := 0; true; i++ {
269 j := strings.IndexByte(s, '\t')
270 if j < 0 {
271 if !f(i, s) {
272 return
273 }
274 return
275 }
276
277 if !f(i, s[:j]) {
278 return
279 }
280 s = s[j+1:]
281 }
282 }
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(err.Error())
78 os.Stderr.WriteString("\n")
79 os.Exit(1)
80 return
81 }
82 }
83
84 func run(w io.Writer, args []string, live bool) error {
85 bw := bufio.NewWriter(w)
86 defer bw.Flush()
87
88 if len(args) == 0 {
89 return plain(bw, os.Stdin, live)
90 }
91
92 for _, name := range args {
93 if err := handleFile(bw, name, live); err != nil {
94 return err
95 }
96 }
97 return nil
98 }
99
100 func handleFile(w *bufio.Writer, name string, live bool) error {
101 if name == `` || name == `-` {
102 return plain(w, os.Stdin, live)
103 }
104
105 f, err := os.Open(name)
106 if err != nil {
107 return errors.New(`can't read from file named "` + name + `"`)
108 }
109 defer f.Close()
110
111 return plain(w, f, live)
112 }
113
114 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
115 // the multi-byte sequences starting with ESC[; the result is a pair of slice
116 // indices which can be independently negative when either the start/end of
117 // a sequence isn't found; given their fairly-common use, even the hyperlink
118 // ESC]8 sequences are supported
119 func indexEscapeSequence(s []byte) (int, int) {
120 var prev byte
121
122 for i, b := range s {
123 if prev == '\x1b' && b == '[' {
124 j := indexLetter(s[i+1:])
125 if j < 0 {
126 return i, -1
127 }
128 return i - 1, i + 1 + j + 1
129 }
130
131 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
132 j := indexPair(s[i+1:], '\x1b', '\\')
133 if j < 0 {
134 return i, -1
135 }
136 return i - 1, i + 1 + j + 2
137 }
138
139 prev = b
140 }
141
142 return -1, -1
143 }
144
145 func indexLetter(s []byte) int {
146 for i, b := range s {
147 upper := b &^ 32
148 if 'A' <= upper && upper <= 'Z' {
149 return i
150 }
151 }
152
153 return -1
154 }
155
156 func indexPair(s []byte, x byte, y byte) int {
157 var prev byte
158
159 for i, b := range s {
160 if prev == x && b == y && i > 0 {
161 return i
162 }
163 prev = b
164 }
165
166 return -1
167 }
168
169 func plain(w *bufio.Writer, r io.Reader, live bool) error {
170 const gb = 1024 * 1024 * 1024
171 sc := bufio.NewScanner(r)
172 sc.Buffer(nil, 8*gb)
173
174 for i := 0; sc.Scan(); i++ {
175 s := sc.Bytes()
176 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
177 s = s[3:]
178 }
179
180 for line := s; len(line) > 0; {
181 i, j := indexEscapeSequence(line)
182 if i < 0 {
183 w.Write(line)
184 break
185 }
186 if j < 0 {
187 j = len(line)
188 }
189
190 if i > 0 {
191 w.Write(line[:i])
192 }
193
194 line = line[j:]
195 }
196
197 if w.WriteByte('\n') != nil {
198 return io.EOF
199 }
200
201 if !live {
202 continue
203 }
204
205 if w.Flush() != nil {
206 return io.EOF
207 }
208 }
209
210 return sc.Err()
211 }
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(err.Error())
75 os.Stderr.WriteString("\n")
76 os.Exit(1)
77 return
78 }
79 }
80
81 func run(w io.Writer, args []string, live bool) error {
82 if len(args) == 0 {
83 return nil
84 }
85
86 bw := bufio.NewWriter(w)
87
88 for i, s := range args {
89 if i > 0 {
90 bw.WriteByte('\t')
91 }
92 bw.WriteString(s)
93 }
94
95 if bw.WriteByte('\n') != nil {
96 bw.Flush()
97 return io.EOF
98 }
99
100 if bw.Flush() != nil {
101 return io.EOF
102 }
103
104 return catl(bw, os.Stdin, live)
105 }
106
107 func catl(w *bufio.Writer, r io.Reader, live bool) error {
108 const gb = 1024 * 1024 * 1024
109 sc := bufio.NewScanner(r)
110 sc.Buffer(nil, 8*gb)
111
112 for i := 0; sc.Scan(); i++ {
113 s := sc.Bytes()
114 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
115 s = s[3:]
116 }
117
118 w.Write(s)
119 if w.WriteByte('\n') != nil {
120 return io.EOF
121 }
122
123 if !live {
124 continue
125 }
126
127 if w.Flush() != nil {
128 return io.EOF
129 }
130 }
131
132 return sc.Err()
133 }
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(err.Error())
58 os.Stderr.WriteString("\n")
59 os.Exit(1)
60 return
61 }
62
63 if n < 0 {
64 n = 0
65 }
66 howMany = n
67 }
68
69 primes(howMany)
70 }
71
72 func primes(left int) {
73 bw := bufio.NewWriter(os.Stdout)
74 defer bw.Flush()
75
76 // 24 bytes are always enough for any 64-bit integer
77 var buf [24]byte
78
79 // 2 is the only even prime number
80 if left > 0 {
81 bw.WriteString("2\n")
82 left--
83 }
84
85 for n := uint64(3); left > 0; n += 2 {
86 if oddPrime(n) {
87 bw.Write(strconv.AppendUint(buf[:0], n, 10))
88 if err := bw.WriteByte('\n'); err != nil {
89 // assume errors come from closed stdout pipes
90 return
91 }
92 left--
93 }
94 }
95 }
96
97 // oddPrime assumes the number given to it is odd
98 func oddPrime(n uint64) bool {
99 max := uint64(math.Sqrt(float64(n)))
100 for div := uint64(3); div <= max; div += 2 {
101 if n%div == 0 {
102 return false
103 }
104 }
105 return true
106 }
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.NewWriter(os.Stdout)
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(err.Error())
77 os.Stderr.WriteString("\n")
78 os.Exit(1)
79 return
80 }
81 }
82
83 // table has all summary info gathered from the data, along with the row
84 // themselves, stored as lines/strings
85 type table struct {
86 Columns int
87
88 Rows []string
89
90 MaxWidth []int
91
92 MaxIntWidth []int
93
94 MaxDotDecimals []int
95
96 LoopItems func(s string, max int, t *table, f itemFunc)
97
98 MaxColumns bool
99 }
100
101 type itemFunc func(i int, s string, t *table)
102
103 func run(paths []string, maxCols bool) error {
104 var res table
105 res.MaxColumns = maxCols
106
107 for _, p := range paths {
108 if err := handleFile(&res, p); err != nil {
109 return err
110 }
111 }
112
113 if len(paths) == 0 {
114 if err := handleReader(&res, os.Stdin); err != nil {
115 return err
116 }
117 }
118
119 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
120 defer bw.Flush()
121 realign(bw, res)
122 return nil
123 }
124
125 func handleFile(res *table, path string) error {
126 f, err := os.Open(path)
127 if err != nil {
128 // on windows, file-not-found error messages may mention `CreateFile`,
129 // even when trying to open files in read-only mode
130 return errors.New(`can't open file named ` + path)
131 }
132 defer f.Close()
133 return handleReader(res, f)
134 }
135
136 func handleReader(t *table, r io.Reader) error {
137 const gb = 1024 * 1024 * 1024
138 sc := bufio.NewScanner(r)
139 sc.Buffer(nil, 8*gb)
140
141 const maxInt = int(^uint(0) >> 1)
142 maxCols := maxInt
143
144 for i := 0; sc.Scan(); i++ {
145 s := sc.Text()
146 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
147 s = s[3:]
148 }
149
150 if len(s) == 0 {
151 if len(t.Rows) > 0 {
152 t.Rows = append(t.Rows, ``)
153 }
154 continue
155 }
156
157 t.Rows = append(t.Rows, s)
158
159 if t.Columns == 0 {
160 if t.LoopItems == nil {
161 if strings.IndexByte(s, '\t') >= 0 {
162 t.LoopItems = loopItemsTSV
163 } else {
164 t.LoopItems = loopItemsSSV
165 }
166 }
167
168 if !t.MaxColumns {
169 t.LoopItems(s, maxCols, t, updateColumnCount)
170 maxCols = t.Columns
171 }
172 }
173
174 t.LoopItems(s, maxCols, t, updateItem)
175 }
176
177 return sc.Err()
178 }
179
180 func updateColumnCount(i int, s string, t *table) {
181 t.Columns = i + 1
182 }
183
184 func updateItem(i int, s string, t *table) {
185 // ensure column-info-slices have enough room
186 if i >= len(t.MaxWidth) {
187 // update column-count if in max-columns mode
188 if t.MaxColumns {
189 t.Columns = i + 1
190 }
191 t.MaxWidth = append(t.MaxWidth, 0)
192 t.MaxIntWidth = append(t.MaxIntWidth, 0)
193 t.MaxDotDecimals = append(t.MaxDotDecimals, 0)
194 }
195
196 // keep track of widest rune-counts for each column
197 w := countWidth(s)
198 if t.MaxWidth[i] < w {
199 t.MaxWidth[i] = w
200 }
201
202 // update stats for numeric items
203 if isNumeric(s) {
204 iw, dd := countNumericWidths(s)
205 if t.MaxIntWidth[i] < iw {
206 t.MaxIntWidth[i] = iw
207 }
208 if t.MaxDotDecimals[i] < dd {
209 t.MaxDotDecimals[i] = dd
210 }
211 }
212 }
213
214 // loopItemsSSV loops over a line's items, allocation-free style; when given
215 // empty strings, the callback func is never called
216 func loopItemsSSV(s string, max int, t *table, f itemFunc) {
217 s = trimTrailingSpaces(s)
218
219 for i := 0; true; i++ {
220 s = trimLeadingSpaces(s)
221 if len(s) == 0 {
222 return
223 }
224
225 if i+1 == max {
226 f(i, s, t)
227 return
228 }
229
230 j := strings.IndexByte(s, ' ')
231 if j < 0 {
232 f(i, s, t)
233 return
234 }
235
236 f(i, s[:j], t)
237 s = s[j+1:]
238 }
239 }
240
241 func trimLeadingSpaces(s string) string {
242 for len(s) > 0 && s[0] == ' ' {
243 s = s[1:]
244 }
245 return s
246 }
247
248 func trimTrailingSpaces(s string) string {
249 for len(s) > 0 && s[len(s)-1] == ' ' {
250 s = s[:len(s)-1]
251 }
252 return s
253 }
254
255 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
256 // when given empty strings, the callback func is never called
257 func loopItemsTSV(s string, max int, t *table, f itemFunc) {
258 if len(s) == 0 {
259 return
260 }
261
262 for i := 0; true; i++ {
263 if i+1 == max {
264 f(i, s, t)
265 return
266 }
267
268 j := strings.IndexByte(s, '\t')
269 if j < 0 {
270 f(i, s, t)
271 return
272 }
273
274 f(i, s[:j], t)
275 s = s[j+1:]
276 }
277 }
278
279 func skipLeadingEscapeSequences(s string) string {
280 for len(s) >= 2 {
281 if s[0] != '\x1b' {
282 return s
283 }
284
285 switch s[1] {
286 case '[':
287 s = skipSingleLeadingANSI(s[2:])
288
289 case ']':
290 if len(s) < 3 || s[2] != '8' {
291 return s
292 }
293 s = skipSingleLeadingOSC(s[3:])
294
295 default:
296 return s
297 }
298 }
299
300 return s
301 }
302
303 func skipSingleLeadingANSI(s string) string {
304 for len(s) > 0 {
305 upper := s[0] &^ 32
306 s = s[1:]
307 if 'A' <= upper && upper <= 'Z' {
308 break
309 }
310 }
311
312 return s
313 }
314
315 func skipSingleLeadingOSC(s string) string {
316 var prev byte
317
318 for len(s) > 0 {
319 b := s[0]
320 s = s[1:]
321 if prev == '\x1b' && b == '\\' {
322 break
323 }
324 prev = b
325 }
326
327 return s
328 }
329
330 // isNumeric checks if a string is valid/useable as a number
331 func isNumeric(s string) bool {
332 if len(s) == 0 {
333 return false
334 }
335
336 s = skipLeadingEscapeSequences(s)
337 if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
338 s = s[1:]
339 }
340
341 s = skipLeadingEscapeSequences(s)
342 if len(s) == 0 {
343 return false
344 }
345 if s[0] == '.' {
346 return isDigits(s[1:])
347 }
348
349 digits := 0
350
351 for {
352 s = skipLeadingEscapeSequences(s)
353 if len(s) == 0 {
354 break
355 }
356
357 if s[0] == '.' {
358 return isDigits(s[1:])
359 }
360
361 if !('0' <= s[0] && s[0] <= '9') {
362 return false
363 }
364
365 digits++
366 s = s[1:]
367 }
368
369 s = skipLeadingEscapeSequences(s)
370 return len(s) == 0 && digits > 0
371 }
372
373 func isDigits(s string) bool {
374 if len(s) == 0 {
375 return false
376 }
377
378 digits := 0
379
380 for {
381 s = skipLeadingEscapeSequences(s)
382 if len(s) == 0 {
383 break
384 }
385
386 if '0' <= s[0] && s[0] <= '9' {
387 s = s[1:]
388 digits++
389 } else {
390 return false
391 }
392 }
393
394 s = skipLeadingEscapeSequences(s)
395 return len(s) == 0 && digits > 0
396 }
397
398 // countDecimals counts decimal digits from the string given, assuming it
399 // represents a valid/useable float64, when parsed
400 func countDecimals(s string) int {
401 dot := strings.IndexByte(s, '.')
402 if dot < 0 {
403 return 0
404 }
405
406 decs := 0
407 s = s[dot+1:]
408
409 for len(s) > 0 {
410 s = skipLeadingEscapeSequences(s)
411 if len(s) == 0 {
412 break
413 }
414 if '0' <= s[0] && s[0] <= '9' {
415 decs++
416 }
417 s = s[1:]
418 }
419
420 return decs
421 }
422
423 // countDotDecimals is like func countDecimals, but this one also includes
424 // the dot, when any decimals are present, else the count stays at 0
425 func countDotDecimals(s string) int {
426 decs := countDecimals(s)
427 if decs > 0 {
428 return decs + 1
429 }
430 return decs
431 }
432
433 // countNumericWidths counts both the integer-width (including leading sign,
434 // if found), and the dot-decimal trail from the string given, assuming it
435 // represents a valid/useable float64, when parsed
436 func countNumericWidths(s string) (intWidth int, dotDecimals int) {
437 sign := 0
438 if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
439 sign++
440 s = s[1:]
441 }
442
443 for len(s) > 0 {
444 s = skipLeadingEscapeSequences(s)
445 if len(s) == 0 {
446 break
447 }
448 if '0' <= s[0] && s[0] <= '9' {
449 intWidth++
450 } else {
451 break
452 }
453 s = s[1:]
454 }
455
456 intWidth += sign
457
458 if len(s) == 0 || s[0] != '.' {
459 return intWidth, dotDecimals
460 }
461
462 for len(s) > 0 {
463 s = skipLeadingEscapeSequences(s)
464 if len(s) == 0 {
465 break
466 }
467 if '0' <= s[0] && s[0] <= '9' {
468 dotDecimals++
469 }
470 s = s[1:]
471 }
472
473 if dotDecimals > 0 {
474 dotDecimals++
475 }
476 return intWidth, dotDecimals
477 }
478
479 func countWidth(s string) int {
480 width := 0
481
482 for len(s) > 0 {
483 i, j := indexEscapeSequence(s)
484 if i < 0 {
485 break
486 }
487 if j < 0 {
488 j = len(s)
489 }
490
491 width += utf8.RuneCountInString(s[:i])
492 s = s[j:]
493 }
494
495 // count trailing/all runes in strings which don't end with ANSI-sequences
496 width += utf8.RuneCountInString(s)
497 return width
498 }
499
500 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
501 // the multi-byte sequences starting with ESC[; the result is a pair of slice
502 // indices which can be independently negative when either the start/end of
503 // a sequence isn't found; given their fairly-common use, even the hyperlink
504 // ESC]8 sequences are supported
505 func indexEscapeSequence(s string) (int, int) {
506 var prev byte
507
508 for i := range s {
509 b := s[i]
510
511 if prev == '\x1b' && b == '[' {
512 j := indexLetter(s[i+1:])
513 if j < 0 {
514 return i, -1
515 }
516 return i - 1, i + 1 + j + 1
517 }
518
519 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
520 j := indexPair(s[i+1:], '\x1b', '\\')
521 if j < 0 {
522 return i, -1
523 }
524 return i - 1, i + 1 + j + 2
525 }
526
527 prev = b
528 }
529
530 return -1, -1
531 }
532
533 func indexLetter(s string) int {
534 for i, b := range s {
535 upper := b &^ 32
536 if 'A' <= upper && upper <= 'Z' {
537 return i
538 }
539 }
540
541 return -1
542 }
543
544 func indexPair(s string, x byte, y byte) int {
545 var prev byte
546
547 for i := range s {
548 b := s[i]
549 if prev == x && b == y && i > 0 {
550 return i
551 }
552 prev = b
553 }
554
555 return -1
556 }
557
558 func realign(w *bufio.Writer, t table) {
559 // decimal numbers can pad beyond their max-width
560 for i, _ := range t.MaxWidth {
561 w := t.MaxIntWidth[i] + t.MaxDotDecimals[i]
562 if t.MaxWidth[i] < w {
563 t.MaxWidth[i] = w
564 }
565 }
566
567 due := 0
568 showItem := func(i int, s string, t *table) {
569 if i > 0 {
570 due += 2
571 }
572
573 if isNumeric(s) {
574 dd := countDotDecimals(s)
575 rpad := t.MaxDotDecimals[i] - dd
576 width := countWidth(s)
577 lpad := t.MaxWidth[i] - (width + rpad) + due
578 writeSpaces(w, lpad)
579 w.WriteString(s)
580 due = rpad
581 return
582 }
583
584 writeSpaces(w, due)
585 w.WriteString(s)
586 due = t.MaxWidth[i] - countWidth(s)
587 }
588
589 for _, line := range t.Rows {
590 due = 0
591 if len(line) > 0 {
592 t.LoopItems(line, t.Columns, &t, showItem)
593 }
594 if w.WriteByte('\n') != nil {
595 break
596 }
597 }
598 }
599
600 // writeSpaces does what it says, minimizing calls to write-like funcs
601 func writeSpaces(w *bufio.Writer, n int) {
602 const spaces = ` `
603 if n < 1 {
604 return
605 }
606
607 for n >= len(spaces) {
608 w.WriteString(spaces)
609 n -= len(spaces)
610 }
611 w.WriteString(spaces[:n])
612 }
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(err.Error())
107 os.Stderr.WriteString("\n")
108 os.Exit(1)
109 return
110 }
111 }
112
113 func run(paths []string, cfg config) error {
114 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
115 defer bw.Flush()
116
117 for _, p := range paths {
118 if err := handleFile(bw, p, &cfg); err != nil {
119 return err
120 }
121 }
122
123 if len(paths) == 0 {
124 if err := handleReader(bw, os.Stdin, &cfg); err != nil {
125 return err
126 }
127 }
128
129 // end current line, if there's anything on it
130 if cfg.pos > 0 {
131 if bw.WriteByte('\n') != nil {
132 return io.EOF
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 // on windows, file-not-found error messages may mention `CreateFile`,
142 // even when trying to open files in read-only mode
143 return errors.New(`can't open file named ` + path)
144 }
145 defer f.Close()
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
154 for i := 0; sc.Scan(); i++ {
155 s := sc.Bytes()
156 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
157 s = s[3:]
158 }
159
160 s = trimTrailingSpaces(s)
161
162 // keep empty(ish) lines as empty lines
163 if len(s) == 0 {
164 if cfg.pos > 0 && w.WriteByte('\n') != nil {
165 return io.EOF
166 }
167 if w.WriteByte('\n') != nil {
168 return io.EOF
169 }
170 if cfg.liveLines && w.Flush() != nil {
171 return io.EOF
172 }
173 cfg.pos = 0
174 continue
175 }
176
177 // reflow all `words` from non-empty lines
178 for len(s) > 0 {
179 var word []byte
180 s = trimLeadingSpaces(s)
181 if i := bytes.IndexByte(s, ' '); i >= 0 {
182 word = s[:i]
183 s = s[i+1:]
184 } else {
185 word = s
186 s = nil
187 }
188
189 word = trimLeadingSpaces(word)
190 width := countWidth(word)
191 if width == 0 {
192 continue
193 }
194
195 if cfg.pos+width > cfg.maxWidth {
196 if cfg.pos > 0 && w.WriteByte('\n') != nil {
197 return io.EOF
198 }
199 if cfg.liveLines && w.Flush() != nil {
200 return io.EOF
201 }
202 cfg.pos = 0
203 }
204
205 if cfg.pos > 0 {
206 w.WriteByte(' ')
207 cfg.pos++
208 }
209 w.Write(word)
210 cfg.pos += width
211 }
212 }
213
214 return sc.Err()
215 }
216
217 func trimLeadingSpaces(s []byte) []byte {
218 for len(s) > 0 && s[0] == ' ' {
219 s = s[1:]
220 }
221 return s
222 }
223
224 func trimTrailingSpaces(s []byte) []byte {
225 for len(s) > 0 && s[len(s)-1] == ' ' {
226 s = s[:len(s)-1]
227 }
228 return s
229 }
230
231 func countWidth(s []byte) int {
232 width := 0
233
234 for len(s) > 0 {
235 i, j := indexEscapeSequence(s)
236 if i < 0 {
237 break
238 }
239 if j < 0 {
240 j = len(s)
241 }
242
243 width += utf8.RuneCount(s[:i])
244 s = s[j:]
245 }
246
247 // count trailing/all runes in strings which don't end with ANSI-sequences
248 width += utf8.RuneCount(s)
249 return width
250 }
251
252 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
253 // the multi-byte sequences starting with ESC[; the result is a pair of slice
254 // indices which can be independently negative when either the start/end of
255 // a sequence isn't found; given their fairly-common use, even the hyperlink
256 // ESC]8 sequences are supported
257 func indexEscapeSequence(s []byte) (int, int) {
258 var prev byte
259
260 for i := range s {
261 b := s[i]
262
263 if prev == '\x1b' && b == '[' {
264 j := indexLetter(s[i+1:])
265 if j < 0 {
266 return i, -1
267 }
268 return i - 1, i + 1 + j + 1
269 }
270
271 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
272 j := indexPair(s[i+1:], '\x1b', '\\')
273 if j < 0 {
274 return i, -1
275 }
276 return i - 1, i + 1 + j + 2
277 }
278
279 prev = b
280 }
281
282 return -1, -1
283 }
284
285 func indexLetter(s []byte) int {
286 for i, b := range s {
287 upper := b &^ 32
288 if 'A' <= upper && upper <= 'Z' {
289 return i
290 }
291 }
292
293 return -1
294 }
295
296 func indexPair(s []byte, x byte, y byte) int {
297 var prev byte
298
299 for i := range s {
300 b := s[i]
301 if prev == x && b == y && i > 0 {
302 return i
303 }
304 prev = b
305 }
306
307 return -1
308 }
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(err.Error())
76 os.Stderr.WriteString("\n")
77 // exit-status 1 is only for when the files have different contents
78 os.Exit(2)
79 return
80 }
81
82 if !same {
83 os.Exit(1)
84 return
85 }
86 }
87
88 func run(path1 string, path2 string) (same bool, err error) {
89 f1, err := os.Open(path1)
90 if err != nil {
91 // on windows, file-not-found error messages may mention `CreateFile`,
92 // even when trying to open files in read-only mode
93 return false, errors.New(`can't open file named ` + path1)
94 }
95 defer f1.Close()
96
97 if path1 == path2 {
98 return true, nil
99 }
100
101 f2, err := os.Open(path2)
102 if err != nil {
103 // on windows, file-not-found error messages may mention `CreateFile`,
104 // even when trying to open files in read-only mode
105 return false, errors.New(`can't open file named ` + path2)
106 }
107 defer f2.Close()
108
109 same, err = sameContents(f1, f2)
110 return same, err
111 }
112
113 // sameContents checks if 2 files have the same bytes/contents
114 func sameContents(f1, f2 *os.File) (same bool, err error) {
115 // don't read anything from files, if they're obviously different in size;
116 // files are always/already opened to check if filenames given are valid
117 if differentSize(f1, f2) {
118 return false, nil
119 }
120
121 const bufSize = 16 * 1024
122 var buf1 [bufSize]byte
123 var buf2 [bufSize]byte
124
125 for {
126 n, err1 := f1.Read(buf1[:])
127 chunk1 := buf1[:n]
128
129 n, err2 := f2.Read(buf2[:])
130 chunk2 := buf2[:n]
131
132 if err1 == io.EOF && err2 == io.EOF {
133 return bytes.Equal(chunk1, chunk2), nil
134 }
135
136 if err1 != nil {
137 return false, err1
138 }
139 if err2 != nil {
140 return false, err2
141 }
142
143 if !bytes.Equal(chunk1, chunk2) {
144 return false, nil
145 }
146 }
147 }
148
149 // differentSize returns whether the 2 files given can be determined to have
150 // different non-zero file-sizes: this is because some special files falsely
151 // advertise being empty; in case of errors false is returned, but the files
152 // can still have different sizes, but that isn't known for sure
153 func differentSize(f1, f2 *os.File) bool {
154 info1, err := f1.Stat()
155 if err != nil {
156 // can't tell size for sure
157 return false
158 }
159
160 info2, err := f2.Stat()
161 if err != nil {
162 // can't tell size for sure
163 return false
164 }
165
166 size1 := info1.Size()
167 size2 := info2.Size()
168
169 if size1 == 0 || size2 == 0 {
170 // some special files falsely advertise being empty
171 return false
172 }
173 return size1 != size2
174 }
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(err.Error())
84 os.Exit(1)
85 return
86 }
87 }
88
89 func run(args []string, ncols int, separator string) error {
90 paths := args
91 if len(paths) == 0 {
92 paths = []string{`-`}
93 }
94
95 if separator == `` {
96 separator = ` `
97 }
98
99 lines, err := gatherLines(paths)
100 if err != nil {
101 return err
102 }
103
104 // choose a default number of columns, if not given an explicit one
105 if ncols < 1 {
106 ncols = chooseNumColumns(lines, separator)
107 }
108
109 w := bufio.NewWriterSize(os.Stdout, 16*1024)
110 defer w.Flush()
111 return showLines(w, lines, ncols, separator)
112 }
113
114 // gatherLines slurps all text-lines from all the filepaths given, expanding
115 // any tabs found; a single dash as a pathname means stdin
116 func gatherLines(paths []string) ([]string, error) {
117 var lines []string
118 var sb strings.Builder
119
120 dashes := 0
121 for _, s := range paths {
122 if s == `-` {
123 dashes++
124 }
125 }
126 if dashes > 1 {
127 return lines, errors.New(`can't use "-" (stdin) more than once`)
128 }
129
130 for _, s := range paths {
131 err := handleNamedInput(s, func(r io.Reader) error {
132 sc := newScanner(r)
133
134 for sc.Scan() {
135 s := sc.Text()
136 if strings.IndexByte(s, '\t') >= 0 {
137 sb.Reset()
138 expand(s, tabstop, &sb)
139 s = strings.Clone(sb.String())
140 }
141 lines = append(lines, s)
142 }
143
144 return sc.Err()
145 })
146
147 if err != nil {
148 return lines, err
149 }
150 }
151
152 return lines, nil
153 }
154
155 // handleNamedInput makes opening/closing files more convenient by using
156 // callbacks to handle processing; this func also handles recognizing `-`
157 // as meaning stdin
158 func handleNamedInput(path string, fn func(r io.Reader) error) error {
159 if path == `-` {
160 return fn(os.Stdin)
161 }
162
163 f, err := os.Open(path)
164 if err != nil {
165 return err
166 }
167 defer f.Close()
168 return fn(f)
169 }
170
171 // showLines handles the use-case of showing/rearranging lines from a single
172 // input source (presumably standard input) into several columns
173 func showLines(w *bufio.Writer, lines []string, ncols int, sep string) error {
174 if ncols < 1 {
175 return nil
176 }
177
178 if ncols == 1 {
179 for _, s := range lines {
180 w.WriteString(s)
181 err := w.WriteByte('\n')
182 if err != nil {
183 // assume error probably results from a closed stdout
184 // pipe, so quit the app right away without complaining
185 return err
186 }
187 }
188 return nil
189 }
190
191 // nothing to show, so don't even bother
192 if len(lines) == 0 {
193 return nil
194 }
195
196 cols, height := splitLines(lines, ncols)
197 widths := make([]int, 0, len(cols))
198 for _, c := range cols {
199 // find the max width of all lines of the current column
200 maxw := 0
201 for _, v := range c {
202 w := width(v)
203 if w > maxw {
204 maxw = w
205 }
206 }
207
208 widths = append(widths, maxw)
209 }
210
211 // endSep is right-trimmed to avoid unneeded trailing spaces on output
212 // lines whose last column is an empty/missing input line
213 endSep := strings.TrimRight(sep, ` `)
214
215 // show columns side by side
216 for r := 0; r < height; r++ {
217 for c := 0; c < len(cols); c++ {
218 badr := r >= len(cols[c])
219
220 // clearly separate columns visually
221 if c > 0 {
222 if c == len(cols)-1 && (badr || cols[c][r] == ``) {
223 // avoid unneeded trailing spaces
224 w.WriteString(endSep)
225 } else {
226 w.WriteString(sep)
227 }
228 }
229
230 if badr {
231 // exceeding items for this (last) column
232 continue
233 }
234
235 // pad all columns, except the last
236 width := 0
237 if c < len(cols)-1 {
238 width = widths[c]
239 }
240
241 // emit maybe-padded column
242 writeItem(w, cols[c][r], width)
243 }
244
245 // end the line
246 err := w.WriteByte('\n')
247 if err != nil {
248 // probably a pipe was closed
249 return nil
250 }
251 }
252
253 return nil
254 }
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(err.Error())
108 os.Stderr.WriteString("\n")
109 os.Exit(1)
110 return
111 }
112 }
113
114 func seq(first, incr, last *big.Rat, zeroPad bool) error {
115 if first.IsInt() && incr.IsInt() && last.IsInt() {
116 f := int(first.Num().Int64())
117 i := int(incr.Num().Int64())
118 l := int(last.Num().Int64())
119 return seqInt(f, i, l, zeroPad)
120 }
121
122 return seqFrac(first, incr, last, zeroPad)
123 }
124
125 func seqInt(first, incr, last int, zeroPad bool) error {
126 if incr == 0 {
127 return nil
128 }
129
130 w := bufio.NewWriterSize(os.Stdout, 32*1024)
131 defer w.Flush()
132
133 var buf [24]byte
134 var maxlen int
135
136 cur := first
137 if zeroPad {
138 var n int
139 n = len(strconv.AppendInt(buf[:0], int64(first), 10))
140 if maxlen < n {
141 maxlen = n
142 }
143 n = len(strconv.AppendInt(buf[:0], int64(last), 10))
144 if maxlen < n {
145 maxlen = n
146 }
147 }
148
149 for {
150 if incr > 0 && cur > last {
151 break
152 }
153 if incr < 0 && cur < last {
154 break
155 }
156
157 s := strconv.AppendInt(buf[:0], int64(cur), 10)
158 if maxlen > 0 {
159 writeZeros(w, maxlen-len(s))
160 }
161 w.Write(s)
162
163 if w.WriteByte('\n') != nil {
164 return io.EOF
165 }
166
167 cur += incr
168 }
169
170 return nil
171 }
172
173 func seqFrac(first, incr, last *big.Rat, zeroPad bool) error {
174 incrSign := incr.Sign()
175 if incrSign == 0 {
176 return nil
177 }
178
179 w := bufio.NewWriterSize(os.Stdout, 32*1024)
180 defer w.Flush()
181
182 var maxlen, prec int
183
184 var p int
185 p = maxPrec(first)
186 if prec < p {
187 prec = p
188 }
189 p = maxPrec(incr)
190 if prec < p {
191 prec = p
192 }
193 p = maxPrec(last)
194 if prec < p {
195 prec = p
196 }
197
198 cur := first
199 if zeroPad {
200 var n int
201 n = len(first.FloatString(prec))
202 if maxlen < n {
203 maxlen = n
204 }
205 n = len(last.FloatString(prec))
206 if maxlen < n {
207 maxlen = n
208 }
209 }
210
211 for {
212 if incrSign > 0 && cur.Cmp(last) > 0 {
213 break
214 }
215 if incrSign < 0 && cur.Cmp(last) < 0 {
216 break
217 }
218
219 s := cur.FloatString(prec)
220 if maxlen > 0 {
221 writeZeros(w, maxlen-len(s))
222 }
223 w.WriteString(s)
224
225 if w.WriteByte('\n') != nil {
226 return io.EOF
227 }
228
229 cur = cur.Add(cur, incr)
230 }
231
232 return nil
233 }
234
235 func maxPrec(n *big.Rat) int {
236 if p, exact := n.FloatPrec(); exact {
237 return p
238 }
239
240 s := n.FloatString(50)
241 i := strings.IndexByte(s, '.')
242 if i < 0 {
243 return 0
244 }
245
246 s = s[i+1:]
247 for len(s) > 0 && s[len(s)-1] == '0' {
248 s = s[:len(s)-1]
249 }
250 return len(s)
251 }
252
253 func writeZeros(w *bufio.Writer, n int) {
254 const (
255 half = `00000000000000000000000000000000`
256 zeros = half + half
257 )
258
259 for n >= len(zeros) {
260 w.WriteString(zeros)
261 n -= len(zeros)
262 }
263 if n > 0 {
264 w.WriteString(zeros[:n])
265 }
266 }
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("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(err.Error())
114 os.Stderr.WriteString("\n")
115 os.Exit(1)
116 return
117 }
118 }
119
120 func run(paths []string, cfg *config) error {
121 w := bufio.NewWriterSize(os.Stdout, 32*1024)
122 defer w.Flush()
123
124 for _, path := range paths {
125 if err := handleFile(w, path, cfg); err != nil {
126 return err
127 }
128 }
129
130 if len(paths) == 0 {
131 if err := handleLines(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 handleLines(w, f, cfg)
145 }
146
147 func handleLines(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.skip > 0 {
154 cfg.skip--
155 continue
156 }
157
158 w.Write(sc.Bytes())
159 if err := w.WriteByte('\n'); err != nil {
160 return io.EOF
161 }
162
163 if !cfg.liveLines {
164 continue
165 }
166
167 if w.Flush() != nil {
168 return io.EOF
169 }
170 }
171
172 return sc.Err()
173 }
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(err.Error())
118 os.Stderr.WriteString("\n")
119 os.Exit(1)
120 return
121 }
122 }
123
124 type config struct {
125 w *bufio.Writer
126 live bool
127 }
128
129 func run(paths []string, rb *ringBuffer, live bool) error {
130 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
131 defer bw.Flush()
132
133 var cfg config
134 cfg.w = bw
135 cfg.live = live
136
137 for _, path := range paths {
138 if err := handleFile(path, rb, cfg); err != nil {
139 return err
140 }
141 }
142
143 if len(paths) == 0 {
144 if err := visit(os.Stdin, rb, cfg); err != nil {
145 return err
146 }
147 }
148 return nil
149 }
150
151 func handleFile(path string, rb *ringBuffer, cfg config) error {
152 f, err := os.Open(path)
153 if err != nil {
154 return err
155 }
156 defer f.Close()
157 return visit(f, rb, cfg)
158 }
159
160 func visit(r io.Reader, rb *ringBuffer, cfg config) error {
161 const gb = 1024 * 1024 * 1024
162 sc := bufio.NewScanner(r)
163 sc.Buffer(nil, 8*gb)
164
165 for sc.Scan() {
166 prev := rb.append(sc.Text())
167 if len(rb.items) < rb.max {
168 continue
169 }
170
171 cfg.w.WriteString(prev)
172
173 if cfg.w.WriteByte('\n') != nil {
174 return io.EOF
175 }
176
177 if !cfg.live {
178 continue
179 }
180
181 if cfg.w.Flush() != nil {
182 return io.EOF
183 }
184 }
185
186 return sc.Err()
187 }
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(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.NewWriter(w)
80 defer bw.Flush()
81
82 if len(args) == 0 {
83 return squeeze(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 squeeze(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 squeeze(w, f, live)
106 }
107
108 func squeeze(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
113 for i := 0; sc.Scan(); i++ {
114 s := sc.Bytes()
115 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
116 s = s[3:]
117 }
118
119 writeSqueezed(w, s)
120 if w.WriteByte('\n') != nil {
121 return io.EOF
122 }
123
124 if !live {
125 continue
126 }
127
128 if w.Flush() != nil {
129 return io.EOF
130 }
131 }
132
133 return sc.Err()
134 }
135
136 func writeSqueezed(w *bufio.Writer, s []byte) {
137 // ignore leading spaces
138 for len(s) > 0 && s[0] == ' ' {
139 s = s[1:]
140 }
141
142 // ignore trailing spaces
143 for len(s) > 0 && s[len(s)-1] == ' ' {
144 s = s[:len(s)-1]
145 }
146
147 space := false
148
149 for len(s) > 0 {
150 switch s[0] {
151 case ' ':
152 s = s[1:]
153 space = true
154
155 case '\t':
156 s = s[1:]
157 space = false
158 for len(s) > 0 && s[0] == ' ' {
159 s = s[1:]
160 }
161 w.WriteByte('\t')
162
163 default:
164 if space {
165 w.WriteByte(' ')
166 space = false
167 }
168 w.WriteByte(s[0])
169 s = s[1:]
170 }
171 }
172 }
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(err.Error())
87 os.Stderr.WriteString("\n")
88 os.Exit(1)
89 return
90 }
91 }
92
93 func run(w io.Writer, args []string, cfg config) error {
94 bw := bufio.NewWriter(w)
95 defer bw.Flush()
96
97 if len(args) == 0 {
98 return squomp(bw, os.Stdin, &cfg)
99 }
100
101 for _, name := range args {
102 if err := handleFile(bw, name, &cfg); err != nil {
103 return err
104 }
105 }
106 return nil
107 }
108
109 func handleFile(w *bufio.Writer, name string, cfg *config) error {
110 if name == `` || name == `-` {
111 return squomp(w, os.Stdin, cfg)
112 }
113
114 f, err := os.Open(name)
115 if err != nil {
116 return errors.New(`can't read from file named "` + name + `"`)
117 }
118 defer f.Close()
119
120 return squomp(w, f, cfg)
121 }
122
123 func squomp(w *bufio.Writer, r io.Reader, cfg *config) error {
124 const gb = 1024 * 1024 * 1024
125 sc := bufio.NewScanner(r)
126 sc.Buffer(nil, 8*gb)
127
128 for i := 0; sc.Scan(); i++ {
129 s := sc.Bytes()
130 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
131 s = s[3:]
132 }
133
134 // trim leading spaces, so the empty-length check which follows can
135 // also detect/ignore empty-ish lines, that is lines with only spaces
136 for len(s) > 0 && s[0] == ' ' {
137 s = s[1:]
138 }
139
140 if len(s) == 0 {
141 if cfg.nonEmpty == 0 {
142 continue
143 }
144 cfg.emptyRun++
145 }
146
147 if cfg.emptyRun > 0 {
148 cfg.emptyRun = 0
149 if w.WriteByte('\n') != nil {
150 return io.EOF
151 }
152 }
153
154 cfg.nonEmpty++
155
156 writeSqueezed(w, s)
157 if w.WriteByte('\n') != nil {
158 return io.EOF
159 }
160
161 if !cfg.liveLines {
162 continue
163 }
164
165 if w.Flush() != nil {
166 return io.EOF
167 }
168 }
169
170 return sc.Err()
171 }
172
173 func writeSqueezed(w *bufio.Writer, s []byte) {
174 // ignore leading spaces
175 for len(s) > 0 && s[0] == ' ' {
176 s = s[1:]
177 }
178
179 // ignore trailing spaces
180 for len(s) > 0 && s[len(s)-1] == ' ' {
181 s = s[:len(s)-1]
182 }
183
184 space := false
185
186 for len(s) > 0 {
187 switch s[0] {
188 case ' ':
189 s = s[1:]
190 space = true
191
192 case '\t':
193 s = s[1:]
194 space = false
195 for len(s) > 0 && s[0] == ' ' {
196 s = s[1:]
197 }
198 w.WriteByte('\t')
199
200 default:
201 if space {
202 w.WriteByte(' ')
203 space = false
204 }
205 w.WriteByte(s[0])
206 s = s[1:]
207 }
208 }
209 }
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(err.Error())
85 os.Stderr.WriteString("\n")
86 os.Exit(1)
87 return
88 }
89 }
90
91 func run(w io.Writer, args []string, cfg config) error {
92 bw := bufio.NewWriter(w)
93 defer bw.Flush()
94
95 dashes := 0
96 for _, name := range args {
97 if name == `-` {
98 dashes++
99 }
100 if dashes > 1 {
101 return errors.New(`can't read stdin (dash) more than once`)
102 }
103 }
104
105 if len(args) == 0 {
106 return stomp(bw, os.Stdin, &cfg)
107 }
108
109 for _, name := range args {
110 if err := handleFile(bw, name, &cfg); err != nil {
111 return err
112 }
113 }
114 return nil
115 }
116
117 func handleFile(w *bufio.Writer, name string, cfg *config) error {
118 if name == `` || name == `-` {
119 return stomp(w, os.Stdin, cfg)
120 }
121
122 f, err := os.Open(name)
123 if err != nil {
124 return errors.New(`can't read from file named "` + name + `"`)
125 }
126 defer f.Close()
127
128 return stomp(w, f, cfg)
129 }
130
131 func stomp(w *bufio.Writer, r io.Reader, cfg *config) error {
132 const gb = 1024 * 1024 * 1024
133 sc := bufio.NewScanner(r)
134 sc.Buffer(nil, 8*gb)
135
136 for i := 0; sc.Scan(); i++ {
137 s := sc.Bytes()
138 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
139 s = s[3:]
140 }
141
142 if len(s) == 0 {
143 if cfg.nonEmpty == 0 {
144 continue
145 }
146 cfg.emptyRun++
147 }
148
149 if cfg.emptyRun > 0 {
150 cfg.emptyRun = 0
151 if w.WriteByte('\n') != nil {
152 return io.EOF
153 }
154 }
155
156 cfg.nonEmpty++
157
158 w.Write(s)
159 if w.WriteByte('\n') != nil {
160 return io.EOF
161 }
162
163 if !cfg.liveLines {
164 continue
165 }
166
167 if w.Flush() != nil {
168 return io.EOF
169 }
170 }
171
172 return sc.Err()
173 }
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(err.Error())
61 os.Stderr.WriteString("\n")
62 os.Exit(1)
63 return
64 }
65 }
66
67 func run(args []string) error {
68 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
69 defer bw.Flush()
70
71 if err := showSymbols(bw, args); err != nil {
72 return err
73 }
74 return nil
75 }
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 faces = `` +
37 `😀😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏😐😑😒😓` +
38 `😔😕😖😗😘😙😚😛😜😝😞😟😠😡😢😣😤😥😦😧` +
39 `😨😩😪😫😬😭😮😯😰😱😲😳😴😵😶😷🙁🙂🙃🙄` +
40 `🧐👶🤓🤐🤑🤒🤔🤕🤗🤠🤡🤢🤣🤤🤥🤧🤨🤩🤪🤫` +
41 `🤬🤭🤮🤯`
42 )
43
44 // `crlf`: "\r\n",
45 // `lf`: "\n",
46 // `linefeed`: "\n",
47 // `tab`: "\t",
48
49 // names2symbols translates common names for common symbols
50 var names2symbols = map[string]string{
51 `adash`: `-`,
52 `amp`: `&`,
53 `ampersand`: `&`,
54 `apostrophe`: `’`,
55 `ast`: `*`,
56 `asterisk`: `*`,
57 `backquote`: "`",
58 `backslash`: `\`,
59 `ball`: `●`,
60 `bang`: `!`,
61 `block`: `█`,
62 `bquote`: "`",
63 `bslash`: `\`,
64 `bullet`: `•`,
65 `caret`: `^`,
66 `colon`: `:`,
67 `comma`: `,`,
68 `commaspace`: `, `,
69 `cquote`: `”`,
70 `cdot`: `·`,
71 `circle`: `●`,
72 `cloud`: `☁️`,
73 `copyright`: `©`,
74 `cross`: `×`,
75 `dash`: `–`,
76 `dollar`: `$`,
77 `dot`: `.`,
78 `dquote`: `"`,
79 `ellip`: `…`,
80 `ellipsis`: `…`,
81 `eq`: `=`,
82 `equal`: `=`,
83 `equals`: `=`,
84 `euro`: `€`,
85 `excl`: `!`,
86 `happy`: `😀`,
87 `hash`: `#`,
88 `heart`: `❤️`,
89 `hellip`: `…`,
90 `hole`: `○`,
91 `lightning`: `🌩️`,
92 `mdot`: `·`,
93 `minus`: `-`,
94 `moon`: `🌕`,
95 `oquote`: `“`,
96 `space`: ` `,
97 `percent`: `%`,
98 `pipe`: `|`,
99 `plus`: `+`,
100 `rain`: `🌧️`,
101 `semi`: `;`,
102 `semicolon`: `;`,
103 `sharp`: `#`,
104 `shit`: `💩`,
105 `slash`: `/`,
106 `slasher`: `⧸`,
107 `smile`: `🙂`,
108 `snow`: `❄️`,
109 `square`: `■`,
110 `squote`: `'`,
111 `star`: `⭐`,
112 `sun`: `☀️`,
113 `tilde`: `~`,
114 `underscore`: `_`,
115 `vbar`: `|`,
116
117 `digits`: `0123456789`,
118 `faces`: faces,
119 `greek`: `ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩαβγδεζηθικλμνξοπρστυφχψω`,
120 `infinity`: `∞`,
121 `latin`: `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`,
122 `letters`: `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`,
123 `lower`: `abcdefghijklmnopqrstuvwxyz`,
124 `loweralpha`: `α`,
125 `lowerbeta`: `β`,
126 `lowerdelta`: `δ`,
127 `lowerepsilon`: `ε`,
128 `lowergamma`: `γ`,
129 `lowerhex`: `0123456789abcdef`,
130 `lowerlambda`: `λ`,
131 `lowerlatin`: `abcdefghijklmnopqrstuvwxyz`,
132 `loweromega`: `ω`,
133 `lowerpi`: `π`,
134 `lowersigma`: `σ`,
135 `lowertau`: `τ`,
136 `lowertheta`: `θ`,
137 `lowergreek`: `αβγδεζηθικλμνξοπρστυφχψω`,
138 `math`: `+-×÷²³±`,
139 `other`: `✓✗✔❌`,
140 `plusminus`: `±`,
141 `product`: `Π`,
142 `punctuation`: `!"#$%&'()*+,-./:;<=>?@[\]^_` + "`" + `{|}~`,
143 `sum`: `Σ`,
144 `upper`: `ABCDEFGHIJKLMNOPQRSTUVWXYZ`,
145 `uppergreek`: `ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ`,
146 `upperhex`: `0123456789ABCDEF`,
147 `upperlatin`: `ABCDEFGHIJKLMNOPQRSTUVWXYZ`,
148
149 `eu`: `🇪🇺`,
150 `eur`: `🇪🇺`,
151
152 `af`: `🇦🇫`,
153 `ax`: `🇦🇽`,
154 `al`: `🇦🇱`,
155 `dz`: `🇩🇿`,
156 `as`: `🇦🇸`,
157 `ad`: `🇦🇩`,
158 `ao`: `🇦🇴`,
159 `ai`: `🇦🇮`,
160 `aq`: `🇦🇶`,
161 `ag`: `🇦🇬`,
162 `ar`: `🇦🇷`,
163 `am`: `🇦🇲`,
164 `aw`: `🇦🇼`,
165 `au`: `🇦🇺`,
166 `at`: `🇦🇹`,
167 `az`: `🇦🇿`,
168 `bs`: `🇧🇸`,
169 `bh`: `🇧🇭`,
170 `bd`: `🇧🇩`,
171 `bb`: `🇧🇧`,
172 `by`: `🇧🇾`,
173 `be`: `🇧🇪`,
174 `bz`: `🇧🇿`,
175 `bj`: `🇧🇯`,
176 `bm`: `🇧🇲`,
177 `bt`: `🇧🇹`,
178 `bo`: `🇧🇴`,
179 `bq`: `🇧🇶`,
180 `ba`: `🇧🇦`,
181 `bw`: `🇧🇼`,
182 `bv`: `🇧🇻`,
183 `br`: `🇧🇷`,
184 `io`: `🇮🇴`,
185 `bn`: `🇧🇳`,
186 `bg`: `🇧🇬`,
187 `bf`: `🇧🇫`,
188 `bi`: `🇧🇮`,
189 `cv`: `🇨🇻`,
190 `kh`: `🇰🇭`,
191 `cm`: `🇨🇲`,
192 `ca`: `🇨🇦`,
193 `ky`: `🇰🇾`,
194 `cf`: `🇨🇫`,
195 `td`: `🇹🇩`,
196 `cl`: `🇨🇱`,
197 `cn`: `🇨🇳`,
198 `cx`: `🇨🇽`,
199 `cc`: `🇨🇨`,
200 `co`: `🇨🇴`,
201 `km`: `🇰🇲`,
202 `cd`: `🇨🇩`,
203 `cg`: `🇨🇬`,
204 `ck`: `🇨🇰`,
205 `cr`: `🇨🇷`,
206 `ci`: `🇨🇮`,
207 `hr`: `🇭🇷`,
208 `cu`: `🇨🇺`,
209 `cw`: `🇨🇼`,
210 `cy`: `🇨🇾`,
211 `cz`: `🇨🇿`,
212 `dk`: `🇩🇰`,
213 `dj`: `🇩🇯`,
214 `dm`: `🇩🇲`,
215 `do`: `🇩🇴`,
216 `ec`: `🇪🇨`,
217 `eg`: `🇪🇬`,
218 `sv`: `🇸🇻`,
219 `gq`: `🇬🇶`,
220 `er`: `🇪🇷`,
221 `ee`: `🇪🇪`,
222 `sz`: `🇸🇿`,
223 `et`: `🇪🇹`,
224 `fk`: `🇫🇰`,
225 `fo`: `🇫🇴`,
226 `fj`: `🇫🇯`,
227 `fi`: `🇫🇮`,
228 `fr`: `🇫🇷`,
229 `gf`: `🇬🇫`,
230 `pf`: `🇵🇫`,
231 `tf`: `🇹🇫`,
232 `ga`: `🇬🇦`,
233 `gm`: `🇬🇲`,
234 `ge`: `🇬🇪`,
235 `de`: `🇩🇪`,
236 `gh`: `🇬🇭`,
237 `gi`: `🇬🇮`,
238 `gr`: `🇬🇷`,
239 `gl`: `🇬🇱`,
240 `gd`: `🇬🇩`,
241 `gp`: `🇬🇵`,
242 `gu`: `🇬🇺`,
243 `gt`: `🇬🇹`,
244 `gg`: `🇬🇬`,
245 `gn`: `🇬🇳`,
246 `gw`: `🇬🇼`,
247 `gy`: `🇬🇾`,
248 `ht`: `🇭🇹`,
249 `hm`: `🇭🇲`,
250 `va`: `🇻🇦`,
251 `hn`: `🇭🇳`,
252 `hk`: `🇭🇰`,
253 `hu`: `🇭🇺`,
254 `is`: `🇮🇸`,
255 `in`: `🇮🇳`,
256 `id`: `🇮🇩`,
257 `ir`: `🇮🇷`,
258 `iq`: `🇮🇶`,
259 `ie`: `🇮🇪`,
260 `im`: `🇮🇲`,
261 `il`: `🇮🇱`,
262 `it`: `🇮🇹`,
263 `jm`: `🇯🇲`,
264 `jp`: `🇯🇵`,
265 `je`: `🇯🇪`,
266 `jo`: `🇯🇴`,
267 `kz`: `🇰🇿`,
268 `ke`: `🇰🇪`,
269 `ki`: `🇰🇮`,
270 `kp`: `🇰🇵`,
271 `kr`: `🇰🇷`,
272 `kw`: `🇰🇼`,
273 `kg`: `🇰🇬`,
274 `la`: `🇱🇦`,
275 `lv`: `🇱🇻`,
276 `lb`: `🇱🇧`,
277 `ls`: `🇱🇸`,
278 `lr`: `🇱🇷`,
279 `ly`: `🇱🇾`,
280 `li`: `🇱🇮`,
281 `lt`: `🇱🇹`,
282 `lu`: `🇱🇺`,
283 `mo`: `🇲🇴`,
284 `mk`: `🇲🇰`,
285 `mg`: `🇲🇬`,
286 `mw`: `🇲🇼`,
287 `my`: `🇲🇾`,
288 `mv`: `🇲🇻`,
289 `ml`: `🇲🇱`,
290 `mt`: `🇲🇹`,
291 `mh`: `🇲🇭`,
292 `mq`: `🇲🇶`,
293 `mr`: `🇲🇷`,
294 `mu`: `🇲🇺`,
295 `yt`: `🇾🇹`,
296 `mx`: `🇲🇽`,
297 `fm`: `🇫🇲`,
298 `md`: `🇲🇩`,
299 `mc`: `🇲🇨`,
300 `mn`: `🇲🇳`,
301 `me`: `🇲🇪`,
302 `ms`: `🇲🇸`,
303 `ma`: `🇲🇦`,
304 `mz`: `🇲🇿`,
305 `mm`: `🇲🇲`,
306 `na`: `🇳🇦`,
307 `nr`: `🇳🇷`,
308 `np`: `🇳🇵`,
309 `nl`: `🇳🇱`,
310 `nc`: `🇳🇨`,
311 `nz`: `🇳🇿`,
312 `ni`: `🇳🇮`,
313 `ne`: `🇳🇪`,
314 `ng`: `🇳🇬`,
315 `nu`: `🇳🇺`,
316 `nf`: `🇳🇫`,
317 `mp`: `🇲🇵`,
318 `no`: `🇳🇴`,
319 `om`: `🇴🇲`,
320 `pk`: `🇵🇰`,
321 `pw`: `🇵🇼`,
322 `ps`: `🇵🇸`,
323 `pa`: `🇵🇦`,
324 `pg`: `🇵🇬`,
325 `py`: `🇵🇾`,
326 `pe`: `🇵🇪`,
327 `ph`: `🇵🇭`,
328 `pn`: `🇵🇳`,
329 `pl`: `🇵🇱`,
330 `pt`: `🇵🇹`,
331 `pr`: `🇵🇷`,
332 `qa`: `🇶🇦`,
333 `re`: `🇷🇪`,
334 `ro`: `🇷🇴`,
335 `ru`: `🇷🇺`,
336 `rw`: `🇷🇼`,
337 `bl`: `🇧🇱`,
338 `sh`: `🇸🇭`,
339 `kn`: `🇰🇳`,
340 `lc`: `🇱🇨`,
341 `mf`: `🇲🇫`,
342 `pm`: `🇵🇲`,
343 `vc`: `🇻🇨`,
344 `ws`: `🇼🇸`,
345 `sm`: `🇸🇲`,
346 `st`: `🇸🇹`,
347 `sa`: `🇸🇦`,
348 `sn`: `🇸🇳`,
349 `rs`: `🇷🇸`,
350 `sc`: `🇸🇨`,
351 `sl`: `🇸🇱`,
352 `sg`: `🇸🇬`,
353 `sx`: `🇸🇽`,
354 `sk`: `🇸🇰`,
355 `si`: `🇸🇮`,
356 `sb`: `🇸🇧`,
357 `so`: `🇸🇴`,
358 `za`: `🇿🇦`,
359 `gs`: `🇬🇸`,
360 `ss`: `🇸🇸`,
361 `es`: `🇪🇸`,
362 `lk`: `🇱🇰`,
363 `sd`: `🇸🇩`,
364 `sr`: `🇸🇷`,
365 `sj`: `🇸🇯`,
366 `se`: `🇸🇪`,
367 `ch`: `🇨🇭`,
368 `sy`: `🇸🇾`,
369 `tw`: `🇹🇼`,
370 `tj`: `🇹🇯`,
371 `tz`: `🇹🇿`,
372 `th`: `🇹🇭`,
373 `tl`: `🇹🇱`,
374 `tg`: `🇹🇬`,
375 `tk`: `🇹🇰`,
376 `to`: `🇹🇴`,
377 `tt`: `🇹🇹`,
378 `tn`: `🇹🇳`,
379 `tr`: `🇹🇷`,
380 `tm`: `🇹🇲`,
381 `tc`: `🇹🇨`,
382 `tv`: `🇹🇻`,
383 `ug`: `🇺🇬`,
384 `ua`: `🇺🇦`,
385 `ae`: `🇦🇪`,
386 `gb`: `🇬🇧`,
387 `um`: `🇺🇲`,
388 `us`: `🇺🇸`,
389 `uy`: `🇺🇾`,
390 `uz`: `🇺🇿`,
391 `vu`: `🇻🇺`,
392 `ve`: `🇻🇪`,
393 `vn`: `🇻🇳`,
394 `vg`: `🇻🇬`,
395 `vi`: `🇻🇮`,
396 `wf`: `🇼🇫`,
397 `eh`: `🇪🇭`,
398 `ye`: `🇾🇪`,
399 `zm`: `🇿🇲`,
400 `zw`: `🇿🇼`,
401
402 `afg`: `🇦🇫`,
403 `ala`: `🇦🇽`,
404 `alb`: `🇦🇱`,
405 `dza`: `🇩🇿`,
406 `asm`: `🇦🇸`,
407 `and`: `🇦🇩`,
408 `ago`: `🇦🇴`,
409 `aia`: `🇦🇮`,
410 `ata`: `🇦🇶`,
411 `atg`: `🇦🇬`,
412 `arg`: `🇦🇷`,
413 `arm`: `🇦🇲`,
414 `abw`: `🇦🇼`,
415 `aus`: `🇦🇺`,
416 `aut`: `🇦🇹`,
417 `aze`: `🇦🇿`,
418 `bhs`: `🇧🇸`,
419 `bhr`: `🇧🇭`,
420 `bgd`: `🇧🇩`,
421 `brb`: `🇧🇧`,
422 `blr`: `🇧🇾`,
423 `bel`: `🇧🇪`,
424 `blz`: `🇧🇿`,
425 `ben`: `🇧🇯`,
426 `bmu`: `🇧🇲`,
427 `btn`: `🇧🇹`,
428 `bol`: `🇧🇴`,
429 `bes`: `🇧🇶`,
430 `bih`: `🇧🇦`,
431 `bwa`: `🇧🇼`,
432 `bvt`: `🇧🇻`,
433 `bra`: `🇧🇷`,
434 `iot`: `🇮🇴`,
435 `brn`: `🇧🇳`,
436 `bgr`: `🇧🇬`,
437 `bfa`: `🇧🇫`,
438 `bdi`: `🇧🇮`,
439 `cpv`: `🇨🇻`,
440 `khm`: `🇰🇭`,
441 `cmr`: `🇨🇲`,
442 `can`: `🇨🇦`,
443 `cym`: `🇰🇾`,
444 `caf`: `🇨🇫`,
445 `tcd`: `🇹🇩`,
446 `chl`: `🇨🇱`,
447 `chn`: `🇨🇳`,
448 `cxr`: `🇨🇽`,
449 `cck`: `🇨🇨`,
450 `col`: `🇨🇴`,
451 `com`: `🇰🇲`,
452 `cod`: `🇨🇩`,
453 `cog`: `🇨🇬`,
454 `cok`: `🇨🇰`,
455 `cri`: `🇨🇷`,
456 `civ`: `🇨🇮`,
457 `hrv`: `🇭🇷`,
458 `cub`: `🇨🇺`,
459 `cuw`: `🇨🇼`,
460 `cyp`: `🇨🇾`,
461 `cze`: `🇨🇿`,
462 `dnk`: `🇩🇰`,
463 `dji`: `🇩🇯`,
464 `dma`: `🇩🇲`,
465 `dom`: `🇩🇴`,
466 `ecu`: `🇪🇨`,
467 `egy`: `🇪🇬`,
468 `slv`: `🇸🇻`,
469 `gnq`: `🇬🇶`,
470 `eri`: `🇪🇷`,
471 `est`: `🇪🇪`,
472 `swz`: `🇸🇿`,
473 `eth`: `🇪🇹`,
474 `flk`: `🇫🇰`,
475 `fro`: `🇫🇴`,
476 `fji`: `🇫🇯`,
477 `fin`: `🇫🇮`,
478 `fra`: `🇫🇷`,
479 `guf`: `🇬🇫`,
480 `pyf`: `🇵🇫`,
481 `atf`: `🇹🇫`,
482 `gab`: `🇬🇦`,
483 `gmb`: `🇬🇲`,
484 `geo`: `🇬🇪`,
485 `deu`: `🇩🇪`,
486 `gha`: `🇬🇭`,
487 `gib`: `🇬🇮`,
488 `grc`: `🇬🇷`,
489 `grl`: `🇬🇱`,
490 `grd`: `🇬🇩`,
491 `glp`: `🇬🇵`,
492 `gum`: `🇬🇺`,
493 `gtm`: `🇬🇹`,
494 `ggy`: `🇬🇬`,
495 `gin`: `🇬🇳`,
496 `gnb`: `🇬🇼`,
497 `guy`: `🇬🇾`,
498 `hti`: `🇭🇹`,
499 `hmd`: `🇭🇲`,
500 `vat`: `🇻🇦`,
501 `hnd`: `🇭🇳`,
502 `hkg`: `🇭🇰`,
503 `hun`: `🇭🇺`,
504 `isl`: `🇮🇸`,
505 `ind`: `🇮🇳`,
506 `idn`: `🇮🇩`,
507 `irn`: `🇮🇷`,
508 `irq`: `🇮🇶`,
509 `irl`: `🇮🇪`,
510 `imn`: `🇮🇲`,
511 `isr`: `🇮🇱`,
512 `ita`: `🇮🇹`,
513 `jam`: `🇯🇲`,
514 `jpn`: `🇯🇵`,
515 `jey`: `🇯🇪`,
516 `jor`: `🇯🇴`,
517 `kaz`: `🇰🇿`,
518 `ken`: `🇰🇪`,
519 `kir`: `🇰🇮`,
520 `prk`: `🇰🇵`,
521 `kor`: `🇰🇷`,
522 `kwt`: `🇰🇼`,
523 `kgz`: `🇰🇬`,
524 `lao`: `🇱🇦`,
525 `lva`: `🇱🇻`,
526 `lbn`: `🇱🇧`,
527 `lso`: `🇱🇸`,
528 `lbr`: `🇱🇷`,
529 `lby`: `🇱🇾`,
530 `lie`: `🇱🇮`,
531 `ltu`: `🇱🇹`,
532 `lux`: `🇱🇺`,
533 `mac`: `🇲🇴`,
534 `mkd`: `🇲🇰`,
535 `mdg`: `🇲🇬`,
536 `mwi`: `🇲🇼`,
537 `mys`: `🇲🇾`,
538 `mdv`: `🇲🇻`,
539 `mli`: `🇲🇱`,
540 `mlt`: `🇲🇹`,
541 `mhl`: `🇲🇭`,
542 `mtq`: `🇲🇶`,
543 `mrt`: `🇲🇷`,
544 `mus`: `🇲🇺`,
545 `myt`: `🇾🇹`,
546 `mex`: `🇲🇽`,
547 `fsm`: `🇫🇲`,
548 `mda`: `🇲🇩`,
549 `mco`: `🇲🇨`,
550 `mng`: `🇲🇳`,
551 `mne`: `🇲🇪`,
552 `msr`: `🇲🇸`,
553 `mar`: `🇲🇦`,
554 `moz`: `🇲🇿`,
555 `mmr`: `🇲🇲`,
556 `nam`: `🇳🇦`,
557 `nru`: `🇳🇷`,
558 `npl`: `🇳🇵`,
559 `nld`: `🇳🇱`,
560 `ncl`: `🇳🇨`,
561 `nzl`: `🇳🇿`,
562 `nic`: `🇳🇮`,
563 `ner`: `🇳🇪`,
564 `nga`: `🇳🇬`,
565 `niu`: `🇳🇺`,
566 `nfk`: `🇳🇫`,
567 `mnp`: `🇲🇵`,
568 `nor`: `🇳🇴`,
569 `omn`: `🇴🇲`,
570 `pak`: `🇵🇰`,
571 `plw`: `🇵🇼`,
572 `pse`: `🇵🇸`,
573 `pan`: `🇵🇦`,
574 `png`: `🇵🇬`,
575 `pry`: `🇵🇾`,
576 `per`: `🇵🇪`,
577 `phl`: `🇵🇭`,
578 `pcn`: `🇵🇳`,
579 `pol`: `🇵🇱`,
580 `prt`: `🇵🇹`,
581 `pri`: `🇵🇷`,
582 `qat`: `🇶🇦`,
583 `reu`: `🇷🇪`,
584 `rou`: `🇷🇴`,
585 `rus`: `🇷🇺`,
586 `rwa`: `🇷🇼`,
587 `blm`: `🇧🇱`,
588 `shn`: `🇸🇭`,
589 `kna`: `🇰🇳`,
590 `lca`: `🇱🇨`,
591 `maf`: `🇲🇫`,
592 `spm`: `🇵🇲`,
593 `vct`: `🇻🇨`,
594 `wsm`: `🇼🇸`,
595 `smr`: `🇸🇲`,
596 `stp`: `🇸🇹`,
597 `sau`: `🇸🇦`,
598 `sen`: `🇸🇳`,
599 `srb`: `🇷🇸`,
600 `syc`: `🇸🇨`,
601 `sle`: `🇸🇱`,
602 `sgp`: `🇸🇬`,
603 `sxm`: `🇸🇽`,
604 `svk`: `🇸🇰`,
605 `svn`: `🇸🇮`,
606 `slb`: `🇸🇧`,
607 `som`: `🇸🇴`,
608 `zaf`: `🇿🇦`,
609 `sgs`: `🇬🇸`,
610 `ssd`: `🇸🇸`,
611 `esp`: `🇪🇸`,
612 `lka`: `🇱🇰`,
613 `sdn`: `🇸🇩`,
614 `sur`: `🇸🇷`,
615 `sjm`: `🇸🇯`,
616 `swe`: `🇸🇪`,
617 `che`: `🇨🇭`,
618 `syr`: `🇸🇾`,
619 `twn`: `🇹🇼`,
620 `tjk`: `🇹🇯`,
621 `tza`: `🇹🇿`,
622 `tha`: `🇹🇭`,
623 `tls`: `🇹🇱`,
624 `tgo`: `🇹🇬`,
625 `tkl`: `🇹🇰`,
626 `ton`: `🇹🇴`,
627 `tto`: `🇹🇹`,
628 `tun`: `🇹🇳`,
629 `tur`: `🇹🇷`,
630 `tkm`: `🇹🇲`,
631 `tca`: `🇹🇨`,
632 `tuv`: `🇹🇻`,
633 `uga`: `🇺🇬`,
634 `ukr`: `🇺🇦`,
635 `are`: `🇦🇪`,
636 `gbr`: `🇬🇧`,
637 `uae`: `🇦🇪`,
638 `umi`: `🇺🇲`,
639 `usa`: `🇺🇸`,
640 `ury`: `🇺🇾`,
641 `uzb`: `🇺🇿`,
642 `vut`: `🇻🇺`,
643 `ven`: `🇻🇪`,
644 `vnm`: `🇻🇳`,
645 `vgb`: `🇻🇬`,
646 `vir`: `🇻🇮`,
647 `wlf`: `🇼🇫`,
648 `esh`: `🇪🇭`,
649 `yem`: `🇾🇪`,
650 `zmb`: `🇿🇲`,
651 `zwe`: `🇿🇼`,
652 }
653
654 // symbolAliases resolves aliases for keys used in table name2symbols
655 var symbolAliases = map[string]string{
656 // greek symbols
657 `alpha`: `loweralpha`,
658 `beta`: `lowerbeta`,
659 `delta`: `lowerdelta`,
660 `epsilon`: `lowerepsilon`,
661 `gamma`: `lowergamma`,
662 `lambda`: `lowerlambda`,
663 `omega`: `loweromega`,
664 `pi`: `lowerpi`,
665 `sigma`: `lowersigma`,
666 `tau`: `lowertau`,
667 `theta`: `lowertheta`,
668
669 // math symbols
670 `inf`: `infinity`,
671 `infty`: `infinity`,
672 `summation`: `sum`,
673
674 // punctuation symbols
675 `apos`: `apostrophe`,
676 `bquo`: `bquote`,
677 `dquo`: `dquote`,
678 `squo`: `squote`,
679 `punct`: `punctuation`,
680
681 // country flags
682 `afghanistan`: `af`,
683 `america`: `us`,
684 `algeria`: `dz`,
685 `angola`: `ao`,
686 `arabia`: `sa`,
687 `arabemirates`: `ae`,
688 `argentina`: `ar`,
689 `australia`: `au`,
690 `austria`: `at`,
691 `bangladesh`: `bd`,
692 `belgium`: `be`,
693 `brazil`: `br`,
694 `britain`: `gb`,
695 `canada`: `ca`,
696 `chile`: `cl`,
697 `china`: `cn`,
698 `colombia`: `co`,
699 `czechia`: `cz`,
700 `czechrepublic`: `cz`,
701 `denmark`: `dk`,
702 `dominicanrepublic`: `do`,
703 `drc`: `cd`,
704 `drcongo`: `cd`,
705 `ecuador`: `ec`,
706 `egypt`: `eg`,
707 `emirates`: `ae`,
708 `england`: `gb`,
709 `ethiopia`: `et`,
710 `finland`: `fi`,
711 `france`: `fr`,
712 `germany`: `de`,
713 `ghana`: `gh`,
714 `greatbritain`: `gb`,
715 `greece`: `gr`,
716 `holland`: `nl`,
717 `hungary`: `hu`,
718 `india`: `in`,
719 `indonesia`: `id`,
720 `iran`: `ir`,
721 `iraq`: `iq`,
722 `ireland`: `ie`,
723 `israel`: `il`,
724 `italy`: `it`,
725 `japan`: `jp`,
726 `kazakhstan`: `kz`,
727 `kenya`: `ke`,
728 `korea`: `kr`,
729 `kuwait`: `kw`,
730 `madagascar`: `mg`,
731 `malaysia`: `my`,
732 `mexico`: `mx`,
733 `morocco`: `ma`,
734 `mozambique`: `mz`,
735 `myanmar`: `mm`,
736 `nepal`: `np`,
737 `netherlands`: `nl`,
738 `newzealand`: `nz`,
739 `nigeria`: `ng`,
740 `northkorea`: `kp`,
741 `norway`: `no`,
742 `pakistan`: `pk`,
743 `peru`: `pe`,
744 `philippines`: `ph`,
745 `poland`: `pl`,
746 `portugal`: `pt`,
747 `qatar`: `qa`,
748 `rok`: `kr`,
749 `romania`: `ro`,
750 `russia`: `ru`,
751 `saudiarabia`: `sa`,
752 `singapore`: `sg`,
753 `somalia`: `so`,
754 `southafrica`: `za`,
755 `southkorea`: `kr`,
756 `spain`: `es`,
757 `srilanka`: `lk`,
758 `sudan`: `sd`,
759 `sweden`: `se`,
760 `switzerland`: `ch`,
761 `taiwan`: `tw`,
762 `tanzania`: `tz`,
763 `thailand`: `th`,
764 `turkey`: `tr`,
765 `uganda`: `ug`,
766 `ukraine`: `ua`,
767 `unitedarabemirates`: `ae`,
768 `unitedemirates`: `ae`,
769 `unitedkingdom`: `gb`,
770 `unitedstates`: `us`,
771 `uzbekistan`: `uz`,
772 `yemen`: `ye`,
773 `venezuela`: `ve`,
774 `vietnam`: `vn`,
775 }
776
777 func showSymbols(w *bufio.Writer, names []string) error {
778 if len(names) == 0 {
779 return showAllSymbols(w)
780 }
781
782 for _, name := range names {
783 // normalize symbol names
784 name = strings.TrimSpace(name)
785 name = strings.ToLower(name)
786 name = strings.ReplaceAll(name, ` `, ``)
787 name = strings.ReplaceAll(name, `-`, ``)
788 name = strings.ReplaceAll(name, `_`, ``)
789
790 if len(name) == 0 {
791 return errors.New(`no symbol name given`)
792 }
793
794 // handle regular symbol-name lookups
795 s := name
796 if alias, ok := symbolAliases[name]; ok {
797 s = alias
798 }
799 sym, ok := names2symbols[s]
800 if !ok {
801 return errors.New(`no symbol named ` + name + ` found`)
802 }
803
804 w.WriteString(sym)
805 if w.WriteByte('\n') != nil {
806 return io.EOF
807 }
808 }
809
810 return nil
811 }
812
813 func showAllSymbols(w *bufio.Writer) error {
814 var keys []string
815 got := make(map[string]struct{}, len(names2symbols)+len(symbolAliases))
816
817 for k := range names2symbols {
818 got[k] = struct{}{}
819 keys = append(keys, k)
820 }
821
822 for k := range symbolAliases {
823 if _, ok := got[k]; ok {
824 continue
825 }
826 got[k] = struct{}{}
827 keys = append(keys, k)
828 }
829
830 sort.Strings(keys)
831
832 for _, s := range keys {
833 k := s
834 if alias, ok := symbolAliases[k]; ok {
835 k = alias
836 }
837
838 w.WriteString(s)
839 w.WriteByte('\t')
840 w.WriteString(names2symbols[k])
841 if w.WriteByte('\n') != nil {
842 return io.EOF
843 }
844 }
845
846 return nil
847 }
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(err.Error())
84 os.Stderr.WriteString("\n")
85 os.Exit(1)
86 return
87 }
88
89 w := bufio.NewWriterSize(os.Stdout, 32*1024)
90 defer w.Flush()
91
92 for i := len(cfg.lines) - 1; i >= 0; i-- {
93 w.WriteString(cfg.lines[i])
94 if w.WriteByte('\n') != nil {
95 break
96 }
97 }
98 }
99
100 func run(w io.Writer, args []string, cfg *config) error {
101 dashes := 0
102 for _, name := range args {
103 if name == `-` {
104 dashes++
105 }
106 if dashes > 1 {
107 return errors.New(`can't read stdin (dash) more than once`)
108 }
109 }
110
111 if len(args) == 0 {
112 return getLines(os.Stdin, cfg)
113 }
114
115 for _, name := range args {
116 if name == `-` {
117 if err := getLines(os.Stdin, cfg); err != nil {
118 return err
119 }
120 continue
121 }
122
123 if err := handleFile(name, cfg); err != nil {
124 return err
125 }
126 }
127 return nil
128 }
129
130 func handleFile(name string, cfg *config) error {
131 if name == `` || name == `-` {
132 return getLines(os.Stdin, cfg)
133 }
134
135 f, err := os.Open(name)
136 if err != nil {
137 return errors.New(`can't read from file named "` + name + `"`)
138 }
139 defer f.Close()
140
141 return getLines(f, cfg)
142 }
143
144 func getLines(r io.Reader, cfg *config) error {
145 const gb = 1024 * 1024 * 1024
146 sc := bufio.NewScanner(r)
147 sc.Buffer(nil, 8*gb)
148 if cfg.null {
149 sc.Split(splitNull)
150 }
151
152 for i := 0; sc.Scan(); i++ {
153 s := sc.Text()
154 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
155 s = s[3:]
156 }
157
158 cfg.lines = append(cfg.lines, sc.Text())
159 }
160
161 return sc.Err()
162 }
163
164 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
165 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
166 // handle leading null-terminated line, if found in the current chunk
167 if i := bytes.IndexByte(data, 0); i >= 0 {
168 return i + 1, data[:i], nil
169 }
170
171 // request more data, in case there's a null coming up later
172 if !atEOF {
173 return 0, nil, nil
174 }
175
176 // handle non-empty non-terminated last chunk
177 if len(data) > 0 {
178 return len(data), data, bufio.ErrFinalToken
179 }
180
181 // handle empty non-terminated last chunk
182 return 0, nil, bufio.ErrFinalToken
183 }
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("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(err.Error())
131 os.Stderr.WriteString("\n")
132 os.Exit(1)
133 return
134 }
135
136 show(os.Stdout, latest)
137 }
138
139 func run(paths []string, rb *ringBuffer) error {
140 for _, path := range paths {
141 if err := handleFile(path, rb); err != nil {
142 return err
143 }
144 }
145
146 if len(paths) == 0 {
147 if err := visit(os.Stdin, rb); err != nil {
148 return err
149 }
150 }
151 return nil
152 }
153
154 func show(w io.Writer, rb ringBuffer) {
155 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
156 defer bw.Flush()
157
158 for i := rb.next; i < len(rb.items); i++ {
159 bw.WriteString(rb.items[i])
160 if err := bw.WriteByte('\n'); err != nil {
161 return
162 }
163 }
164
165 for i := 0; i < len(rb.items) && i < rb.next; i++ {
166 bw.WriteString(rb.items[i])
167 if err := bw.WriteByte('\n'); err != nil {
168 return
169 }
170 }
171 }
172
173 func handleFile(path string, rb *ringBuffer) error {
174 f, err := os.Open(path)
175 if err != nil {
176 return err
177 }
178 defer f.Close()
179 return visit(f, rb)
180 }
181
182 func visit(r io.Reader, rb *ringBuffer) error {
183 const gb = 1024 * 1024 * 1024
184 sc := bufio.NewScanner(r)
185 sc.Buffer(nil, 8*gb)
186
187 for sc.Scan() {
188 rb.append(sc.Text())
189 }
190 return sc.Err()
191 }
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(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.NewWriter(w)
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 tcatl(bw, os.Stdin, `<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 := tcatl(bw, os.Stdin, `<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 {
145 continue
146 }
147
148 if err := bw.Flush(); err != nil {
149 return io.EOF
150 }
151
152 continue
153 }
154
155 if err := handleFile(bw, name, cfg); err != nil {
156 return err
157 }
158 }
159 return nil
160 }
161
162 func handleFile(w *bufio.Writer, name string, cfg config) error {
163 if name == `` || name == `-` {
164 return tcatl(w, os.Stdin, `<stdin>`, cfg)
165 }
166
167 f, err := os.Open(name)
168 if err != nil {
169 return errors.New(`can't read from file named "` + name + `"`)
170 }
171 defer f.Close()
172
173 return tcatl(w, f, name, cfg)
174 }
175
176 func tcatl(w *bufio.Writer, r io.Reader, name string, cfg config) error {
177 w.WriteString("\x1b[7m")
178 w.WriteString(name)
179 writeSpaces(w, 80-utf8.RuneCountInString(name))
180 w.WriteString("\x1b[0m\n")
181 if err := w.Flush(); err != nil {
182 // a write error may be the consequence of stdout being closed,
183 // perhaps by another app along a pipe
184 return io.EOF
185 }
186
187 if !cfg.liveLines {
188 return catlFast(w, r, cfg.null)
189 }
190
191 const gb = 1024 * 1024 * 1024
192 sc := bufio.NewScanner(r)
193 sc.Buffer(nil, 8*gb)
194 if cfg.null {
195 sc.Split(splitNull)
196 }
197
198 for i := 0; sc.Scan(); i++ {
199 s := sc.Bytes()
200 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
201 s = s[3:]
202 }
203
204 w.Write(s)
205 if w.WriteByte('\n') != nil {
206 return io.EOF
207 }
208
209 if w.Flush() != nil {
210 return io.EOF
211 }
212 }
213
214 return sc.Err()
215 }
216
217 func catlFast(w *bufio.Writer, r io.Reader, null bool) error {
218 var buf [32 * 1024]byte
219 var last byte = '\n'
220
221 for i := 0; true; i++ {
222 n, err := r.Read(buf[:])
223 if n > 0 && err == io.EOF {
224 err = nil
225 }
226 if err == io.EOF {
227 if last != '\n' {
228 w.WriteByte('\n')
229 }
230 return nil
231 }
232
233 if err != nil {
234 return err
235 }
236
237 chunk := buf[:n]
238 if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
239 chunk = chunk[3:]
240 }
241
242 // change nulls into line-feeds to handle null-terminated lines
243 if null {
244 for i, b := range chunk {
245 if b == 0 {
246 chunk[i] = '\n'
247 }
248 }
249 }
250
251 if len(chunk) >= 1 {
252 if _, err := w.Write(chunk); err != nil {
253 return io.EOF
254 }
255 last = chunk[len(chunk)-1]
256 }
257 }
258
259 return nil
260 }
261
262 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
263 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
264 // handle leading null-terminated line, if found in the current chunk
265 if i := bytes.IndexByte(data, 0); i >= 0 {
266 return i + 1, data[:i], nil
267 }
268
269 // request more data, in case there's a null coming up later
270 if !atEOF {
271 return 0, nil, nil
272 }
273
274 // handle non-empty non-terminated last chunk
275 if len(data) > 0 {
276 return len(data), data, bufio.ErrFinalToken
277 }
278
279 // handle empty non-terminated last chunk
280 return 0, nil, bufio.ErrFinalToken
281 }
282
283 // writeSpaces bulk-emits the number of spaces given
284 func writeSpaces(w *bufio.Writer, n int) {
285 const spaces = ` `
286 for ; n > len(spaces); n -= len(spaces) {
287 w.WriteString(spaces)
288 }
289 if n > 0 {
290 w.WriteString(spaces[:n])
291 }
292 }
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(err.Error())
65 os.Stderr.WriteString("\n")
66 os.Exit(1)
67 return
68 }
69 }
70
71 func run(w io.Writer, args []string) error {
72 dashes := 0
73 for _, name := range args {
74 if name == `-` {
75 dashes++
76 }
77 if dashes > 1 {
78 return errors.New(`can't read stdin (dash) more than once`)
79 }
80 }
81
82 if len(args) == 0 {
83 return teletype(w, os.Stdin)
84 }
85
86 for _, name := range args {
87 if name == `-` {
88 if err := teletype(w, os.Stdin); err != nil {
89 return err
90 }
91 continue
92 }
93
94 if err := handleFile(w, name); err != nil {
95 return err
96 }
97 }
98 return nil
99 }
100
101 func handleFile(w io.Writer, name string) error {
102 if name == `` || name == `-` {
103 return teletype(w, os.Stdin)
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 teletype(w, f)
113 }
114
115 func teletype(w io.Writer, 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 s := sc.Text()
122 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
123 s = s[3:]
124 }
125
126 var buf [4]byte
127 for _, r := range s {
128 time.Sleep(15 * time.Millisecond)
129 if _, err := w.Write(utf8.AppendRune(buf[:0], r)); err != nil {
130 return io.EOF
131 }
132 }
133
134 time.Sleep(750 * time.Millisecond)
135 if _, err := w.Write([]byte{'\n'}); err != nil {
136 return io.EOF
137 }
138 }
139
140 return sc.Err()
141 }
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 "io/fs"
31 "math"
32 "os"
33 "sort"
34 "strconv"
35 "time"
36 "unicode/utf8"
37
38 "./ngron"
39 "./now"
40 "./zcat"
41 )
42
43 func args() {
44 args := os.Args[1:]
45 if len(args) == 0 {
46 return
47 }
48
49 w := bufio.NewWriterSize(os.Stdout, 32*1024)
50 defer w.Flush()
51
52 for _, s := range args {
53 w.WriteString(s)
54 if err := w.WriteByte('\n'); err != nil {
55 break
56 }
57 }
58 }
59
60 func clear() {
61 if len(os.Args) > 1 {
62 switch os.Args[1] {
63 case `-h`, `--h`, `-help`, `--help`, `help`:
64 os.Stdout.WriteString("Clear the screen\n")
65 return
66
67 case `-x`:
68 // clear all but the scrollback buffer
69 os.Stdout.WriteString("\x1b[H\x1b[2J")
70 return
71 }
72 }
73
74 os.Stdout.WriteString("\x1b[H\x1b[2J\x1b[3J")
75 }
76
77 func cls() {
78 if len(os.Args) > 1 {
79 switch os.Args[1] {
80 case `-h`, `--h`, `-help`, `--help`, `help`:
81 os.Stdout.WriteString("Clear the Screen\n")
82 return
83 }
84 }
85
86 os.Stdout.WriteString("\x1b[H\x1b[2J\x1b[3J")
87 }
88
89 func decompress() {
90 zcat.Main()
91 }
92
93 const divInfo = `
94 div [options...] [x...] [y]
95
96 DIVide 2 numbers in 3 different ways, showing each result in its own line.
97
98 When given 2 numbers, the results are x / y, y / x, and 1 - (x / y), where
99 x is the smaller number, and y is the larger number.
100
101 When given just 1 number, the second number is 1 by default, showing you the
102 inverse of the number given explicitly, among the other results.
103
104 The options are, available both in single and double-dash versions
105
106 -h, -help show this help message
107 `
108
109 func div() {
110 args := os.Args[1:]
111
112 if len(args) > 0 {
113 switch args[0] {
114 case `-h`, `--h`, `-help`, `--help`:
115 os.Stdout.WriteString(divInfo[1:])
116 return
117 }
118 }
119
120 if len(args) > 0 && args[0] == `--` {
121 args = args[1:]
122 }
123
124 var nums []float64
125
126 for len(args) > 0 {
127 f, err := strconv.ParseFloat(args[0], 64)
128 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
129 nums = append(nums, f)
130 args = args[1:]
131 continue
132 }
133
134 break
135 }
136
137 switch len(nums) {
138 case 0:
139 os.Stderr.WriteString(divInfo[1:])
140 os.Exit(1)
141 return
142
143 case 1:
144 nums = append(nums, nums[0])
145 nums[0] = 1
146 }
147
148 if len(nums)%2 != 0 {
149 os.Stderr.WriteString(divInfo[1:])
150 os.Exit(1)
151 return
152 }
153
154 for len(nums) >= 2 {
155 x, y := nums[0], nums[1]
156 nums = nums[2:]
157
158 if x > y {
159 x, y = y, x
160 }
161 comp := 1 - (x / y)
162
163 const prec = 6
164 os.Stdout.WriteString(strconv.FormatFloat(x/y, 'f', prec, 64) + "\n")
165 os.Stdout.WriteString(strconv.FormatFloat(y/x, 'f', prec, 64) + "\n")
166 os.Stdout.WriteString(strconv.FormatFloat(comp, 'f', prec, 64) + "\n")
167 }
168 }
169
170 func echo() {
171 args := os.Args[1:]
172 if len(args) == 0 {
173 os.Stdout.WriteString("\n")
174 return
175 }
176
177 w := bufio.NewWriterSize(os.Stdout, 32*1024)
178 defer w.Flush()
179
180 for i, s := range args {
181 if i > 0 {
182 if err := w.WriteByte(' '); err != nil {
183 return
184 }
185 }
186 w.WriteString(s)
187 }
188
189 w.WriteByte('\n')
190 }
191
192 func echobar() {
193 const (
194 half = ` `
195 spaces = half + half
196 )
197
198 args := os.Args[1:]
199 if len(args) == 0 {
200 os.Stdout.WriteString("\x1b[7m" + spaces + "\x1b[0m\n")
201 return
202 }
203
204 w := bufio.NewWriterSize(os.Stdout, 32*1024)
205 defer w.Flush()
206
207 w.WriteString("\x1b[7m")
208
209 left := len(spaces)
210
211 for i, s := range args {
212 if i > 0 {
213 if err := w.WriteByte(' '); err != nil {
214 return
215 }
216 left--
217 }
218 w.WriteString(s)
219 left -= utf8.RuneCountInString(s)
220 }
221
222 if 0 < left && left < len(spaces) {
223 w.WriteString(spaces[:left])
224 }
225 w.WriteString("\x1b[0m\n")
226 }
227
228 const failInfo = `
229 fail [options...] [exit code...]
230
231 Fail with the exit code given. If no exit code is given, fail with code 1 by
232 default. If given code 0, this tool paradoxically succeeds, as code 0 means
233 success.
234
235 The options are, available both in single and double-dash versions
236
237 -h, -help show this help message
238 `
239
240 func fail() {
241 args := os.Args[1:]
242
243 if len(args) > 0 {
244 switch args[0] {
245 case `-h`, `--h`, `-help`, `--help`:
246 os.Stdout.WriteString(failInfo[1:])
247 return
248 }
249 }
250
251 if len(args) > 0 && args[0] == `--` {
252 args = args[1:]
253 }
254
255 code := 1
256 if len(args) > 0 {
257 if n, err := strconv.ParseInt(args[0], 10, 64); err == nil && n >= 0 {
258 code = int(n)
259 }
260 }
261
262 os.Exit(code)
263 return
264 }
265
266 const falseInfo = `
267 false [options...]
268
269 Quit right away, using exit code 1.
270
271 All (optional) leading options start with either single or double-dash:
272
273 -h, -help show this help message
274 `
275
276 func falseMain() {
277 if len(os.Args) > 1 {
278 switch os.Args[1] {
279 case `-h`, `--h`, `-help`, `--help`, `help`:
280 os.Stdout.WriteString(falseInfo[1:])
281 return
282 }
283 }
284
285 os.Exit(1)
286 return
287 }
288
289 const ignoreInfo = `
290 ignore [options...] [command...] [command args...]
291
292 Ignore the command given, acting as a stdio-passthru instead. This allows
293 quick editing of pipes to temporarily exclude a pipe-step, while still
294 having the disabled step show in the pipe-chain as a memo of sorts.
295
296 The options are, available both in single and double-dash versions
297
298 -h, -help show this help message
299 `
300
301 func ignore() {
302 if args := os.Args[1:]; len(args) > 0 {
303 switch args[0] {
304 case `-h`, `--h`, `-help`, `--help`, `help`:
305 os.Stdout.WriteString(ignoreInfo[1:])
306 return
307 }
308 }
309
310 var buf [32 * 1024]byte
311 for {
312 got, err := os.Stdin.Read(buf[:])
313 if got > 0 {
314 if _, err := os.Stdout.Write(buf[:got]); err != nil {
315 break
316 }
317 }
318 if err != nil {
319 break
320 }
321 }
322 }
323
324 func hecho() {
325 args := os.Args[1:]
326 if len(args) == 0 {
327 os.Stdout.WriteString("\n")
328 return
329 }
330
331 w := bufio.NewWriterSize(os.Stdout, 32*1024)
332 defer w.Flush()
333
334 w.WriteString("\x1b[7m")
335
336 for i, s := range args {
337 if i > 0 {
338 if err := w.WriteByte(' '); err != nil {
339 return
340 }
341 }
342 w.WriteString(s)
343 }
344
345 w.WriteString("\x1b[0m\n")
346 }
347
348 const mopInfo = `
349 mop [options...] [x...] [y]
350
351 Multiple OPerations runs multiple arithmetic calculations on 2 numbers,
352 showing each result in its own line.
353
354 The options are, available both in single and double-dash versions
355
356 -h, -help show this help message
357 `
358
359 func mop() {
360 args := os.Args[1:]
361
362 if len(args) > 0 {
363 switch args[0] {
364 case `-h`, `--h`, `-help`, `--help`:
365 os.Stdout.WriteString(mopInfo[1:])
366 return
367 }
368 }
369
370 if len(args) > 0 && args[0] == `--` {
371 args = args[1:]
372 }
373
374 var nums []float64
375
376 for len(args) > 0 {
377 f, err := strconv.ParseFloat(args[0], 64)
378 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
379 nums = append(nums, f)
380 args = args[1:]
381 continue
382 }
383
384 break
385 }
386
387 if len(nums) == 0 || len(nums)%2 != 0 {
388 os.Stderr.WriteString(mopInfo[1:])
389 os.Exit(1)
390 return
391 }
392
393 for len(nums) >= 2 {
394 x, y := nums[0], nums[1]
395 nums = nums[2:]
396
397 if x > y {
398 x, y = y, x
399 }
400
401 fmt.Printf("%.6f + %.6f ~ %.6f\n", x, y, x+y)
402 fmt.Printf("%.6f - %.6f ~ %.6f\n", x, y, x-y)
403 fmt.Printf("%.6f * %.6f ~ %.6f\n", x, y, x*y)
404 fmt.Printf("%.6f / %.6f ~ %.6f\n", x, y, x/y)
405 fmt.Printf("%.6f / %.6f ~ %.6f\n", y, x, y/x)
406 fmt.Printf("%.6f %% %.6f ~ %.6f\n", x, y, math.Mod(x, y))
407 fmt.Printf("%.6f %% %.6f ~ %.6f\n", y, x, math.Mod(y, x))
408 fmt.Printf("%.6f ** %.6f ~ %.6f\n", x, y, math.Pow(x, y))
409 fmt.Printf("%.6f ** %.6f ~ %.6f\n", y, x, math.Pow(y, x))
410 }
411 }
412
413 const nilInfo = `
414 nil [options...]
415
416 Emit nothing, also discarding all stdin bytes, if piped.
417
418 All (optional) leading options start with either single or double-dash:
419
420 -h, -help show this help message
421 `
422
423 func null() {
424 if args := os.Args[1:]; len(args) > 0 {
425 switch args[0] {
426 case `-h`, `--h`, `-help`, `--help`, `help`:
427 os.Stdout.WriteString(nilInfo[1:])
428 return
429 }
430 }
431
432 info, err := os.Stdin.Stat()
433 if err != nil {
434 os.Stderr.WriteString(err.Error())
435 os.Stderr.WriteString("\n")
436 os.Exit(1)
437 return
438 }
439
440 piped := int(info.Mode()&fs.ModeNamedPipe) != 0
441 if !piped {
442 return
443 }
444
445 var buf [32 * 1024]byte
446 for {
447 _, err := os.Stdin.Read(buf[:])
448 if err != nil {
449 break
450 }
451 }
452 }
453
454 func nothing() {
455 // deliberately do nothing
456 }
457
458 const prechoInfo = `
459 precho [options...] [words...]
460
461 PRecede ECHO emits a line with the words given as command-line arguments,
462 then copies all bytes from the standard input into the standard output.
463
464 The options are, available both in single and double-dash versions
465
466 -h, -help show this help message
467 `
468
469 func precho() {
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(prechoInfo[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 return
486 }
487
488 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
489
490 for i, s := range args {
491 if i > 0 {
492 if err := bw.WriteByte(' '); err != nil {
493 bw.Flush()
494 return
495 }
496 }
497 bw.WriteString(s)
498 }
499
500 if bw.WriteByte('\n') != nil {
501 bw.Flush()
502 return
503 }
504 if bw.Flush() != nil {
505 return
506 }
507 bw = nil
508
509 var buf [32 * 1024]byte
510 for {
511 got, err := os.Stdin.Read(buf[:])
512 if got > 0 {
513 if _, err := os.Stdout.Write(buf[:got]); err != nil {
514 break
515 }
516 }
517 if err != nil {
518 break
519 }
520 }
521 }
522
523 const rulerInfo = `
524 ruler [width...]
525
526 Emit a line with a ruler-like pattern, which helps check the width of output
527 lines right above it. If given a valid number, it's used as the ruler width,
528 or 80 is used as the default width.
529
530 The options are, available both in single and double-dash versions
531
532 -h, -help show this help message
533 `
534
535 func ruler() {
536 if args := os.Args[1:]; len(args) > 0 {
537 switch args[0] {
538 case `-h`, `--h`, `-help`, `--help`, `help`:
539 os.Stdout.WriteString(rulerInfo[1:])
540 return
541 }
542 }
543
544 width := 80
545 if len(os.Args) > 1 {
546 if n, err := strconv.Atoi(os.Args[1]); err == nil {
547 width = n
548 }
549 }
550
551 bw := bufio.NewWriter(os.Stdout)
552 defer bw.Flush()
553
554 // avoid a single line-feed byte for empty rulers
555 if width < 1 {
556 return
557 }
558
559 for width >= 10 {
560 if _, err := bw.WriteString(`····╵····│`); err != nil {
561 return
562 }
563 width -= 10
564 }
565
566 if width >= 5 {
567 bw.WriteString(`····╵`)
568 width -= 5
569 }
570
571 for width > 0 {
572 bw.WriteRune('·')
573 width--
574 }
575
576 bw.WriteByte('\n')
577 }
578
579 const sleepInfo = `
580 sleep [options...] [duration/seconds...]
581
582 Wait for the amount of time given.
583
584 All (optional) leading options start with either single or double-dash:
585
586 -h, -help show this help message
587 `
588
589 func sleep() {
590 args := os.Args[1:]
591
592 if len(args) > 0 {
593 switch args[0] {
594 case `-h`, `--h`, `-help`, `--help`, `help`:
595 os.Stdout.WriteString(sleepInfo[1:])
596 return
597 }
598 }
599
600 if len(args) > 0 && args[0] == `--` {
601 args = args[1:]
602 }
603
604 if len(args) == 0 {
605 os.Stderr.WriteString(sleepInfo[1:])
606 // os.Stderr.WriteString("forgot the duration to wait for\n")
607 os.Exit(1)
608 return
609 }
610
611 var delay time.Duration
612
613 for _, s := range args {
614 f, err := strconv.ParseFloat(s, 64)
615 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
616 if f < 0 {
617 f = 0
618 }
619 delay += time.Duration(f * float64(time.Second))
620 continue
621 }
622
623 if d, err := time.ParseDuration(s); err == nil {
624 delay += d
625 continue
626 }
627
628 os.Stderr.WriteString("invalid duration " + s)
629 os.Stderr.WriteString("\n")
630 os.Exit(1)
631 return
632 }
633
634 time.Sleep(delay)
635 }
636
637 const timezonesInfo = `
638 timezones [options...] [places...]
639
640 Lookup full timezone names from the city/place names given.
641
642 All (optional) leading options start with either single or double-dash:
643
644 -h, -help show this help message
645 `
646
647 func timezones() {
648 args := os.Args[1:]
649
650 if len(args) > 0 {
651 switch args[0] {
652 case `-h`, `--h`, `-help`, `--help`, `help`:
653 os.Stdout.WriteString(timezonesInfo[1:])
654 return
655 }
656 }
657
658 if len(args) > 0 && args[0] == `--` {
659 args = args[1:]
660 }
661
662 if len(args) == 0 {
663 os.Stderr.WriteString(timezonesInfo[1:])
664 return
665 }
666
667 nerr := 0
668 w := bufio.NewWriterSize(os.Stdout, 32*1024)
669 defer w.Flush()
670
671 for _, place := range args {
672 name, ok := now.LookupName(place)
673
674 if !ok {
675 w.Flush()
676 fmt.Fprintf(os.Stderr, "timezone for %q not found\n", place)
677 nerr++
678 continue
679 }
680
681 w.WriteString(name)
682 w.WriteByte('\n')
683 }
684
685 if nerr > 0 {
686 w.Flush()
687 os.Exit(1)
688 return
689 }
690 }
691
692 const toolsInfo = `
693 tools [options...]
694
695 Show all tools (officially) available.
696
697 All (optional) leading options start with either single or double-dash:
698
699 -a, -aliases, -all also show all aliases available, after the tools
700 -h, -help show this help message
701 `
702
703 func tools() {
704 showAliases := false
705 if len(os.Args) > 1 {
706 switch os.Args[1] {
707 case `-a`, `--a`, `-aliases`, `--aliases`, `-all`, `--all`:
708 showAliases = true
709
710 case `-h`, `--h`, `-help`, `--help`, `help`:
711 os.Stdout.WriteString(toolsInfo[1:])
712 return
713 }
714 }
715
716 n := len(mains)
717 if n < len(aliases) {
718 n = len(aliases)
719 }
720
721 names := make([]string, 0, n)
722 for k := range mains {
723 names = append(names, k)
724 }
725
726 sort.Strings(names)
727
728 for _, s := range names {
729 fmt.Fprintln(os.Stdout, s)
730 }
731
732 if !showAliases {
733 return
734 }
735
736 names = names[:0]
737 for k := range aliases {
738 names = append(names, k)
739 }
740
741 sort.Strings(names)
742
743 for _, s := range names {
744 fmt.Fprintf(os.Stdout, "%s -> %s\n", s, aliases[s])
745 }
746 }
747
748 const trueInfo = `
749 true [options...]
750
751 Quit right away successfully, using exit code 0.
752
753 All (optional) leading options start with either single or double-dash:
754
755 -h, -help show this help message
756 `
757
758 func trueMain() {
759 if len(os.Args) > 1 {
760 switch os.Args[1] {
761 case `-h`, `--h`, `-help`, `--help`, `help`:
762 os.Stdout.WriteString(trueInfo[1:])
763 return
764 }
765 }
766 }
767
768 const ungronInfo = `
769 ungron [options...]
770
771 Turn GRON-style lines into valid JSON. This is the same as directly running
772 the 'ngron' tool with the '-u' option.
773
774 All (optional) leading options start with either single or double-dash:
775
776 -h, -help show this help message
777 `
778
779 func ungronMain() {
780 args := os.Args[1:]
781
782 for _, s := range args {
783 switch s {
784 case `-h`, `--h`, `-help`, `--help`, `help`:
785 os.Stdout.WriteString(ungronInfo[1:])
786 return
787 }
788 }
789
790 gronArgs := make([]string, 0, len(os.Args)+1)
791 gronArgs = append(gronArgs, os.Args[0])
792 gronArgs = append(gronArgs, `-u`)
793 gronArgs = append(gronArgs, args...)
794 os.Args = gronArgs
795 ngron.Main()
796 }
797
798 const yesInfo = `
799 yes [options...] [message...]
800
801 Keep emitting the line with the message given, or "y" (short for "yes") by
802 default.
803
804 All (optional) leading options start with either single or double-dash:
805
806 -h, -help show this help message
807 `
808
809 func yes() {
810 args := os.Args[1:]
811
812 if len(args) > 0 {
813 switch args[0] {
814 case `-h`, `--h`, `-help`, `--help`, `help`:
815 os.Stdout.WriteString(yesInfo[1:])
816 return
817 }
818 }
819
820 if len(args) > 0 && args[0] == `--` {
821 args = args[1:]
822 }
823
824 if len(args) == 0 {
825 args = []string{`y`}
826 }
827
828 w := bufio.NewWriterSize(os.Stdout, 32*1024)
829 defer w.Flush()
830
831 for {
832 for i, s := range args {
833 if i > 0 {
834 if err := w.WriteByte(' '); err != nil {
835 return
836 }
837 }
838 w.WriteString(s)
839 if w.WriteByte('\n') != nil {
840 return
841 }
842 }
843 }
844 }
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(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.NewWriter(w)
84 defer bw.Flush()
85
86 if len(args) == 0 {
87 return toLower(bw, os.Stdin, live)
88 }
89
90 for _, name := range args {
91 if err := handleFile(bw, name, live); err != nil {
92 return err
93 }
94 }
95 return nil
96 }
97
98 func handleFile(w *bufio.Writer, name string, live bool) error {
99 if name == `` || name == `-` {
100 return toLower(w, os.Stdin, live)
101 }
102
103 f, err := os.Open(name)
104 if err != nil {
105 return errors.New(`can't read from file named "` + name + `"`)
106 }
107 defer f.Close()
108
109 return toLower(w, f, live)
110 }
111
112 func toLower(w *bufio.Writer, r io.Reader, live bool) error {
113 const gb = 1024 * 1024 * 1024
114 sc := bufio.NewScanner(r)
115 sc.Buffer(nil, 8*gb)
116
117 for i := 0; sc.Scan(); i++ {
118 s := sc.Bytes()
119 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
120 s = s[3:]
121 }
122
123 if needsLowercasing(s) {
124 writeLowercase(w, s)
125 } else {
126 w.Write(s)
127 }
128
129 if w.WriteByte('\n') != nil {
130 return io.EOF
131 }
132
133 if !live {
134 continue
135 }
136
137 if w.Flush() != nil {
138 return io.EOF
139 }
140 }
141
142 return sc.Err()
143 }
144
145 func needsLowercasing(src []byte) bool {
146 for _, b := range src {
147 if b > 127 {
148 return true
149 }
150 if 'A' <= b && b <= 'Z' {
151 return true
152 }
153 }
154
155 return false
156 }
157
158 func writeLowercase(w *bufio.Writer, src []byte) {
159 for len(src) > 0 {
160 r, size := utf8.DecodeRune(src)
161 w.WriteRune(unicode.ToLower(r))
162 src = src[size:]
163 }
164 }
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(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.NewWriter(w)
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 return handleReader(bw, os.Stdin, &cfg)
121 }
122
123 for _, name := range args {
124 if name == `-` {
125 if err := handleReader(bw, os.Stdin, &cfg); err != nil {
126 return err
127 }
128 continue
129 }
130
131 if err := handleFile(bw, name, &cfg); err != nil {
132 return err
133 }
134 }
135 return nil
136 }
137
138 func handleFile(w *bufio.Writer, name string, cfg *config) error {
139 if name == `` || name == `-` {
140 return handleReader(w, os.Stdin, cfg)
141 }
142
143 f, err := os.Open(name)
144 if err != nil {
145 return errors.New(`can't read from file named "` + name + `"`)
146 }
147 defer f.Close()
148
149 return handleReader(w, f, cfg)
150 }
151
152 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
153 const gb = 1024 * 1024 * 1024
154 sc := bufio.NewScanner(r)
155 sc.Buffer(nil, 8*gb)
156
157 every := cfg.every
158
159 for i := 0; sc.Scan(); i++ {
160 s := sc.Bytes()
161 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
162 s = s[3:]
163 }
164
165 cfg.n++
166 var u bool
167 if cfg.top {
168 u = every > 0 && (cfg.n == 1 || ((cfg.n-1)%every == 0))
169 } else {
170 u = (every > 0 && cfg.n%every == 0 && cfg.n != 1) || every == 1
171 }
172
173 if u {
174 if underline(w, s) != nil {
175 return io.EOF
176 }
177 } else {
178 w.Write(s)
179 if w.WriteByte('\n') != nil {
180 return io.EOF
181 }
182 }
183
184 if !cfg.liveLines {
185 continue
186 }
187
188 if w.Flush() != nil {
189 return io.EOF
190 }
191 }
192
193 return sc.Err()
194 }
195
196 func underline(w *bufio.Writer, s []byte) error {
197 w.WriteString("\x1b[4m")
198 for len(s) > 0 {
199 i := bytes.Index(s, []byte("\x1b[0m"))
200 if i < 0 {
201 break
202 }
203
204 j := i + len("\x1b[0m")
205 w.Write(s[:j])
206 w.WriteString("\x1b[4m")
207 s = s[j:]
208 }
209
210 if len(s) > 0 {
211 w.Write(s)
212 }
213
214 if _, err := w.WriteString("\x1b[0m\n"); err != nil {
215 return io.EOF
216 }
217 return nil
218 }
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(err.Error())
65 os.Stderr.WriteString("\n")
66 os.Exit(1)
67 return
68 }
69 }
70
71 func run(w io.Writer, args []string) error {
72 bw := bufio.NewWriter(w)
73 defer bw.Flush()
74
75 if len(args) == 0 {
76 dumpAll(bw, []float64{1})
77 return nil
78 }
79
80 from := ``
81 var values []float64
82
83 for _, s := range args {
84 f, err := strconv.ParseFloat(s, 64)
85 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
86 values = append(values, f)
87 continue
88 }
89
90 if len(values) == 0 {
91 values = append(values, 1)
92 }
93 from = strings.ToLower(s)
94
95 if dump(bw, values, from) {
96 values = values[:0]
97 from = ``
98 continue
99 }
100
101 return fmt.Errorf("unit %q not supported", from)
102 }
103
104 if len(values) > 0 {
105 dumpAll(bw, values)
106 return nil
107 }
108
109 if from != `` && !dump(bw, values, from) {
110 return fmt.Errorf("unit %q not supported", from)
111 }
112 return nil
113 }
114
115 func dump(w *bufio.Writer, values []float64, unit string) bool {
116 if s, ok := aliases[unit]; ok {
117 unit = s
118 }
119
120 c, ok := mainConverters[unit]
121 if !ok {
122 c, ok = otherConverters[unit]
123 if !ok {
124 return false
125 }
126 }
127
128 const fs = "%.4f %-4s = %.4f %s\n"
129
130 if len(values) == 0 {
131 res := c.Mul + c.Add
132 fmt.Fprintf(w, fs, 1.0, unit, res, c.To)
133 return true
134 }
135
136 for _, v := range values {
137 res := c.Mul*v + c.Add
138 fmt.Fprintf(w, fs, v, unit, res, c.To)
139 }
140 return true
141 }
142
143 func dumpAll(w *bufio.Writer, values []float64) bool {
144 if len(values) == 0 {
145 values = []float64{1}
146 }
147
148 units := make([]string, 0, len(mainConverters))
149 for k := range mainConverters {
150 units = append(units, k)
151 }
152 sort.Strings(units)
153
154 for _, from := range units {
155 dump(w, values, from)
156 }
157 return true
158 }
159
160 var aliases = map[string]string{
161 `acre`: `ac`,
162 `acres`: `ac`,
163 `days`: `day`,
164 `foot`: `ft`,
165 `feet`: `ft`,
166 `feet2`: `ft²`,
167 `feet3`: `ft³`,
168 `foot2`: `ft²`,
169 `foot3`: `ft³`,
170 `ft2`: `ft²`,
171 `ft3`: `ft³`,
172 `gallon`: `gal`,
173 `gallons`: `gal`,
174 `gals`: `gal`,
175 `inch`: `in`,
176 `inches`: `in`,
177 `mile`: `mi`,
178 `miles`: `mi`,
179 `mile²`: `mi²`,
180 `miles²`: `mi²`,
181 `minute`: `min`,
182 `minutes`: `min`,
183 `nmile`: `nmi`,
184 `nmiles`: `nmi`,
185 `ounce`: `oz`,
186 `ounces`: `oz`,
187 `ozs`: `oz`,
188 `weeks`: `week`,
189 `wk`: `week`,
190 `wks`: `week`,
191 `yard`: `yd`,
192 `yards`: `yd`,
193 `yard2`: `yd²`,
194 `yard²`: `yd²`,
195 `yards2`: `yd²`,
196 `yards²`: `yd²`,
197 `yds`: `yd`,
198 `yds2`: `yd²`,
199 `yds²`: `yd²`,
200 }
201
202 type converter struct {
203 To string
204 Mul float64
205 Add float64
206 }
207
208 var mainConverters = map[string]converter{
209 `ft`: converter{`m`, 0.3048, 0},
210 `gal`: converter{`L`, 3.785411784, 0},
211 `in`: converter{`cm`, 2.54, 0},
212 `mi`: converter{`km`, 1.609344, 0},
213 `mpg`: converter{`kpl`, 0.425143707, 0},
214 `mph`: converter{`kph`, 1.609344, 0},
215 `nmi`: converter{`km`, 1.852, 0},
216 `oz`: converter{`g`, 28.349523125, 0},
217 `yd`: converter{`m`, 0.9144, 0},
218 }
219
220 var otherConverters = map[string]converter{
221 `ac`: converter{`m²`, 4046.8564224, 0},
222 `day`: converter{`s`, 24 * 60 * 60, 0},
223 `ft²`: converter{`m²`, 0.09290304, 0},
224 `ft³`: converter{`m³`, 0.028316846592, 0},
225 `mi²`: converter{`km²`, 2.5899881103360, 0},
226 `min`: converter{`s`, 60, 0},
227 `psi`: converter{`Pa`, 6894.757293168, 0},
228 `week`: converter{`s`, 7 * 24 * 60 * 60, 0},
229 `yd²`: converter{`m²`, 0.83612736, 0},
230 }
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(err.Error())
67 os.Stderr.WriteString("\n")
68 os.Exit(1)
69 return
70 }
71 }
72
73 func run(w io.Writer, args []string) error {
74 bw := bufio.NewWriter(w)
75 defer bw.Flush()
76
77 for _, path := range args {
78 if err := handleFile(bw, path); err != nil {
79 return err
80 }
81 }
82
83 if len(args) == 0 {
84 return utfate(bw, os.Stdin)
85 }
86 return nil
87 }
88
89 func handleFile(w *bufio.Writer, name string) error {
90 if name == `-` {
91 return utfate(w, os.Stdin)
92 }
93
94 f, err := os.Open(name)
95 if err != nil {
96 return errors.New(`can't read from file named "` + name + `"`)
97 }
98 defer f.Close()
99
100 return utfate(w, f)
101 }
102
103 func utfate(w io.Writer, r io.Reader) error {
104 br := bufio.NewReader(r)
105 bw := bufio.NewWriter(w)
106 defer bw.Flush()
107
108 lead, err := br.Peek(4)
109 if err != nil && err != io.EOF {
110 return err
111 }
112
113 if bytes.HasPrefix(lead, []byte{'\x00', '\x00', '\xfe', '\xff'}) {
114 br.Discard(4)
115 return utf32toUTF8(bw, br, binary.BigEndian)
116 }
117
118 if bytes.HasPrefix(lead, []byte{'\xff', '\xfe', '\x00', '\x00'}) {
119 br.Discard(4)
120 return utf32toUTF8(bw, br, binary.LittleEndian)
121 }
122
123 if bytes.HasPrefix(lead, []byte{'\xfe', '\xff'}) {
124 br.Discard(2)
125 return utf16toUTF8(bw, br, readBytePairBE)
126 }
127
128 if bytes.HasPrefix(lead, []byte{'\xff', '\xfe'}) {
129 br.Discard(2)
130 return utf16toUTF8(bw, br, readBytePairLE)
131 }
132
133 if bytes.HasPrefix(lead, []byte{'\xef', '\xbb', '\xbf'}) {
134 br.Discard(3)
135 return handleUTF8(bw, br)
136 }
137
138 return handleUTF8(bw, br)
139 }
140
141 func handleUTF8(w *bufio.Writer, r *bufio.Reader) error {
142 for {
143 c, _, err := r.ReadRune()
144 if c == unicode.ReplacementChar {
145 return errors.New(`invalid UTF-8 stream`)
146 }
147 if err == io.EOF {
148 return nil
149 }
150 if err != nil {
151 return err
152 }
153
154 if _, err := w.WriteRune(c); err != nil {
155 return io.EOF
156 }
157 }
158 }
159
160 // fancyHandleUTF8 is kept only for reference, as its attempts at being clever
161 // don't seem to speed things up much when given ASCII input
162 func fancyHandleUTF8(w *bufio.Writer, r *bufio.Reader) error {
163 lookahead := 1
164 maxAhead := r.Size() / 2
165
166 for {
167 // look ahead to check for ASCII runs
168 ahead, err := r.Peek(lookahead)
169 if err == io.EOF {
170 return nil
171 }
172 if err != nil {
173 return err
174 }
175
176 // copy leading ASCII runs
177 n := leadASCII(ahead)
178 if n > 0 {
179 w.Write(ahead[:n])
180 r.Discard(n)
181 }
182
183 // adapt lookahead size
184 if n == len(ahead) && lookahead < maxAhead {
185 lookahead *= 2
186 } else if lookahead > 1 {
187 lookahead /= 2
188 }
189
190 if n == len(ahead) {
191 continue
192 }
193
194 c, _, err := r.ReadRune()
195 if c == unicode.ReplacementChar {
196 return errors.New(`invalid UTF-8 stream`)
197 }
198 if err == io.EOF {
199 return nil
200 }
201 if err != nil {
202 return err
203 }
204
205 if _, err := w.WriteRune(c); err != nil {
206 return io.EOF
207 }
208 }
209 }
210
211 // leadASCII is used by func fancyHandleUTF8
212 func leadASCII(buf []byte) int {
213 for i, b := range buf {
214 if b >= 128 {
215 return i
216 }
217 }
218 return len(buf)
219 }
220
221 // readPairFunc narrows source-code lines below
222 type readPairFunc func(*bufio.Reader) (byte, byte, error)
223
224 // utf16toUTF8 handles UTF-16 inputs for func utfate
225 func utf16toUTF8(w *bufio.Writer, r *bufio.Reader, read2 readPairFunc) error {
226 for {
227 a, b, err := read2(r)
228 if err == io.EOF {
229 return nil
230 }
231 if err != nil {
232 return err
233 }
234
235 c := rune(256*int(a) + int(b))
236 if utf16.IsSurrogate(c) {
237 a, b, err := read2(r)
238 if err == io.EOF {
239 return nil
240 }
241 if err != nil {
242 return err
243 }
244
245 next := rune(256*int(a) + int(b))
246 c = utf16.DecodeRune(c, next)
247 }
248
249 if _, err := w.WriteRune(c); err != nil {
250 return io.EOF
251 }
252 }
253 }
254
255 // readBytePairBE gets you a pair of bytes in big-endian (original) order
256 func readBytePairBE(br *bufio.Reader) (byte, byte, error) {
257 a, err := br.ReadByte()
258 if err != nil {
259 return a, 0, err
260 }
261
262 b, err := br.ReadByte()
263 return a, b, err
264 }
265
266 // readBytePairLE gets you a pair of bytes in little-endian order
267 func readBytePairLE(br *bufio.Reader) (byte, byte, error) {
268 a, b, err := readBytePairBE(br)
269 return b, a, err
270 }
271
272 // utf32toUTF8 handles UTF-32 inputs for func utfate
273 func utf32toUTF8(w *bufio.Writer, r *bufio.Reader, o binary.ByteOrder) error {
274 var n uint32
275 for {
276 err := binary.Read(r, o, &n)
277 if err == io.EOF {
278 return nil
279 }
280 if err != nil {
281 return err
282 }
283
284 if _, err := w.WriteRune(rune(n)); err != nil {
285 return io.EOF
286 }
287 }
288 }
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 # similar to the door-opening sound from a dsc powerseries home alarm
52 waveout 'sin(4096 * tau * t) * exp(-10 * (t%0.1))' > home-alarm.wav
53
54 # watch your ears: quickly increases frequency up to 2khz
55 waveout 'sin(2_000 * t * tau * t)' > frequency-sweep.wav
56
57 # 1-second 400hz test tone
58 waveout 'sin(400 * tau * t)' > test-tone-400.wav
59
60 # 2s of a 440hz test tone, also called an A440 sound
61 waveout 2s 'sin(440 * tau * t)' > a440.wav
62
63 # 1s 400hz test tone with sudden volume drop at the end, to avoid clip
64 waveout 'sin(400 * tau * t) * min(1, exp(-100*(t-0.9)))' > nice-tone.wav
65
66 # old ringtone used in north america
67 waveout '0.5*sin(350 * tau * t) + 0.5*sin(450 * tau * t)' > na-ringtone.wav
68
69 # 20 seconds of periodic pings
70 waveout 20s 'sin(800 * tau * u) * exp(-20 * u)' > pings.wav
71
72 # 2 seconds of a european-style dial-tone
73 waveout 2s '(sin(350 * tau * t) + sin(450 * tau * t)) / 2' > dial-tone.wav
74
75 # 4 seconds of a north-american-style busy-phone signal
76 waveout 4s '(u < 0.5) * (sin(480*tau * t) + sin(620*tau * t)) / 2' > na-busy.wav
77
78 # hit the 51st key on a synthetic piano-like instrument
79 waveout 'sin(tau * 440 * 2**((51 - 49)/12) * t) * exp(-10*u)' > piano-key.wav
80
81 # hit of a synthetic snare-like sound
82 waveout 'random() * exp(-10 * t)' > synth-snare.wav
83
84 # a stereotypical `laser` sound
85 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(err.Error())
64 os.Stderr.WriteString("\n")
65 os.Exit(1)
66 return
67 }
68 }
69
70 if len(args) == 0 {
71 if err := zcat(os.Stdout, os.Stdin); err != nil && err != io.EOF {
72 os.Stderr.WriteString(err.Error())
73 os.Stderr.WriteString("\n")
74 os.Exit(1)
75 return
76 }
77 }
78 }
79
80 func handleFile(w io.Writer, path string) error {
81 f, err := os.Open(path)
82 if err != nil {
83 return err
84 }
85 defer f.Close()
86 return zcat(w, f)
87 }
88
89 func zcat(w io.Writer, r io.Reader) error {
90 r, err := gzip.NewReader(r)
91 if err != nil {
92 return err
93 }
94 return cat(w, r)
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: ./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(err.Error())
76 os.Stderr.WriteString("\n")
77 os.Exit(1)
78 return
79 }
80
81 var avoid sets
82 avoid.indices = make(map[int]struct{})
83 avoid.keys = make(map[string]struct{})
84
85 data, err = zoom(data, args, &avoid)
86 if err != nil && err != io.EOF {
87 os.Stderr.WriteString(err.Error())
88 os.Stderr.WriteString("\n")
89 os.Exit(1)
90 return
91 }
92
93 if err := json0(os.Stdout, data); err != nil && err != io.EOF {
94 os.Stderr.WriteString(err.Error())
95 os.Stderr.WriteString("\n")
96 os.Exit(1)
97 return
98 }
99 }
100
101 func load(r io.Reader) (any, error) {
102 // dec := json.NewDecoder(r)
103 dec := json.NewDecoder(bufio.NewReaderSize(r, 32*1024))
104 // avoid parsing numbers, so unusually-long numbers are kept verbatim,
105 // even if JSON parsers aren't required to guarantee such input-fidelity
106 // for numbers
107 dec.UseNumber()
108
109 t, err := dec.Token()
110 if err == io.EOF {
111 return nil, errors.New(`input has no JSON values`)
112 }
113
114 data, err := loadToken(dec, t)
115 if err != nil {
116 return data, err
117 }
118
119 _, err = dec.Token()
120 if err == io.EOF {
121 // input is over, so it's a success
122 return data, nil
123 }
124
125 if err == nil {
126 // a successful `read` is a failure, as it means there are
127 // trailing JSON tokens
128 return data, errors.New(`unexpected trailing data`)
129 }
130
131 // any other error
132 return data, err
133 }
134
135 // loadToken handles recursion for func load
136 func loadToken(dec *json.Decoder, t json.Token) (any, error) {
137 switch t := t.(type) {
138 case json.Delim:
139 switch t {
140 case json.Delim('['):
141 return loadArray(dec)
142 case json.Delim('{'):
143 return loadObject(dec)
144 default:
145 return nil, errors.New(`unsupported JSON syntax ` + string(t))
146 }
147
148 case nil, bool, json.Number, string:
149 return t, nil
150
151 default:
152 // return nil, fmt.Errorf(`unsupported token type %T`, t)
153 return nil, errors.New(`invalid JSON token`)
154 }
155 }
156
157 // loadArray handles arrays for func loadToken
158 func loadArray(dec *json.Decoder) ([]any, error) {
159 var items []any
160
161 for i := 0; true; i++ {
162 t, err := dec.Token()
163 if err == io.EOF {
164 return items, errors.New(`end of JSON before array was closed`)
165 }
166 if err != nil {
167 return items, err
168 }
169
170 if t == json.Delim(']') {
171 return items, nil
172 }
173
174 v, err := loadToken(dec, t)
175 if err != nil {
176 return items, err
177 }
178 items = append(items, v)
179 }
180
181 // make the compiler happy
182 return items, nil
183 }
184
185 // loadObject handles objects for func loadToken
186 func loadObject(dec *json.Decoder) (dictionary, error) {
187 var items dictionary
188
189 for i := 0; true; i++ {
190 t, err := dec.Token()
191 if err == io.EOF {
192 return items, errors.New(`end of JSON before object was closed`)
193 }
194 if err != nil {
195 return items, err
196 }
197
198 if t == json.Delim('}') {
199 return items, nil
200 }
201
202 k, ok := t.(string)
203 if !ok {
204 return items, errors.New(`expected a string for a key-value pair`)
205 }
206
207 t, err = dec.Token()
208 if err == io.EOF {
209 return items, errors.New(`expected a value for a key-value pair`)
210 }
211
212 v, err := loadToken(dec, t)
213 if err != nil {
214 return items, err
215 }
216
217 if i == 0 {
218 items.Map = make(map[string]any)
219 }
220 if _, ok := items.Map[k]; !ok {
221 items.Keys = append(items.Keys, k)
222 }
223 items.Map[k] = v
224 }
225
226 // make the compiler happy
227 return items, nil
228 }
229
230 func zoom(data any, keys []string, avoid *sets) (any, error) {
231 for i, k := range keys {
232 switch v := data.(type) {
233 case nil:
234 return v, errors.New(`too many keys: can't zoom a null value`)
235
236 case bool:
237 return v, errors.New(`too many keys: can't zoom a boolean value`)
238
239 case json.Number:
240 return v, errors.New(`too many keys: can't zoom a number`)
241
242 case string:
243 v, err := zoomString(v, k)
244 if err != nil {
245 return v, err
246 }
247 data = v
248
249 case []any:
250 switch k {
251 case `.`:
252 return loopZoomArray(v, keys[i+1:], avoid)
253 case `+`:
254 return pickArrayItems(v, keys[i+1:])
255 case `-`:
256 clear(avoid.indices)
257 appendIndices(avoid.indices, v, keys[i+1:])
258 return dropArrayItems(v, avoid.indices)
259 }
260
261 res, err := zoomArray(v, k)
262 if err != nil {
263 return data, err
264 }
265 data = res
266
267 case dictionary:
268 if v, ok := v.Map[k]; ok {
269 data = v
270 continue
271 }
272
273 switch k {
274 case `+`:
275 return pickObjectItems(v, keys[i+1:])
276 case `-`:
277 clear(avoid.keys)
278 for _, k := range keys[i+1:] {
279 avoid.keys[k] = struct{}{}
280 }
281 return dropObjectItems(v, avoid.keys)
282 }
283
284 if _, v, ok := matchObjectKey(v, k); ok {
285 data = v
286 } else {
287 data = nil
288 }
289
290 default:
291 return v, errors.New(`too many keys: can't zoom basic values`)
292 }
293 }
294 return data, nil
295 }
296
297 func zoomArray(items []any, k string) (any, error) {
298 // trim leading spaces
299 for len(k) > 0 && k[0] == ' ' {
300 k = k[1:]
301 }
302
303 // trim trailing spaces
304 for len(k) > 0 && k[len(k)-1] == ' ' {
305 k = k[:len(k)-1]
306 }
307
308 if i, j, ok := tryArraySlice(items, k); ok {
309 if j >= len(items) {
310 j = len(items)
311 }
312 if i < 0 || j < 0 || i > j {
313 return []any(nil), nil
314 }
315 return items[i:j], nil
316 }
317
318 i, err := strconv.ParseInt(k, 10, 64)
319 if err != nil {
320 return nil, nil
321 }
322
323 if i < 0 {
324 i += int64(len(items))
325 }
326
327 if 0 <= i && i < int64(len(items)) {
328 return items[i], nil
329 }
330 return nil, nil
331 }
332
333 func tryArraySlice(items []any, k string) (i int, j int, ok bool) {
334 if k == `` {
335 return 0, 0, false
336 }
337
338 colon := strings.IndexByte(k, ':')
339 if colon < 0 {
340 if dots := indexPair(k, '.', '.'); dots >= 0 {
341 return tryIncArraySlice(items, k, dots)
342 }
343
344 return 0, 0, false
345 }
346
347 // handle omitted/implied starting 0
348 if colon == 0 {
349 j, err := strconv.ParseInt(k[colon+1:], 10, 64)
350 if err != nil {
351 return 0, 0, false
352 }
353
354 if j < 0 {
355 j += int64(len(items))
356 }
357
358 return 0, int(j), true
359 }
360
361 // handle omitted/implied until the end
362 if colon == len(k)-1 {
363 i, err := strconv.ParseInt(k[:colon], 10, 64)
364 if err != nil {
365 return 0, 0, false
366 }
367
368 if i < 0 {
369 i += int64(len(items))
370 }
371
372 return int(i), len(items), true
373 }
374
375 start, err := strconv.ParseInt(k[:colon], 10, 64)
376 if err != nil {
377 return 0, 0, false
378 }
379
380 if start < 0 {
381 start += int64(len(items))
382 }
383
384 end, err := strconv.ParseInt(k[colon+1:], 10, 64)
385 if err != nil {
386 return 0, 0, false
387 }
388
389 if end < 0 {
390 end += int64(len(items))
391 }
392
393 return int(start), int(end), ok
394 }
395
396 func tryIncArraySlice(items []any, k string, dots int) (i int, j int, ok bool) {
397 if k == `` {
398 return 0, 0, false
399 }
400
401 if dots < 0 {
402 return 0, 0, false
403 }
404
405 // handle omitted/implied starting 0
406 if dots == 0 {
407 j, err := strconv.ParseInt(k[dots+2:], 10, 64)
408 if err != nil {
409 return 0, 0, false
410 }
411
412 if j < 0 {
413 j += int64(len(items))
414 }
415 if j >= 0 {
416 j++
417 }
418
419 return 0, int(j), true
420 }
421
422 // handle omitted/implied until the end
423 if dots == len(k)-1 {
424 i, err := strconv.ParseInt(k[:dots], 10, 64)
425 if err != nil {
426 return 0, 0, false
427 }
428
429 if i < 0 {
430 i += int64(len(items))
431 }
432
433 return int(i), len(items), true
434 }
435
436 start, err := strconv.ParseInt(k[:dots], 10, 64)
437 if err != nil {
438 return 0, 0, false
439 }
440
441 if start < 0 {
442 start += int64(len(items))
443 }
444
445 end, err := strconv.ParseInt(k[dots+2:], 10, 64)
446 if err != nil {
447 return 0, 0, false
448 }
449
450 if end < 0 {
451 end += int64(len(items))
452 }
453 if end >= 0 {
454 end++
455 }
456 return int(start), int(end), ok
457 }
458
459 func matchObjectKey(items dictionary, k string) (match string, v any, ok bool) {
460 // first, try direct key lookup
461 if v, ok := items.Map[k]; ok {
462 return k, v, true
463 }
464
465 // second, try case-insensitive key lookup
466 for s := range items.Map {
467 if strings.EqualFold(k, s) {
468 return s, items.Map[s], true
469 }
470 }
471
472 // finally, try integer/index lookup
473 i, err := strconv.ParseInt(k, 10, 64)
474 if err != nil {
475 return ``, nil, false
476 }
477
478 if i < 0 {
479 i += int64(len(items.Keys))
480 }
481
482 if 0 <= i && i < int64(len(items.Keys)) {
483 k := items.Keys[i]
484 return k, items.Map[k], true
485 }
486
487 // nothing worked
488 return ``, nil, false
489 }
490
491 func zoomString(s string, k string) (string, error) {
492 if i, j, ok := tryRuneSlice(s, k); ok {
493 if i > j {
494 return ``, nil
495 }
496 return sliceRunes(s, i, j), nil
497 }
498
499 i, err := strconv.ParseInt(k, 10, 64)
500 if err != nil {
501 return ``, err
502 }
503
504 // don't bother looping when the index given is obviously out of bounds
505 if int(i) >= len(s) || int(-i) > len(s) {
506 return ``, nil
507 }
508
509 if i < 0 {
510 // shrink string backward from the end
511 for len(s) > 0 && i < 0 {
512 _, size := utf8.DecodeLastRuneInString(s)
513 s = s[:len(s)-size]
514 i++
515 }
516
517 if len(s) > 0 && i == 0 {
518 _, size := utf8.DecodeLastRuneInString(s)
519 return s[len(s)-size:], nil
520 }
521 return ``, nil
522 }
523
524 // shrink string forward from the start
525 for len(s) > 0 && i > 0 {
526 _, size := utf8.DecodeRuneInString(s)
527 s = s[size:]
528 i--
529 }
530
531 if len(s) > 0 && i == 0 {
532 _, size := utf8.DecodeRuneInString(s)
533 return s[:size], nil
534 }
535 return ``, nil
536 }
537
538 func tryRuneSlice(s string, k string) (i int, j int, ok bool) {
539 if k == `` {
540 return 0, 0, false
541 }
542
543 colon := strings.IndexByte(k, ':')
544 if colon < 0 {
545 return 0, 0, false
546 }
547
548 // handle omitted/implied starting 0
549 if colon == 0 {
550 j, err := strconv.ParseInt(k[colon+1:], 10, 64)
551 if err != nil {
552 return 0, 0, false
553 }
554
555 return 0, int(j), true
556 }
557
558 // handle omitted/implied until the end
559 if colon == len(k)-1 {
560 i, err := strconv.ParseInt(k[:colon], 10, 64)
561 if err != nil {
562 return 0, 0, false
563 }
564
565 return int(i), len(s), true
566 }
567
568 start, err := strconv.ParseInt(k[:colon], 10, 64)
569 if err != nil {
570 return 0, 0, false
571 }
572
573 end, err := strconv.ParseInt(k[colon+1:], 10, 64)
574 if err != nil {
575 return 0, 0, false
576 }
577
578 return int(start), int(end), ok
579 }
580
581 func sliceRunes(s string, i int, j int) string {
582 if i >= j {
583 return ``
584 }
585
586 // to do: backward-indexing
587 if i < 0 || j < 0 {
588 return ``
589 }
590
591 // don't bother looping when the index given is obviously out of bounds
592 if int(i) >= len(s) || int(-i) > len(s) {
593 return ``
594 }
595
596 // skip leading runes, according to the first index
597 for len(s) > 0 && i > 0 {
598 _, size := utf8.DecodeRuneInString(s)
599 s = s[size:]
600 i--
601 }
602
603 if len(s) == 0 {
604 return ``
605 }
606
607 end := 0
608 rest := s
609 for len(rest) > 0 && j > 0 {
610 _, size := utf8.DecodeRuneInString(rest)
611 rest = rest[size:]
612 end += size
613 j--
614 }
615
616 if len(s) > 0 && j == 0 {
617 return s[:end]
618 }
619 return ``
620 }
621
622 func json0(w io.Writer, data any) error {
623 bw := bufio.NewWriterSize(w, 32*1024)
624 defer bw.Flush()
625
626 err := writeValue(bw, data)
627 bw.WriteByte('\n')
628
629 if err == io.EOF {
630 return nil
631 }
632 return err
633 }
634
635 func loopZoomArray(items []any, keys []string, avoid *sets) (any, error) {
636 res := items[:0]
637 for _, v := range items {
638 v, err := zoom(v, keys, avoid)
639 if err != nil {
640 return res, err
641 }
642 res = append(res, v)
643 }
644 return res, nil
645 }
646
647 func pickArrayItems(items []any, keys []string) (any, error) {
648 res := items[:0]
649 for _, k := range keys {
650 v, err := zoomArray(items, k)
651 if err != nil {
652 return res, err
653 }
654 res = append(res, v)
655 }
656 return res, nil
657 }
658
659 func dropArrayItems(items []any, avoid map[int]struct{}) (any, error) {
660 res := items[:0]
661 for i, v := range items {
662 if _, ok := avoid[i]; ok {
663 continue
664 }
665 res = append(res, v)
666 }
667 return res, nil
668 }
669
670 func pickObjectItems(items dictionary, keys []string) (any, error) {
671 var res dictionary
672 res.Keys = items.Keys[:0]
673 res.Map = items.Map
674
675 for _, k := range keys {
676 match, _, ok := matchObjectKey(items, k)
677 if !ok {
678 continue
679 }
680
681 if _, ok := res.Map[match]; !ok {
682 res.Keys = append(res.Keys, match)
683 }
684 }
685
686 return res, nil
687 }
688
689 func dropObjectItems(items dictionary, avoid map[string]struct{}) (any, error) {
690 var res dictionary
691 res.Keys = items.Keys[:0]
692 res.Map = items.Map
693
694 for _, k := range items.Keys {
695 if hasFold(avoid, k) {
696 continue
697 }
698
699 if _, ok := res.Map[k]; !ok {
700 res.Keys = append(res.Keys, k)
701 }
702 }
703
704 return res, nil
705 }
706
707 func hasFold(avoid map[string]struct{}, s string) bool {
708 for v := range avoid {
709 if v == s || strings.EqualFold(v, s) {
710 return true
711 }
712 }
713 return false
714 }
715
716 func appendIndices(dest map[int]struct{}, items []any, keys []string) {
717 for _, k := range keys {
718 i, err := strconv.ParseInt(k, 10, 64)
719 if err != nil {
720 continue
721 }
722
723 if i < 0 {
724 i += int64(len(items))
725 }
726
727 if 0 <= i && i < int64(len(items)) {
728 dest[int(i)] = struct{}{}
729 }
730 }
731 }
732
733 func writeValue(w *bufio.Writer, data any) error {
734 switch data := data.(type) {
735 case nil:
736 return writeKeyword(w, `null`)
737 case bool:
738 if data {
739 return writeKeyword(w, `true`)
740 }
741 return writeKeyword(w, `false`)
742 case json.Number:
743 if _, err := w.WriteString(data.String()); err != nil {
744 return io.EOF
745 }
746 return nil
747 case string:
748 return writeEscapedString(w, data)
749 case []any:
750 return writeArray(w, data)
751 case dictionary:
752 return writeObject(w, data)
753 default:
754 return errors.New(`invalid JSON value`)
755 }
756 }
757
758 func writeByte(w *bufio.Writer, b byte) error {
759 err := w.WriteByte(b)
760 if err != nil {
761 return io.EOF
762 }
763 return nil
764 }
765
766 func writeKeyword(w *bufio.Writer, s string) error {
767 if _, err := w.WriteString(s); err == nil {
768 return nil
769 }
770 return io.EOF
771 }
772
773 func writeEscapedString(w *bufio.Writer, s string) error {
774 if !needsEscaping(s) {
775 w.WriteByte('"')
776 w.WriteString(s)
777 return writeByte(w, '"')
778 }
779
780 w.WriteByte('"')
781
782 for _, r := range s {
783 if ' ' <= r && r <= '~' && r != '\\' && r != '"' {
784 w.WriteRune(r)
785 continue
786 }
787
788 switch r {
789 case '\\':
790 w.WriteString(`\\`)
791 case '"':
792 w.WriteString(`\"`)
793 default:
794 writeEscapedHex(w, r)
795 }
796 }
797
798 return writeByte(w, '"')
799 }
800
801 func writeEscapedHex(w *bufio.Writer, r rune) error {
802 w.WriteByte('\\')
803 w.WriteByte('u')
804 writeHex(w, byte(r>>24))
805 writeHex(w, byte(r>>16))
806 writeHex(w, byte(r>>8))
807 writeHex(w, byte(r))
808 return nil
809 }
810
811 // writeHex is faster than calling fmt.Fprintf(w, `%04x`, b)
812 func writeHex(w *bufio.Writer, b byte) {
813 const hexDigits = `0123456789abcdef`
814 w.WriteByte(hexDigits[b>>4])
815 w.WriteByte(hexDigits[b&0x0f])
816 }
817
818 func needsEscaping(s string) bool {
819 for i := range s {
820 if b := s[i]; ' ' <= b && b <= '~' && b != '\\' && b != '"' {
821 continue
822 }
823 return true
824 }
825
826 return false
827 }
828
829 func writeArray(w *bufio.Writer, items []any) error {
830 w.WriteByte('[')
831 for i, v := range items {
832 if i > 0 {
833 if err := writeByte(w, ','); err != nil {
834 return err
835 }
836 }
837 if err := writeValue(w, v); err != nil {
838 return err
839 }
840 }
841 return writeByte(w, ']')
842 }
843
844 func writeObject(w *bufio.Writer, items dictionary) error {
845 w.WriteByte('{')
846 for i, k := range items.Keys {
847 if i > 0 {
848 if err := writeByte(w, ','); err != nil {
849 return err
850 }
851 }
852 writeEscapedString(w, k)
853 w.WriteByte(':')
854 if err := writeValue(w, items.Map[k]); err != nil {
855 return err
856 }
857 }
858 return writeByte(w, '}')
859 }
860
861 func indexPair(s string, x byte, y byte) int {
862 var cur, prev byte
863
864 for i := range s {
865 cur = s[i]
866 if prev == x && cur == y && i > 0 {
867 return i
868 }
869 prev = cur
870 }
871
872 return -1
873 }
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 }