/* The MIT License (MIT) Copyright © 2026 pacman64 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* To compile a smaller-sized command-line app, you can use the `go` command as follows: go build -ldflags "-s -w" -trimpath dejsonl.go */ package main import ( "bufio" "encoding/json" "errors" "io" "os" "strings" ) const info = ` dejsonl [filepath...] Turn JSON Lines (JSONL) into proper-JSON arrays. The JSON Lines format is simply plain-text lines, where each line is valid JSON on its own. ` const indent = ` ` // errNoMoreOutput is a generic dummy output-error, which is meant to be // ultimately ignored, being just an excuse to quit the app immediately // and successfully var errNoMoreOutput = errors.New(`no more output`) func main() { if len(os.Args) > 1 { switch os.Args[1] { case `-h`, `--h`, `-help`, `--help`: os.Stdout.WriteString(info[1:]) return } } if len(os.Args) > 2 { os.Stderr.WriteString("multiple inputs not allowed\n") os.Exit(1) } // figure out whether input should come from a named file or from stdin path := `-` if len(os.Args) > 1 { path = os.Args[1] } liveLines := true if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil { liveLines = false } err := handleInput(os.Stdout, path, liveLines) if err != nil && err != io.EOF && err != errNoMoreOutput { os.Stderr.WriteString(err.Error()) os.Stderr.WriteString("\n") os.Exit(1) } } // handleInput simplifies control-flow for func main func handleInput(w io.Writer, path string, live bool) error { if path == `-` { return dejsonl(w, os.Stdin, live) } // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) { // resp, err := http.Get(path) // if err != nil { // return err // } // defer resp.Body.Close() // return dejsonl(w, resp.Body, live) // } f, err := os.Open(path) if err != nil { // on windows, file-not-found error messages may mention `CreateFile`, // even when trying to open files in read-only mode return errors.New(`can't open file named ` + path) } defer f.Close() return dejsonl(w, f, live) } // dejsonl simplifies control-flow for func handleInput func dejsonl(w io.Writer, r io.Reader, live bool) error { bw := bufio.NewWriter(w) defer bw.Flush() const gb = 1024 * 1024 * 1024 sc := bufio.NewScanner(r) sc.Buffer(nil, 8*gb) got := 0 for i := 0; sc.Scan(); i++ { s := sc.Text() if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") { s = s[3:] } // trim spaces at both ends of the current line for len(s) > 0 && s[0] == ' ' { s = s[1:] } for len(s) > 0 && s[len(s)-1] == ' ' { s = s[:len(s)-1] } // ignore empty(ish) lines if len(s) == 0 { continue } // ignore lines starting with unix-style comments if len(s) > 0 && s[0] == '#' { continue } if err := checkJSONL(strings.NewReader(s)); err != nil { return err } if got == 0 { bw.WriteByte('[') } else { bw.WriteByte(',') } if bw.WriteByte('\n') != nil { return errNoMoreOutput } bw.WriteString(indent) bw.WriteString(s) got++ if !live { continue } if err := bw.Flush(); err != nil { return errNoMoreOutput } } if got == 0 { bw.WriteString("[\n]\n") } else { bw.WriteString("\n]\n") } return sc.Err() } func checkJSONL(r io.Reader) error { dec := json.NewDecoder(r) // avoid parsing numbers, so unusually-long numbers are kept verbatim, // even if JSON parsers aren't required to guarantee such input-fidelity // for numbers dec.UseNumber() t, err := dec.Token() if err == io.EOF { return errors.New(`input has no JSON values`) } if err := checkToken(dec, t); err != nil { return err } _, err = dec.Token() if err == io.EOF { // input is over, so it's a success return nil } if err == nil { // a successful `read` is a failure, as it means there are // trailing JSON tokens return errors.New(`unexpected trailing data`) } // any other error, perhaps some invalid-JSON-syntax-type error return err } // checkToken handles recursion for func checkJSONL func checkToken(dec *json.Decoder, t json.Token) error { switch t := t.(type) { case json.Delim: switch t { case json.Delim('['): return checkArray(dec) case json.Delim('{'): return checkObject(dec) default: return errors.New(`unsupported JSON syntax ` + string(t)) } case nil, bool, float64, json.Number, string: return nil default: // return fmt.Errorf(`unsupported token type %T`, t) return errors.New(`invalid JSON token`) } } // handleArray handles arrays for func checkToken func checkArray(dec *json.Decoder) error { for { t, err := dec.Token() if err != nil { return err } if t == json.Delim(']') { return nil } if err := checkToken(dec, t); err != nil { return err } } // make the compiler happy return nil } // handleObject handles objects for func checkToken func checkObject(dec *json.Decoder) error { for { t, err := dec.Token() if err != nil { return err } if t == json.Delim('}') { return nil } if _, ok := t.(string); !ok { return errors.New(`expected a string for a key-value pair`) } t, err = dec.Token() if err == io.EOF || t == json.Delim('}') { return errors.New(`expected a value for a key-value pair`) } if err := checkToken(dec, t); err != nil { return err } } // make the compiler happy return nil }