This is what I'm using to pretty-print json (calling the jq command):
#lang racket
(require racket/string)
(require racket/system)
(define (pretty-print-json json_str)
; for escaping '\' and '"' characters in bash
(define str_clean (string-replace json_str "\"" "\\\""))
(system (string-append "echo \"" str_clean "\" | jq .")))
It's ugly and I don't think that it's secure. Do you know a better way to do it ?
Ideally I would like to not have to depend on jq, I just want to be able to display a json with indentation and spaces.
You're correct in that using system
is pretty fragile and error-prone if the string to pass to a shell to run isn't properly sanitized. It's better to use subprocess
to run jq
and have a pair of pipes connected to its standard input and output. You can then write your JSON text to the input put and read the formatted data from the output end. You do have to account for large chunks of data possibly causing deadlocks because of one of the pipes filling up if you try to write it all and only then read it - using threads to do both the reading and writing asynchronously is a robust approach.
An example Racket module (Call it jq-format.rkt
):
#lang racket/base
(require racket/contract racket/function racket/port json)
(provide
(contract-out
[jsexpr->pretty-json (-> jsexpr? string?)]
[format-json (-> string? string?)]
[pretty-print-jsexpr (->* (jsexpr?) (output-port?) void?)]
[pretty-print-json (->* (string?) (output-port?) void?)]))
; Like jsexpr->string but nicely formatted output
(define (jsexpr->pretty-json js)
(format-json (jsexpr->string js)))
; Reformat JSON text into nicely formatted JSON
(define (format-json json)
(call-with-output-string (curry pretty-print-json json)))
; Pretty-print a jsexpr to the given port
(define (pretty-print-jsexpr js [out-port (current-output-port)])
(pretty-print-json (jsexpr->string js) out-port))
; Pretty print a JSON text string to the given port
(define (pretty-print-json json [out-port (current-output-port)])
; Start a jq child process to format the JSON.
(let*-values ([(jq-process jq-out jq-in jq-err)
(subprocess #f #f 'stdout (find-executable-path "jq") "--unbuffered" ".")]
[(writer) ; One thread to write the JSON to jq and close the input pipe when done
(thread (thunk
(call-with-input-string
json
(lambda (json-port)
(copy-port json-port jq-in)
(close-output-port jq-in)))))]
[(reader) ; Thread to read the formatted JSON from jq and send it to the output port
(thread (thunk
(copy-port jq-out out-port)
(close-input-port jq-out)))])
; Wait for the threads to end and then for the jq subprocess to exit
(thread-wait writer)
(thread-wait reader)
(subprocess-wait jq-process)))
Example usage:
Welcome to Racket, version 8.8 [cs].
> (require "jq-format.rkt")
> (pretty-print-jsexpr '(1 2 3 4))
[
1,
2,
3,
4
]
>
Now available in improved form as part of my json-format
package.