简体   繁体   中英

How to substitute strings in JSON with jq based on the input

Given the input in this form

[
  {
    "DIR" : "/foo/bar/a/b/c",
    "OUT" : "/foo/bar/x/y/z",
    "ARG" : [ "aaa", "bbb", "/foo/bar/a", "BASE=/foo/bar" ]
  },
  {
    "DIR" : "/foo/baz/d/e/f",
    "OUT" : "/foo/baz/x/y/z",
    "ARG" : [ "ccc", "ddd", "/foo/baz/b", "BASE=/foo/baz" ]
  },
  { 
    "foo" : "bar"
  }
]

I'm trying to find out how to make jq transform that into this:

[
  {
    "DIR" : "BASE/a/b/c",
    "OUT" : "BASE/x/y/z",
    "ARG" : [ "aaa", "bbb", "BASE/a", "BASE=/foo/bar" ]
  },

  {
    "DIR" : "BASE/d/e/f",
    "OUT" : "BASE/x/y/z",
    "ARG" : [ "ccc", "ddd", "BASE/b", "BASE=/foo/baz" ]
  },
  { 
    "foo" : "bar"
  }
]

In other words, objects having an "ARG" array, containing a string that starts with "BASE=" should use the string after "BASE=" , eg "/foo" to substitute other string values that start with " /foo" (except the "BASE=/foo" which should remain unchanged")

I'm not even close to finding a solution myself, and at this point I'm unsure that jq alone will do the job.

Don't worry, jq alone will do the job:

jq 'def sub_base($base): if (startswith("BASE") | not) then sub($base; "BASE") else . end;
    map(if .["ARG"] then ((.ARG[] | select(startswith("BASE=")) | split("=")[1]) as $base
            | to_entries
            | map(if (.value | type == "string") then .value |= sub_base($base)
                  else .value |= map(sub_base($base)) end)
            | from_entries)
        else . end)' input.json

The output:

[
  {
    "DIR": "BASE/a/b/c",
    "OUT": "BASE/x/y/z",
    "ARG": [
      "aaa",
      "bbb",
      "BASE/a",
      "BASE=/foo/bar"
    ]
  },
  {
    "DIR": "BASE/d/e/f",
    "OUT": "BASE/x/y/z",
    "ARG": [
      "ccc",
      "ddd",
      "BASE/b",
      "BASE=/foo/baz"
    ]
  },
  {
    "foo": "bar"
  }
]

With jq :

#!/usr/bin/jq -f

# fix-base.jq
def fix_base:
  (.ARG[] | select(startswith("BASE=")) | split("=")[1]) as $base
  | .DIR?|="BASE"+ltrimstr($base)
  | .OUT?|="BASE"+ltrimstr($base)
  | .ARG|=map(if startswith($base) then "BASE"+ltrimstr($base) else . end)
  ;

map(if .ARG? then fix_base  else . end)

You can run it like this:

jq -f fix-base.jq input.json

or make it an executable like this:

chmod +x fix-base.jq
./fix-base.jq input.json

Some helper functions make the going much easier. The first is generic and worthy perhaps of your standard library:

# Returns the integer index, $i, corresponding to the first element
# at which f is truthy, else null
def indexof(f):
  label $out
  | foreach .[] as $x (null; .+1; 
      if ($x|f) then (.-1, break $out) else empty end) // null;

# Change the string $base to BASE using gsub
def munge($base):
  if type == "string" and (test("^BASE=")|not) then gsub($base; "BASE")
  elif type=="array" then map(munge($base))
  elif type=="object" then map_values(munge($base))
  else .
  end;

And now the easy part:

map(if has("ARG") 
    then (.ARG|indexof(test("^BASE="))) as $ix
    | if $ix
      then (.ARG[$ix]|sub("^BASE=";"")) as $base | munge($base)
      else . end 
    else . end )

Some points to note:

  • You may wish to use sub rather than gsub in munge ;
  • The above solution assumes you want to make the change in all keys, not just "DIR", "OUT", and "ARG"
  • The above solution allows specifications of BASE that include one or more occurrences of "=".

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM