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) 226 case json.Delim('{'): 227 return handleObject(w, dec, pre, level) 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 for i := 0; true; i++ { 263 t, err := dec.Token() 264 if err != nil { 265 return err 266 } 267 268 if t == json.Delim(']') { 269 if i == 0 { 270 writeSpaces(w, 2*pre) 271 w.WriteByte('[') 272 w.WriteByte(']') 273 } else { 274 w.WriteByte('\n') 275 writeSpaces(w, 2*level) 276 w.WriteByte(']') 277 } 278 return nil 279 } 280 281 if i == 0 { 282 writeSpaces(w, 2*pre) 283 w.WriteByte('[') 284 w.WriteByte('\n') 285 } else { 286 w.WriteByte(',') 287 w.WriteByte('\n') 288 if err := w.Flush(); err != nil { 289 // a write error may be the consequence of stdout being closed, 290 // perhaps by another app along a pipe 291 return errNoMoreOutput 292 } 293 } 294 295 err = handleToken(w, dec, t, level+1, level+1) 296 if err != nil { 297 return err 298 } 299 } 300 301 // make the compiler happy 302 return nil 303 } 304 305 // handleObject handles objects for func handleToken 306 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error { 307 for i := 0; true; i++ { 308 t, err := dec.Token() 309 if err != nil { 310 return err 311 } 312 313 if t == json.Delim('}') { 314 if i == 0 { 315 writeSpaces(w, 2*pre) 316 w.WriteByte('{') 317 w.WriteByte('}') 318 } else { 319 w.WriteByte('\n') 320 writeSpaces(w, 2*level) 321 w.WriteByte('}') 322 } 323 return nil 324 } 325 326 if i == 0 { 327 writeSpaces(w, 2*pre) 328 w.WriteByte('{') 329 w.WriteByte('\n') 330 } else { 331 w.WriteByte(',') 332 w.WriteByte('\n') 333 if err := w.Flush(); err != nil { 334 // a write error may be the consequence of stdout being closed, 335 // perhaps by another app along a pipe 336 return errNoMoreOutput 337 } 338 } 339 340 k, ok := t.(string) 341 if !ok { 342 return errors.New(`expected a string for a key-value pair`) 343 } 344 345 err = handleString(w, k, level+1) 346 if err != nil { 347 return err 348 } 349 350 w.WriteString(": ") 351 352 t, err = dec.Token() 353 if err == io.EOF { 354 return errors.New(`expected a value for a key-value pair`) 355 } 356 357 err = handleToken(w, dec, t, 0, level+1) 358 if err != nil { 359 return err 360 } 361 } 362 363 // make the compiler happy 364 return nil 365 } 366 367 // handleString handles strings for func handleToken, and keys for func 368 // handleObject 369 func handleString(w *bufio.Writer, s string, level int) error { 370 writeSpaces(w, 2*level) 371 w.WriteByte('"') 372 for i := range s { 373 w.Write(escapedStringBytes[s[i]]) 374 } 375 w.WriteByte('"') 376 return nil 377 }