I would like to make a small jq
-like function that does get-or-default-if-not-present similar to Python’s dict.get(key, default)
. This is the desired behavior:
% echo '{"nested": {"key": "value", "tricky": null}}' > file.json
% my-jq nested.key "default" file.json
"value"
% my-jq nested.tricky "default" file.json
null
% my-jq nested.dne "default" file.json
"default"
I have tried playing with this answer to a similar question but it doesn’t work for nested keys. Does anyone have a suggestion?
function my-jq () {
jq --arg key "$1" --arg default "$2"
'if has($key) then .[$key] else $default | fromjson end' "$3"
}
3
Answers
You can’t use a dotted path as a sequence of keys. You can turn such a path into a valid path for use with the
getpath
function, however. (There may be a cleaner, more robust way to do this.) The//
operator provides for an alternate value should the left-hand side producefalse
ornull
.$key | ltrimstr(".") | split(".")
first gets rid of the leading.
, then splits the remaining string on the remaining.
to produce a list of separate keys.getpath
produces a filter using that list of keys;getpath(["nested", "key"])
is equivalent (AFAIK) to.nested.key
.Your requirements go against JSON’s grain a bit, but here is one possible solution, or the basis of a family of solutions.
This particular solution assumes that the path is given in array form:
Examples:
Based on your projection to make a "jq-like" function, it’s fair to assume that your parameter should then be considered not just a path expression but a general jq filter, i.e. code. With the current versions of jq, there is no option or other shorthand method that works for code similar to how
--arg
and--argjson
do for data.You can, however, import code fragments by means of jq’s library/module system, but with the task at hand you’d need to store the code from your function’s parameter into a (temporary) file, reference it in the static part of the actual jq filter, and delete the file afterwards. This not only is cumbersome, it also needs some static overhead in the module file, which inevitably opens up Pandora’s box labeled "Code injection", so you could just as well submit to the unleashed evil, and compose the actual jq filter on the fly using the literal (and potentially malicious) content of the parameter. (Note that this assumes a valid jq expression, thus using
.nested.key
etc. with a dot up front):This minimal approach uses the alternative operator
//
, which fails to tell an actual but falsy value ("null" or "false") apart from an empty stream (missing value). To counteract that, you could perform a check on the existence of thepath
input among the all thepaths
of the base document. This drastically reduces the kind of filters trivially accepted by your function (which with your use case in mind may even be considered a good thing, yet malicious injection is still possible), and the comparison with all paths may come with a performance penalty for base documents with complex structuring, but it meets your three test cases:Eventually, the potential performance penalty could be mitigated by combining both approaches, i.e. starting off with the faster first one for the general case, but reverting to the possibly slower second one if the first one produced an ambiguous falsy value: