skip to Main Content

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


  1. 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 produce false or null.

    $ jq  'getpath($key | ltrimstr(".") | split(".")) // $default' file.json --arg key .nested.key --arg default foobar
    "value"
    $ jq  'getpath($key | ltrimstr(".") | split(".")) // $default' file.json --arg key .nested.dne --arg default foobar
    "foobar"
    

    $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.

    Login or Signup to reply.
  2. 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:

    function my-jq () {
      jq -c --argjson key "$1" --arg default "$2" '
         first(tostream | select(length == 2 and .[0] == $key)) // null
         | if . then .[1] else $default end
      ' 
    }
    

    Examples:

    echo '{"nested": {"key": "value", "tricky": null}}' | my-jq2 '["nested","key"]' haha
    "value"
    
    echo '{"nested": {"key": "value", "tricky": null}}' | my-jq2 '["nested","nokey"]' haha
    "haha"
    
    echo '{"nested": {"key": "value", "tricky": null}}' | my-jq2 '["nested","tricky"]' haha
    null
    
    Login or Signup to reply.
  3. 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):

    function my-jq() { jq "($1) // ($2)" "$3"; }
    
    % my-jq .nested.key "default" file.json
    "value"
    
    my-jq .nested.tricky "default" file.json  # fails
    "default"
    
    % my-jq .nested.dne "default" file.json
    "default"
    

    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 the path input among the all the paths 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:

    function my-jq() { jq "if any(path($1) == paths; .) then ($1) else ($2) end" "$3"; }
    
    % my-jq .nested.key "default" file.json
    "value"
    
    my-jq .nested.tricky "default" file.json
    null
    
    % my-jq .nested.dne "default" file.json
    "default"
    

    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:

    function my-jq() { jq "($1) // if any(path($1) == paths; .) then ($1) else ($2) end" "$3"; }
    
    % my-jq .nested.key "default" file.json
    "value"
    
    my-jq .nested.tricky "default" file.json
    null
    
    % my-jq .nested.dne "default" file.json
    "default"
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search