"If the first version of your shell script is more than five lines long, you should have written it in Python."
I think there's a lot of truth in that. None of the examples presented in the article look better than had they been written in some existing scripting/programming language. In fact, had they been written in Python or Javascript, it would have been far more obvious what the resulting output would have been, considering that those languages already use {} for objects and [] for lists.
For example, take this example:
jo -p name=JP object=$(jo fruit=Orange point=$(jo x=10 y=20) number=17) sunday=false
In Python you would write it like this: json.dumps({"name": "JP", "object": {"fruit": "Orange", "point": {"x": 10, "y": 20}, "number": 17}, "sunday": False})
Only a bit more code, but at least it won't suffer from the Norway problem. Even though Python isn't the fastest language out there, it's likely still faster than the shell command above. There is no need to construct a fork bomb just to generate some JSON.Taking these two command lines:
jo -p name=JP object=$(jo fruit=Orange point=$(jo x=10 y=20) number=17) sunday=false >/dev/null
python -c 'import json;print(json.dumps({"name": "JP", "object": {"fruit": "Orange", "point": {"x": 10, "y": 20}, "number": 17}, "sunday": False}))' >/dev/null
For jo (x86_64, Rosetta2), python2 (x86_64, Rosetta2), jo (arm64), and python3 (arm64), running 1000 iterations, with `tai64n` doing the timing. 2022-02-05 21:25:38.357228500 start-jo-x86
2022-02-05 21:25:45.319337500 stop-jo
2022-02-05 21:25:45.319338500 start-python2-x86
2022-02-05 21:26:18.876235500 stop-python2-x86
2022-02-05 21:26:18.876235500 start-jo-arm
2022-02-05 21:26:22.316063500 stop-jo-arm
2022-02-05 21:26:22.316064500 start-python3-arm
2022-02-05 21:26:40.379063500 stop-python3-arm
I make it: 7s for jo-x86, 33.5s for python2-x86, 3.5s for jo-arm, 18s for python3-arm.Test script is at https://pastebin.com/4tTVrDia
$ time python3 -c ''
real 0m0.029s
$ time python2 -c ''
real 0m0.010s
$ time bash -c ''
real 0m0.001s
Which means - you probably don't want to have python scripts on a busy webserver, being called from classic cgi-bin (do people still use those?), or run it as -exec argument to a "find" iterating over many thousands files. Maybe a couple more of such examples. For most use-cases though, that's still fast enough.I also wrote my own tool, xidel [1]:
time for i in $(seq 1 $count); do xidel -se '{"name": "JP", "object": {"fruit": "Orange", "point": {"x": 10, "y": 20}, "number": 17}, "sunday": false()}' > /dev/null; done
which gives me 1,575sBut if you actually want to repeat something a thousand times, you would use a loop in the query for 0,017s:
time xidel -se 'for $i in 1 to 1000 return {"name": "JP", "object": {"fruit": "Orange", "point": {"x": 10, "y": 20}, "number": 17}, "sunday": false()}' > /dev/null
(a python3 loop gives me 0,029s) $ time cat<<EOL
{"name": "JP", "object": {"fruit": "Orange", "point": {"x": 10, "y": 20}, "number": 17},
"sunday": false}
EOL
{"name": "JP", "object": {"fruit": "Orange", "point": {"x": 10, "y": 20}, "number": 17},
"sunday": false}
real 0m0.002s
user 0m0.000s
sys 0m0.002sI've never really found any language that feels good for that kind of thing, there's definately a middle ground where it's getting too much for bash, but jumping to a language loses too much at the initial conversion to make it feel worth it until you are well past the point where your future self will think you should have made the switch.
Some languages have things like backticks in php to inter-operate but it's still not great experience to mix between them. For my own little things I'm currently looking at fish, but bash is omnipresent.
This tool seems to delay that point even further, as currently dealing with generating json is definately a pain point (whereas manipulating it in jq is often really good).
But if anyone can point to good examples of this transition in python then I'd be very interested.
edit: jq is more powerful than I thought for creating json, see https://spin.atomicobject.com/2021/06/08/jq-creating-updatin...
The problem with python here is that while python-sh might be nice, you have to install any extra libraries you need with it, and that's not a trivial problem for installing scripts into prod.
Xonsh is better since you kind of get both the benefits of python and a shell like language, but frankly it's broken in a number of ways still. I use it daily, but hopefully you don't need to ctrl-c out of something since signal handling is iffy at best. It is kind of nice to be able to import python directly on the command line and use it as python though...
Up until now I've been creating temp JSON files to feed the commands and I thought jo would be a great tool to make this easier. Now that I know jq can also create JSON, I'll just use that instead.
> "If the first version of your shell script is more than five lines long, you should have written it in Python."
Seriously, these kind of "common knowledge", "universal truth in a sentence" sayings are often the mark of wannabe guru mid career engineers that have no clue what they are talking about.
That is going a bit far. By all means use Python. Go ahead and attack people who use the shell. But let's be honest. The shell is faster, assuming one knows how to use it. A similar claim is often made by Python advocates, something along the lines of Python is not slow if one knows how to use it.
The startup time of a Python interpreter is enormous for someone who is used to a Bourne shell. This is what always stops me from using Python as a shell replacement for relatively simple jobs; I have never written a large shell script and doubt I ever will. I write small scripts.
If anyone knows how to mitigate the Python startup delay, feel free to share. I might become more interested in Python.
Anyway, this "jo" thing seems a bit silly. Someone at Google spent their 20% time writing a language called jsonnet to emit JSON. It has been discussed on HN before. People have suggested dhall is a perhaps better alternative.
That's not a shell command any more than running "python" is. `jo` is its own executable.
And doing the sub-commands are not necessary; `jo` supports nested data natively.
Besides that, looking at json only, it's part of the standard library so is more likely to already exist on any given machine rather than this.
echo '{"name": "JP", "object": {"fruit": "Orange", "point": {"x": 10, "y": 20}, "number": 17}, "sunday": false}'
is fine as a scriptAnd this lets you write json without (or with less) braces, commas and quotes. That alone is already a big win.
Paired with httpie [2] aliases [3] it produces concise APL-like syntax:
POST https://httpbin.org/post test:=j`a=b c=`e=3` l=`*1 2 3``
Which translates to: http POST https://httpbin.org/post test:="$(jo a=b c="$(jo e=3)" l="$(jo -a 1 2 3)")"
Or, in other words, sending POST with the following body: {"a":"b","c":{"e":3},"l":[1,2,3]}
[1]: https://github.com/seletskiy/dotfiles/blob/78ac45c01bdf019ae...
[2]: https://httpie.io/
[3]: https://github.com/seletskiy/dotfiles/blob/78ac45c01bdf019ae... $ http pie.dev/post test[a]=b test[c][e]:=3 test[l][]:=1
[0] https://httpie.io/blog/httpie-3.0.0Btw loving your new desktop app so far!
`jq` can construct JSON "safely" from shell constructs, but is rather more verbose - e.g. with the same examples:
$ jq -n --arg name Jane '{"name":$name}'
$ jq -n \
--argjson time $(date +%s) \
--arg dir $HOME \
'{"time":$time,"dir":$dir}'
$ jq -n '$ARGS.positional' --args spring summer winter
$ jq -n \
--arg name JP \
--argjson object "$(
jq -n \
--arg fruit Orange \
--argjson point "$(
jq -n \
--argjson x 10 \
--argjson y 20 \
'{"x":$x,"y":$y}' \
)" \
--argjson number 17 \
'{"fruit":$fruit,"point":$point,"number":$number}' \
)" \
--argjson sunday false \
'{"name":$name,"object":$object,"sunday":$sunday}'I'm very skeptical of this. If I put x=001979 in as a value I dont think I want you trying to guess if that's supposed to be an integer or a string.
This sounds like the Norway Problem waiting to happen.
But, clearly there is a use-case for producing json with integer, null, and boolean values.
jo -- -s opaque_id=001979
{"opaque_id":"001979"}Avoid using this command in prod.
> jo normally treats value as a literal string value, unless it begins with one of the following characters:
> value action
> @file substitute the contents of file as-is
> %file substitute the contents of file in base64-encoded form
> :file interpret the contents of file as JSON, and substitute the result
This is convenient but also very dangerous. This feature will cause the content of an arbitrary file to be embed in the JSON if any value starts with a `@`, `%`, or `:`.This will be a source of bugs or security issues for any script generating json with dynamic values.
$ ./jo foo=1 bar=2 obj=$(./jo -a 1 2 3 "</script" '"')
{"foo":1,"obj":[1,2,3,"<\/script","\""],"bar":2}
$ ./jo foo='abc
> def
> ghi'
{"foo":"abc\ndef\nghi"}
$ cat jo
#!/usr/local/bin/txr --lisp
(define-option-struct jo-opts nil
(a array :bool
"Produce array instead of object")
(nil help :bool
"Print this help"))
(defvarl jo-name *load-path*)
(defun json-val (str)
(match-case str
("true" t)
("false" nil)
("null" 'null)
(`{@nil` (get-json str))
(`[@nil` (get-json str))
(@else (iflet ((num (tofloat else)))
num
else))))
(let ((o (new jo-opts)))
o.(getopts *args*)
(when o.help
(put-line "Usage:\n")
(put-line ` @{jo-name} [options] arg*`)
o.(opthelp)
(exit 0))
(if o.array
(let ((items [mapcar json-val o.out-args]))
(put-jsonl (vec-list items)))
(let ((pairs [mapcar (lambda (:match)
((`@this=@that`) (list (json-val this) (json-val that)))
((@else) (error "~a: arguments must be name=obj pairs" jo-name)))
o.out-args]))
(put-jsonl ^#H(() ,*pairs)))))For anyone else wondering: https://www.nongnu.org/txr/
In the spirit of "do one thing well", I'd so rather use this to construct JSON payloads to curl requests than the curl project's own "json part" proposal[1] under consideration.
Curl does do one thing: make network requests. This feature is making it easier to make network requests, i.e. it makes it better at doing the one thing that it does.
curl looks like it has hundreds of flags.
There's one for yaml too that works well in my experience: https://github.com/redhat-developer/yaml-language-server
https://play.google.com/store/apps/details?id=com.alexsci.an...
I wonder if a formal syntax would help? Perhaps including relevant shell syntax (interpolation, subshell). It could clarify issues, and this different perspective might suggest improvements or different approaches.
It's written in C and is not actively developed. The latest commit, it seems, was a pull request from me back in 2018 that fixed a null-termination issue that led to memory corruption.
Because I couldn't rely on jshon being correct, I rewrote it in Haskell here:
https://github.com/dapphub/dapptools/tree/master/src/jays
This is also not developed actively but it's a single simple ~200 line Haskell program.
$ alias fooson="node --eval \"console.log(JSON.stringify(eval('(' + process.argv[1] + ')')))\""
$ fooson "{time: $(date +%s), dir: '$HOME'}"
{"time":1457195712,"dir":"/Users/jpm"}
It may be a bit nicer to place that JavaScript in your path as a node script instead of using an alias. #!/usr/bin/env node
console.log(JSON.stringify(eval('(' + process.argv[2] + ')')))
Since fooson's argument is being interpreted as JavaScript, you can access your environment through process.env. But you could make a slightly easier syntax in various ways. Like with this script: #!/usr/bin/env node
for(const [k, v] of Object.entries(process.env)) {
if (!global.hasOwnProperty(k)) {
global[k] = v;
}
}
console.log(JSON.stringify(eval('(' + process.argv[2] + ')')))
Now environmental variables can be access as if they were JS variables. This can let you handle strings with annoying quoting. $ export BAR="\"'''\"\""
$ fooson '{bar: BAR}'
{"bar": "\"'''\"\""}
If you wanted to do this without trusting your input so much, a JSON dialect where you can use single-quoted strings would get you pretty far. $ fooson "{'time': $(date +%s), 'dir': '$HOME'}"
{"time":1457195712,"dir":"/Users/jpm"}
If you taught the utility to expand env variables itself you'd be able to handle strings with mixed quoting as well. $ export BAR="\"'''\"\""
$ fooson '{"bar": "$BAR"}'
{"bar": "\"'''\"\""}
You'd only need small modifications to a JSON parser to make this work.jo -- -b foo=no -s a=12 q=[1,2,3] {"foo":true,"a":"12","q":[1,2,3]}
I actually think the "Norway problem" is a PEBKAC from users not learning the data format. But this tool may confuse some people or applications who don't know what a boolean, integer, float or string are, and try to mix types when the program reading them wasn't designed to. Probably the issue will come up whenever people mix different kinds of versions ("1", "1.1", "1.1.1" should be parsed as an int, float, and string, respectively)
Should “a” be number or a string in the resulting JSON?
And if it’s number, how can I tell it to output a string?
Disclosure: I'm the author
Project link: https://github.com/ngs-lang/ngs
Enjoy!
You guys do that for "written in Rust"
python -c "import json; print(json.dumps(dict(a=10, b=False)))"