File: j2.go 1 /* 2 The MIT License (MIT) 3 4 Copyright © 2020-2025 pacman64 5 6 Permission is hereby granted, free of charge, to any person obtaining a copy of 7 this software and associated documentation files (the “Software”), to deal 8 in the Software without restriction, including without limitation the rights to 9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 of the Software, and to permit persons to whom the Software is furnished to do 11 so, subject to the following conditions: 12 13 The above copyright notice and this permission notice shall be included in all 14 copies or substantial portions of the Software. 15 16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 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 writeSpaces(w, 2*pre) 234 w.WriteString(`null`) 235 return nil 236 237 case bool: 238 writeSpaces(w, 2*pre) 239 if t { 240 w.WriteString(`true`) 241 } else { 242 w.WriteString(`false`) 243 } 244 return nil 245 246 case json.Number: 247 writeSpaces(w, 2*pre) 248 w.WriteString(t.String()) 249 return nil 250 251 case string: 252 return handleString(w, t, pre) 253 254 default: 255 // return fmt.Errorf(`unsupported token type %T`, t) 256 return errors.New(`invalid JSON token`) 257 } 258 } 259 260 // handleArray handles arrays for func handleToken 261 func handleArray(w *bufio.Writer, dec *json.Decoder, pre, level int) error { 262 writeSpaces(w, 2*pre) 263 w.WriteByte('[') 264 265 for i := 0; true; i++ { 266 t, err := dec.Token() 267 if err == io.EOF { 268 endComposite(i, w, level-1, ']') 269 return nil 270 } 271 272 if err != nil { 273 return err 274 } 275 276 if t == json.Delim(']') { 277 endComposite(i, w, level-1, ']') 278 return nil 279 } 280 281 if i > 0 { 282 _, err := w.WriteString(",\n") 283 if err != nil { 284 return errNoMoreOutput 285 } 286 } else { 287 w.WriteByte('\n') 288 } 289 290 err = handleToken(w, dec, t, level, level) 291 if err != nil { 292 return err 293 } 294 } 295 296 // make the compiler happy 297 return nil 298 } 299 300 // handleObject handles objects for func handleToken 301 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error { 302 writeSpaces(w, 2*pre) 303 w.WriteByte('{') 304 305 for i := 0; true; i++ { 306 t, err := dec.Token() 307 if err == io.EOF { 308 endComposite(i, w, pre, '}') 309 return nil 310 } 311 312 if err != nil { 313 return err 314 } 315 316 if t == json.Delim('}') { 317 endComposite(i, w, pre, '}') 318 return nil 319 } 320 321 if i > 0 { 322 _, err := w.WriteString(",\n") 323 if err != nil { 324 return errNoMoreOutput 325 } 326 } else { 327 w.WriteByte('\n') 328 } 329 330 k, ok := t.(string) 331 if !ok { 332 return errors.New(`expected a string for a key-value pair`) 333 } 334 335 err = handleString(w, k, level) 336 if err != nil { 337 return err 338 } 339 340 w.WriteString(": ") 341 342 t, err = dec.Token() 343 if err == io.EOF { 344 return errors.New(`expected a value for a key-value pair`) 345 } 346 347 err = handleToken(w, dec, t, 0, level) 348 if err != nil { 349 return err 350 } 351 } 352 353 // make the compiler happy 354 return nil 355 } 356 357 // handleString handles strings for func handleToken, and keys for func 358 // handleObject 359 func handleString(w *bufio.Writer, s string, level int) error { 360 writeSpaces(w, 2*level) 361 w.WriteByte('"') 362 for i := range s { 363 w.Write(escapedStringBytes[s[i]]) 364 } 365 w.WriteByte('"') 366 return nil 367 } 368 369 // endComposite handles common closing logic which is used twice both in each 370 // of func handleArray and func handleObject 371 func endComposite(i int, w *bufio.Writer, pre int, end byte) { 372 if i > 0 { 373 w.WriteByte('\n') 374 writeSpaces(w, 2*pre) 375 } 376 w.WriteByte(end) 377 }