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 `
46
47 func Main() {
48 nerr := 0
49 buffered := false
50 sensitive := true
51 args := os.Args[1:]
52
53 for len(args) > 0 {
54 switch args[0] {
55 case `-b`, `--b`, `-buffered`, `--buffered`:
56 buffered = true
57 args = args[1:]
58 continue
59
60 case `-h`, `--h`, `-help`, `--help`:
61 os.Stdout.WriteString(info[1:])
62 return
63
64 case `-i`, `--i`, `-ins`, `--ins`:
65 sensitive = false
66 args = args[1:]
67 continue
68 }
69
70 break
71 }
72
73 if len(args) > 0 && args[0] == `--` {
74 args = args[1:]
75 }
76
77 liveLines := !buffered
78 if !buffered {
79 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
80 liveLines = false
81 }
82 }
83
84 if len(args) == 0 {
85 args = []string{`^$`}
86 }
87
88 exprs := make([]*regexp.Regexp, 0, len(args))
89
90 for _, src := range args {
91 var err error
92 var exp *regexp.Regexp
93 if !sensitive {
94 exp, err = regexp.Compile(`(?i)` + src)
95 } else {
96 exp, err = regexp.Compile(src)
97 }
98
99 if err != nil {
100 os.Stderr.WriteString(err.Error())
101 os.Stderr.WriteString("\n")
102 nerr++
103 }
104
105 exprs = append(exprs, exp)
106 }
107
108 if nerr > 0 {
109 os.Exit(1)
110 return
111 }
112
113 var buf []byte
114 sc := bufio.NewScanner(os.Stdin)
115 sc.Buffer(nil, 8*1024*1024*1024)
116 bw := bufio.NewWriter(os.Stdout)
117
118 for i := 0; sc.Scan(); i++ {
119 line := sc.Bytes()
120 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
121 line = line[3:]
122 }
123
124 s := line
125 if bytes.IndexByte(s, '\x1b') >= 0 {
126 buf = plain(buf[:0], s)
127 s = buf
128 }
129
130 if !match(s, exprs) {
131 bw.Write(line)
132 bw.WriteByte('\n')
133
134 if !liveLines {
135 continue
136 }
137
138 if err := bw.Flush(); err != nil {
139 return
140 }
141 }
142 }
143 }
144
145 func match(what []byte, with []*regexp.Regexp) bool {
146 for _, e := range with {
147 if e.Match(what) {
148 return true
149 }
150 }
151 return false
152 }
153
154 func plain(dst []byte, src []byte) []byte {
155 for len(src) > 0 {
156 i, j := indexEscapeSequence(src)
157 if i < 0 {
158 dst = append(dst, src...)
159 break
160 }
161 if j < 0 {
162 j = len(src)
163 }
164
165 if i > 0 {
166 dst = append(dst, src[:i]...)
167 }
168
169 src = src[j:]
170 }
171
172 return dst
173 }
174
175 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
176 // the multi-byte sequences starting with ESC[; the result is a pair of slice
177 // indices which can be independently negative when either the start/end of
178 // a sequence isn't found; given their fairly-common use, even the hyperlink
179 // ESC]8 sequences are supported
180 func indexEscapeSequence(s []byte) (int, int) {
181 var prev byte
182
183 for i, b := range s {
184 if prev == '\x1b' && b == '[' {
185 j := indexLetter(s[i+1:])
186 if j < 0 {
187 return i, -1
188 }
189 return i - 1, i + 1 + j + 1
190 }
191
192 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
193 j := indexPair(s[i+1:], '\x1b', '\\')
194 if j < 0 {
195 return i, -1
196 }
197 return i - 1, i + 1 + j + 2
198 }
199
200 prev = b
201 }
202
203 return -1, -1
204 }
205
206 func indexLetter(s []byte) int {
207 for i, b := range s {
208 upper := b &^ 32
209 if 'A' <= upper && upper <= 'Z' {
210 return i
211 }
212 }
213
214 return -1
215 }
216
217 func indexPair(s []byte, x byte, y byte) int {
218 var prev byte
219
220 for i, b := range s {
221 if prev == x && b == y && i > 0 {
222 return i
223 }
224 prev = b
225 }
226
227 return -1
228 }
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 "go/ast"
30 "go/parser"
31 "go/token"
32 "io"
33 "math"
34 "math/big"
35 "os"
36 "strings"
37 )
38
39 const info = `
40 ca [options...] [go expressions...]
41
42 CAlculate arbitrary-size fractions, using go expressions. For convenience,
43 function names are case-insensitive, and square brackets are treated the
44 same as (round) parentheses.
45
46 Several functions are available, along with their aliases:
47
48 abs(x)
49 bits(x)
50 c(n, k) choose, com, comb, combinations
51 ceil(x) ceiling
52 dbin(x, n, p) dbinom
53 den(x) denom, denominator
54 digits(x)
55 f(x) fac, fact, factorial
56 floor(x)
57 isprime(n) prime
58 gcd(x, y) gcf
59 lcm(x, y)
60 num(x) numer, numerator
61 p(n, k) per, perm, permutations
62 pow(x, y) power
63 pow2(x) power2
64 pow10(x) power10
65 rem(x, y) remainder
66 sgn(x) sign
67
68 avg(...) mean
69 max(...)
70 min(...)
71 polyval(x, ...) horner
72
73 Note: when the exponent given to the pow/power function isn't an integer,
74 the result is a double-precision floating-point approximation.
75
76 All (optional) leading options start with either single or double-dash:
77
78 -d, -decs, -decimals show decimal digits, instead of fractions
79 -h, -help show this help message
80 `
81
82 func Main() {
83 args := os.Args[1:]
84 showAsFrac := true
85
86 if len(args) > 0 {
87 switch args[0] {
88 case `-h`, `--h`, `-help`, `--help`:
89 os.Stdout.WriteString(info[1:])
90 return
91
92 case `-d`, `--d`, `-decs`, `--decs`, `-decimals`, `--decimals`:
93 showAsFrac = false
94 args = args[1:]
95 }
96 }
97
98 if len(args) > 0 && args[0] == `--` {
99 args = args[1:]
100 }
101
102 if len(args) == 0 {
103 os.Stderr.WriteString(info[1:])
104 os.Exit(1)
105 return
106 }
107
108 if err := run(os.Stdout, args, showAsFrac); 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(w io.Writer, args []string, showAsFrac bool) error {
117 for _, src := range args {
118 src = strings.ToLower(src)
119
120 // treat square brackets like parentheses, for convenience
121 src = strings.Replace(src, `[`, `(`, -1)
122 src = strings.Replace(src, `]`, `)`, -1)
123
124 expr, err := parser.ParseExpr(src)
125 if err != nil {
126 return err
127 }
128
129 n, err := eval(expr)
130 if err != nil {
131 return err
132 }
133
134 // only show the numerator, when the denominator is 1; when showing
135 // results as numbers with decimals, all trailing zero decimals are
136 // ignored
137 s := ``
138 if n.IsInt() {
139 s = n.Num().String()
140 } else if showAsFrac {
141 s = n.String()
142 } else {
143 s = trimDecimals(n.FloatString(100))
144 }
145
146 io.WriteString(w, s)
147 _, err = io.WriteString(w, "\n")
148
149 if err != nil {
150 break
151 }
152 }
153
154 return nil
155 }
156
157 // trimDecimals ignores excessive trailing decimal zeros, if any, as well as
158 // the decimal dot itself, if all decimals turn out to be zeros; integers are
159 // returned as given
160 func trimDecimals(s string) string {
161 // with no decimals, keep all/any trailing zeros
162 if strings.IndexByte(s, '.') < 0 {
163 return s
164 }
165
166 // ignore all trailing zero decimals
167 for len(s) > 0 && s[len(s)-1] == '0' {
168 s = s[:len(s)-1]
169 }
170 // ignore trailing decimal
171 if len(s) > 0 && s[len(s)-1] == '.' {
172 s = s[:len(s)-1]
173 }
174 return s
175 }
176
177 func eval(expr ast.Expr) (*big.Rat, error) {
178 switch expr := expr.(type) {
179 case *ast.BasicLit:
180 return evalLit(expr)
181 case *ast.ParenExpr:
182 return eval(expr.X)
183 case *ast.UnaryExpr:
184 return evalUnary(expr)
185 case *ast.BinaryExpr:
186 return evalBinary(expr)
187 case *ast.CallExpr:
188 return evalCall(expr)
189 case *ast.Ident:
190 return evalIdent(expr)
191 }
192
193 return nil, errors.New(`unsupported expression type`)
194 }
195
196 func evalLit(expr *ast.BasicLit) (*big.Rat, error) {
197 switch expr.Kind {
198 case token.INT, token.FLOAT:
199 n := big.NewRat(0, 1)
200 n, _ = n.SetString(expr.Value)
201 return n, nil
202 }
203
204 return nil, errors.New(`unsupported literal type`)
205 }
206
207 func evalUnary(expr *ast.UnaryExpr) (*big.Rat, error) {
208 switch expr.Op {
209 case token.ADD:
210 return eval(expr.X)
211
212 case token.SUB:
213 n, err := eval(expr.X)
214 if n != nil {
215 n = n.Neg(n)
216 }
217 return n, err
218
219 case token.NOT:
220 return eval(&ast.CallExpr{
221 Fun: ast.NewIdent(`factorial`),
222 Args: []ast.Expr{expr.X},
223 })
224 }
225
226 return nil, errors.New(`unsupported unary operation ` + expr.Op.String())
227 }
228
229 func evalBinary(expr *ast.BinaryExpr) (*big.Rat, error) {
230 x, err := eval(expr.X)
231 if err != nil {
232 return nil, err
233 }
234
235 y, err := eval(expr.Y)
236 if err != nil {
237 return nil, err
238 }
239
240 z := big.NewRat(0, 1)
241
242 switch expr.Op {
243 case token.ADD:
244 z = z.Add(x, y)
245 return z, nil
246
247 case token.SUB:
248 z = z.Sub(x, y)
249 return z, nil
250
251 case token.MUL:
252 z = z.Mul(x, y)
253 return z, nil
254
255 case token.QUO:
256 if y.Sign() == 0 {
257 return nil, errors.New(`can't divide by zero`)
258 }
259 z = z.Quo(x, y)
260 return z, nil
261
262 case token.REM:
263 return remainder(x, y)
264 }
265
266 return nil, errors.New(`unsupported binary operation ` + expr.Op.String())
267 }
268
269 func evalCall(expr *ast.CallExpr) (*big.Rat, error) {
270 ident, ok := expr.Fun.(*ast.Ident)
271 if !ok {
272 return nil, errors.New(`unsupported function type`)
273 }
274 s := ident.Name
275
276 if _, ok := varFuncs[s]; ok {
277 return evalVarCall(s, expr)
278 }
279
280 switch len(expr.Args) {
281 case 1:
282 return evalCall1(s, expr)
283 case 2:
284 return evalCall2(s, expr)
285 case 3:
286 return evalCall3(s, expr)
287 }
288
289 return nil, errors.New(`function '` + s + `' not available`)
290 }
291
292 func evalIdent(expr *ast.Ident) (*big.Rat, error) {
293 s := strings.ToLower(expr.Name)
294 if v, ok := values[s]; ok {
295 if f, ok := big.NewRat(0, 1).SetString(v); ok {
296 return f, nil
297 }
298 return nil, errors.New(`value '` + s + `' isn't a valid number`)
299 }
300 return nil, errors.New(`value '` + s + `' not available`)
301 }
302
303 func copyFrac(x *big.Rat) *big.Rat {
304 y := big.NewRat(0, 1)
305 y = y.Add(y, x)
306 return y
307 }
308
309 var values = map[string]string{
310 `kb`: `1024`,
311 `mb`: `1048576`,
312 `gb`: `1073741824`,
313 `tb`: `1099511627776`,
314 `pb`: `1125899906842624`,
315 `kib`: `1024`,
316 `mib`: `1048576`,
317 `gib`: `1073741824`,
318 `tib`: `1099511627776`,
319 `pib`: `1125899906842624`,
320
321 `hour`: `3600`,
322 `hr`: `3600`,
323 `day`: `86400`,
324 `week`: `604800`,
325 `wk`: `604800`,
326
327 `mol`: `602214076000000000000000`,
328 `mole`: `602214076000000000000000`,
329 }
330
331 var funcs1 = map[string]func(*big.Rat) (*big.Rat, error){
332 `abs`: abs,
333 `bits`: bits,
334 `ceil`: ceiling,
335 `ceiling`: ceiling,
336 `den`: denominator,
337 `denom`: denominator,
338 `denominator`: denominator,
339 `digits`: digits,
340 `f`: factorial,
341 `fac`: factorial,
342 `fact`: factorial,
343 `factorial`: factorial,
344 `floor`: floor,
345 `isprime`: isPrime,
346 `prime`: isPrime,
347 `num`: numerator,
348 `numer`: numerator,
349 `numerator`: numerator,
350 `pow2`: power2,
351 `power2`: power2,
352 `pow10`: power10,
353 `power10`: power10,
354 `sgn`: sign,
355 `sign`: sign,
356 }
357
358 func evalCall1(name string, expr *ast.CallExpr) (*big.Rat, error) {
359 x, err := eval(expr.Args[0])
360 if err != nil {
361 return nil, err
362 }
363
364 fn, ok := funcs1[name]
365 if !ok {
366 return nil, errors.New(`function '` + name + `' not available`)
367 }
368
369 return fn(x)
370 }
371
372 var funcs2 = map[string]func(*big.Rat, *big.Rat) (*big.Rat, error){
373 `c`: combinations,
374 `com`: combinations,
375 `comb`: combinations,
376 `combinations`: combinations,
377 `choose`: combinations,
378 `gcd`: gcd,
379 `gcf`: gcd,
380 `lcm`: lcm,
381 `p`: permutations,
382 `per`: permutations,
383 `perm`: permutations,
384 `permutations`: permutations,
385 `pow`: power,
386 `power`: power,
387 `rem`: remainder,
388 `remainder`: remainder,
389 }
390
391 func evalCall2(name string, expr *ast.CallExpr) (*big.Rat, error) {
392 x, err := eval(expr.Args[0])
393 if err != nil {
394 return nil, err
395 }
396
397 y, err := eval(expr.Args[1])
398 if err != nil {
399 return nil, err
400 }
401
402 fn, ok := funcs2[name]
403 if !ok {
404 return nil, errors.New(`function '` + name + `' not available`)
405 }
406
407 return fn(x, y)
408 }
409
410 var funcs3 = map[string]func(*big.Rat, *big.Rat, *big.Rat) (*big.Rat, error){
411 `db`: dbinom,
412 `dbin`: dbinom,
413 `dbinom`: dbinom,
414 }
415
416 func evalCall3(name string, expr *ast.CallExpr) (*big.Rat, error) {
417 x, err := eval(expr.Args[0])
418 if err != nil {
419 return nil, err
420 }
421
422 y, err := eval(expr.Args[1])
423 if err != nil {
424 return nil, err
425 }
426
427 z, err := eval(expr.Args[2])
428 if err != nil {
429 return nil, err
430 }
431
432 fn, ok := funcs3[name]
433 if !ok {
434 return nil, errors.New(`function '` + name + `' not available`)
435 }
436
437 return fn(x, y, z)
438 }
439
440 var varFuncs = map[string]func(...*big.Rat) (*big.Rat, error){
441 `avg`: avgNum,
442 `horner`: polyval,
443 `max`: maxNum,
444 `mean`: avgNum,
445 `min`: minNum,
446 `polyval`: polyval,
447 `sum`: sumNum,
448 }
449
450 func evalVarCall(name string, expr *ast.CallExpr) (*big.Rat, error) {
451 fn, ok := varFuncs[name]
452 if !ok {
453 return nil, errors.New(`function '` + name + `' not available`)
454 }
455
456 inputs := make([]*big.Rat, 0, len(expr.Args))
457 for _, a := range expr.Args {
458 v, err := eval(a)
459 if err != nil {
460 return nil, err
461 }
462 inputs = append(inputs, v)
463 }
464
465 return fn(inputs...)
466 }
467
468 func abs(n *big.Rat) (*big.Rat, error) {
469 n = n.Abs(n)
470 return n, nil
471 }
472
473 func avgNum(values ...*big.Rat) (*big.Rat, error) {
474 if len(values) == 0 {
475 return nil, errors.New(`mean: no numbers given`)
476 }
477
478 res := big.NewRat(0, 1)
479 for _, v := range values {
480 res = res.Add(res, v)
481 }
482 res = res.Quo(res, big.NewRat(int64(len(values)), 1))
483 return res, nil
484 }
485
486 func bits(n *big.Rat) (*big.Rat, error) {
487 if !n.IsInt() {
488 return nil, errors.New(`function 'bits' only works with integers`)
489 }
490
491 bits := big.NewRat(0, 1)
492 bits.SetInt64(int64(n.Num().BitLen()))
493 return bits, nil
494 }
495
496 func ceiling(n *big.Rat) (*big.Rat, error) {
497 if n.IsInt() {
498 return n, nil
499 }
500
501 v := big.NewInt(0)
502 v = v.Quo(n.Num(), n.Denom())
503 if n.Sign() >= 0 {
504 v = v.Add(v, big.NewInt(1))
505 }
506 n = n.SetInt(v)
507 return n, nil
508 }
509
510 func combinations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
511 if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
512 const msg = `combinations are defined only for non-negative integers`
513 return nil, errors.New(msg)
514 }
515
516 v, err := permutations(n, k)
517 if err != nil {
518 return v, err
519 }
520
521 f, err := factorial(k)
522 if err != nil {
523 return nil, err
524 }
525
526 if f.Sign() <= 0 {
527 return nil, errors.New(`combinations: factorial isn't positive`)
528 }
529 return v.Quo(v, f), nil
530 }
531
532 func dbinom(x *big.Rat, n *big.Rat, p *big.Rat) (*big.Rat, error) {
533 a, err := combinations(copyFrac(n), copyFrac(x))
534 if err != nil {
535 return nil, err
536 }
537
538 b, err := power(copyFrac(p), copyFrac(x))
539 if err != nil {
540 return nil, err
541 }
542
543 // c = (1 - p) ** (n - x)
544 y := big.NewRat(1, 1)
545 y = y.Sub(y, p)
546 z := copyFrac(n)
547 z = z.Sub(z, x)
548 c, err := power(y, z)
549 if err != nil {
550 return nil, err
551 }
552
553 // return combinations(n, x) * (p ** x) * ((1 - p) ** (n - x))
554 d := big.NewRat(0, 1)
555 d = d.Add(d, a)
556 d = d.Mul(d, b)
557 d = d.Mul(d, c)
558 return d, nil
559 }
560
561 func denominator(n *big.Rat) (*big.Rat, error) {
562 return big.NewRat(0, 1).SetFrac(n.Denom(), big.NewInt(1)), nil
563 }
564
565 func digits(n *big.Rat) (*big.Rat, error) {
566 if !n.IsInt() {
567 return nil, errors.New(`function 'digits' only works with integers`)
568 }
569
570 digits := big.NewRat(0, 1)
571 digits.SetInt64(int64(len(n.Num().String())))
572 return digits, nil
573 }
574
575 func factorial(n *big.Rat) (*big.Rat, error) {
576 sign := n.Sign()
577 if sign < 0 {
578 return nil, errors.New(`factorials aren't defined for negatives`)
579 }
580 if sign == 0 {
581 return big.NewRat(1, 1), nil
582 }
583
584 f := big.NewRat(1, 1)
585 for one := big.NewRat(1, 1); n.Sign() > 0; n = n.Sub(n, one) {
586 f = f.Mul(f, n)
587 }
588 return f, nil
589 }
590
591 func floor(n *big.Rat) (*big.Rat, error) {
592 if n.IsInt() {
593 return n, nil
594 }
595
596 v := big.NewInt(0)
597 v = v.Quo(n.Num(), n.Denom())
598 if n.Sign() < 0 {
599 v = v.Sub(v, big.NewInt(1))
600 }
601 n = n.SetInt(v)
602 return n, nil
603 }
604
605 func gcd(x *big.Rat, y *big.Rat) (*big.Rat, error) {
606 if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
607 const msg = `gcd are defined only for positive integers`
608 return nil, errors.New(msg)
609 }
610
611 gcd := big.NewRat(0, 1)
612 gcd = gcd.Add(gcd, x)
613 gcd = gcd.Mul(gcd, y)
614
615 lcm, err := lcm(x, y)
616 if err != nil {
617 return nil, err
618 }
619 if lcm.Sign() <= 0 {
620 return nil, errors.New(`gcd: lcm isn't positive`)
621 }
622
623 gcd = gcd.Quo(gcd, lcm)
624 return gcd, nil
625 }
626
627 func isPrime(n *big.Rat) (*big.Rat, error) {
628 if !n.IsInt() {
629 return nil, errors.New(`function 'isprime' only works with integers`)
630 }
631
632 if n.Sign() <= 0 {
633 return big.NewRat(0, 1), nil
634 }
635
636 v := n.Num()
637 if v.IsInt64() {
638 n := v.Int64()
639 if n == 2 {
640 return big.NewRat(1, 1), nil
641 }
642 if n < 2 || n%2 == 0 {
643 return big.NewRat(0, 1), nil
644 }
645 }
646
647 two := big.NewInt(2)
648 max := big.NewInt(1).Sqrt(v)
649 mod := big.NewInt(0)
650 for i := big.NewInt(3); i.Cmp(max) <= 0; i = i.Add(i, two) {
651 mod = mod.Rem(v, i)
652 if mod.Sign() == 0 {
653 return big.NewRat(0, 1), nil
654 }
655 }
656 return big.NewRat(1, 1), nil
657 }
658
659 func lcm(x *big.Rat, y *big.Rat) (*big.Rat, error) {
660 if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
661 const msg = `lcm is defined only for positive integers`
662 return nil, errors.New(msg)
663 }
664
665 // a = min(x, y)
666 // b = max(x, y)
667 var a, b *big.Int
668 if x.Cmp(y) < 0 {
669 a = x.Num()
670 b = y.Num()
671 } else {
672 a = y.Num()
673 b = x.Num()
674 }
675
676 // c = b
677 c := big.NewInt(0)
678 c = c.Add(c, b)
679
680 // while (c % a > 0) c += b
681 for r := big.NewInt(1); r.Sign() > 0; r = r.Rem(c, a) {
682 c = c.Add(c, b)
683 }
684
685 // return c
686 return big.NewRat(0, 1).SetFrac(c, big.NewInt(1)), nil
687 }
688
689 func maxNum(values ...*big.Rat) (*big.Rat, error) {
690 if len(values) == 0 {
691 return nil, errors.New(`max: no numbers given`)
692 }
693
694 var max *big.Rat
695 for i, v := range values {
696 if i == 0 || max.Cmp(v) < 0 {
697 max = v
698 }
699 }
700 return max, nil
701 }
702
703 func minNum(values ...*big.Rat) (*big.Rat, error) {
704 if len(values) == 0 {
705 return nil, errors.New(`min: no numbers given`)
706 }
707
708 var min *big.Rat
709 for i, v := range values {
710 if i == 0 || min.Cmp(v) < 0 {
711 min = v
712 }
713 }
714 return min, nil
715 }
716
717 func numerator(n *big.Rat) (*big.Rat, error) {
718 return big.NewRat(0, 1).SetFrac(n.Num(), big.NewInt(1)), nil
719 }
720
721 func permutations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
722 if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
723 const msg = `permutations are defined only for non-negative integers`
724 return nil, errors.New(msg)
725 }
726
727 one := big.NewRat(1, 1)
728 perm := big.NewRat(1, 1)
729 // end = n - k + 1
730 end := big.NewRat(1, 1).Set(n)
731 end = end.Sub(end, k)
732 end = end.Add(end, one)
733
734 for v := big.NewRat(1, 1).Set(n); v.Cmp(end) >= 0; v = v.Sub(v, one) {
735 perm = perm.Mul(perm, v)
736 }
737 return perm, nil
738 }
739
740 // polyval evaluates a polynomial using Horner's algorithm: the first number is
741 // the x value to evaulate the polynomial with, followed by all the polynomial
742 // coefficients in textbook order, from the highest power down to the final
743 // constant
744 func polyval(values ...*big.Rat) (*big.Rat, error) {
745 if len(values) == 0 {
746 // return big.NewRat(0, 1), nil
747 return nil, errors.New(`polyval: no numbers given`)
748 }
749
750 x0 := values[0]
751 values = values[1:]
752
753 x := big.NewRat(1, 1)
754 y := big.NewRat(0, 1)
755 prod := big.NewRat(0, 1)
756
757 for i := len(values) - 1; i >= 0; i-- {
758 prod = prod.Mul(values[i], x)
759 y = y.Add(y, prod)
760 x = x.Mul(x, x0)
761 }
762
763 return y, nil
764 }
765
766 func power(x *big.Rat, y *big.Rat) (*big.Rat, error) {
767 // if !y.IsInt() {
768 // return nil, errors.New(`only integer exponents are supported`)
769 // }
770
771 if !y.IsInt() {
772 a, _ := x.Float64()
773 b, _ := y.Float64()
774 c := math.Pow(a, b)
775 if math.IsNaN(c) || math.IsInf(c, 0) {
776 return nil, errors.New(`can't calculate/approximate power given`)
777 }
778 z := big.NewRat(0, 1)
779 z = z.SetFloat64(c)
780 return z, nil
781 }
782
783 if x.Sign() == 0 && y.Sign() == 0 {
784 return nil, errors.New(`zero to the zero power isn't defined`)
785 }
786
787 if x.Sign() == 0 {
788 return big.NewRat(0, 1), nil
789 }
790 if y.Sign() == 0 {
791 return big.NewRat(1, 1), nil
792 }
793
794 return powFractionInPlace(x, y.Num())
795 }
796
797 // powFractionInPlace calculates values in place: since bignums are pointers
798 // to their representations, this means the original values will change
799 func powFractionInPlace(x *big.Rat, y *big.Int) (*big.Rat, error) {
800 xsign := x.Sign()
801 ysign := y.Sign()
802
803 // 0 ** 0 is undefined
804 if xsign == 0 && ysign == 0 {
805 const msg = `0 to the 0 doesn't make sense`
806 return nil, errors.New(msg)
807 }
808
809 // otherwise x ** 0 is 1
810 if ysign == 0 {
811 return big.NewRat(1, 1), nil
812 }
813
814 // x ** (y < 0) is like (1/x) ** -y
815 if ysign < 0 {
816 inv := big.NewRat(1, 1).Inv(x)
817 neg := big.NewInt(1).Neg(y)
818 return powFractionInPlace(inv, neg)
819 }
820
821 // 0 ** (y > 0) is 0
822 if xsign == 0 {
823 return x, nil
824 }
825
826 // x ** 0 is 0
827 if ysign == 0 {
828 return big.NewRat(0, 1), nil
829 }
830
831 // x ** 1 is x
832 if y.IsInt64() && y.Int64() == 1 {
833 return x, nil
834 }
835
836 return _powFractionRec(x, y), nil
837 }
838
839 func _powFractionRec(x *big.Rat, y *big.Int) *big.Rat {
840 switch y.Sign() {
841 case -1:
842 return big.NewRat(0, 1)
843 case 0:
844 return big.NewRat(1, 1)
845 case 1:
846 if y.IsInt64() && y.Int64() == 1 {
847 return x
848 }
849 }
850
851 yhalf := big.NewInt(0)
852 oddrem := big.NewInt(0)
853 yhalf.QuoRem(y, big.NewInt(2), oddrem)
854
855 if oddrem.Sign() == 0 {
856 xsquare := big.NewRat(0, 1)
857 return _powFractionRec(xsquare.Mul(x, x), yhalf)
858 }
859 prevpow := _powFractionRec(x, y.Sub(y, big.NewInt(1)))
860 return prevpow.Mul(prevpow, x)
861 }
862
863 func power2(x *big.Rat) (*big.Rat, error) {
864 return power(big.NewRat(2, 1), x)
865 }
866
867 func power10(x *big.Rat) (*big.Rat, error) {
868 return power(big.NewRat(10, 1), x)
869 }
870
871 func remainder(x *big.Rat, y *big.Rat) (*big.Rat, error) {
872 if !x.IsInt() || !y.IsInt() {
873 return nil, errors.New(`remainder only works with 2 integers`)
874 }
875
876 if y.Sign() == 0 {
877 return nil, errors.New(`can't divide by 0`)
878 }
879
880 a := x.Num()
881 b := y.Num()
882 c := big.NewInt(0)
883 c = c.Rem(a, b)
884 rem := big.NewRat(0, 1)
885 rem = rem.SetInt(c)
886 return rem, nil
887 }
888
889 func sign(n *big.Rat) (*big.Rat, error) {
890 sign := n.Sign()
891 if sign > 0 {
892 n = big.NewRat(1, 1)
893 } else if sign < 0 {
894 n = big.NewRat(-1, 1)
895 } else {
896 n = big.NewRat(0, 1)
897 }
898 return n, nil
899 }
900
901 func sumNum(values ...*big.Rat) (*big.Rat, error) {
902 sum := big.NewRat(0, 1)
903 for _, v := range values {
904 sum = sum.Add(sum, v)
905 }
906 return sum, nil
907 }
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: ./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 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 )
33
34 const info = `
35 dedup [options...] [file...]
36
37
38 DEDUPlicate lines prevents the same line from appearing again in the output,
39 after the first time. Unique lines are remembered across inputs.
40
41 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
42 feeds 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 stringSet map[string]struct{}
50
51 func Main() {
52 buffered := false
53 args := os.Args[1:]
54
55 if len(args) > 0 {
56 switch args[0] {
57 case `-b`, `--b`, `-buffered`, `--buffered`:
58 buffered = true
59 args = args[1:]
60
61 case `-h`, `--h`, `-help`, `--help`:
62 os.Stdout.WriteString(info[1:])
63 return
64 }
65 }
66
67 if len(args) > 0 && args[0] == `--` {
68 args = args[1:]
69 }
70
71 liveLines := !buffered
72 if !buffered {
73 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
74 liveLines = false
75 }
76 }
77
78 err := run(os.Stdout, args, liveLines)
79 if err != nil && err != io.EOF {
80 os.Stderr.WriteString(err.Error())
81 os.Stderr.WriteString("\n")
82 os.Exit(1)
83 return
84 }
85 }
86
87 func run(w io.Writer, args []string, live bool) error {
88 files := make(stringSet)
89 lines := make(stringSet)
90 bw := bufio.NewWriter(w)
91 defer bw.Flush()
92
93 for _, name := range args {
94 if _, ok := files[name]; ok {
95 continue
96 }
97 files[name] = struct{}{}
98
99 if err := handleFile(bw, name, lines, live); err != nil {
100 return err
101 }
102 }
103
104 if len(args) == 0 {
105 return dedup(bw, os.Stdin, lines, live)
106 }
107 return nil
108 }
109
110 func handleFile(w *bufio.Writer, name string, got stringSet, live bool) error {
111 if name == `` || name == `-` {
112 return dedup(w, os.Stdin, got, live)
113 }
114
115 f, err := os.Open(name)
116 if err != nil {
117 return errors.New(`can't read from file named "` + name + `"`)
118 }
119 defer f.Close()
120
121 return dedup(w, f, got, live)
122 }
123
124 func dedup(w *bufio.Writer, r io.Reader, got stringSet, live bool) error {
125 const gb = 1024 * 1024 * 1024
126 sc := bufio.NewScanner(r)
127 sc.Buffer(nil, 8*gb)
128
129 for sc.Scan() {
130 line := sc.Text()
131 if _, ok := got[line]; ok {
132 continue
133 }
134 got[line] = struct{}{}
135
136 w.Write(sc.Bytes())
137 if w.WriteByte('\n') != nil {
138 return io.EOF
139 }
140
141 if !live {
142 continue
143 }
144
145 if w.Flush() != nil {
146 return io.EOF
147 }
148 }
149
150 return sc.Err()
151 }
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: ./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 if len(args) > 0 && args[0] == `--` {
68 args = args[1:]
69 }
70
71 linesLeft := 1
72 if len(args) > 0 {
73 s := strings.Replace(args[0], `_`, ``, -1)
74 n, err := strconv.ParseInt(s, 10, 64)
75 if err == nil {
76 linesLeft = int(n)
77 args = args[1:]
78 }
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`:
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 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
142 // pattern is a regular-expression pattern which distinguishes between the
143 // start/end of a line and those of the chunks it can be used to match
144 type pattern struct {
145 // expr is the regular-expression
146 expr *regexp.Regexp
147
148 // begin is whether the regexp refers to the start of a line
149 begin bool
150
151 // end is whether the regexp refers to the end of a line
152 end bool
153 }
154
155 func compile(src string) (pattern, error) {
156 expr, err := regexp.Compile(src)
157
158 var pat pattern
159 pat.expr = expr
160 pat.begin = strings.HasPrefix(src, `^`) || strings.HasPrefix(src, `(?i)^`)
161 pat.end = strings.HasSuffix(src, `$`) && !strings.HasSuffix(src, `\$`)
162 return pat, err
163 }
164
165 func (p pattern) findIndex(s []byte, i int, last int) (start int, stop int) {
166 if i > 0 && p.begin {
167 return -1, -1
168 }
169 if i != last && p.end {
170 return -1, -1
171 }
172
173 span := p.expr.FindIndex(s)
174 // also ignore empty regex matches to avoid infinite outer loops,
175 // as skipping empty slices isn't advancing at all, leaving the
176 // string stuck to being empty-matched forever by the same regex
177 if len(span) != 2 || span[0] == span[1] {
178 return -1, -1
179 }
180
181 return span[0], span[1]
182 }
183
184 func run(w io.Writer, r io.Reader, pats []pattern, filter, live bool) error {
185 sc := bufio.NewScanner(r)
186 sc.Buffer(nil, 8*1024*1024*1024)
187 bw := bufio.NewWriter(w)
188 defer bw.Flush()
189
190 for i := 0; sc.Scan(); i++ {
191 s := sc.Bytes()
192 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
193 s = s[3:]
194 }
195
196 n := 0
197 last := countChunks(s) - 1
198 if last < 0 {
199 last = 0
200 }
201
202 if filter && !matches(s, pats, last) {
203 continue
204 }
205
206 for len(s) > 0 {
207 i, j := indexEscapeSequence(s)
208 if i < 0 {
209 handleChunk(bw, s, pats, n, last)
210 break
211 }
212 if j < 0 {
213 j = len(s)
214 }
215
216 handleChunk(bw, s[:i], pats, n, last)
217 if i > 0 {
218 n++
219 }
220
221 bw.Write(s[i:j])
222
223 s = s[j:]
224 }
225
226 if bw.WriteByte('\n') != nil {
227 return io.EOF
228 }
229
230 if !live {
231 continue
232 }
233
234 if bw.Flush() != nil {
235 return io.EOF
236 }
237 }
238
239 return sc.Err()
240 }
241
242 // matches finds out if any regex matches any substring around ANSI-sequences
243 func matches(s []byte, patterns []pattern, last int) bool {
244 n := 0
245
246 for len(s) > 0 {
247 i, j := indexEscapeSequence(s)
248 if i < 0 {
249 for _, p := range patterns {
250 if begin, _ := p.findIndex(s, n, last); begin >= 0 {
251 return true
252 }
253 }
254 return false
255 }
256
257 if j < 0 {
258 j = len(s)
259 }
260
261 for _, p := range patterns {
262 if begin, _ := p.findIndex(s[:i], n, last); begin >= 0 {
263 return true
264 }
265 }
266
267 if i > 0 {
268 n++
269 }
270
271 s = s[j:]
272 }
273
274 return false
275 }
276
277 func countChunks(s []byte) int {
278 chunks := 0
279
280 for len(s) > 0 {
281 i, j := indexEscapeSequence(s)
282 if i < 0 {
283 break
284 }
285
286 if i > 0 {
287 chunks++
288 }
289
290 if j < 0 {
291 break
292 }
293 s = s[j:]
294 }
295
296 if len(s) > 0 {
297 chunks++
298 }
299 return chunks
300 }
301
302 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
303 // the multi-byte sequences starting with ESC[; the result is a pair of slice
304 // indices which can be independently negative when either the start/end of
305 // a sequence isn't found; given their fairly-common use, even the hyperlink
306 // ESC]8 sequences are supported
307 func indexEscapeSequence(s []byte) (int, int) {
308 var prev byte
309
310 for i, b := range s {
311 if prev == '\x1b' && b == '[' {
312 j := indexLetter(s[i+1:])
313 if j < 0 {
314 return i, -1
315 }
316 return i - 1, i + 1 + j + 1
317 }
318
319 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
320 j := indexPair(s[i+1:], '\x1b', '\\')
321 if j < 0 {
322 return i, -1
323 }
324 return i - 1, i + 1 + j + 2
325 }
326
327 prev = b
328 }
329
330 return -1, -1
331 }
332
333 func indexLetter(s []byte) int {
334 for i, b := range s {
335 upper := b &^ 32
336 if 'A' <= upper && upper <= 'Z' {
337 return i
338 }
339 }
340
341 return -1
342 }
343
344 func indexPair(s []byte, x byte, y byte) int {
345 var prev byte
346
347 for i, b := range s {
348 if prev == x && b == y && i > 0 {
349 return i
350 }
351 prev = b
352 }
353
354 return -1
355 }
356
357 // note: looking at the results of restoring ANSI-styles after style-resets
358 // doesn't seem to be worth it, as a previous version used to do
359
360 // handleChunk handles line-slices around any detected ANSI-style sequences,
361 // or even whole lines, when no ANSI-styles are found in them
362 func handleChunk(w *bufio.Writer, s []byte, with []pattern, n int, last int) {
363 for len(s) > 0 {
364 start, end := -1, -1
365 for _, p := range with {
366 i, j := p.findIndex(s, n, last)
367 if i >= 0 && (i < start || start < 0) {
368 start, end = i, j
369 }
370 }
371
372 if start < 0 {
373 w.Write(s)
374 return
375 }
376
377 w.Write(s[:start])
378 w.WriteString(highlightStyle)
379 w.Write(s[start:end])
380 w.WriteString("\x1b[0m")
381
382 s = s[end:]
383 }
384 }
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 both rest-of-line and multi-line comments
47 - ignores extra/trailing commas in arrays and objects
48 - turns single-quoted strings/keys into double-quoted strings
49 - double-quotes unquoted object keys
50 - changes \x 2-hex-digit into \u 4-hex-digit string-escapes
51
52 All options available can either start with a single or a double-dash
53
54 -h, -help show this help message
55 -jsonl emit JSON Lines, when top-level value is an array
56 `
57
58 const (
59 bufSize = 32 * 1024
60 chunkPeekSize = 16
61 )
62
63 func Main() {
64 args := os.Args[1:]
65 buffered := false
66 handler := json0
67
68 for len(args) > 0 {
69 switch args[0] {
70 case `-b`, `--b`, `-buffered`, `--buffered`:
71 buffered = true
72 args = args[1:]
73 continue
74
75 case `-h`, `--h`, `-help`, `--help`:
76 os.Stdout.WriteString(info[1:])
77 return
78
79 case `-jsonl`, `--jsonl`:
80 handler = jsonl
81 args = args[1:]
82 continue
83 }
84
85 break
86 }
87
88 if len(args) > 0 && args[0] == `--` {
89 args = args[1:]
90 }
91
92 if len(args) > 1 {
93 const msg = "multiple inputs aren't allowed\n"
94 os.Stderr.WriteString(msg)
95 os.Exit(1)
96 return
97 }
98
99 liveLines := !buffered
100 if !buffered {
101 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
102 liveLines = false
103 }
104 }
105
106 name := `-`
107 if len(args) == 1 {
108 name = args[0]
109 }
110
111 if err := run(os.Stdout, name, handler, liveLines); err != nil && err != io.EOF {
112 os.Stderr.WriteString(err.Error())
113 os.Stderr.WriteString("\n")
114 os.Exit(1)
115 return
116 }
117 }
118
119 type handlerFunc func(w *bufio.Writer, r *bufio.Reader, live bool) error
120
121 func run(w io.Writer, name string, handler handlerFunc, live bool) error {
122 // f, _ := os.Create(`json0.prof`)
123 // defer f.Close()
124 // pprof.StartCPUProfile(f)
125 // defer pprof.StopCPUProfile()
126
127 if name == `` || name == `-` {
128 bw := bufio.NewWriterSize(w, bufSize)
129 br := bufio.NewReaderSize(os.Stdin, bufSize)
130 defer bw.Flush()
131 return handler(bw, br, live)
132 }
133
134 f, err := os.Open(name)
135 if err != nil {
136 return errors.New(`can't read from file named "` + name + `"`)
137 }
138 defer f.Close()
139
140 bw := bufio.NewWriterSize(w, bufSize)
141 br := bufio.NewReaderSize(f, bufSize)
142 defer bw.Flush()
143 return handler(bw, br, live)
144 }
145
146 var (
147 errCommentEarlyEnd = errors.New(`unexpected early-end of comment`)
148 errInputEarlyEnd = errors.New(`expected end of input data`)
149 errInvalidComment = errors.New(`expected / or *`)
150 errInvalidHex = errors.New(`expected a base-16 digit`)
151 errInvalidRune = errors.New(`invalid UTF-8 bytes`)
152 errInvalidToken = errors.New(`invalid JSON token`)
153 errNoDigits = errors.New(`expected numeric digits`)
154 errNoStringQuote = errors.New(`expected " or '`)
155 errNoArrayComma = errors.New(`missing comma between array values`)
156 errNoObjectComma = errors.New(`missing comma between key-value pairs`)
157 errStringEarlyEnd = errors.New(`unexpected early-end of string`)
158 errExtraBytes = errors.New(`unexpected extra input bytes`)
159 )
160
161 // linePosError is a more descriptive kind of error, showing the source of
162 // the input-related problem, as 1-based a line/pos number pair in front
163 // of the error message
164 type linePosError struct {
165 // line is the 1-based line count from the input
166 line int
167
168 // pos is the 1-based `horizontal` position in its line
169 pos int
170
171 // err is the error message to `decorate` with the position info
172 err error
173 }
174
175 // Error satisfies the error interface
176 func (lpe linePosError) Error() string {
177 where := strconv.Itoa(lpe.line) + `:` + strconv.Itoa(lpe.pos)
178 return where + `: ` + lpe.err.Error()
179 }
180
181 // isIdentifier improves control-flow of func handleKey, when it handles
182 // unquoted object keys
183 var isIdentifier = [256]bool{
184 '_': true,
185
186 '0': true, '1': true, '2': true, '3': true, '4': true,
187 '5': true, '6': true, '7': true, '8': true, '9': true,
188
189 'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true,
190 'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true,
191 'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true,
192 'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
193 'Y': true, 'Z': true,
194
195 'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true,
196 'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true,
197 'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true,
198 's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true,
199 'y': true, 'z': true,
200 }
201
202 // matchHex both figures out if a byte is a valid ASCII hex-digit, by not
203 // being 0, and normalizes letter-case for the hex letters
204 var matchHex = [256]byte{
205 '0': '0', '1': '1', '2': '2', '3': '3', '4': '4',
206 '5': '5', '6': '6', '7': '7', '8': '8', '9': '9',
207 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F',
208 'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F',
209 }
210
211 // json0 converts JSON/pseudo-JSON into (valid) minimal JSON; final boolean
212 // value isn't used, and is just there to match the signature of func jsonl
213 func json0(w *bufio.Writer, r *bufio.Reader, live bool) error {
214 jr := jsonReader{r, 1, 1}
215 defer w.Flush()
216
217 if err := jr.handleLeadingJunk(); err != nil {
218 return err
219 }
220
221 // handle a single top-level JSON value
222 err := handleValue(w, &jr)
223
224 // end the only output-line with a line-feed; this also avoids showing
225 // error messages on the same line as the main output, since JSON-0
226 // output has no line-feeds before its last byte
227 outputByte(w, '\n')
228
229 if err != nil {
230 return err
231 }
232 return jr.handleTrailingJunk()
233 }
234
235 // jsonl converts JSON/pseudo-JSON into (valid) minimal JSON Lines; this func
236 // avoids writing a trailing line-feed, leaving that up to its caller
237 func jsonl(w *bufio.Writer, r *bufio.Reader, live bool) error {
238 jr := jsonReader{r, 1, 1}
239
240 if err := jr.handleLeadingJunk(); err != nil {
241 return err
242 }
243
244 chunk, err := jr.r.Peek(1)
245 if err == nil && len(chunk) >= 1 {
246 switch b := chunk[0]; b {
247 case '[', '(':
248 return handleArrayJSONL(w, &jr, b, live)
249 }
250 }
251
252 // handle a single top-level JSON value
253 err = handleValue(w, &jr)
254
255 // end the only output-line with a line-feed; this also avoids showing
256 // error messages on the same line as the main output, since JSON-0
257 // output has no line-feeds before its last byte
258 outputByte(w, '\n')
259
260 if err != nil {
261 return err
262 }
263 return jr.handleTrailingJunk()
264 }
265
266 // handleArrayJSONL handles top-level arrays for func jsonl
267 func handleArrayJSONL(w *bufio.Writer, jr *jsonReader, start byte, live bool) error {
268 if err := jr.demandSyntax(start); err != nil {
269 return err
270 }
271
272 var end byte = ']'
273 if start == '(' {
274 end = ')'
275 }
276
277 for n := 0; true; n++ {
278 // there may be whitespace/comments before the next comma
279 if err := jr.seekNext(); err != nil {
280 return err
281 }
282
283 // handle commas between values, as well as trailing ones
284 comma := false
285 b, _ := jr.peekByte()
286 if b == ',' {
287 jr.readByte()
288 comma = true
289
290 // there may be whitespace/comments before an ending ']'
291 if err := jr.seekNext(); err != nil {
292 return err
293 }
294 b, _ = jr.peekByte()
295 }
296
297 // handle end of array
298 if b == end {
299 jr.readByte()
300 if n > 0 {
301 err := outputByte(w, '\n')
302 if live {
303 w.Flush()
304 }
305 return err
306 }
307 return nil
308 }
309
310 // turn commas between adjacent values into line-feeds, as the
311 // output for this custom func is supposed to be JSON Lines
312 if n > 0 {
313 if !comma {
314 return errNoArrayComma
315 }
316 if err := outputByte(w, '\n'); err != nil {
317 return err
318 }
319 if live {
320 w.Flush()
321 }
322 }
323
324 // handle the next value
325 if err := jr.seekNext(); err != nil {
326 return err
327 }
328 if err := handleValue(w, jr); err != nil {
329 return err
330 }
331 }
332
333 // make the compiler happy
334 return nil
335 }
336
337 // jsonReader reads data via a buffer, keeping track of the input position:
338 // this in turn allows showing much more useful errors, when these happen
339 type jsonReader struct {
340 // r is the actual reader
341 r *bufio.Reader
342
343 // line is the 1-based line-counter for input bytes, and gives errors
344 // useful position info
345 line int
346
347 // pos is the 1-based `horizontal` position in its line, and gives
348 // errors useful position info
349 pos int
350 }
351
352 // improveError makes any error more useful, by giving it info about the
353 // current input-position, as a 1-based line/within-line-position pair
354 func (jr jsonReader) improveError(err error) error {
355 if _, ok := err.(linePosError); ok {
356 return err
357 }
358
359 if err == io.EOF {
360 return linePosError{jr.line, jr.pos, errInputEarlyEnd}
361 }
362 if err != nil {
363 return linePosError{jr.line, jr.pos, err}
364 }
365 return nil
366 }
367
368 func (jr *jsonReader) handleLeadingJunk() error {
369 // input is already assumed to be UTF-8: a leading UTF-8 BOM (byte-order
370 // mark) gives no useful info if present, as UTF-8 leaves no ambiguity
371 // about byte-order by design
372 jr.skipUTF8BOM()
373
374 // ignore leading whitespace and/or comments
375 return jr.seekNext()
376 }
377
378 func (jr *jsonReader) handleTrailingJunk() error {
379 // ignore trailing whitespace and/or comments
380 if err := jr.seekNext(); err != nil {
381 return err
382 }
383
384 // ignore trailing semicolons
385 for {
386 if b, ok := jr.peekByte(); !ok || b != ';' {
387 break
388 }
389
390 jr.readByte()
391 // ignore trailing whitespace and/or comments
392 if err := jr.seekNext(); err != nil {
393 return err
394 }
395 }
396
397 // beyond trailing whitespace and/or comments, any more bytes
398 // make the whole input data invalid JSON
399 if _, ok := jr.peekByte(); ok {
400 return jr.improveError(errExtraBytes)
401 }
402 return nil
403 }
404
405 // demandSyntax fails with an error when the next byte isn't the one given;
406 // when it is, the byte is then read/skipped, and a nil error is returned
407 func (jr *jsonReader) demandSyntax(syntax byte) error {
408 chunk, err := jr.r.Peek(1)
409 if err == io.EOF {
410 return jr.improveError(errInputEarlyEnd)
411 }
412 if err != nil {
413 return jr.improveError(err)
414 }
415
416 if len(chunk) < 1 || chunk[0] != syntax {
417 msg := `expected ` + string(rune(syntax))
418 return jr.improveError(errors.New(msg))
419 }
420
421 jr.readByte()
422 return nil
423 }
424
425 // peekByte simplifies control-flow for various other funcs
426 func (jr jsonReader) peekByte() (b byte, ok bool) {
427 chunk, err := jr.r.Peek(1)
428 if err == nil && len(chunk) >= 1 {
429 return chunk[0], true
430 }
431 return 0, false
432 }
433
434 // readByte does what it says, updating the reader's position info
435 func (jr *jsonReader) readByte() (b byte, err error) {
436 b, err = jr.r.ReadByte()
437 if err == nil {
438 if b == '\n' {
439 jr.line += 1
440 jr.pos = 1
441 } else {
442 jr.pos++
443 }
444 return b, nil
445 }
446 return b, jr.improveError(err)
447 }
448
449 // readRune does what it says, updating the reader's position info
450 func (jr *jsonReader) readRune() (r rune, err error) {
451 r, _, err = jr.r.ReadRune()
452 if err == nil {
453 if r == '\n' {
454 jr.line += 1
455 jr.pos = 1
456 } else {
457 jr.pos++
458 }
459 return r, nil
460 }
461 return r, jr.improveError(err)
462 }
463
464 // seekNext skips/seeks the next token, ignoring runs of whitespace symbols
465 // and comments, either single-line (starting with //) or general (starting
466 // with /* and ending with */)
467 func (jr *jsonReader) seekNext() error {
468 for {
469 b, ok := jr.peekByte()
470 if !ok {
471 return nil
472 }
473
474 // case ' ', '\t', '\f', '\v', '\r', '\n':
475 if b <= 32 {
476 // keep skipping whitespace bytes
477 jr.readByte()
478 continue
479 }
480
481 if b == '#' {
482 if err := jr.skipLine(); err != nil {
483 return err
484 }
485 continue
486 }
487
488 if b != '/' {
489 // reached the next token
490 return nil
491 }
492
493 if err := jr.skipComment(); err != nil {
494 return err
495 }
496
497 // after comments, keep looking for more whitespace and/or comments
498 }
499 }
500
501 // skipComment helps func seekNext skip over comments, simplifying the latter
502 // func's control-flow
503 func (jr *jsonReader) skipComment() error {
504 err := jr.demandSyntax('/')
505 if err != nil {
506 return err
507 }
508
509 b, ok := jr.peekByte()
510 if !ok {
511 return nil
512 }
513
514 switch b {
515 case '/':
516 // handle single-line comments
517 return jr.skipLine()
518
519 case '*':
520 // handle (potentially) multi-line comments
521 return jr.skipGeneralComment()
522
523 default:
524 return jr.improveError(errInvalidComment)
525 }
526 }
527
528 // skipLine handles single-line comments for func skipComment
529 func (jr *jsonReader) skipLine() error {
530 for {
531 b, err := jr.readByte()
532 if err == io.EOF {
533 // end of input is fine in this case
534 return nil
535 }
536 if err != nil {
537 return err
538 }
539
540 if b == '\n' {
541 return nil
542 }
543 }
544 }
545
546 // skipGeneralComment handles (potentially) multi-line comments for func
547 // skipComment
548 func (jr *jsonReader) skipGeneralComment() error {
549 var prev byte
550 for {
551 b, err := jr.readByte()
552 if err != nil {
553 return jr.improveError(errCommentEarlyEnd)
554 }
555
556 if prev == '*' && b == '/' {
557 return nil
558 }
559 if b == '\n' {
560 jr.line++
561 }
562 prev = b
563 }
564 }
565
566 // skipUTF8BOM does what it says, if a UTF-8 BOM is present
567 func (jr *jsonReader) skipUTF8BOM() {
568 lead, err := jr.r.Peek(3)
569 if err != nil {
570 return
571 }
572
573 if len(lead) > 2 && lead[0] == 0xef && lead[1] == 0xbb && lead[2] == 0xbf {
574 jr.readByte()
575 jr.readByte()
576 jr.readByte()
577 }
578 }
579
580 // outputByte is a small wrapper on func WriteByte, which adapts any error
581 // into a custom dummy output-error, which is in turn meant to be ignored,
582 // being just an excuse to quit the app immediately and successfully
583 func outputByte(w *bufio.Writer, b byte) error {
584 err := w.WriteByte(b)
585 if err == nil {
586 return nil
587 }
588 return io.EOF
589 }
590
591 // handleArray handles arrays for func handleValue
592 func handleArray(w *bufio.Writer, jr *jsonReader, start byte) error {
593 if err := jr.demandSyntax(start); err != nil {
594 return err
595 }
596
597 var end byte = ']'
598 if start == '(' {
599 end = ')'
600 }
601
602 w.WriteByte('[')
603
604 for n := 0; true; n++ {
605 // there may be whitespace/comments before the next comma
606 if err := jr.seekNext(); err != nil {
607 return err
608 }
609
610 // handle commas between values, as well as trailing ones
611 comma := false
612 b, _ := jr.peekByte()
613 if b == ',' {
614 jr.readByte()
615 comma = true
616
617 // there may be whitespace/comments before an ending ']'
618 if err := jr.seekNext(); err != nil {
619 return err
620 }
621 b, _ = jr.peekByte()
622 }
623
624 // handle end of array
625 if b == end {
626 jr.readByte()
627 w.WriteByte(']')
628 return nil
629 }
630
631 // don't forget commas between adjacent values
632 if n > 0 {
633 if !comma {
634 return errNoArrayComma
635 }
636 if err := outputByte(w, ','); err != nil {
637 return err
638 }
639 }
640
641 // handle the next value
642 if err := jr.seekNext(); err != nil {
643 return err
644 }
645 if err := handleValue(w, jr); err != nil {
646 return err
647 }
648 }
649
650 // make the compiler happy
651 return nil
652 }
653
654 // handleDigits helps various number-handling funcs do their job
655 func handleDigits(w *bufio.Writer, jr *jsonReader) error {
656 if trySimpleDigits(w, jr) {
657 return nil
658 }
659
660 for n := 0; true; n++ {
661 b, _ := jr.peekByte()
662
663 // support `nice` long numbers by ignoring their underscores
664 if b == '_' {
665 jr.readByte()
666 continue
667 }
668
669 if '0' <= b && b <= '9' {
670 jr.readByte()
671 w.WriteByte(b)
672 continue
673 }
674
675 if n == 0 {
676 return errNoDigits
677 }
678 return nil
679 }
680
681 // make the compiler happy
682 return nil
683 }
684
685 // trySimpleDigits tries to handle (more quickly) digit-runs where all bytes
686 // are just digits: this is a very common case for numbers; returns whether
687 // it succeeded, so this func's caller knows knows if it needs to do anything,
688 // the slower way
689 func trySimpleDigits(w *bufio.Writer, jr *jsonReader) (gotIt bool) {
690 chunk, _ := jr.r.Peek(chunkPeekSize)
691
692 for i, b := range chunk {
693 if '0' <= b && b <= '9' {
694 continue
695 }
696
697 if i == 0 || b == '_' {
698 return false
699 }
700
701 // bulk-writing the chunk is this func's whole point
702 w.Write(chunk[:i])
703
704 jr.r.Discard(i)
705 jr.pos += i
706 return true
707 }
708
709 // maybe the digits-run is ok, but it's just longer than the chunk
710 return false
711 }
712
713 // handleDot handles pseudo-JSON numbers which start with a decimal dot
714 func handleDot(w *bufio.Writer, jr *jsonReader) error {
715 if err := jr.demandSyntax('.'); err != nil {
716 return err
717 }
718 w.Write([]byte{'0', '.'})
719 return handleDigits(w, jr)
720 }
721
722 // handleKey is used by func handleObjects and generalizes func handleString,
723 // by allowing unquoted object keys; it's not used anywhere else, as allowing
724 // unquoted string values is ambiguous with actual JSON-keyword values null,
725 // false, and true.
726 func handleKey(w *bufio.Writer, jr *jsonReader) error {
727 quote, ok := jr.peekByte()
728 if !ok {
729 return jr.improveError(errStringEarlyEnd)
730 }
731
732 if quote == '"' || quote == '\'' {
733 return handleString(w, jr, quote)
734 }
735
736 w.WriteByte('"')
737 for {
738 if b, _ := jr.peekByte(); isIdentifier[b] {
739 jr.readByte()
740 w.WriteByte(b)
741 continue
742 }
743
744 w.WriteByte('"')
745 return nil
746 }
747 }
748
749 // trySimpleString tries to handle (more quickly) inner-strings where all bytes
750 // are unescaped ASCII symbols: this is a very common case for strings, and is
751 // almost always the case for object keys; returns whether it succeeded, so
752 // this func's caller knows knows if it needs to do anything, the slower way
753 func trySimpleString(w *bufio.Writer, jr *jsonReader, quote byte) (gotIt bool) {
754 end := -1
755 chunk, _ := jr.r.Peek(chunkPeekSize)
756
757 for i, b := range chunk {
758 if 32 <= b && b <= 127 && b != '\\' && b != '\'' && b != '"' {
759 continue
760 }
761
762 if b == byte(quote) {
763 end = i
764 break
765 }
766 return false
767 }
768
769 if end < 0 {
770 return false
771 }
772
773 // bulk-writing the chunk is this func's whole point
774 w.WriteByte('"')
775 w.Write(chunk[:end])
776 w.WriteByte('"')
777
778 jr.r.Discard(end + 1)
779 jr.pos += end + 1
780 return true
781 }
782
783 // handleKeyword is used by funcs handleFalse, handleNull, and handleTrue
784 func handleKeyword(w *bufio.Writer, jr *jsonReader, kw []byte) error {
785 for rest := kw; len(rest) > 0; rest = rest[1:] {
786 b, err := jr.readByte()
787 if err == nil && b == rest[0] {
788 // keywords given to this func have no line-feeds
789 jr.pos++
790 continue
791 }
792
793 msg := `expected JSON value ` + string(kw)
794 return jr.improveError(errors.New(msg))
795 }
796
797 w.Write(kw)
798 return nil
799 }
800
801 func replaceKeyword(w *bufio.Writer, jr *jsonReader, kw, with []byte) error {
802 for rest := kw; len(rest) > 0; rest = rest[1:] {
803 b, err := jr.readByte()
804 if err == nil && b == rest[0] {
805 // keywords given to this func have no line-feeds
806 jr.pos++
807 continue
808 }
809
810 msg := `expected JSON value ` + string(kw)
811 return jr.improveError(errors.New(msg))
812 }
813
814 w.Write(with)
815 return nil
816 }
817
818 // handleNegative handles numbers starting with a negative sign for func
819 // handleValue
820 func handleNegative(w *bufio.Writer, jr *jsonReader) error {
821 if err := jr.demandSyntax('-'); err != nil {
822 return err
823 }
824
825 w.WriteByte('-')
826 if b, _ := jr.peekByte(); b == '.' {
827 jr.readByte()
828 w.Write([]byte{'0', '.'})
829 return handleDigits(w, jr)
830 }
831 return handleNumber(w, jr)
832 }
833
834 // handleNumber handles numeric values/tokens, including invalid-JSON cases,
835 // such as values starting with a decimal dot
836 func handleNumber(w *bufio.Writer, jr *jsonReader) error {
837 // handle integer digits
838 if err := handleDigits(w, jr); err != nil {
839 return err
840 }
841
842 // handle optional decimal digits, starting with a leading dot
843 if b, _ := jr.peekByte(); b == '.' {
844 jr.readByte()
845 w.WriteByte('.')
846 return handleDigits(w, jr)
847 }
848
849 // handle optional exponent digits
850 if b, _ := jr.peekByte(); b == 'e' || b == 'E' {
851 jr.readByte()
852 w.WriteByte(b)
853 b, _ = jr.peekByte()
854 if b == '+' {
855 jr.readByte()
856 } else if b == '-' {
857 w.WriteByte('-')
858 jr.readByte()
859 }
860 return handleDigits(w, jr)
861 }
862
863 return nil
864 }
865
866 // handleObject handles objects for func handleValue
867 func handleObject(w *bufio.Writer, jr *jsonReader) error {
868 if err := jr.demandSyntax('{'); err != nil {
869 return err
870 }
871 w.WriteByte('{')
872
873 for npairs := 0; true; npairs++ {
874 // there may be whitespace/comments before the next comma
875 if err := jr.seekNext(); err != nil {
876 return err
877 }
878
879 // handle commas between key-value pairs, as well as trailing ones
880 comma := false
881 b, _ := jr.peekByte()
882 if b == ',' {
883 jr.readByte()
884 comma = true
885
886 // there may be whitespace/comments before an ending '}'
887 if err := jr.seekNext(); err != nil {
888 return err
889 }
890 b, _ = jr.peekByte()
891 }
892
893 // handle end of object
894 if b == '}' {
895 jr.readByte()
896 w.WriteByte('}')
897 return nil
898 }
899
900 // don't forget commas between adjacent key-value pairs
901 if npairs > 0 {
902 if !comma {
903 return errNoObjectComma
904 }
905 if err := outputByte(w, ','); err != nil {
906 return err
907 }
908 }
909
910 // handle the next pair's key
911 if err := jr.seekNext(); err != nil {
912 return err
913 }
914 if err := handleKey(w, jr); err != nil {
915 return err
916 }
917
918 // demand a colon right after the key
919 if err := jr.seekNext(); err != nil {
920 return err
921 }
922 if err := jr.demandSyntax(':'); err != nil {
923 return err
924 }
925 w.WriteByte(':')
926
927 // handle the next pair's value
928 if err := jr.seekNext(); err != nil {
929 return err
930 }
931 if err := handleValue(w, jr); err != nil {
932 return err
933 }
934 }
935
936 // make the compiler happy
937 return nil
938 }
939
940 // handlePositive handles numbers starting with a positive sign for func
941 // handleValue
942 func handlePositive(w *bufio.Writer, jr *jsonReader) error {
943 if err := jr.demandSyntax('+'); err != nil {
944 return err
945 }
946
947 // valid JSON isn't supposed to have leading pluses on numbers, so
948 // emit nothing for it, unlike for negative numbers
949
950 if b, _ := jr.peekByte(); b == '.' {
951 jr.readByte()
952 w.Write([]byte{'0', '.'})
953 return handleDigits(w, jr)
954 }
955 return handleNumber(w, jr)
956 }
957
958 // handleString handles strings for funcs handleValue and handleObject, and
959 // supports both single-quotes and double-quotes, always emitting the latter
960 // in the output, of course
961 func handleString(w *bufio.Writer, jr *jsonReader, quote byte) error {
962 if quote != '"' && quote != '\'' {
963 return errNoStringQuote
964 }
965
966 jr.readByte()
967
968 // try the quicker no-escapes ASCII handler
969 if trySimpleString(w, jr, quote) {
970 return nil
971 }
972
973 // it's a non-trivial inner-string, so handle it byte-by-byte
974 w.WriteByte('"')
975 escaped := false
976
977 for quote := rune(quote); true; {
978 r, err := jr.readRune()
979 if r == unicode.ReplacementChar {
980 return jr.improveError(errInvalidRune)
981 }
982 if err != nil {
983 if err == io.EOF {
984 return jr.improveError(errStringEarlyEnd)
985 }
986 return jr.improveError(err)
987 }
988
989 if !escaped {
990 if r == '\\' {
991 escaped = true
992 continue
993 }
994
995 // handle end of string
996 if r == quote {
997 return outputByte(w, '"')
998 }
999
1000 if r <= 127 {
1001 w.Write(escapedStringBytes[byte(r)])
1002 } else {
1003 w.WriteRune(r)
1004 }
1005 continue
1006 }
1007
1008 // handle escaped items
1009 escaped = false
1010
1011 switch r {
1012 case 'u':
1013 // \u needs exactly 4 hex-digits to follow it
1014 w.Write([]byte{'\\', 'u'})
1015 if err := copyHex(w, 4, jr); err != nil {
1016 return jr.improveError(err)
1017 }
1018
1019 case 'x':
1020 // JSON only supports 4 escaped hex-digits, so pad the 2
1021 // expected hex-digits with 2 zeros
1022 w.Write([]byte{'\\', 'u', '0', '0'})
1023 if err := copyHex(w, 2, jr); err != nil {
1024 return jr.improveError(err)
1025 }
1026
1027 case 't', 'f', 'r', 'n', 'b', '\\', '"':
1028 // handle valid-JSON escaped string sequences
1029 w.WriteByte('\\')
1030 w.WriteByte(byte(r))
1031
1032 case '\'':
1033 // escaped single-quotes aren't standard JSON, but they can
1034 // be handy when the input uses non-standard single-quoted
1035 // strings
1036 w.WriteByte('\'')
1037
1038 default:
1039 if r <= 127 {
1040 w.Write(escapedStringBytes[byte(r)])
1041 } else {
1042 w.WriteRune(r)
1043 }
1044 }
1045 }
1046
1047 return nil
1048 }
1049
1050 // copyHex handles a run of hex-digits for func handleString, starting right
1051 // after the leading `\u` (or `\x`) part; this func doesn't `improve` its
1052 // errors with position info: that's up to the caller
1053 func copyHex(w *bufio.Writer, n int, jr *jsonReader) error {
1054 for i := 0; i < n; i++ {
1055 b, err := jr.readByte()
1056 if err == io.EOF {
1057 return errStringEarlyEnd
1058 }
1059 if err != nil {
1060 return err
1061 }
1062
1063 if b >= 128 {
1064 return errInvalidHex
1065 }
1066
1067 if b := matchHex[b]; b != 0 {
1068 w.WriteByte(b)
1069 continue
1070 }
1071
1072 return errInvalidHex
1073 }
1074
1075 return nil
1076 }
1077
1078 // handleValue is a generic JSON-token handler, which allows the recursive
1079 // behavior to handle any kind of JSON/pseudo-JSON input
1080 func handleValue(w *bufio.Writer, jr *jsonReader) error {
1081 chunk, err := jr.r.Peek(1)
1082 if err == nil && len(chunk) >= 1 {
1083 return handleValueDispatch(w, jr, chunk[0])
1084 }
1085
1086 if err == io.EOF {
1087 return jr.improveError(errInputEarlyEnd)
1088 }
1089 return jr.improveError(errInputEarlyEnd)
1090 }
1091
1092 // handleValueDispatch simplifies control-flow for func handleValue
1093 func handleValueDispatch(w *bufio.Writer, jr *jsonReader, b byte) error {
1094 switch b {
1095 case '#':
1096 return jr.skipLine()
1097 case 'f':
1098 return handleKeyword(w, jr, []byte{'f', 'a', 'l', 's', 'e'})
1099 case 'n':
1100 return handleKeyword(w, jr, []byte{'n', 'u', 'l', 'l'})
1101 case 't':
1102 return handleKeyword(w, jr, []byte{'t', 'r', 'u', 'e'})
1103 case 'F':
1104 return replaceKeyword(w, jr, []byte(`False`), []byte(`false`))
1105 case 'N':
1106 return replaceKeyword(w, jr, []byte(`None`), []byte(`null`))
1107 case 'T':
1108 return replaceKeyword(w, jr, []byte(`True`), []byte(`true`))
1109 case '.':
1110 return handleDot(w, jr)
1111 case '+':
1112 return handlePositive(w, jr)
1113 case '-':
1114 return handleNegative(w, jr)
1115 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
1116 return handleNumber(w, jr)
1117 case '\'', '"':
1118 return handleString(w, jr, b)
1119 case '[', '(':
1120 return handleArray(w, jr, b)
1121 case '{':
1122 return handleObject(w, jr)
1123 default:
1124 return jr.improveError(errInvalidToken)
1125 }
1126 }
1127
1128 // escapedStringBytes helps func handleString treat all string bytes quickly
1129 // and correctly, using their officially-supported JSON escape sequences
1130 //
1131 // https://www.rfc-editor.org/rfc/rfc8259#section-7
1132 var escapedStringBytes = [256][]byte{
1133 {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
1134 {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
1135 {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
1136 {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
1137 {'\\', 'b'}, {'\\', 't'},
1138 {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
1139 {'\\', 'f'}, {'\\', 'r'},
1140 {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
1141 {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
1142 {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
1143 {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
1144 {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
1145 {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
1146 {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
1147 {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
1148 {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
1149 {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
1150 {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
1151 {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
1152 {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
1153 {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
1154 {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
1155 {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
1156 {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
1157 {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
1158 {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
1159 {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
1160 {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
1161 {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
1162 {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
1163 {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
1164 {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
1165 {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
1166 {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
1167 {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
1168 {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
1169 {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
1170 {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
1171 {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
1172 {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
1173 {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
1174 {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
1175 {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
1176 {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
1177 }
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
64 `""`: `""`,
65 `''`: `""`,
66 `"\""`: `"\""`,
67 `'\"'`: `"\""`,
68 `'\''`: `"'"`,
69 `'abc\u0e9A'`: `"abc\u0E9A"`,
70 `'abc\x1f[0m'`: `"abc\u001F[0m"`,
71 `"abc●def"`: `"abc●def"`,
72
73 `[ ]`: `[]`,
74 `[ , ]`: `[]`,
75 `[.345, false,null , ]`: `[0.345,false,null]`,
76
77 `( )`: `[]`,
78 `( , )`: `[]`,
79 `(.345, false,null , )`: `[0.345,false,null]`,
80
81 `{ }`: `{}`,
82 `{ , }`: `{}`,
83
84 `{ 'abc': .345, "def" : false, 'xyz':null , }`: `{"abc":0.345,"def":false,"xyz":null}`,
85
86 `{0problems:123,}`: `{"0problems":123}`,
87 `{0_problems:123}`: `{"0_problems":123}`,
88 }
89
90 for input, expected := range tests {
91 t.Run(input, func(t *testing.T) {
92 var out strings.Builder
93 w := bufio.NewWriter(&out)
94 r := bufio.NewReader(strings.NewReader(input))
95 if err := json0(w, r, false); err != nil && err != io.EOF {
96 t.Fatal(err)
97 return
98 }
99 // don't forget to flush the buffer, or output will be empty
100 w.Flush()
101
102 // output may have a final line-feed: get rid of it, or every
103 // single test-case will fail
104 got := out.String()
105 if len(got) > 0 && got[len(got)-1] == '\n' {
106 got = got[:len(got)-1]
107 }
108
109 if got != expected {
110 t.Fatalf("<got>\n%s\n<expected>\n%s", got, expected)
111 return
112 }
113 })
114 }
115 }
116
117 func TestEscapedStringBytes(t *testing.T) {
118 var escaped = map[rune][]byte{
119 '\x00': {'\\', 'u', '0', '0', '0', '0'},
120 '\x01': {'\\', 'u', '0', '0', '0', '1'},
121 '\x02': {'\\', 'u', '0', '0', '0', '2'},
122 '\x03': {'\\', 'u', '0', '0', '0', '3'},
123 '\x04': {'\\', 'u', '0', '0', '0', '4'},
124 '\x05': {'\\', 'u', '0', '0', '0', '5'},
125 '\x06': {'\\', 'u', '0', '0', '0', '6'},
126 '\x07': {'\\', 'u', '0', '0', '0', '7'},
127 '\x0b': {'\\', 'u', '0', '0', '0', 'b'},
128 '\x0e': {'\\', 'u', '0', '0', '0', 'e'},
129 '\x0f': {'\\', 'u', '0', '0', '0', 'f'},
130 '\x10': {'\\', 'u', '0', '0', '1', '0'},
131 '\x11': {'\\', 'u', '0', '0', '1', '1'},
132 '\x12': {'\\', 'u', '0', '0', '1', '2'},
133 '\x13': {'\\', 'u', '0', '0', '1', '3'},
134 '\x14': {'\\', 'u', '0', '0', '1', '4'},
135 '\x15': {'\\', 'u', '0', '0', '1', '5'},
136 '\x16': {'\\', 'u', '0', '0', '1', '6'},
137 '\x17': {'\\', 'u', '0', '0', '1', '7'},
138 '\x18': {'\\', 'u', '0', '0', '1', '8'},
139 '\x19': {'\\', 'u', '0', '0', '1', '9'},
140 '\x1a': {'\\', 'u', '0', '0', '1', 'a'},
141 '\x1b': {'\\', 'u', '0', '0', '1', 'b'},
142 '\x1c': {'\\', 'u', '0', '0', '1', 'c'},
143 '\x1d': {'\\', 'u', '0', '0', '1', 'd'},
144 '\x1e': {'\\', 'u', '0', '0', '1', 'e'},
145 '\x1f': {'\\', 'u', '0', '0', '1', 'f'},
146
147 '\t': {'\\', 't'},
148 '\f': {'\\', 'f'},
149 '\b': {'\\', 'b'},
150 '\r': {'\\', 'r'},
151 '\n': {'\\', 'n'},
152 '\\': {'\\', '\\'},
153 '"': {'\\', '"'},
154 }
155
156 if n := len(escapedStringBytes); n != 256 {
157 t.Errorf(`expected 256 entries, instead of %d`, n)
158 }
159
160 for i, v := range escapedStringBytes {
161 exp := []byte{byte(i)}
162 if esc, ok := escaped[rune(i)]; ok {
163 exp = esc
164 }
165
166 if !bytes.Equal(v, exp) {
167 t.Errorf("%d: expected %#v, got %#v", i, exp, v)
168 }
169 }
170 }
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 func Main() {
43 args := os.Args[1:]
44
45 if len(args) > 0 {
46 switch args[0] {
47 case `-h`, `--h`, `-help`, `--help`:
48 os.Stdout.WriteString(info[1:])
49 return
50 }
51 }
52
53 if len(args) > 0 && args[0] == `--` {
54 args = args[1:]
55 }
56
57 if len(args) > 1 {
58 const msg = "multiple inputs aren't allowed\n"
59 os.Stderr.WriteString(msg)
60 os.Exit(1)
61 return
62 }
63
64 // figure out whether input should come from a named file or from stdin
65 name := `-`
66 if len(args) == 1 {
67 name = args[0]
68 }
69
70 if err := handleInput(os.Stdout, name); 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 // handleInput simplifies control-flow for func main
79 func handleInput(w io.Writer, path string) error {
80 if path == `-` {
81 return convert(w, os.Stdin)
82 }
83
84 f, err := os.Open(path)
85 if err != nil {
86 // on windows, file-not-found error messages may mention `CreateFile`,
87 // even when trying to open files in read-only mode
88 return errors.New(`can't open file named ` + path)
89 }
90 defer f.Close()
91 return convert(w, f)
92 }
93
94 // convert simplifies control-flow for func handleInput
95 func convert(w io.Writer, r io.Reader) error {
96 bw := bufio.NewWriter(w)
97 defer bw.Flush()
98 return json2(bw, r)
99 }
100
101 // escapedStringBytes helps func handleString treat all string bytes quickly
102 // and correctly, using their officially-supported JSON escape sequences
103 //
104 // https://www.rfc-editor.org/rfc/rfc8259#section-7
105 var escapedStringBytes = [256][]byte{
106 {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
107 {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
108 {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
109 {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
110 {'\\', 'b'}, {'\\', 't'},
111 {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
112 {'\\', 'f'}, {'\\', 'r'},
113 {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
114 {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
115 {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
116 {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
117 {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
118 {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
119 {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
120 {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
121 {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
122 {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
123 {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
124 {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
125 {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
126 {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
127 {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
128 {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
129 {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
130 {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
131 {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
132 {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
133 {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
134 {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
135 {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
136 {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
137 {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
138 {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
139 {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
140 {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
141 {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
142 {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
143 {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
144 {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
145 {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
146 {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
147 {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
148 {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
149 {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
150 }
151
152 // writeSpaces does what it says, minimizing calls to write-like funcs
153 func writeSpaces(w *bufio.Writer, n int) {
154 const spaces = ` `
155 if n < 1 {
156 return
157 }
158
159 for n >= len(spaces) {
160 w.WriteString(spaces)
161 n -= len(spaces)
162 }
163 w.WriteString(spaces[:n])
164 }
165
166 // json2 does it all, given a reader and a writer
167 func json2(w *bufio.Writer, r io.Reader) error {
168 dec := json.NewDecoder(r)
169 // avoid parsing numbers, so unusually-long numbers are kept verbatim,
170 // even if JSON parsers aren't required to guarantee such input-fidelity
171 // for numbers
172 dec.UseNumber()
173
174 t, err := dec.Token()
175 if err == io.EOF {
176 return errors.New(`input has no JSON values`)
177 }
178
179 if err = handleToken(w, dec, t, 0, 0); err != nil {
180 return err
181 }
182 // don't forget ending the last line for the last value
183 w.WriteByte('\n')
184
185 _, err = dec.Token()
186 if err == io.EOF {
187 // input is over, so it's a success
188 return nil
189 }
190
191 if err == nil {
192 // a successful `read` is a failure, as it means there are
193 // trailing JSON tokens
194 return errors.New(`unexpected trailing data`)
195 }
196
197 // any other error, perhaps some invalid-JSON-syntax-type error
198 return err
199 }
200
201 // handleToken handles recursion for func json2
202 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, pre, level int) error {
203 switch t := t.(type) {
204 case json.Delim:
205 switch t {
206 case json.Delim('['):
207 return handleArray(w, dec, pre, level)
208 case json.Delim('{'):
209 return handleObject(w, dec, pre, level)
210 default:
211 return errors.New(`unsupported JSON syntax ` + string(t))
212 }
213
214 case nil:
215 writeSpaces(w, 2*pre)
216 w.WriteString(`null`)
217 return nil
218
219 case bool:
220 writeSpaces(w, 2*pre)
221 if t {
222 w.WriteString(`true`)
223 } else {
224 w.WriteString(`false`)
225 }
226 return nil
227
228 case json.Number:
229 writeSpaces(w, 2*pre)
230 w.WriteString(t.String())
231 return nil
232
233 case string:
234 return handleString(w, t, pre)
235
236 default:
237 // return fmt.Errorf(`unsupported token type %T`, t)
238 return errors.New(`invalid JSON token`)
239 }
240 }
241
242 // handleArray handles arrays for func handleToken
243 func handleArray(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
244 for i := 0; true; i++ {
245 t, err := dec.Token()
246 if err == io.EOF {
247 return errors.New(`end of JSON before array was closed`)
248 }
249 if err != nil {
250 return err
251 }
252
253 if t == json.Delim(']') {
254 if i == 0 {
255 writeSpaces(w, 2*pre)
256 w.WriteByte('[')
257 w.WriteByte(']')
258 } else {
259 w.WriteByte('\n')
260 writeSpaces(w, 2*level)
261 w.WriteByte(']')
262 }
263 return nil
264 }
265
266 if i == 0 {
267 writeSpaces(w, 2*pre)
268 w.WriteByte('[')
269 w.WriteByte('\n')
270 } else {
271 w.WriteByte(',')
272 w.WriteByte('\n')
273 if err := w.Flush(); err != nil {
274 // a write error may be the consequence of stdout being closed,
275 // perhaps by another app along a pipe
276 return io.EOF
277 }
278 }
279
280 err = handleToken(w, dec, t, level+1, level+1)
281 if err != nil {
282 return err
283 }
284 }
285
286 // make the compiler happy
287 return nil
288 }
289
290 // handleObject handles objects for func handleToken
291 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
292 for i := 0; true; i++ {
293 t, err := dec.Token()
294 if err == io.EOF {
295 return errors.New(`end of JSON before object was closed`)
296 }
297 if err != nil {
298 return err
299 }
300
301 if t == json.Delim('}') {
302 if i == 0 {
303 writeSpaces(w, 2*pre)
304 w.WriteByte('{')
305 w.WriteByte('}')
306 } else {
307 w.WriteByte('\n')
308 writeSpaces(w, 2*level)
309 w.WriteByte('}')
310 }
311 return nil
312 }
313
314 if i == 0 {
315 writeSpaces(w, 2*pre)
316 w.WriteByte('{')
317 w.WriteByte('\n')
318 } else {
319 w.WriteByte(',')
320 w.WriteByte('\n')
321 }
322
323 k, ok := t.(string)
324 if !ok {
325 return errors.New(`expected a string for a key-value pair`)
326 }
327
328 err = handleString(w, k, level+1)
329 if err != nil {
330 return err
331 }
332
333 w.WriteString(": ")
334
335 t, err = dec.Token()
336 if err == io.EOF {
337 return errors.New(`expected a value for a key-value pair`)
338 }
339
340 err = handleToken(w, dec, t, 0, level+1)
341 if err != nil {
342 return err
343 }
344 }
345
346 // make the compiler happy
347 return nil
348 }
349
350 // handleString handles strings for func handleToken, and keys for func
351 // handleObject
352 func handleString(w *bufio.Writer, s string, level int) error {
353 writeSpaces(w, 2*level)
354 w.WriteByte('"')
355 for i := range s {
356 w.Write(escapedStringBytes[s[i]])
357 }
358 w.WriteByte('"')
359 return nil
360 }
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 func handleString treat all string bytes quickly
126 // and correctly, using their officially-supported JSON escape sequences
127 //
128 // https://www.rfc-editor.org/rfc/rfc8259#section-7
129 var escapedStringBytes = [256][]byte{
130 {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
131 {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
132 {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
133 {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
134 {'\\', 'b'}, {'\\', 't'},
135 {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
136 {'\\', 'f'}, {'\\', 'r'},
137 {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
138 {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
139 {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
140 {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
141 {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
142 {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
143 {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
144 {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
145 {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
146 {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
147 {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
148 {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
149 {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
150 {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
151 {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
152 {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
153 {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
154 {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
155 {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
156 {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
157 {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
158 {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
159 {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
160 {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
161 {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
162 {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
163 {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
164 {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
165 {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
166 {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
167 {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
168 {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
169 {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
170 {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
171 {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
172 {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
173 {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
174 }
175
176 // jsonl does it all, given a reader and a writer
177 func jsonl(w *bufio.Writer, r io.Reader, live bool) error {
178 dec := json.NewDecoder(r)
179 // avoid parsing numbers, so unusually-long numbers are kept verbatim,
180 // even if JSON parsers aren't required to guarantee such input-fidelity
181 // for numbers
182 dec.UseNumber()
183
184 t, err := dec.Token()
185 if err == io.EOF {
186 // return errors.New(`input has no JSON values`)
187 return nil
188 }
189
190 if t == json.Delim('[') {
191 if err := handleTopLevelArray(w, dec, live); err != nil {
192 return err
193 }
194 } else {
195 if err := handleToken(w, dec, t); err != nil {
196 return err
197 }
198 w.WriteByte('\n')
199 }
200
201 _, err = dec.Token()
202 if err == io.EOF {
203 // input is over, so it's a success
204 return nil
205 }
206
207 if err == nil {
208 // a successful `read` is a failure, as it means there are
209 // trailing JSON tokens
210 return errors.New(`unexpected trailing data`)
211 }
212
213 // any other error, perhaps some invalid-JSON-syntax-type error
214 return err
215 }
216
217 // handleToken handles recursion for func json2
218 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token) error {
219 switch t := t.(type) {
220 case json.Delim:
221 switch t {
222 case json.Delim('['):
223 return handleArray(w, dec)
224 case json.Delim('{'):
225 return handleObject(w, dec)
226 default:
227 return errors.New(`unsupported JSON syntax ` + string(t))
228 }
229
230 case nil:
231 w.WriteString(`null`)
232 return nil
233
234 case bool:
235 if t {
236 w.WriteString(`true`)
237 } else {
238 w.WriteString(`false`)
239 }
240 return nil
241
242 case json.Number:
243 w.WriteString(t.String())
244 return nil
245
246 case string:
247 return handleString(w, t)
248
249 default:
250 // return fmt.Errorf(`unsupported token type %T`, t)
251 return errors.New(`invalid JSON token`)
252 }
253 }
254
255 func handleTopLevelArray(w *bufio.Writer, dec *json.Decoder, live bool) error {
256 for i := 0; true; i++ {
257 t, err := dec.Token()
258 if err == io.EOF {
259 return nil
260 }
261
262 if err != nil {
263 return err
264 }
265
266 if t == json.Delim(']') {
267 return nil
268 }
269
270 err = handleToken(w, dec, t)
271 if err != nil {
272 return err
273 }
274
275 if w.WriteByte('\n') != nil {
276 return io.EOF
277 }
278
279 if !live {
280 continue
281 }
282
283 if w.Flush() != nil {
284 return io.EOF
285 }
286 }
287
288 // make the compiler happy
289 return nil
290 }
291
292 // handleArray handles arrays for func handleToken
293 func handleArray(w *bufio.Writer, dec *json.Decoder) error {
294 w.WriteByte('[')
295
296 for i := 0; true; i++ {
297 t, err := dec.Token()
298 if err == io.EOF {
299 return errors.New(`end of JSON before array was closed`)
300 }
301 if err != nil {
302 return err
303 }
304
305 if t == json.Delim(']') {
306 w.WriteByte(']')
307 return nil
308 }
309
310 if i > 0 {
311 _, err := w.WriteString(", ")
312 if err != nil {
313 return io.EOF
314 }
315 }
316
317 err = handleToken(w, dec, t)
318 if err != nil {
319 return err
320 }
321 }
322
323 // make the compiler happy
324 return nil
325 }
326
327 // handleObject handles objects for func handleToken
328 func handleObject(w *bufio.Writer, dec *json.Decoder) error {
329 w.WriteByte('{')
330
331 for i := 0; true; i++ {
332 t, err := dec.Token()
333 if err == io.EOF {
334 return errors.New(`end of JSON before object was closed`)
335 }
336 if err != nil {
337 return err
338 }
339
340 if t == json.Delim('}') {
341 w.WriteByte('}')
342 return nil
343 }
344
345 if i > 0 {
346 _, err := w.WriteString(", ")
347 if err != nil {
348 return io.EOF
349 }
350 }
351
352 k, ok := t.(string)
353 if !ok {
354 return errors.New(`expected a string for a key-value pair`)
355 }
356
357 err = handleString(w, k)
358 if err != nil {
359 return err
360 }
361
362 w.WriteString(": ")
363
364 t, err = dec.Token()
365 if err == io.EOF {
366 return errors.New(`expected a value for a key-value pair`)
367 }
368
369 err = handleToken(w, dec, t)
370 if err != nil {
371 return err
372 }
373 }
374
375 // make the compiler happy
376 return nil
377 }
378
379 // handleString handles strings for func handleToken, and keys for func
380 // handleObject
381 func handleString(w *bufio.Writer, s string) error {
382 w.WriteByte('"')
383 for i := range s {
384 w.Write(escapedStringBytes[s[i]])
385 }
386 w.WriteByte('"')
387 return nil
388 }
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 "./base64"
49 "./bitdump"
50 "./breakdown"
51 "./bytedump"
52 "./calc"
53 "./cat"
54 "./catl"
55 "./coby"
56 "./compress"
57 "./countdown"
58 "./datauri"
59 "./dc"
60 "./dcol"
61 "./debase64"
62 "./decsv"
63 "./dedent"
64 "./dedup"
65 "./dejsonl"
66 "./delay"
67 "./dessv"
68 "./detab"
69 "./ecoli"
70 "./erase"
71 "./expand"
72 "./factor"
73 "./fh"
74 "./files"
75 "./filesizes"
76 "./finfo"
77 "./first"
78 "./fixlines"
79 "./folders"
80 "./gsub"
81 "./head"
82 "./hima"
83 "./htmlify"
84 "./id3pic"
85 "./indent"
86 "./items"
87 "./json0"
88 "./json2"
89 "./jsonl"
90 "./jsons"
91 "./junk"
92 "./label"
93 "./last"
94 "./leak"
95 "./limit"
96 "./lineup"
97 "./ls"
98 "./match"
99 "./n"
100 "./ncol"
101 "./ngron"
102 "./nhex"
103 "./njson"
104 "./nl"
105 "./nn"
106 "./now"
107 "./nts"
108 "./pac"
109 "./pad"
110 "./pcol"
111 "./plain"
112 "./pretsv"
113 "./primes"
114 "./realign"
115 "./reprose"
116 "./sbs"
117 "./seq"
118 "./skip"
119 "./skiplast"
120 "./squeeze"
121 "./squomp"
122 "./stomp"
123 "./tacl"
124 "./tail"
125 "./tcatl"
126 "./teletype"
127 "./tolower"
128 "./underline"
129 "./units"
130 "./utfate"
131 "./waveout"
132 "./zcat"
133 "./zj"
134
135 _ "embed"
136 )
137
138 //go:embed info.txt
139 var info string
140
141 // mains has some entries starting as nil to avoid circular-dependency errors
142 var mains = map[string]func(){
143 `args`: args,
144 `avoid`: avoid.Main,
145 `bitdump`: bitdump.Main,
146 `breakdown`: breakdown.Main,
147 `bytedump`: bytedump.Main,
148 `calc`: calc.Main,
149 `catl`: catl.Main,
150 `cls`: cls,
151 `coby`: coby.Main,
152 `compress`: compress.Main,
153 `countdown`: countdown.Main,
154 `datauri`: datauri.Main,
155 `dcol`: dcol.Main,
156 `debase64`: debase64.Main,
157 `decompress`: decompress,
158 `decsv`: decsv.Main,
159 `dedent`: dedent.Main,
160 `dedup`: dedup.Main,
161 `dejsonl`: dejsonl.Main,
162 `delay`: delay.Main,
163 `dessv`: dessv.Main,
164 `detab`: detab.Main,
165 `echobar`: echobar,
166 `ecoli`: ecoli.Main,
167 `erase`: erase.Main,
168 `fail`: fail,
169 `files`: files.Main,
170 `filesizes`: filesizes.Main,
171 `finfo`: finfo.Main,
172 `first`: first.Main,
173 `fixlines`: fixlines.Main,
174 `folders`: folders.Main,
175 `gsub`: gsub.Main,
176 `hecho`: hecho,
177 `hima`: hima.Main,
178 `htmlify`: htmlify.Main,
179 `id3pic`: id3pic.Main,
180 `ignore`: ignore,
181 `indent`: indent.Main,
182 `items`: items.Main,
183 `json0`: json0.Main,
184 `json2`: json2.Main,
185 `jsonl`: jsonl.Main,
186 `jsons`: jsons.Main,
187 `junk`: junk.Main,
188 `label`: label.Main,
189 `last`: last.Main,
190 `leak`: leak.Main,
191 `limit`: limit.Main,
192 `lineup`: lineup.Main,
193 `match`: match.Main,
194 `n`: n.Main,
195 `ncol`: ncol.Main,
196 `ngron`: ngron.Main,
197 `nhex`: nhex.Main,
198 `nil`: null,
199 `njson`: njson.Main,
200 `nn`: nn.Main,
201 `now`: now.Main,
202 `nts`: nts.Main,
203 `pad`: pad.Main,
204 `pcol`: pcol.Main,
205 `plain`: plain.Main,
206 `precho`: precho,
207 `pretsv`: pretsv.Main,
208 `primes`: primes.Main,
209 `realign`: realign.Main,
210 `reprose`: reprose.Main,
211 `ruler`: ruler,
212 `sbs`: sbs.Main,
213 `skip`: skip.Main,
214 `skiplast`: skiplast.Main,
215 `squeeze`: squeeze.Main,
216 `squomp`: squomp.Main,
217 `stomp`: stomp.Main,
218 `tacl`: tacl.Main,
219 `tcatl`: tcatl.Main,
220 `teletype`: teletype.Main,
221 `timezones`: timezones,
222 `tolower`: tolower.Main,
223 `underline`: underline.Main,
224 `units`: units.Main,
225 `utfate`: utfate.Main,
226 `waveout`: waveout.Main,
227 }
228
229 var experimental = map[string]func(){
230 `div`: div,
231 `fh`: fh.Main,
232 `mop`: mop,
233 `pac`: pac.Main,
234 `nothing`: nothing,
235 `tinker`: tinker,
236 `zj`: zj.Main,
237 }
238
239 var remakes = map[string]func(){
240 `base64`: base64.Main,
241 `cat`: cat.Main,
242 `clear`: clear,
243 `dc`: dc.Main,
244 `echo`: echo,
245 `expand`: expand.Main,
246 `factor`: factor.Main,
247 `false`: falseMain,
248 `head`: head.Main,
249 `ls`: ls.Main,
250 `nl`: nl.Main,
251 `seq`: seq.Main,
252 `sleep`: sleep,
253 `tail`: tail.Main,
254 `true`: trueMain,
255 `yes`: yes,
256 `zcat`: zcat.Main,
257 }
258
259 var aliases = map[string]string{
260 `break`: `breakdown`,
261 `breaklines`: `breakdown`,
262 `ca`: `calc`,
263 `calculate`: `calc`,
264 `calculator`: `calc`,
265 `fc`: `calc`,
266 `frac`: `calc`,
267 `fraca`: `calc`,
268 `fracalc`: `calc`,
269 `bytes`: `cat`,
270 `catenate`: `cat`,
271 `concat`: `cat`,
272 `concatenate`: `cat`,
273 `load`: `cat`,
274 `lines`: `catl`,
275 `dcols`: `dcol`,
276 `dropcol`: `dcol`,
277 `dropcols`: `dcol`,
278 `dropcolumns`: `dcol`,
279 `unbase64`: `debase64`,
280 `uncompress`: `decompress`,
281 `uncsv`: `decsv`,
282 `deduplicate`: `dedup`,
283 `undup`: `dedup`,
284 `unique`: `dedup`,
285 `unjsonl`: `dejsonl`,
286 `unssv`: `dessv`,
287 `untab`: `detab`,
288 `fileinfo`: `finfo`,
289 `detrail`: `fixlines`,
290 `id3picture`: `id3pic`,
291 `id3thumb`: `id3pic`,
292 `id3thumbnail`: `id3pic`,
293 `mp3pic`: `id3pic`,
294 `mp3picture`: `id3pic`,
295 `mp3thumb`: `id3pic`,
296 `mp3thumbnail`: `id3pic`,
297 `idem`: `ignore`,
298 `iden`: `ignore`,
299 `identity`: `ignore`,
300 `j0`: `json0`,
301 `j2`: `json2`,
302 `jl`: `jsonl`,
303 `detsv`: `jsons`,
304 `prelabel`: `label`,
305 `prememo`: `label`,
306 `keep`: `match`,
307 `mops`: `mop`,
308 `ncols`: `ncol`,
309 `nicecol`: `ncol`,
310 `nicecols`: `ncol`,
311 `nicecolumns`: `ncol`,
312 `nicegron`: `ngron`,
313 `nh`: `nhex`,
314 `nicehex`: `nhex`,
315 `null`: `nil`,
316 `nicejson`: `njson`,
317 `nj`: `njson`,
318 `nicedigits`: `nn`,
319 `nicenum`: `nn`,
320 `nicenumbers`: `nn`,
321 `nicenums`: `nn`,
322 `ok`: `nothing`,
323 `rpn`: `pac`,
324 `pcols`: `pcol`,
325 `pickcol`: `pcol`,
326 `pickcols`: `pcol`,
327 `pickcolumns`: `pcol`,
328 `tty`: `teletype`,
329 `timezone`: `timezones`,
330 `lower`: `tolower`,
331 `lowercase`: `tolower`,
332 `u`: `underline`,
333 `uline`: `underline`,
334 `utf8`: `utfate`,
335 `zoomjson`: `zj`,
336 `degzip`: `zcat`,
337 `ungzip`: `zcat`,
338 }
339
340 var blurbs = map[string]string{
341 `args`: `show all ARGumentS given after the tool name, one per line`,
342 `avoid`: `ignore lines matching any of the regexes given`,
343 `bitdump`: `show all bits for all input bytes`,
344 `breakdown`: `break input lines into multiple output lines by regexes`,
345 `bytedump`: `show bytes as hex values, with a wide ASCII panel`,
346 `calc`: `fractional calculator, with floating-point powers`,
347 `catl`: `conCATenate Lines, ensures text ends with a line-feed`,
348 `cls`: `CLear the Screen`,
349 `coby`: `COunt BYtes, and many other byte/text-related stats`,
350 `compress`: `gzip-compress all concatenated inputs given`,
351 `countdown`: `countdown the seconds/minutes/hours given`,
352 `datauri`: `turn bytes into data-URIs, auto-detecting MIME types`,
353 `dcol`: `Drop COLumns by name or by 1-based index`,
354 `debase64`: `decode base64 text and data-URIs`,
355 `decompress`: `gzip-decompress concatenated bytes from all the inputs`,
356 `decsv`: `convert CSV tables into TSV tables, or into JSON`,
357 `dedent`: `ignore the common leading-space indentation`,
358 `dedup`: `deduplicate lines, emitting each unique line only once`,
359 `dejsonl`: `turn JSON Lines into proper JSON`,
360 `delay`: `wait the seconds given, before emitting each input line`,
361 `dessv`: `turn tables of space-separated values into TSV tables`,
362 `detab`: `expand tabs into runs of up to n spaces, or up to 4`,
363 `div`: `DIVide 2 numbers, or invert 1 number`,
364 `echobar`: `like 'echo', but right-pads and highlights its output`,
365 `ecoli`: `Expressions COloring LInes color-codes matching lines`,
366 `erase`: `ignore/erase all matching regexes away from each line`,
367 `fail`: `fail with the exit code given, or using 1 by default`,
368 `fh`: `Function Heatmapper draws 2-input (x, y) functions`,
369 `files`: `list all files in the folder(s) given`,
370 `filesizes`: `show sizes of files and block-counts (4K by default)`,
371 `finfo`: `show various file info, for plain-text and/or media files`,
372 `first`: `keep only the first n lines, or the first line by default`,
373 `fixlines`: `ignore carriage-returns, or even trailing spaces`,
374 `folders`: `list all folders in the folder(s) given`,
375 `gsub`: `Globally SUBstitute all regular expression matches`,
376 `hecho`: `Highlighted ECHO shows an ANSI-styled line`,
377 `help`: `show the help message for "easybox"`,
378 `hima`: `HIlight MAtches using the regexes given`,
379 `htmlify`: `turn plain-text lines into HTML documents`,
380 `id3pic`: `get the encoded picture out of audio files, if present`,
381 `ignore`: `ignore all arguments, copying stdin into stdout instead`,
382 `indent`: `indent lines, using 4 leading spaces by default`,
383 `items`: `emit words/items from input lines as single-item lines`,
384 `json0`: `minimize/fix JSON into the smallest-possible size`,
385 `json2`: `indent JSON into multiple lines, using 2 spaces per level`,
386 `jsonl`: `turn items from top-level JSON arrays into JSON Lines`,
387 `jsons`: `JSON Strings turns TSV into arrays of objects of strings`,
388 `junk`: `emit a given count of random bytes, or 1024 by default`,
389 `label`: `precede input lines with a styled line`,
390 `last`: `keep only the last n lines, or the last line by default`,
391 `leak`: `emit lines both to stdout and stderr; good to debug pipes`,
392 `limit`: `emit up to the first n bytes, or the first 1024 by default`,
393 `lineup`: `put input lines into (up to) n-items tab-separated lines`,
394 `match`: `only keep lines matching any of the regexes given`,
395 `mop`: `Multiple OPerations, using the 2 numbers given`,
396 `n`: `Number lines, putting tabs between numbers and contents`,
397 `ncol`: `Nice COLumns realigns tables, color-coding their values`,
398 `ngron`: `Nice GRON mimics a subset of "gron", using better colors`,
399 `nhex`: `Nice HEXadecimal shows bytes as hex values and ASCII`,
400 `nil`: `emit nothing, also discarding all stdin bytes, if piped`,
401 `njson`: `Nice JSON indents and color-codes JSON data`,
402 `nn`: `Nice Numbers color-codes groups of digits for legibility`,
403 `now`: `show the current date and time, also for other timezones`,
404 `nts`: `Nice TimeStamp precedes each line with styled date/time`,
405 `pac`: `Postfix Array Calculator is an RPN-style calculator`,
406 `pad`: `right-pad lines, so they all have the same symbol-count`,
407 `pcol`: `Pick COLumns by name or by 1-based index`,
408 `plain`: `ignore all ANSI-sequences, leaving unstyled text`,
409 `precho`: `PRecede ECHO, precedes input lines with an SSV line`,
410 `pretsv`: `PREcede TSV, precedes input lines with a TSV line`,
411 `primes`: `find prime numbers, up to the first million by default`,
412 `realign`: `realign items from the SSV/TSV tables given`,
413 `reprose`: `reflow/trim lines of text/prose to improve its legibility`,
414 `ruler`: `show a ruler-like pattern on a single line`,
415 `sbs`: `Side By Side, wraps input lines in multiple columns`,
416 `seq`: `emit a sequence of numbers, one per line`,
417 `skip`: `skip at most the first n lines, or the first by default`,
418 `skiplast`: `skip at most the last n lines, or the last by default`,
419 `squeeze`: `aggressively ignore spaces, especially runs of spaces`,
420 `squomp`: `squeeze non-empty lines and stomp empty(-ish) lines`,
421 `stomp`: `ignore leading/trailing empty lines, squeezing empty runs`,
422 `tacl`: `TAC Lines emits text lines in backward-order`,
423 `tcatl`: `Titled conCATenate Lines, is like "catl" but with names`,
424 `teletype`: `mimic old-fashioned teletype devices, by delaying output`,
425 `timezones`: `lookup full timezone names from the city/place names given`,
426 `tolower`: `turn all uppercase letters into lowercase ones`,
427 `tools`: `list all tools available`,
428 `underline`: `underline every nth line, or every 5th line by default`,
429 `units`: `convert weird units into the international standard ones`,
430 `utfate`: `decode all other types of UTF text into UTF-8`,
431 `waveout`: `emit/calculate WAV-format sounds by formula`,
432 `yes`: `keep emitting a line with "yes", or the message given`,
433 `zj`: `Zoom Json, using the keys/indices given as arguments`,
434 }
435
436 func main() {
437 // try to use the app's `name`, in case it's being called from a file-link
438 // named after one of the tools
439 if tool, ok := lookupTool(path.Base(os.Args[0])); ok {
440 tool()
441 return
442 }
443
444 if len(os.Args) < 2 {
445 showHelp(os.Stderr)
446 fmt.Fprintln(os.Stderr, ``)
447 fmt.Fprintln(os.Stderr, `easybox: no tool name given`)
448 os.Exit(1)
449 return
450 }
451
452 // try normal tool-lookup using the first command-line argument
453
454 name := os.Args[1]
455 os.Args = os.Args[1:]
456
457 if tool, ok := lookupTool(name); ok {
458 tool()
459 return
460 }
461
462 switch name {
463 case `-h`, `--h`, `-help`, `--help`, `help`:
464 showHelp(os.Stdout)
465
466 case `-l`, `--l`, `-list`, `--list`:
467 tools()
468
469 case `-links`, `--links`:
470 showLinksCommands(os.Stdout)
471
472 case `-t`, `--t`, `-tools`, `--tools`, `tools`:
473 tools()
474
475 default:
476 const fs = "easybox: tool/alias named %q not found\n"
477 fmt.Fprintf(os.Stderr, fs, name)
478 os.Exit(1)
479 return
480 }
481 }
482
483 // dealias tries to lookup a string to the aliases given, returning the name
484 // given if the lookup fails
485 func dealias(aliases map[string]string, name string) string {
486 if s, ok := aliases[name]; ok {
487 return s
488 }
489 return name
490 }
491
492 func help() {
493 showHelp(os.Stdout)
494 }
495
496 func lookupTool(name string) (tool func(), ok bool) {
497 name = strings.ReplaceAll(name, `-`, ``)
498 name = strings.ReplaceAll(name, `_`, ``)
499 key := dealias(aliases, name)
500
501 if tool, ok := mains[key]; ok {
502 return tool, ok
503 }
504
505 if tool, ok := remakes[key]; ok {
506 return tool, ok
507 }
508
509 tool, ok = experimental[key]
510 return tool, ok
511 }
512
513 // showHelp has a parameter to write either to stdout or stderr
514 func showHelp(w io.Writer) {
515 fmt.Fprintln(w, info)
516
517 fmt.Fprintln(w, "\nTools Available")
518
519 maxlen := 0
520 names := make([]string, 0, max(len(mains), len(aliases)))
521 for k := range mains {
522 names = append(names, k)
523 maxlen = max(maxlen, utf8.RuneCountInString(k))
524 }
525
526 sort.Strings(names)
527
528 for _, s := range names {
529 fmt.Fprintf(w, " - %-*s %s\n", maxlen, s, blurbs[s])
530 }
531
532 fmt.Fprintln(w, "\nAliases Available")
533
534 maxlen = 0
535 names = names[:0]
536 for k := range aliases {
537 names = append(names, k)
538 maxlen = max(maxlen, utf8.RuneCountInString(k))
539 }
540
541 sort.Strings(names)
542
543 for _, k := range names {
544 fmt.Fprintf(w, " - %-*s -> %s\n", maxlen, k, aliases[k])
545 }
546
547 names = names[:0]
548 for k := range experimental {
549 names = append(names, k)
550 }
551
552 if len(names) == 0 {
553 return
554 }
555
556 fmt.Fprintln(w, "\nExperimental Tools Available")
557
558 for _, k := range names {
559 fmt.Fprintf(w, " - %s\n", k)
560 }
561 }
562
563 // showLinksCommands has a parameter to write either to stdout or stderr
564 func showLinksCommands(w io.Writer) {
565 names := make([]string, 0, len(mains))
566 for k := range mains {
567 names = append(names, k)
568 }
569
570 sort.Strings(names)
571
572 for _, s := range names {
573 fmt.Fprintf(w, "ln -s \"$(which easybox)\" ./%s\n", s)
574 }
575 }
576
577 // tinker is for little throw-away experiments
578 func tinker() {
579 }
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 `
47
48 const linkRegexp = `(?i)https?://[a-z0-9+_.:%-]+(/[a-z0-9+_.%/,#?&=-]*)*`
49
50 func Main() {
51 nerr := 0
52 links := false
53 buffered := false
54 avoid := false
55 sensitive := true
56 args := os.Args[1:]
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 sensitive = false
71 args = args[1:]
72 continue
73
74 case `-iv`, `-vi`:
75 sensitive = false
76 avoid = true
77 args = args[1:]
78 continue
79
80 case `-l`, `--l`, `-links`, `--links`:
81 links = true
82 args = args[1:]
83 continue
84
85 case `-v`, `--v`, `-inv`, `--inv`, `-neg`, `--neg`:
86 avoid = true
87 args = args[1:]
88 continue
89 }
90
91 break
92 }
93
94 if len(args) > 0 && args[0] == `--` {
95 args = args[1:]
96 }
97
98 liveLines := !buffered
99 if !buffered {
100 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
101 liveLines = false
102 }
103 }
104
105 if len(args) == 0 {
106 args = []string{`.`}
107 }
108
109 var exprs []*regexp.Regexp
110 if links {
111 exprs = make([]*regexp.Regexp, 0, len(args)+1)
112 exprs = append(exprs, regexp.MustCompile(linkRegexp))
113 } else {
114 exprs = make([]*regexp.Regexp, 0, len(args))
115 }
116
117 for _, src := range args {
118 var err error
119 var exp *regexp.Regexp
120 if !sensitive {
121 exp, err = regexp.Compile(`(?i)` + src)
122 } else {
123 exp, err = regexp.Compile(src)
124 }
125
126 if err != nil {
127 os.Stderr.WriteString(err.Error())
128 os.Stderr.WriteString("\n")
129 nerr++
130 }
131
132 exprs = append(exprs, exp)
133 }
134
135 if nerr > 0 {
136 os.Exit(1)
137 return
138 }
139
140 var buf []byte
141 sc := bufio.NewScanner(os.Stdin)
142 sc.Buffer(nil, 8*1024*1024*1024)
143 bw := bufio.NewWriter(os.Stdout)
144
145 for i := 0; sc.Scan(); i++ {
146 line := sc.Bytes()
147 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
148 line = line[3:]
149 }
150
151 s := line
152 if bytes.IndexByte(s, '\x1b') >= 0 {
153 buf = plain(buf[:0], s)
154 s = buf
155 }
156
157 if match(s, exprs) {
158 if avoid {
159 continue
160 }
161
162 if err := emit(bw, line, liveLines); err != nil {
163 return
164 }
165 }
166
167 if avoid {
168 if err := emit(bw, line, liveLines); err != nil {
169 return
170 }
171 }
172 }
173 }
174
175 func emit(w *bufio.Writer, line []byte, live bool) error {
176 w.Write(line)
177 w.WriteByte('\n')
178
179 if !live {
180 return nil
181 }
182
183 return w.Flush()
184 }
185
186 func match(what []byte, with []*regexp.Regexp) bool {
187 for _, e := range with {
188 if e.Match(what) {
189 return true
190 }
191 }
192 return false
193 }
194
195 func plain(dst []byte, src []byte) []byte {
196 for len(src) > 0 {
197 i, j := indexEscapeSequence(src)
198 if i < 0 {
199 dst = append(dst, src...)
200 break
201 }
202 if j < 0 {
203 j = len(src)
204 }
205
206 if i > 0 {
207 dst = append(dst, src[:i]...)
208 }
209
210 src = src[j:]
211 }
212
213 return dst
214 }
215
216 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
217 // the multi-byte sequences starting with ESC[; the result is a pair of slice
218 // indices which can be independently negative when either the start/end of
219 // a sequence isn't found; given their fairly-common use, even the hyperlink
220 // ESC]8 sequences are supported
221 func indexEscapeSequence(s []byte) (int, int) {
222 var prev byte
223
224 for i, b := range s {
225 if prev == '\x1b' && b == '[' {
226 j := indexLetter(s[i+1:])
227 if j < 0 {
228 return i, -1
229 }
230 return i - 1, i + 1 + j + 1
231 }
232
233 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
234 j := indexPair(s[i+1:], '\x1b', '\\')
235 if j < 0 {
236 return i, -1
237 }
238 return i - 1, i + 1 + j + 2
239 }
240
241 prev = b
242 }
243
244 return -1, -1
245 }
246
247 func indexLetter(s []byte) int {
248 for i, b := range s {
249 upper := b &^ 32
250 if 'A' <= upper && upper <= 'Z' {
251 return i
252 }
253 }
254
255 return -1
256 }
257
258 func indexPair(s []byte, x byte, y byte) int {
259 var prev byte
260
261 for i, b := range s {
262 if prev == x && b == y && i > 0 {
263 return i
264 }
265 prev = b
266 }
267
268 return -1
269 }
File: ./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/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/URI...]
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.
42
43 This tool uses nicer ANSI styles than the original, hence its name, but
44 can't convert its output back into JSON, unlike the latter.
45
46 Unlike the original 'gron', there's no sort-mode. When not given a named
47 source (filepath/URI) to read from, data are read from standard input.
48
49 Options, where leading double-dashes are also allowed:
50
51 -h show this help message
52 -help show this help message
53
54 -m monochrome (default), enables unstyled output-mode
55 -c color, enables ANSI-styled output-mode
56 -color enables ANSI-styled output-mode
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
100 for len(args) > 0 {
101 switch args[0] {
102 case `-c`, `--c`, `-color`, `--color`:
103 config = styled
104 args = args[1:]
105 continue
106
107 case `-h`, `--h`, `-help`, `--help`:
108 os.Stdout.WriteString(info[1:])
109 return
110
111 case `-m`, `--m`:
112 config = monochrome
113 args = args[1:]
114 continue
115 }
116
117 break
118 }
119
120 if len(args) > 0 && args[0] == `--` {
121 args = args[1:]
122 }
123
124 if len(args) > 2 {
125 os.Stderr.WriteString("multiple inputs not allowed\n")
126 os.Exit(1)
127 return
128 }
129
130 // figure out whether input should come from a named file or from stdin
131 path := `-`
132 if len(args) > 0 {
133 path = args[0]
134 }
135
136 err := handleInput(os.Stdout, path)
137 if 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 handlerFunc func(*bufio.Writer, *json.Decoder, json.Token, []any) error
146
147 // handleInput simplifies control-flow for func main
148 func handleInput(w io.Writer, path string) error {
149 if path == `-` {
150 bw := bufio.NewWriter(w)
151 defer bw.Flush()
152 return run(bw, os.Stdin)
153 }
154
155 f, err := os.Open(path)
156 if err != nil {
157 // on windows, file-not-found error messages may mention `CreateFile`,
158 // even when trying to open files in read-only mode
159 return errors.New(`can't open file named ` + path)
160 }
161 defer f.Close()
162
163 bw := bufio.NewWriter(w)
164 defer bw.Flush()
165 return run(bw, f)
166 }
167
168 // escapedStringBytes helps func handleString treat all string bytes quickly
169 // and correctly, using their officially-supported JSON escape sequences
170 //
171 // https://www.rfc-editor.org/rfc/rfc8259#section-7
172 var escapedStringBytes = [256][]byte{
173 {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
174 {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
175 {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
176 {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
177 {'\\', 'b'}, {'\\', 't'},
178 {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
179 {'\\', 'f'}, {'\\', 'r'},
180 {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
181 {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
182 {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
183 {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
184 {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
185 {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
186 {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
187 {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
188 {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
189 {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
190 {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
191 {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
192 {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
193 {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
194 {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
195 {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
196 {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
197 {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
198 {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
199 {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
200 {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
201 {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
202 {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
203 {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
204 {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
205 {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
206 {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
207 {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
208 {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
209 {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
210 {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
211 {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
212 {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
213 {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
214 {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
215 {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
216 {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
217 }
218
219 // run does it all, given a reader and a writer
220 func run(w *bufio.Writer, r io.Reader) error {
221 dec := json.NewDecoder(r)
222 // avoid parsing numbers, so unusually-long numbers are kept verbatim,
223 // even if JSON parsers aren't required to guarantee such input-fidelity
224 // for numbers
225 dec.UseNumber()
226
227 t, err := dec.Token()
228 if err == io.EOF {
229 return errors.New(`input has no JSON values`)
230 }
231
232 if err = handleToken(w, dec, t, make([]any, 0, 50)); err != nil {
233 return err
234 }
235
236 _, err = dec.Token()
237 if err == io.EOF {
238 // input is over, so it's a success
239 return nil
240 }
241
242 if err == nil {
243 // a successful `read` is a failure, as it means there are
244 // trailing JSON tokens
245 return errors.New(`unexpected trailing data`)
246 }
247
248 // any other error, perhaps some invalid-JSON-syntax-type error
249 return err
250 }
251
252 // handleToken handles recursion for func run
253 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, path []any) error {
254 switch t := t.(type) {
255 case json.Delim:
256 switch t {
257 case json.Delim('['):
258 return handleArray(w, dec, path)
259 case json.Delim('{'):
260 return handleObject(w, dec, path)
261 default:
262 return errors.New(`unsupported JSON syntax ` + string(t))
263 }
264
265 case nil:
266 config.path(w, path)
267 config.null(w)
268 return endLine(w)
269
270 case bool:
271 config.path(w, path)
272 config.boolean(w, t)
273 return endLine(w)
274
275 case json.Number:
276 config.path(w, path)
277 config.number(w, t)
278 return endLine(w)
279
280 case string:
281 config.path(w, path)
282 config.text(w, t)
283 return endLine(w)
284
285 default:
286 // return fmt.Errorf(`unsupported token type %T`, t)
287 return errors.New(`invalid JSON token`)
288 }
289 }
290
291 // handleArray handles arrays for func handleToken
292 func handleArray(w *bufio.Writer, dec *json.Decoder, path []any) error {
293 config.path(w, path)
294 w.WriteString(config.arrayDecl)
295 if err := endLine(w); err != nil {
296 return err
297 }
298
299 path = append(path, 0)
300 last := len(path) - 1
301
302 for i := 0; true; i++ {
303 path[last] = i
304
305 t, err := dec.Token()
306 if err == io.EOF {
307 return errors.New(`end of JSON before array was closed`)
308 }
309 if err != nil {
310 return err
311 }
312
313 if t == json.Delim(']') {
314 return nil
315 }
316
317 err = handleToken(w, dec, t, path)
318 if err != nil {
319 return err
320 }
321 }
322
323 // make the compiler happy
324 return nil
325 }
326
327 // handleObject handles objects for func handleToken
328 func handleObject(w *bufio.Writer, dec *json.Decoder, path []any) error {
329 config.path(w, path)
330 w.WriteString(config.objectDecl)
331 if err := endLine(w); err != nil {
332 return err
333 }
334
335 path = append(path, ``)
336 last := len(path) - 1
337
338 for i := 0; true; i++ {
339 t, err := dec.Token()
340 if err == io.EOF {
341 return errors.New(`end of JSON before object was closed`)
342 }
343 if err != nil {
344 return err
345 }
346
347 if t == json.Delim('}') {
348 return nil
349 }
350
351 k, ok := t.(string)
352 if !ok {
353 return errors.New(`expected a string for a key-value pair`)
354 }
355
356 path[last] = k
357 if err != nil {
358 return err
359 }
360
361 t, err = dec.Token()
362 if err == io.EOF {
363 return errors.New(`expected a value for a key-value pair`)
364 }
365
366 err = handleToken(w, dec, t, path)
367 if err != nil {
368 return err
369 }
370 }
371
372 // make the compiler happy
373 return nil
374 }
375
376 func monoPath(w *bufio.Writer, path []any) error {
377 var buf [24]byte
378
379 w.WriteString(`json`)
380
381 for _, v := range path {
382 switch v := v.(type) {
383 case int:
384 w.WriteByte('[')
385 w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
386 w.WriteByte(']')
387
388 case string:
389 if !needsEscaping(v) {
390 w.WriteByte('.')
391 w.WriteString(v)
392 continue
393 }
394 w.WriteByte('[')
395 monoString(w, v)
396 w.WriteByte(']')
397 }
398 }
399
400 w.WriteString(` = `)
401 return nil
402 }
403
404 func monoNull(w *bufio.Writer) error {
405 w.WriteString(`null`)
406 return nil
407 }
408
409 func monoBool(w *bufio.Writer, b bool) error {
410 if b {
411 w.WriteString(`true`)
412 } else {
413 w.WriteString(`false`)
414 }
415 return nil
416 }
417
418 func monoNumber(w *bufio.Writer, n json.Number) error {
419 w.WriteString(n.String())
420 return nil
421 }
422
423 func monoString(w *bufio.Writer, s string) error {
424 w.WriteByte('"')
425 for i := range s {
426 w.Write(escapedStringBytes[s[i]])
427 }
428 w.WriteByte('"')
429 return nil
430 }
431
432 func styledPath(w *bufio.Writer, path []any) error {
433 var buf [24]byte
434
435 w.WriteString("\x1b[38;2;135;95;255mjson\x1b[0m")
436
437 for _, v := range path {
438 switch v := v.(type) {
439 case int:
440 w.WriteString("\x1b[38;2;168;168;168m[")
441 w.WriteString("\x1b[38;2;0;135;95m")
442 w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
443 w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
444
445 case string:
446 if !needsEscaping(v) {
447 w.WriteString("\x1b[38;2;168;168;168m.")
448 w.WriteString("\x1b[38;2;135;95;255m")
449 w.WriteString(v)
450 w.WriteString("\x1b[0m")
451 continue
452 }
453
454 w.WriteString("\x1b[38;2;168;168;168m[")
455 styledString(w, v)
456 w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
457 }
458 }
459
460 w.WriteString(" \x1b[38;2;168;168;168m=\x1b[0m ")
461 return nil
462 }
463
464 func styledNull(w *bufio.Writer) error {
465 w.WriteString("\x1b[38;2;168;168;168m")
466 w.WriteString(`null`)
467 w.WriteString("\x1b[0m")
468 return nil
469 }
470
471 func styledBool(w *bufio.Writer, b bool) error {
472 if b {
473 w.WriteString("\x1b[38;2;95;175;215mtrue\x1b[0m")
474 } else {
475 w.WriteString("\x1b[38;2;95;175;215mfalse\x1b[0m")
476 }
477 return nil
478 }
479
480 func styledNumber(w *bufio.Writer, n json.Number) error {
481 w.WriteString("\x1b[38;2;0;135;95m")
482 w.WriteString(n.String())
483 w.WriteString("\x1b[0m")
484 return nil
485 }
486
487 func styledString(w *bufio.Writer, s string) error {
488 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
489 for i := range s {
490 w.Write(escapedStringBytes[s[i]])
491 }
492 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
493 return nil
494 }
495
496 func needsEscaping(s string) bool {
497 for _, r := range s {
498 if r < ' ' || r > '~' {
499 return true
500 }
501
502 switch r {
503 case '"', '\'', '\\':
504 return true
505 }
506 }
507
508 return false
509 }
510
511 func endLine(w *bufio.Writer) error {
512 w.WriteByte(';')
513 if err := w.WriteByte('\n'); err == nil {
514 return nil
515 }
516 return io.EOF
517 }
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 h, help show this help message
69 i, iota push numbers 1..n
70 l, len, length the item-count in the top/latest array/value
71 ln, log natural logarithm
72 log10 base-10 logarithm
73 log2 base-2 logarithm
74 neg, negate flip the sign
75 p, print output the top/latest array/value on a single line
76 pi the number known as 'PI'
77 pop forget the top/latest value
78 pow10 10^x
79 pow2 2^x
80 rand, random, rnd emit n random numbers in range [0, 1)
81 round round numbers
82 s, swap, swap2 swap the 2 top/latest values
83 sgn, sign the sign function
84 shift forget the oldest value still in the stack
85 sin the sine
86 sinh the hyperbolic sine
87 tan the tangent
88 tanh the hyperbolic tangent
89 tau twice the number known as 'PI'
90 top forget all values but the top/latest
91 u, unit push the number 1 onto the stack n times
92
93 Examples
94
95 # push numbers on the stack; only the last one will show as the result
96 pac 4.5 -6.22 905_900
97
98 # push numbers on the stack, then flatten everything, so all values show
99 pac 4.5 -6.22 905_900 flat
100 `
101
102 var (
103 needAtLeast1 = errors.New(`need at least 1 number or array on the stack`)
104 needAtLeast2 = errors.New(`need at least 2 numbers or arrays on the stack`)
105 )
106
107 func Main() {
108 args := os.Args[1:]
109
110 if len(args) > 0 {
111 switch args[0] {
112 case `-h`, `--h`, `-help`, `--help`:
113 os.Stdout.WriteString(info[1:])
114 return
115 }
116 }
117
118 if len(args) > 0 && args[0] == `--` {
119 args = args[1:]
120 }
121
122 if len(args) == 0 {
123 os.Stderr.WriteString(info[1:])
124 os.Exit(1)
125 return
126 }
127
128 if err := pac(os.Stdout, args); err != nil && err != io.EOF {
129 os.Stderr.WriteString(err.Error())
130 os.Stderr.WriteString("\n")
131 os.Exit(1)
132 return
133 }
134 }
135
136 type context struct {
137 stack [][]float64
138 random *rand.Rand
139 }
140
141 func pac(w io.Writer, args []string) error {
142 var ctx context
143 ctx.stack = make([][]float64, 0, 64)
144 reset(&ctx)
145
146 if err := run(args, &ctx); err != nil {
147 return err
148 }
149
150 // if len(ctx.stack) > 1 {
151 // os.Stderr.WriteString("multiple results left on the stack\n")
152 // }
153 if len(ctx.stack) == 0 {
154 return errors.New(`no results left on the stack`)
155 }
156
157 show(w, &ctx)
158 return nil
159 }
160
161 func show(w io.Writer, ctx *context) {
162 if len(ctx.stack) == 0 {
163 return
164 }
165
166 bw := bufio.NewWriter(w)
167 defer bw.Flush()
168
169 var buf [24]byte
170 for i, f := range ctx.stack[len(ctx.stack)-1] {
171 if i > 0 {
172 if err := bw.WriteByte(' '); err != nil {
173 break
174 }
175 }
176 bw.Write(strconv.AppendFloat(buf[:0], f, 'f', -1, 64))
177 }
178 bw.WriteByte('\n')
179 }
180
181 func debug(w io.StringWriter, ctx *context) {
182 for i, v := range ctx.stack {
183 w.WriteString("\t")
184 w.WriteString(strconv.Itoa(i))
185 w.WriteString(":")
186 for _, f := range v {
187 w.WriteString(" ")
188 w.WriteString(strconv.FormatFloat(f, 'f', -1, 64))
189 }
190
191 w.WriteString("\n")
192 }
193
194 w.WriteString("\n")
195 }
196
197 func run(args []string, ctx *context) error {
198 for len(args) > 0 {
199 s := args[0]
200 args = args[1:]
201
202 for len(s) > 0 {
203 for len(s) > 0 && s[0] == ' ' {
204 s = s[1:]
205 }
206 if len(s) == 0 {
207 break
208 }
209
210 var cmd string
211 if i := strings.IndexByte(s, ' '); i >= 0 {
212 cmd = s[:i]
213 } else {
214 cmd = s
215 }
216 s = s[len(cmd):]
217
218 if f, err := strconv.ParseFloat(cmd, 64); err == nil {
219 ctx.stack = append(ctx.stack, []float64{f})
220 continue
221 }
222
223 if cmd == `[` {
224 rest, err := appendArray(ctx, args)
225 if err != nil {
226 return err
227 }
228 args = rest
229 continue
230 }
231
232 if cmd == `.` {
233 debug(os.Stderr, ctx)
234 continue
235 }
236
237 if f, ok := funcs[cmd]; ok {
238 if err := call(ctx, f, cmd); err != nil {
239 return err
240 }
241 continue
242 }
243
244 return errors.New(`unknown command ` + cmd)
245 }
246 }
247
248 return nil
249 }
250
251 func appendArray(ctx *context, args []string) ([]string, error) {
252 end := -1
253 for i, s := range args {
254 if s == `]` {
255 end = i
256 break
257 }
258 }
259
260 if end < 0 {
261 return args, errors.New(`missing the ']' to close array`)
262 }
263
264 items := args[:end]
265 rest := args[end+1:]
266
267 var sub context
268 err := run(items, &sub)
269 flatten(&sub)
270 if len(sub.stack) > 0 {
271 ctx.stack = append(ctx.stack, sub.stack[len(sub.stack)-1])
272 }
273 return rest, err
274 }
275
276 func call(ctx *context, f any, name string) error {
277 var err error
278
279 switch f := f.(type) {
280 case float64:
281 ctx.stack = append(ctx.stack, []float64{f})
282 case func(ctx *context) error:
283 err = f(ctx)
284 case func(x float64) float64:
285 err = loop(ctx, f)
286 case func(x, y float64) float64:
287 err = parallel(ctx, f)
288 case func(x, y float64) (float64, float64):
289 err = parallel2(ctx, f)
290 case func(ctx *context, n int) error:
291 err = repeat(ctx, f)
292 default:
293 err = errors.New(`unsupported function type`)
294 }
295
296 if err == io.EOF {
297 return io.EOF
298 }
299 if err != nil {
300 return errors.New(name + `: ` + err.Error())
301 }
302 return nil
303 }
304
305 func cloneArray(x []float64) []float64 {
306 clone := make([]float64, 0, len(x))
307 for _, f := range x {
308 clone = append(clone, f)
309 }
310 return clone
311 }
312
313 var funcs = map[string]any{
314 `+`: add,
315 `-`: sub,
316 `*`: mul,
317 `/`: div,
318 `%`: math.Mod,
319 `^`: math.Pow,
320 `**`: math.Pow,
321
322 `add`: add,
323 `sub`: sub,
324 `mul`: mul,
325 `div`: div,
326 `mod`: math.Mod,
327 `modulus`: math.Mod,
328 `pow`: math.Pow,
329 `power`: math.Pow,
330
331 `m`: mul,
332
333 `a`: anchor,
334 `abs`: math.Abs,
335 `anchor`: anchor,
336 `ceil`: math.Ceil,
337 `ceiling`: math.Ceil,
338 `cos`: math.Cos,
339 `cosh`: math.Cosh,
340 `cosine`: math.Cos,
341 `div2`: div2,
342 `down`: math.Floor,
343 `exp`: math.Exp,
344 `floor`: math.Floor,
345 `hypot`: math.Hypot,
346 `hypotenuse`: math.Hypot,
347 `hypothenuse`: math.Hypot,
348 `mid`: mid,
349 `middle`: mid,
350 `neg`: negate,
351 `negate`: negate,
352 `ln`: math.Log,
353 `log`: math.Log,
354 `log10`: math.Log10,
355 `log2`: math.Log2,
356 `pow10`: pow10,
357 `pow2`: pow2,
358 `round`: math.Round,
359 `sgn`: sign,
360 `sign`: sign,
361 `sin`: math.Sin,
362 `sine`: math.Sin,
363 `sinh`: math.Sinh,
364 `tan`: math.Tan,
365 `tangent`: math.Tan,
366 `tanh`: math.Tanh,
367 `up`: math.Ceil,
368
369 `bail`: bail,
370 `bye`: bye,
371 `c`: empty,
372 `clear`: empty,
373 `d`: dup,
374 `dump`: dump,
375 `dup`: dup,
376 `dup2`: dup2,
377 `e`: math.E,
378 `empty`: empty,
379 `f`: flatten,
380 `flat`: flatten,
381 `flatten`: flatten,
382 `h`: help,
383 `help`: help,
384 `i`: iota,
385 `inv`: invert,
386 `inverse`: invert,
387 `invert`: invert,
388 `iota`: iota,
389 `l`: toplen,
390 `len`: toplen,
391 `length`: toplen,
392 `p`: toprint,
393 `pi`: math.Pi,
394 `print`: toprint,
395 `pop`: pop,
396 `q`: bye,
397 `quit`: bye,
398 `rand`: random,
399 `random`: random,
400 `rnd`: random,
401 `s`: swap2,
402 `shift`: shift,
403 `swap`: swap2,
404 `swap2`: swap2,
405 `tau`: 2 * math.Pi,
406 `top`: top,
407 `u`: unit,
408 `unit`: unit,
409 }
410
411 func invert(x float64) float64 { return 1 / x }
412 func negate(x float64) float64 { return -x }
413
414 func add(x, y float64) float64 { return x + y }
415 func sub(x, y float64) float64 { return x - y }
416 func mid(x, y float64) float64 { return 0.5 * (x + y) }
417 func mul(x, y float64) float64 { return x * y }
418 func div(x, y float64) float64 { return x / y }
419 func pow10(x float64) float64 { return math.Pow(10, x) }
420 func pow2(x float64) float64 { return math.Pow(2, x) }
421
422 func div2(x, y float64) (float64, float64) { return x / y, y / x }
423
424 func anchor(x float64) float64 {
425 const eps = 1e6
426 if i, f := math.Modf(x); -eps <= f && f <= +eps {
427 return float64(i)
428 }
429 return x
430 }
431
432 func sign(x float64) float64 {
433 if x > 0 {
434 return +1
435 }
436 if x < 0 {
437 return -1
438 }
439 return x
440 }
441
442 func bail(ctx *context) error {
443 dump(ctx)
444 return bye(ctx)
445 }
446
447 func bye(ctx *context) error {
448 return io.EOF
449 }
450
451 func dump(ctx *context) error {
452 if len(ctx.stack) == 0 {
453 return nil
454 }
455
456 bw := bufio.NewWriter(os.Stdout)
457 defer bw.Flush()
458
459 var buf [24]byte
460 for _, v := range ctx.stack {
461 for i, f := range v {
462 if i > 0 {
463 if err := bw.WriteByte(' '); err != nil {
464 break
465 }
466 }
467 bw.Write(strconv.AppendFloat(buf[:0], f, 'f', -1, 64))
468 }
469
470 if err := bw.WriteByte('\n'); err != nil {
471 return io.EOF
472 }
473 }
474
475 return nil
476 }
477
478 func dup(ctx *context) error {
479 if len(ctx.stack) > 0 {
480 x := ctx.stack[len(ctx.stack)-1]
481 ctx.stack = append(ctx.stack, cloneArray(x))
482 }
483 return nil
484 }
485
486 func dup2(ctx *context) error {
487 if len(ctx.stack) < 2 {
488 return needAtLeast2
489 }
490
491 x := ctx.stack[len(ctx.stack)-2]
492 y := ctx.stack[len(ctx.stack)-1]
493 ctx.stack = append(ctx.stack, cloneArray(x))
494 ctx.stack = append(ctx.stack, cloneArray(y))
495 return nil
496 }
497
498 func empty(ctx *context) error {
499 ctx.stack = ctx.stack[:0]
500 return nil
501 }
502
503 func flatten(ctx *context) error {
504 n := 0
505 for _, v := range ctx.stack {
506 n += len(v)
507 }
508
509 top := make([]float64, 0, n)
510 for _, v := range ctx.stack {
511 top = append(top, v...)
512 }
513 ctx.stack = append(ctx.stack[:0], top)
514 return nil
515 }
516
517 func help(ctx *context) error {
518 if _, err := os.Stdout.WriteString(info[1:]); err != nil {
519 return io.EOF
520 }
521 return nil
522 }
523
524 func iota(ctx *context, n int) error {
525 if n < 1 {
526 return nil
527 }
528
529 top := make([]float64, n)
530 for i := range top {
531 top[i] = float64(i + 1)
532 }
533 ctx.stack = append(ctx.stack, top)
534
535 return nil
536 }
537
538 func pop(ctx *context) error {
539 if len(ctx.stack) > 0 {
540 ctx.stack = ctx.stack[:len(ctx.stack)-1]
541 }
542 return nil
543 }
544
545 func random(ctx *context, n int) error {
546 if n < 1 {
547 return nil
548 }
549
550 top := make([]float64, n)
551 for i := range top {
552 top[i] = ctx.random.Float64()
553 }
554 ctx.stack = append(ctx.stack, top)
555
556 return nil
557 }
558
559 func reset(ctx *context) error {
560 ctx.stack = ctx.stack[:0]
561 ctx.random = rand.New(rand.NewSource(time.Now().UnixNano()))
562 return nil
563 }
564
565 func shift(ctx *context) error {
566 for i := 1; i < len(ctx.stack); i++ {
567 ctx.stack[i-1] = ctx.stack[i]
568 }
569
570 if len(ctx.stack) > 0 {
571 ctx.stack = ctx.stack[:len(ctx.stack)-1]
572 }
573
574 return nil
575 }
576
577 func toplen(ctx *context) error {
578 if len(ctx.stack) < 1 {
579 return needAtLeast1
580 }
581
582 top := ctx.stack[len(ctx.stack)-1]
583 ctx.stack = append(ctx.stack, []float64{float64(len(top))})
584 return nil
585 }
586
587 func swap2(ctx *context) error {
588 if len(ctx.stack) < 2 {
589 return needAtLeast2
590 }
591
592 x := ctx.stack[len(ctx.stack)-2]
593 y := ctx.stack[len(ctx.stack)-1]
594 ctx.stack[len(ctx.stack)-2] = y
595 ctx.stack[len(ctx.stack)-1] = x
596 return nil
597 }
598
599 func top(ctx *context) error {
600 if len(ctx.stack) < 1 {
601 return needAtLeast1
602 }
603
604 ctx.stack = append(ctx.stack[:0], ctx.stack[len(ctx.stack)-1])
605 return nil
606 }
607
608 func toprint(ctx *context) error {
609 show(os.Stdout, ctx)
610 return nil
611 }
612
613 func unit(ctx *context, n int) error {
614 if n < 1 {
615 return nil
616 }
617
618 top := make([]float64, n)
619 for i := range top {
620 top[i] = 1
621 }
622 ctx.stack = append(ctx.stack, top)
623
624 return nil
625 }
626
627 func loop(ctx *context, f func(x float64) float64) error {
628 if len(ctx.stack) < 1 {
629 return needAtLeast1
630 }
631
632 top := ctx.stack[len(ctx.stack)-1]
633 for i, v := range top {
634 top[i] = f(v)
635 }
636 return nil
637 }
638
639 func parallel(ctx *context, f func(x, y float64) float64) error {
640 if len(ctx.stack) < 2 {
641 return needAtLeast2
642 }
643
644 l := len(ctx.stack)
645 xa := ctx.stack[l-2]
646 ya := ctx.stack[l-1]
647 ctx.stack = ctx.stack[:l-2]
648
649 if len(xa) == 0 || len(ya) == 0 {
650 return nil
651 }
652
653 var za []float64
654 if len(xa) < len(ya) {
655 za = ya
656 } else {
657 za = xa
658 }
659
660 for i := range za {
661 za[i] = f(xa[i%len(xa)], ya[i%len(ya)])
662 }
663 ctx.stack = append(ctx.stack, za)
664 return nil
665 }
666
667 func parallel2(ctx *context, f func(x, y float64) (float64, float64)) error {
668 if len(ctx.stack) < 2 {
669 return needAtLeast2
670 }
671
672 l := len(ctx.stack)
673 xa := ctx.stack[l-2]
674 ya := ctx.stack[l-1]
675 ctx.stack = ctx.stack[:l-2]
676
677 if len(xa) == 0 || len(ya) == 0 {
678 return nil
679 }
680
681 var za []float64
682 if len(xa) < len(ya) {
683 za = ya
684 } else {
685 za = xa
686 }
687
688 r1 := make([]float64, len(za))
689 r2 := make([]float64, len(za))
690 for i := range r1 {
691 x, y := f(xa[i%len(xa)], ya[i%len(ya)])
692 r1[i] = x
693 r2[i] = y
694 }
695 ctx.stack = append(ctx.stack, r1)
696 ctx.stack = append(ctx.stack, r2)
697 return nil
698 }
699
700 func repeat(ctx *context, f func(ctx *context, n int) error) error {
701 if len(ctx.stack) < 1 {
702 return needAtLeast1
703 }
704
705 counts := ctx.stack[len(ctx.stack)-1]
706 ctx.stack = ctx.stack[:len(ctx.stack)-1]
707
708 for _, n := range counts {
709 if err := f(ctx, int(n)); err != nil {
710 return err
711 }
712 }
713
714 return nil
715 }
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: ./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 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: ./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 MaxDotDecimals []int
93
94 LoopItems func(s string, max int, t *table, f itemFunc)
95
96 MaxColumns bool
97 }
98
99 type itemFunc func(i int, s string, t *table)
100
101 func run(paths []string, maxCols bool) error {
102 var res table
103 res.MaxColumns = maxCols
104
105 for _, p := range paths {
106 if err := handleFile(&res, p); err != nil {
107 return err
108 }
109 }
110
111 if len(paths) == 0 {
112 if err := handleReader(&res, os.Stdin); err != nil {
113 return err
114 }
115 }
116
117 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
118 defer bw.Flush()
119 realign(bw, res)
120 return nil
121 }
122
123 func handleFile(res *table, path string) error {
124 f, err := os.Open(path)
125 if err != nil {
126 // on windows, file-not-found error messages may mention `CreateFile`,
127 // even when trying to open files in read-only mode
128 return errors.New(`can't open file named ` + path)
129 }
130 defer f.Close()
131 return handleReader(res, f)
132 }
133
134 func handleReader(t *table, r io.Reader) error {
135 const gb = 1024 * 1024 * 1024
136 sc := bufio.NewScanner(r)
137 sc.Buffer(nil, 8*gb)
138
139 const maxInt = int(^uint(0) >> 1)
140 maxCols := maxInt
141
142 for i := 0; sc.Scan(); i++ {
143 s := sc.Text()
144 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
145 s = s[3:]
146 }
147
148 if len(s) == 0 {
149 if len(t.Rows) > 0 {
150 t.Rows = append(t.Rows, ``)
151 }
152 continue
153 }
154
155 t.Rows = append(t.Rows, s)
156
157 if t.Columns == 0 {
158 if t.LoopItems == nil {
159 if strings.IndexByte(s, '\t') >= 0 {
160 t.LoopItems = loopItemsTSV
161 } else {
162 t.LoopItems = loopItemsSSV
163 }
164 }
165
166 if !t.MaxColumns {
167 t.LoopItems(s, maxCols, t, updateColumnCount)
168 maxCols = t.Columns
169 }
170 }
171
172 t.LoopItems(s, maxCols, t, updateItem)
173 }
174
175 return sc.Err()
176 }
177
178 func updateColumnCount(i int, s string, t *table) {
179 t.Columns = i + 1
180 }
181
182 func updateItem(i int, s string, t *table) {
183 // ensure column-info-slices have enough room
184 if i >= len(t.MaxWidth) {
185 // update column-count if in max-columns mode
186 if t.MaxColumns {
187 t.Columns = i + 1
188 }
189 t.MaxWidth = append(t.MaxWidth, 0)
190 t.MaxDotDecimals = append(t.MaxDotDecimals, 0)
191 }
192
193 // keep track of widest rune-counts for each column
194 w := countWidth(s)
195 if t.MaxWidth[i] < w {
196 t.MaxWidth[i] = w
197 }
198
199 // update stats for numeric items
200 if isNumeric(s) {
201 dd := countDotDecimals(s)
202 if t.MaxDotDecimals[i] < dd {
203 t.MaxDotDecimals[i] = dd
204 }
205 }
206 }
207
208 // loopItemsSSV loops over a line's items, allocation-free style; when given
209 // empty strings, the callback func is never called
210 func loopItemsSSV(s string, max int, t *table, f itemFunc) {
211 s = trimTrailingSpaces(s)
212
213 for i := 0; true; i++ {
214 s = trimLeadingSpaces(s)
215 if len(s) == 0 {
216 return
217 }
218
219 if i+1 == max {
220 f(i, s, t)
221 return
222 }
223
224 j := strings.IndexByte(s, ' ')
225 if j < 0 {
226 f(i, s, t)
227 return
228 }
229
230 f(i, s[:j], t)
231 s = s[j+1:]
232 }
233 }
234
235 func trimLeadingSpaces(s string) string {
236 for len(s) > 0 && s[0] == ' ' {
237 s = s[1:]
238 }
239 return s
240 }
241
242 func trimTrailingSpaces(s string) string {
243 for len(s) > 0 && s[len(s)-1] == ' ' {
244 s = s[:len(s)-1]
245 }
246 return s
247 }
248
249 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
250 // when given empty strings, the callback func is never called
251 func loopItemsTSV(s string, max int, t *table, f itemFunc) {
252 if len(s) == 0 {
253 return
254 }
255
256 for i := 0; true; i++ {
257 if i+1 == max {
258 f(i, s, t)
259 return
260 }
261
262 j := strings.IndexByte(s, '\t')
263 if j < 0 {
264 f(i, s, t)
265 return
266 }
267
268 f(i, s[:j], t)
269 s = s[j+1:]
270 }
271 }
272
273 func skipLeadingEscapeSequences(s string) string {
274 for len(s) >= 2 {
275 if s[0] != '\x1b' {
276 return s
277 }
278
279 switch s[1] {
280 case '[':
281 s = skipSingleLeadingANSI(s[2:])
282
283 case ']':
284 if len(s) < 3 || s[2] != '8' {
285 return s
286 }
287 s = skipSingleLeadingOSC(s[3:])
288
289 default:
290 return s
291 }
292 }
293
294 return s
295 }
296
297 func skipSingleLeadingANSI(s string) string {
298 for len(s) > 0 {
299 upper := s[0] &^ 32
300 s = s[1:]
301 if 'A' <= upper && upper <= 'Z' {
302 break
303 }
304 }
305
306 return s
307 }
308
309 func skipSingleLeadingOSC(s string) string {
310 var prev byte
311
312 for len(s) > 0 {
313 b := s[0]
314 s = s[1:]
315 if prev == '\x1b' && b == '\\' {
316 break
317 }
318 prev = b
319 }
320
321 return s
322 }
323
324 // isNumeric checks if a string is valid/useable as a number
325 func isNumeric(s string) bool {
326 if len(s) == 0 {
327 return false
328 }
329
330 s = skipLeadingEscapeSequences(s)
331 if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
332 s = s[1:]
333 }
334
335 s = skipLeadingEscapeSequences(s)
336 if len(s) == 0 {
337 return false
338 }
339 if s[0] == '.' {
340 return isDigits(s[1:])
341 }
342
343 digits := 0
344
345 for {
346 s = skipLeadingEscapeSequences(s)
347 if len(s) == 0 {
348 break
349 }
350
351 if s[0] == '.' {
352 return isDigits(s[1:])
353 }
354
355 if !('0' <= s[0] && s[0] <= '9') {
356 return false
357 }
358
359 digits++
360 s = s[1:]
361 }
362
363 s = skipLeadingEscapeSequences(s)
364 return len(s) == 0 && digits > 0
365 }
366
367 func isDigits(s string) bool {
368 if len(s) == 0 {
369 return false
370 }
371
372 digits := 0
373
374 for {
375 s = skipLeadingEscapeSequences(s)
376 if len(s) == 0 {
377 break
378 }
379
380 if '0' <= s[0] && s[0] <= '9' {
381 s = s[1:]
382 digits++
383 } else {
384 return false
385 }
386 }
387
388 s = skipLeadingEscapeSequences(s)
389 return len(s) == 0 && digits > 0
390 }
391
392 // countDecimals counts decimal digits from the string given, assuming it
393 // represents a valid/useable float64, when parsed
394 func countDecimals(s string) int {
395 dot := strings.IndexByte(s, '.')
396 if dot < 0 {
397 return 0
398 }
399
400 decs := 0
401 s = s[dot+1:]
402
403 for len(s) > 0 {
404 s = skipLeadingEscapeSequences(s)
405 if len(s) == 0 {
406 break
407 }
408 if '0' <= s[0] && s[0] <= '9' {
409 decs++
410 }
411 s = s[1:]
412 }
413
414 return decs
415 }
416
417 // countDotDecimals is like func countDecimals, but this one also includes
418 // the dot, when any decimals are present, else the count stays at 0
419 func countDotDecimals(s string) int {
420 decs := countDecimals(s)
421 if decs > 0 {
422 return decs + 1
423 }
424 return decs
425 }
426
427 func countWidth(s string) int {
428 width := 0
429
430 for len(s) > 0 {
431 i, j := indexEscapeSequence(s)
432 if i < 0 {
433 break
434 }
435 if j < 0 {
436 j = len(s)
437 }
438
439 width += utf8.RuneCountInString(s[:i])
440 s = s[j:]
441 }
442
443 // count trailing/all runes in strings which don't end with ANSI-sequences
444 width += utf8.RuneCountInString(s)
445 return width
446 }
447
448 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
449 // the multi-byte sequences starting with ESC[; the result is a pair of slice
450 // indices which can be independently negative when either the start/end of
451 // a sequence isn't found; given their fairly-common use, even the hyperlink
452 // ESC]8 sequences are supported
453 func indexEscapeSequence(s string) (int, int) {
454 var prev byte
455
456 for i := range s {
457 b := s[i]
458
459 if prev == '\x1b' && b == '[' {
460 j := indexLetter(s[i+1:])
461 if j < 0 {
462 return i, -1
463 }
464 return i - 1, i + 1 + j + 1
465 }
466
467 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
468 j := indexPair(s[i+1:], '\x1b', '\\')
469 if j < 0 {
470 return i, -1
471 }
472 return i - 1, i + 1 + j + 2
473 }
474
475 prev = b
476 }
477
478 return -1, -1
479 }
480
481 func indexLetter(s string) int {
482 for i, b := range s {
483 upper := b &^ 32
484 if 'A' <= upper && upper <= 'Z' {
485 return i
486 }
487 }
488
489 return -1
490 }
491
492 func indexPair(s string, x byte, y byte) int {
493 var prev byte
494
495 for i := range s {
496 b := s[i]
497 if prev == x && b == y && i > 0 {
498 return i
499 }
500 prev = b
501 }
502
503 return -1
504 }
505
506 func realign(w *bufio.Writer, t table) {
507 due := 0
508 showItem := func(i int, s string, t *table) {
509 if i > 0 {
510 due += 2
511 }
512
513 if isNumeric(s) {
514 dd := countDotDecimals(s)
515 rpad := t.MaxDotDecimals[i] - dd
516 width := countWidth(s)
517 lpad := t.MaxWidth[i] - (width + rpad) + due
518 writeSpaces(w, lpad)
519 w.WriteString(s)
520 due = rpad
521 return
522 }
523
524 writeSpaces(w, due)
525 w.WriteString(s)
526 due = t.MaxWidth[i] - countWidth(s)
527 }
528
529 for _, line := range t.Rows {
530 due = 0
531 if len(line) > 0 {
532 t.LoopItems(line, t.Columns, &t, showItem)
533 }
534 if w.WriteByte('\n') != nil {
535 break
536 }
537 }
538 }
539
540 // writeSpaces does what it says, minimizing calls to write-like funcs
541 func writeSpaces(w *bufio.Writer, n int) {
542 const spaces = ` `
543 if n < 1 {
544 return
545 }
546
547 for n >= len(spaces) {
548 w.WriteString(spaces)
549 n -= len(spaces)
550 }
551 w.WriteString(spaces[:n])
552 }
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 }
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: ./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 // separator is the string put between adjacent columns
36 separator = ` █ `
37
38 // maxAutoWidth is the output max-width, chosen to fit very old monitors
39 maxAutoWidth = 79
40 )
41
42 // chooseNumColumns implements heuristics to auto-pick the number of columns
43 // to show: this func is used when the app is using data from standard-input
44 func chooseNumColumns(lines []string) int {
45 if len(lines) == 0 {
46 return 1
47 }
48
49 // sepw is the separator width
50 sepw := width(separator)
51
52 // see if lines can even fit a single column
53 if !columnsCanFit(1, lines, sepw) {
54 return 1
55 }
56
57 // starting from the max possible columns which may fit, keep trying
58 // with 1 fewer column, until the columns fit
59 for ncols := int(maxAutoWidth / sepw); ncols > 1; ncols-- {
60 if columnsCanFit(ncols, lines, sepw) {
61 // success: found the most columns which fit
62 return ncols
63 }
64 }
65
66 // avoid multiple columns if some lines are too wide
67 return 1
68 }
69
70 // columnsCanFit checks whether the number of columns given would fit the
71 // display max-width constant
72 func columnsCanFit(ncols int, lines []string, gap int) bool {
73 if ncols < 1 {
74 // avoid surprises when called with non-sense column counts
75 return true
76 }
77
78 // stack-allocate the backing-array behind slice maxw
79 var buf [maxAutoWidth / 2]int
80 maxw := buf[:0]
81
82 // find the column max-height, to chunk lines into columns
83 h := int(math.Ceil(float64(len(lines)) / float64(ncols)))
84
85 // find column max-width by looping over chunks of lines
86 for len(lines) >= h {
87 w := findMaxWidth(lines[:h])
88 maxw = append(maxw, w)
89 lines = lines[h:]
90 }
91
92 // don't forget the last column
93 if len(lines) > 0 {
94 w := findMaxWidth(lines)
95 maxw = append(maxw, w)
96 }
97
98 // remember to add the gaps/separators between columns, along with
99 // all the individual column max-widths
100 w := (ncols - 1) * gap
101 for _, n := range maxw {
102 w += n
103 }
104
105 // do the columns fit?
106 return w <= maxAutoWidth
107 }
108
109 // findMaxWidth finds the max width in the slice given, ignoring ANSI codes
110 func findMaxWidth(lines []string) int {
111 maxw := 0
112 for _, s := range lines {
113 w := width(s)
114 if w > maxw {
115 maxw = w
116 }
117 }
118 return maxw
119 }
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
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 if len(os.Args) > 1 {
44 switch os.Args[1] {
45 case `-h`, `--h`, `-help`, `--help`:
46 os.Stderr.WriteString(usage)
47 os.Exit(0)
48 }
49 }
50
51 err := run(os.Args[1:])
52 if err != nil && err != errDoneWriting {
53 os.Stderr.WriteString("\x1b[31m")
54 os.Stderr.WriteString(err.Error())
55 os.Stderr.WriteString("\x1b[0m\n")
56 os.Exit(1)
57 }
58 }
59
60 func run(args []string) error {
61 w := bufio.NewWriterSize(os.Stdout, 16*1024)
62 defer w.Flush()
63
64 if len(args) == 0 {
65 // return errors.New(`expected a leading number for the column count`)
66 args = []string{`0`}
67 }
68
69 ncols := 0
70 f, err := strconv.ParseFloat(args[0], 64)
71 if err != nil || math.IsInf(f, 0) || math.IsNaN(f) {
72 return errors.New(`first argument isn't a valid number of columns`)
73 }
74 ncols = int(f)
75
76 paths := args[1:]
77 if len(paths) == 0 {
78 paths = []string{`-`}
79 }
80
81 lines, err := gatherLines(paths)
82 if err != nil {
83 return err
84 }
85
86 // choose a default number of columns, if not given an explicit one
87 if ncols < 1 {
88 ncols = chooseNumColumns(lines)
89 }
90
91 return handleLines(w, lines, ncols)
92 }
93
94 // gatherLines slurps all text-lines from all the filepaths given, expanding
95 // any tabs found; a single dash as a pathname means stdin
96 func gatherLines(paths []string) ([]string, error) {
97 var lines []string
98 var sb strings.Builder
99
100 dashes := 0
101 for _, s := range paths {
102 if s == `-` {
103 dashes++
104 }
105 }
106 if dashes > 1 {
107 return lines, errors.New(`can't use "-" (stdin) more than once`)
108 }
109
110 for _, s := range paths {
111 err := handleNamedInput(s, func(r io.Reader) error {
112 sc := newScanner(r)
113
114 for sc.Scan() {
115 s := sc.Text()
116 if strings.Contains(s, "\t") {
117 sb.Reset()
118 expand(s, tabstop, &sb)
119 s = strings.Clone(sb.String())
120 }
121 lines = append(lines, s)
122 }
123 return sc.Err()
124 })
125
126 if err != nil {
127 return lines, err
128 }
129 }
130
131 return lines, nil
132 }
133
134 // handleNamedInput makes opening/closing files more convenient by using
135 // callbacks to handle processing; this func also handles recognizing `-`
136 // as meaning stdin
137 func handleNamedInput(path string, fn func(r io.Reader) error) error {
138 if path == `-` {
139 return fn(os.Stdin)
140 }
141
142 f, err := os.Open(path)
143 if err != nil {
144 return err
145 }
146 defer f.Close()
147 return fn(f)
148 }
149
150 // handleLines handles the use-case of showing/rearranging lines from a
151 // single input source (presumably standard input) into several columns
152 func handleLines(w *bufio.Writer, lines []string, ncols int) error {
153 if ncols < 1 {
154 return nil
155 }
156
157 if ncols == 1 {
158 for _, s := range lines {
159 w.WriteString(s)
160 err := w.WriteByte('\n')
161 if err != nil {
162 // assume error probably results from a closed stdout
163 // pipe, so quit the app right away without complaining
164 return err
165 }
166 }
167 return nil
168 }
169
170 // nothing to show, so don't even bother
171 if len(lines) == 0 {
172 return nil
173 }
174
175 cols, height := splitLines(lines, ncols)
176 widths := make([]int, 0, len(cols))
177 for _, c := range cols {
178 // find the max width of all lines of the current column
179 maxw := 0
180 for _, v := range c {
181 w := width(v)
182 if w > maxw {
183 maxw = w
184 }
185 }
186
187 widths = append(widths, maxw)
188 }
189
190 // endSep is right-trimmed to avoid unneeded trailing spaces on output
191 // lines whose last column is an empty/missing input line
192 endSep := strings.TrimRight(separator, ` `)
193
194 // show columns side by side
195 for r := 0; r < height; r++ {
196 for c := 0; c < len(cols); c++ {
197 badr := r >= len(cols[c])
198
199 // clearly separate columns visually
200 if c > 0 {
201 if c == len(cols)-1 && (badr || cols[c][r] == ``) {
202 // avoid unneeded trailing spaces
203 w.WriteString(endSep)
204 } else {
205 w.WriteString(separator)
206 }
207 }
208
209 if badr {
210 // exceeding items for this (last) column
211 continue
212 }
213
214 // pad all columns, except the last
215 width := 0
216 if c < len(cols)-1 {
217 width = widths[c]
218 }
219
220 // emit maybe-padded column
221 writeItem(w, cols[c][r], width)
222 }
223
224 // end the line
225 err := w.WriteByte('\n')
226 if err != nil {
227 // probably a pipe was closed
228 return nil
229 }
230 }
231
232 return nil
233 }
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: ./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 "./now"
39 "./zcat"
40 )
41
42 func args() {
43 args := os.Args[1:]
44 if len(args) == 0 {
45 return
46 }
47
48 w := bufio.NewWriterSize(os.Stdout, 32*1024)
49 defer w.Flush()
50
51 for _, s := range args {
52 w.WriteString(s)
53 if err := w.WriteByte('\n'); err != nil {
54 break
55 }
56 }
57 }
58
59 func clear() {
60 if len(os.Args) > 1 {
61 switch os.Args[1] {
62 case `-h`, `--h`, `-help`, `--help`, `help`:
63 os.Stdout.WriteString("Clear the screen\n")
64 return
65
66 case `-x`:
67 // clear all but the scrollback buffer
68 os.Stdout.WriteString("\x1b[H\x1b[2J")
69 return
70 }
71 }
72
73 os.Stdout.WriteString("\x1b[H\x1b[2J\x1b[3J")
74 }
75
76 func cls() {
77 if len(os.Args) > 1 {
78 switch os.Args[1] {
79 case `-h`, `--h`, `-help`, `--help`, `help`:
80 os.Stdout.WriteString("Clear the Screen\n")
81 return
82 }
83 }
84
85 os.Stdout.WriteString("\x1b[H\x1b[2J\x1b[3J")
86 }
87
88 func decompress() {
89 zcat.Main()
90 }
91
92 const divInfo = `
93 div [options...] [x...] [y]
94
95 DIVide 2 numbers in 3 different ways, showing each result in its own line.
96
97 When given 2 numbers, the results are x / y, y / x, and 1 - (x / y), where
98 x is the smaller number, and y is the larger number.
99
100 When given just 1 number, the second number is 1 by default, showing you the
101 inverse of the number given explicitly, among the other results.
102
103 The options are, available both in single and double-dash versions
104
105 -h, -help show this help message
106 `
107
108 func div() {
109 args := os.Args[1:]
110
111 if len(args) > 0 {
112 switch args[0] {
113 case `-h`, `--h`, `-help`, `--help`:
114 os.Stdout.WriteString(divInfo[1:])
115 return
116 }
117 }
118
119 if len(args) > 0 && args[0] == `--` {
120 args = args[1:]
121 }
122
123 var nums []float64
124
125 for len(args) > 0 {
126 f, err := strconv.ParseFloat(args[0], 64)
127 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
128 nums = append(nums, f)
129 args = args[1:]
130 continue
131 }
132
133 break
134 }
135
136 switch len(nums) {
137 case 0:
138 os.Stderr.WriteString(divInfo[1:])
139 os.Exit(1)
140 return
141
142 case 1:
143 nums = append(nums, nums[0])
144 nums[0] = 1
145 }
146
147 if len(nums)%2 != 0 {
148 os.Stderr.WriteString(divInfo[1:])
149 os.Exit(1)
150 return
151 }
152
153 for len(nums) >= 2 {
154 x, y := nums[0], nums[1]
155 nums = nums[2:]
156
157 if x > y {
158 x, y = y, x
159 }
160 comp := 1 - (x / y)
161
162 const prec = 6
163 os.Stdout.WriteString(strconv.FormatFloat(x/y, 'f', prec, 64) + "\n")
164 os.Stdout.WriteString(strconv.FormatFloat(y/x, 'f', prec, 64) + "\n")
165 os.Stdout.WriteString(strconv.FormatFloat(comp, 'f', prec, 64) + "\n")
166 }
167 }
168
169 func echo() {
170 args := os.Args[1:]
171 if len(args) == 0 {
172 os.Stdout.WriteString("\n")
173 return
174 }
175
176 w := bufio.NewWriterSize(os.Stdout, 32*1024)
177 defer w.Flush()
178
179 for i, s := range args {
180 if i > 0 {
181 if err := w.WriteByte(' '); err != nil {
182 return
183 }
184 }
185 w.WriteString(s)
186 }
187
188 w.WriteByte('\n')
189 }
190
191 func echobar() {
192 const (
193 half = ` `
194 spaces = half + half
195 )
196
197 args := os.Args[1:]
198 if len(args) == 0 {
199 os.Stdout.WriteString("\x1b[7m" + spaces + "\x1b[0m\n")
200 return
201 }
202
203 w := bufio.NewWriterSize(os.Stdout, 32*1024)
204 defer w.Flush()
205
206 w.WriteString("\x1b[7m")
207
208 left := len(spaces)
209
210 for i, s := range args {
211 if i > 0 {
212 if err := w.WriteByte(' '); err != nil {
213 return
214 }
215 left--
216 }
217 w.WriteString(s)
218 left -= utf8.RuneCountInString(s)
219 }
220
221 if 0 < left && left < len(spaces) {
222 w.WriteString(spaces[:left])
223 }
224 w.WriteString("\x1b[0m\n")
225 }
226
227 const failInfo = `
228 fail [options...] [exit code...]
229
230 Fail with the exit code given. If no exit code is given, fail with code 1 by
231 default. If given code 0, this tool paradoxically succeeds, as code 0 means
232 success.
233
234 The options are, available both in single and double-dash versions
235
236 -h, -help show this help message
237 `
238
239 func fail() {
240 args := os.Args[1:]
241
242 if len(args) > 0 {
243 switch args[0] {
244 case `-h`, `--h`, `-help`, `--help`:
245 os.Stdout.WriteString(failInfo[1:])
246 return
247 }
248 }
249
250 if len(args) > 0 && args[0] == `--` {
251 args = args[1:]
252 }
253
254 code := 1
255 if len(args) > 0 {
256 if n, err := strconv.ParseInt(args[0], 10, 64); err == nil {
257 code = int(n)
258 }
259 }
260
261 os.Exit(code)
262 return
263 }
264
265 const falseInfo = `
266 false [options...]
267
268 Quit right away, using exit code 1.
269
270 All (optional) leading options start with either single or double-dash:
271
272 -h, -help show this help message
273 `
274
275 func falseMain() {
276 if len(os.Args) > 1 {
277 switch os.Args[1] {
278 case `-h`, `--h`, `-help`, `--help`, `help`:
279 os.Stdout.WriteString(falseInfo[1:])
280 return
281 }
282 }
283
284 os.Exit(1)
285 return
286 }
287
288 const ignoreInfo = `
289 ignore [options...] [command...] [command args...]
290
291 Ignore the command given, acting as a stdio-passthru instead. This allows
292 quick editing of pipes to temporarily exclude a pipe-step, while still
293 having the disabled step show in the pipe-chain as a memo of sorts.
294
295 The options are, available both in single and double-dash versions
296
297 -h, -help show this help message
298 `
299
300 func ignore() {
301 if args := os.Args[1:]; len(args) > 0 {
302 switch args[0] {
303 case `-h`, `--h`, `-help`, `--help`, `help`:
304 os.Stdout.WriteString(ignoreInfo[1:])
305 return
306 }
307 }
308
309 var buf [32 * 1024]byte
310 for {
311 got, err := os.Stdin.Read(buf[:])
312 if got > 0 {
313 if _, err := os.Stdout.Write(buf[:got]); err != nil {
314 break
315 }
316 }
317 if err != nil {
318 break
319 }
320 }
321 }
322
323 func hecho() {
324 args := os.Args[1:]
325 if len(args) == 0 {
326 os.Stdout.WriteString("\n")
327 return
328 }
329
330 w := bufio.NewWriterSize(os.Stdout, 32*1024)
331 defer w.Flush()
332
333 w.WriteString("\x1b[7m")
334
335 for i, s := range args {
336 if i > 0 {
337 if err := w.WriteByte(' '); err != nil {
338 return
339 }
340 }
341 w.WriteString(s)
342 }
343
344 w.WriteString("\x1b[0m\n")
345 }
346
347 const mopInfo = `
348 mop [options...] [x...] [y]
349
350 Multiple OPerations runs multiple arithmetic calculations on 2 numbers,
351 showing each result in its own line.
352
353 The options are, available both in single and double-dash versions
354
355 -h, -help show this help message
356 `
357
358 func mop() {
359 args := os.Args[1:]
360
361 if len(args) > 0 {
362 switch args[0] {
363 case `-h`, `--h`, `-help`, `--help`:
364 os.Stdout.WriteString(mopInfo[1:])
365 return
366 }
367 }
368
369 if len(args) > 0 && args[0] == `--` {
370 args = args[1:]
371 }
372
373 var nums []float64
374
375 for len(args) > 0 {
376 f, err := strconv.ParseFloat(args[0], 64)
377 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
378 nums = append(nums, f)
379 args = args[1:]
380 continue
381 }
382
383 break
384 }
385
386 if len(nums) == 0 || len(nums)%2 != 0 {
387 os.Stderr.WriteString(mopInfo[1:])
388 os.Exit(1)
389 return
390 }
391
392 for len(nums) >= 2 {
393 x, y := nums[0], nums[1]
394 nums = nums[2:]
395
396 if x > y {
397 x, y = y, x
398 }
399
400 fmt.Printf("%.6f + %.6f ~ %.6f\n", x, y, x+y)
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", y, x, y/x)
405 fmt.Printf("%.6f %% %.6f ~ %.6f\n", x, y, math.Mod(x, y))
406 fmt.Printf("%.6f %% %.6f ~ %.6f\n", y, x, math.Mod(y, x))
407 fmt.Printf("%.6f ** %.6f ~ %.6f\n", x, y, math.Pow(x, y))
408 fmt.Printf("%.6f ** %.6f ~ %.6f\n", y, x, math.Pow(y, x))
409 }
410 }
411
412 const nilInfo = `
413 nil [options...]
414
415 Emit nothing, also discarding all stdin bytes, if piped.
416
417 All (optional) leading options start with either single or double-dash:
418
419 -h, -help show this help message
420 `
421
422 func null() {
423 if args := os.Args[1:]; len(args) > 0 {
424 switch args[0] {
425 case `-h`, `--h`, `-help`, `--help`, `help`:
426 os.Stdout.WriteString(nilInfo[1:])
427 return
428 }
429 }
430
431 info, err := os.Stdin.Stat()
432 if err != nil {
433 os.Stderr.WriteString(err.Error())
434 os.Stderr.WriteString("\n")
435 os.Exit(1)
436 return
437 }
438
439 piped := int(info.Mode()&fs.ModeNamedPipe) != 0
440 if !piped {
441 return
442 }
443
444 var buf [32 * 1024]byte
445 for {
446 _, err := os.Stdin.Read(buf[:])
447 if err != nil {
448 break
449 }
450 }
451 }
452
453 func nothing() {
454 // deliberately do nothing
455 }
456
457 const prechoInfo = `
458 precho [options...] [words...]
459
460 PRecede ECHO emits a line with the words given as command-line arguments,
461 then copies all bytes from the standard input into the standard output.
462
463 The options are, available both in single and double-dash versions
464
465 -h, -help show this help message
466 `
467
468 func precho() {
469 args := os.Args[1:]
470
471 if len(args) > 0 {
472 switch args[0] {
473 case `-h`, `--h`, `-help`, `--help`, `help`:
474 os.Stdout.WriteString(prechoInfo[1:])
475 return
476 }
477 }
478
479 if len(args) > 0 && args[0] == `--` {
480 args = args[1:]
481 }
482
483 if len(args) == 0 {
484 return
485 }
486
487 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
488
489 for i, s := range args {
490 if i > 0 {
491 if err := bw.WriteByte(' '); err != nil {
492 bw.Flush()
493 return
494 }
495 }
496 bw.WriteString(s)
497 }
498
499 if bw.WriteByte('\n') != nil {
500 bw.Flush()
501 return
502 }
503 if bw.Flush() != nil {
504 return
505 }
506 bw = nil
507
508 var buf [32 * 1024]byte
509 for {
510 got, err := os.Stdin.Read(buf[:])
511 if got > 0 {
512 if _, err := os.Stdout.Write(buf[:got]); err != nil {
513 break
514 }
515 }
516 if err != nil {
517 break
518 }
519 }
520 }
521
522 const rulerInfo = `
523 ruler [width...]
524
525 Emit a line with a ruler-like pattern, which helps check the width of output
526 lines right above it. If given a valid number, it's used as the ruler width,
527 or 80 is used as the default width.
528
529 The options are, available both in single and double-dash versions
530
531 -h, -help show this help message
532 `
533
534 func ruler() {
535 if args := os.Args[1:]; len(args) > 0 {
536 switch args[0] {
537 case `-h`, `--h`, `-help`, `--help`, `help`:
538 os.Stdout.WriteString(rulerInfo[1:])
539 return
540 }
541 }
542
543 width := 80
544 if len(os.Args) > 1 {
545 if n, err := strconv.Atoi(os.Args[1]); err == nil {
546 width = n
547 }
548 }
549
550 bw := bufio.NewWriter(os.Stdout)
551 defer bw.Flush()
552
553 // avoid a single line-feed byte for empty rulers
554 if width < 1 {
555 return
556 }
557
558 for width >= 10 {
559 bw.WriteString(`····╵····│`)
560 width -= 10
561 }
562
563 if width >= 5 {
564 bw.WriteString(`····╵`)
565 width -= 5
566 }
567
568 for width > 0 {
569 bw.WriteRune('·')
570 width--
571 }
572
573 bw.WriteByte('\n')
574 }
575
576 const sleepInfo = `
577 sleep [options...] [duration/seconds...]
578
579 Wait for the amount of time given.
580
581 All (optional) leading options start with either single or double-dash:
582
583 -h, -help show this help message
584 `
585
586 func sleep() {
587 args := os.Args[1:]
588
589 if len(args) > 0 {
590 switch args[0] {
591 case `-h`, `--h`, `-help`, `--help`, `help`:
592 os.Stdout.WriteString(sleepInfo[1:])
593 return
594 }
595 }
596
597 if len(args) > 0 && args[0] == `--` {
598 args = args[1:]
599 }
600
601 if len(args) == 0 {
602 os.Stderr.WriteString(sleepInfo[1:])
603 // os.Stderr.WriteString("forgot the duration to wait for\n")
604 os.Exit(1)
605 return
606 }
607
608 var delay time.Duration
609
610 for _, s := range args {
611 f, err := strconv.ParseFloat(s, 64)
612 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
613 if f < 0 {
614 f = 0
615 }
616 delay += time.Duration(f * float64(time.Second))
617 continue
618 }
619
620 if d, err := time.ParseDuration(s); err == nil {
621 delay += d
622 continue
623 }
624
625 os.Stderr.WriteString("invalid duration " + s)
626 os.Stderr.WriteString("\n")
627 os.Exit(1)
628 return
629 }
630
631 time.Sleep(delay)
632 }
633
634 const timezonesInfo = `
635 timezones [options...] [places...]
636
637 Lookup full timezone names from the city/place names given.
638
639 All (optional) leading options start with either single or double-dash:
640
641 -h, -help show this help message
642 `
643
644 func timezones() {
645 args := os.Args[1:]
646
647 if len(args) > 0 {
648 switch args[0] {
649 case `-h`, `--h`, `-help`, `--help`, `help`:
650 os.Stdout.WriteString(timezonesInfo[1:])
651 return
652 }
653 }
654
655 if len(args) > 0 && args[0] == `--` {
656 args = args[1:]
657 }
658
659 if len(args) == 0 {
660 os.Stderr.WriteString(timezonesInfo[1:])
661 return
662 }
663
664 nerr := 0
665 w := bufio.NewWriterSize(os.Stdout, 32*1024)
666 defer w.Flush()
667
668 for _, place := range args {
669 name, ok := now.LookupName(place)
670
671 if !ok {
672 w.Flush()
673 fmt.Fprintf(os.Stderr, "timezone for %q not found\n", place)
674 nerr++
675 continue
676 }
677
678 w.WriteString(name)
679 w.WriteByte('\n')
680 }
681
682 if nerr > 0 {
683 w.Flush()
684 os.Exit(1)
685 return
686 }
687 }
688
689 const toolsInfo = `
690 tools [options...]
691
692 Show all tools (officially) available.
693
694 All (optional) leading options start with either single or double-dash:
695
696 -a, -aliases, -all also show all aliases available, after the tools
697 -h, -help show this help message
698 `
699
700 func tools() {
701 showAliases := false
702 if len(os.Args) > 1 {
703 switch os.Args[1] {
704 case `-a`, `--a`, `-aliases`, `--aliases`, `-all`, `--all`:
705 showAliases = true
706
707 case `-h`, `--h`, `-help`, `--help`, `help`:
708 os.Stdout.WriteString(toolsInfo[1:])
709 return
710 }
711 }
712
713 n := len(mains)
714 if n < len(aliases) {
715 n = len(aliases)
716 }
717
718 names := make([]string, 0, n)
719 for k := range mains {
720 names = append(names, k)
721 }
722
723 sort.Strings(names)
724
725 for _, s := range names {
726 fmt.Fprintln(os.Stdout, s)
727 }
728
729 if !showAliases {
730 return
731 }
732
733 names = names[:0]
734 for k := range aliases {
735 names = append(names, k)
736 }
737
738 sort.Strings(names)
739
740 for _, s := range names {
741 fmt.Fprintf(os.Stdout, "%s -> %s\n", s, aliases[s])
742 }
743 }
744
745 const trueInfo = `
746 true [options...]
747
748 Quit right away successfully, using exit code 0.
749
750 All (optional) leading options start with either single or double-dash:
751
752 -h, -help show this help message
753 `
754
755 func trueMain() {
756 if len(os.Args) > 1 {
757 switch os.Args[1] {
758 case `-h`, `--h`, `-help`, `--help`, `help`:
759 os.Stdout.WriteString(trueInfo[1:])
760 return
761 }
762 }
763 }
764
765 const yesInfo = `
766 yes [options...] [message...]
767
768 Keep emitting the line with the message given, or "yes" by default.
769
770 All (optional) leading options start with either single or double-dash:
771
772 -h, -help show this help message
773 `
774
775 func yes() {
776 args := os.Args[1:]
777
778 if len(args) > 0 {
779 switch args[0] {
780 case `-h`, `--h`, `-help`, `--help`, `help`:
781 os.Stdout.WriteString(yesInfo[1:])
782 return
783 }
784 }
785
786 if len(args) > 0 && args[0] == `--` {
787 args = args[1:]
788 }
789
790 msg := "yes\n"
791 if len(args) > 0 {
792 msg = args[0] + "\n"
793 }
794
795 for {
796 if _, err := os.Stdout.WriteString(msg); err != nil {
797 break
798 }
799 }
800 }
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 len(args) == 0 {
64 // os.Stderr.WriteString(info[1:])
65 // os.Exit(1)
66 // return
67 // }
68
69 if err := run(os.Stdout, args); err != nil {
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 from := ``
82 low := ``
83 var values []float64
84
85 dump := func(unit string) bool {
86 if s, ok := aliases[unit]; ok {
87 unit = s
88 }
89
90 c, ok := converters[unit]
91 if !ok {
92 return false
93 }
94
95 for _, v := range values {
96 res := c.Mul*v + c.Add
97 const fs = "%.4f %-4s = %.4f %s\n"
98 fmt.Fprintf(bw, fs, v, unit, res, c.To)
99 }
100 return true
101 }
102
103 for _, s := range args {
104 f, err := strconv.ParseFloat(s, 64)
105 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
106 values = append(values, f)
107 continue
108 }
109
110 if len(values) == 0 {
111 values = append(values, 1)
112 }
113 from = s
114 low = strings.ToLower(from)
115
116 if dump(low) {
117 values = values[:0]
118 from = ``
119 low = ``
120 continue
121 }
122
123 return fmt.Errorf("unit %q not supported\n", low)
124 }
125
126 if from == `` {
127 // return errors.New(`no source units given`)
128
129 if len(values) == 0 {
130 values = []float64{1}
131 }
132
133 units := make([]string, 0, len(converters))
134 for k := range converters {
135 units = append(units, k)
136 }
137 sort.Strings(units)
138 for _, from := range units {
139 dump(from)
140 }
141 return nil
142 }
143
144 if !dump(from) {
145 return fmt.Errorf("unit %q not supported\n", low)
146 }
147 return nil
148 }
149
150 var aliases = map[string]string{
151 `acre`: `ac`,
152 `acres`: `ac`,
153 `days`: `day`,
154 `foot`: `ft`,
155 `feet`: `ft`,
156 `feet2`: `ft²`,
157 `feet3`: `ft³`,
158 `foot2`: `ft²`,
159 `foot3`: `ft³`,
160 `ft2`: `ft²`,
161 `ft3`: `ft³`,
162 `gallon`: `gal`,
163 `gallons`: `gal`,
164 `gals`: `gal`,
165 `inch`: `in`,
166 `inches`: `in`,
167 `mile`: `mi`,
168 `miles`: `mi`,
169 `mile²`: `mi²`,
170 `miles²`: `mi²`,
171 `minute`: `min`,
172 `minutes`: `min`,
173 `nmile`: `nmi`,
174 `nmiles`: `nmi`,
175 `ounce`: `oz`,
176 `ounces`: `oz`,
177 `ozs`: `oz`,
178 `weeks`: `week`,
179 `wk`: `week`,
180 `wks`: `week`,
181 `yard`: `yd`,
182 `yards`: `yd`,
183 `yard2`: `yd²`,
184 `yard²`: `yd²`,
185 `yards2`: `yd²`,
186 `yards²`: `yd²`,
187 `yds`: `yd`,
188 `yds2`: `yd²`,
189 `yds²`: `yd²`,
190 }
191
192 type converter struct {
193 To string
194 Mul float64
195 Add float64
196 }
197
198 var converters = map[string]converter{
199 `ac`: converter{`m²`, 4046.8564224, 0},
200 `day`: converter{`s`, 86400, 0},
201 `ft`: converter{`m`, 0.3048, 0},
202 `ft²`: converter{`m²`, 0.09290304, 0},
203 `ft³`: converter{`m³`, 0.028316846592, 0},
204 `gal`: converter{`L`, 3.785411784, 0},
205 `in`: converter{`cm`, 2.54, 0},
206 `mi`: converter{`km`, 1.609344, 0},
207 `mi²`: converter{`km²`, 2.5899881103360, 0},
208 `min`: converter{`s`, 60, 0},
209 `mpg`: converter{`kpl`, 0.425143707, 0},
210 `mph`: converter{`kph`, 1.609344, 0},
211 `nmi`: converter{`km`, 1.852, 0},
212 `oz`: converter{`g`, 28.349523125, 0},
213 `week`: converter{`s`, 604800, 0},
214 `yd`: converter{`m`, 0.9144, 0},
215 `yd²`: converter{`m²`, 0.83612736, 0},
216 }
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 }