File: j2.go 1 /* 2 The MIT License (MIT) 3 4 Copyright © 2024 pacman64 5 6 Permission is hereby granted, free of charge, to any person obtaining a copy of 7 this software and associated documentation files (the “Software”), to deal 8 in the Software without restriction, including without limitation the rights to 9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 of the Software, and to permit persons to whom the Software is furnished to do 11 so, subject to the following conditions: 12 13 The above copyright notice and this permission notice shall be included in all 14 copies or substantial portions of the Software. 15 16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 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 Single-file source-code for j2: this version has no http(s) support. Even 27 the unit-tests from the original j2 are omitted. 28 29 To compile a smaller-sized command-line app, you can use the `go` command as 30 follows: 31 32 go build -ldflags "-s -w" -trimpath j2.go 33 */ 34 35 package main 36 37 import ( 38 "bufio" 39 "encoding/json" 40 "errors" 41 "io" 42 "os" 43 ) 44 45 const info = ` 46 j2 [filepath...] 47 48 Json-2 indents valid JSON input into multi-line JSON which uses 2 spaces for 49 each indentation level. 50 ` 51 52 // errNoMoreOutput is a generic dummy output-error, which is meant to be 53 // ultimately ignored, being just an excuse to quit the app immediately 54 // and successfully 55 var errNoMoreOutput = errors.New(`no more output`) 56 57 func main() { 58 if len(os.Args) > 1 { 59 switch os.Args[1] { 60 case `-h`, `--h`, `-help`, `--help`: 61 os.Stderr.WriteString(info[1:]) 62 return 63 } 64 } 65 66 if len(os.Args) > 2 { 67 const msg = "\x1b[31mmultiple inputs not allowed\x1b[0m\n" 68 os.Stderr.WriteString(msg) 69 os.Exit(1) 70 } 71 72 // figure out whether input should come from a named file or from stdin 73 path := `-` 74 if len(os.Args) > 1 { 75 path = os.Args[1] 76 } 77 78 err := handleInput(os.Stdout, path) 79 if err != nil && err != io.EOF && err != errNoMoreOutput { 80 os.Stderr.WriteString("\x1b[31m") 81 os.Stderr.WriteString(err.Error()) 82 os.Stderr.WriteString("\x1b[0m\n") 83 os.Exit(1) 84 } 85 } 86 87 // handleInput simplifies control-flow for func main 88 func handleInput(w io.Writer, path string) error { 89 if path == `-` { 90 return convert(w, os.Stdin) 91 } 92 93 // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) { 94 // resp, err := http.Get(path) 95 // if err != nil { 96 // return err 97 // } 98 // defer resp.Body.Close() 99 // return convert(w, resp.Body) 100 // } 101 102 f, err := os.Open(path) 103 if err != nil { 104 // on windows, file-not-found error messages may mention `CreateFile`, 105 // even when trying to open files in read-only mode 106 return errors.New(`can't open file named ` + path) 107 } 108 defer f.Close() 109 return convert(w, f) 110 } 111 112 // convert simplifies control-flow for func handleInput 113 func convert(w io.Writer, r io.Reader) error { 114 bw := bufio.NewWriter(w) 115 defer bw.Flush() 116 return json2(bw, r) 117 } 118 119 // escapedStringBytes helps func handleString treat all string bytes quickly 120 // and correctly, using their officially-supported JSON escape sequences 121 // 122 // https://www.rfc-editor.org/rfc/rfc8259#section-7 123 var escapedStringBytes = [256][]byte{ 124 {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'}, 125 {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'}, 126 {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'}, 127 {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'}, 128 {'\\', 'b'}, {'\\', 't'}, 129 {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'}, 130 {'\\', 'f'}, {'\\', 'r'}, 131 {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'}, 132 {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'}, 133 {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'}, 134 {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'}, 135 {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'}, 136 {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'}, 137 {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'}, 138 {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'}, 139 {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'}, 140 {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39}, 141 {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47}, 142 {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55}, 143 {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63}, 144 {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71}, 145 {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79}, 146 {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87}, 147 {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95}, 148 {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103}, 149 {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111}, 150 {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119}, 151 {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127}, 152 {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135}, 153 {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143}, 154 {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151}, 155 {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159}, 156 {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167}, 157 {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175}, 158 {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183}, 159 {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191}, 160 {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199}, 161 {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207}, 162 {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215}, 163 {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223}, 164 {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231}, 165 {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239}, 166 {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247}, 167 {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255}, 168 } 169 170 // writeSpaces does what it says, minimizing calls to write-like funcs 171 func writeSpaces(w *bufio.Writer, n int) { 172 const spaces = ` ` 173 if n < 1 { 174 return 175 } 176 177 for n >= len(spaces) { 178 w.WriteString(spaces) 179 n -= len(spaces) 180 } 181 w.WriteString(spaces[:n]) 182 } 183 184 // json2 does it all, given a reader and a writer 185 func json2(w *bufio.Writer, r io.Reader) error { 186 dec := json.NewDecoder(r) 187 // avoid parsing numbers, so unusually-long numbers are kept verbatim, 188 // even if JSON parsers aren't required to guarantee such input-fidelity 189 // for numbers 190 dec.UseNumber() 191 192 t, err := dec.Token() 193 if err == io.EOF { 194 return errors.New(`input has no JSON values`) 195 } 196 197 if err = handleToken(w, dec, t, 0, 0); err != nil { 198 return err 199 } 200 // don't forget ending the last line for the last value 201 w.WriteByte('\n') 202 203 _, err = dec.Token() 204 if err == io.EOF { 205 // input is over, so it's a success 206 return nil 207 } 208 209 if err == nil { 210 // a successful `read` is a failure, as it means there are 211 // trailing JSON tokens 212 return errors.New(`unexpected trailing data`) 213 } 214 215 // any other error, perhaps some invalid-JSON-syntax-type error 216 return err 217 } 218 219 // handleToken handles recursion for func json2 220 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, pre, level int) error { 221 switch t := t.(type) { 222 case json.Delim: 223 switch t { 224 case json.Delim('['): 225 return handleArray(w, dec, pre, level+1) 226 case json.Delim('{'): 227 return handleObject(w, dec, pre, level+1) 228 default: 229 return errors.New(`unsupported JSON syntax ` + string(t)) 230 } 231 232 case nil: 233 w.WriteString(`null`) 234 return nil 235 236 case bool: 237 if t { 238 w.WriteString(`true`) 239 } else { 240 w.WriteString(`false`) 241 } 242 return nil 243 244 case json.Number: 245 writeSpaces(w, 2*pre) 246 w.WriteString(t.String()) 247 return nil 248 249 case string: 250 return handleString(w, t, pre) 251 252 default: 253 // return fmt.Errorf(`unsupported token type %T`, t) 254 return errors.New(`invalid JSON token`) 255 } 256 } 257 258 // handleArray handles arrays for func handleToken 259 func handleArray(w *bufio.Writer, dec *json.Decoder, pre, level int) error { 260 writeSpaces(w, 2*pre) 261 w.WriteByte('[') 262 263 for i := 0; true; i++ { 264 t, err := dec.Token() 265 if err == io.EOF { 266 endComposite(i, w, level-1, ']') 267 return nil 268 } 269 270 if err != nil { 271 return err 272 } 273 274 if t == json.Delim(']') { 275 endComposite(i, w, level-1, ']') 276 return nil 277 } 278 279 if i > 0 { 280 _, err := w.WriteString(",\n") 281 if err != nil { 282 return errNoMoreOutput 283 } 284 } else { 285 w.WriteByte('\n') 286 } 287 288 err = handleToken(w, dec, t, level, level) 289 if err != nil { 290 return err 291 } 292 } 293 294 // make the compiler happy 295 return nil 296 } 297 298 // handleObject handles objects for func handleToken 299 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error { 300 writeSpaces(w, 2*pre) 301 w.WriteByte('{') 302 303 for i := 0; true; i++ { 304 t, err := dec.Token() 305 if err == io.EOF { 306 endComposite(i, w, pre, '}') 307 return nil 308 } 309 310 if err != nil { 311 return err 312 } 313 314 if t == json.Delim('}') { 315 endComposite(i, w, pre, '}') 316 return nil 317 } 318 319 if i > 0 { 320 _, err := w.WriteString(",\n") 321 if err != nil { 322 return errNoMoreOutput 323 } 324 } else { 325 w.WriteByte('\n') 326 } 327 328 k, ok := t.(string) 329 if !ok { 330 return errors.New(`expected a string for a key-value pair`) 331 } 332 333 err = handleString(w, k, level) 334 if err != nil { 335 return err 336 } 337 338 w.WriteString(": ") 339 340 t, err = dec.Token() 341 if err == io.EOF { 342 return errors.New(`expected a value for a key-value pair`) 343 } 344 345 err = handleToken(w, dec, t, 0, level) 346 if err != nil { 347 return err 348 } 349 } 350 351 // make the compiler happy 352 return nil 353 } 354 355 // handleString handles strings for func handleToken, and keys for func 356 // handleObject 357 func handleString(w *bufio.Writer, s string, level int) error { 358 writeSpaces(w, 2*level) 359 w.WriteByte('"') 360 for i := range s { 361 w.Write(escapedStringBytes[s[i]]) 362 } 363 w.WriteByte('"') 364 return nil 365 } 366 367 // endComposite handles common closing logic which is used twice both in each 368 // of func handleArray and func handleObject 369 func endComposite(i int, w *bufio.Writer, pre int, end byte) { 370 if i > 0 { 371 w.WriteByte('\n') 372 writeSpaces(w, 2*pre) 373 } 374 w.WriteByte(end) 375 }