99

I need your help to solve the following problem: I have a JSON file that looks like this:

{
  "key1": "value1",
  "key2": "value2",
  "key3": "value3"
}

how can I add and remove a new key (i.e "key4": "value4") by bash script? I see also the issue to add or remove a comma at the end of last key in the file before adding or removing the new one.

Thank you

mklement0
  • 382,024
  • 64
  • 607
  • 775
user3155074
  • 1,093
  • 1
  • 9
  • 13

6 Answers6

161

Your best bet is to use a JSON CLI such as jq:

  • On Debian-based systems such as Ubuntu, you can install it via
    sudo apt-get install jq
  • On macOS, with Homebrew (http://brew.sh/) installed, use
    brew install jq

Examples, based on the following input string - output is to stdout:

jsonStr='{ "key1": "value1", "key2": "value2", "key3": "value3" }'
Remove "key3":
jq 'del(.key3)' <<<"$jsonStr"
Add property "key4" with value "value4":
jq '. + { "key4": "value4" }' <<<"$jsonStr"
Change the value of existing property "key1" to "new-value1":
jq '.key1 = "new-value1"' <<<"$jsonStr"

A more robust alternative thanks, Lars Kiesow :
If you pass the new value with --arg, jq takes care of properly escaping the value:

jq '.key1 = $newVal' --arg newVal '3 " of rain' <<<"$jsonStr"

If you want to update a JSON file in place (conceptually speaking), using the example of deleting "key3":

# Create test file.
echo '{ "key1": "value1", "key2": "value2", "key3": "value3" }' > test.json

# Remove "key3" and write results back to test.json (recreate it with result).
jq -c 'del(.key3)' test.json > tmp.$$.json && mv tmp.$$.json test.json

You cannot replace the input file directly, so the result is written to a temporary file that replaces the input file on success.

Note the -c option, which produces compact rather than pretty-printed JSON.

For all options and commands, see the manual at https://jqlang.github.io/jq/manual/.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • I am recevied a output from a script which is actually in purple color, when I replace it withing the file it set as in between following `\u001b[0;1;34m` `\u001b[0;m`, does `jq` able to ignore coloring of a string? – alper Jul 03 '20 at 21:22
  • 1
    Thank you @mklement0 I was able solve it by using following answer (https://stackoverflow.com/a/14693789/2402577) in order to remove the ANSI character. – alper Jul 04 '20 at 08:24
  • 1
    You can use sponge (from moreutils package) to edit the file directly `jq -c 'del(.key3)' test.json | sponge.exe test.json` It's also destructive, so be careful – poboxy Jun 01 '22 at 13:11
55

Not the answer for everyone, but if you already happen to have NodeJs installed in your system, you can use it to easily manipulate JSON.

eg:

#!/usr/bin/env bash
jsonFile=$1;

node > out_${jsonFile} <<EOF
//Read data
var data = require('./${jsonFile}');

//Manipulate data
delete data.key3
data.key4 = 'new value!';

//Output data
console.log(JSON.stringify(data));

EOF

Heck, if you only need to do JSON manipulation and you have node (ie: You don't really need any other bash functionality) you could directly write a script using node as the interpreter:

#! /usr/bin/env node
var data = require('./'+ process.argv[2]);
/*manipulate*/
console.log(JSON.stringify(data));
Lenny Markus
  • 3,398
  • 2
  • 24
  • 30
  • Is the console.log() supposed to write the data to the file or do I need fs? – Shawn Mclean Aug 11 '17 at 03:24
  • 2
    prints to stdout. You can then redirect to a file. eg: `./myScript.js myInput.json > outfile.txt` – Lenny Markus Aug 12 '17 at 04:59
  • any programming language with a JSON library could do the exact same. The main point of using bash is that you don't rely on software that isn't already installed on your system. If that is not a requirement, then there's no reason to suggest Node for this over any other language, and just because you can run it inline from a bash script doesnt make it a bash answer. – user5359531 Aug 22 '18 at 14:52
  • 17
    I started my answer with "Not the answer for everyone," for that precise reason. There are many ways to skin a cat, and in the three years since I posted this answer, Node has continued to grow in popularity, so many people have it installed already. It offers native JSON support, so no need for a library is nice. At the end of the day, I shared a bit of knowledge, and it has helped 16 people. That makes me happy :) – Lenny Markus Aug 28 '18 at 22:11
  • Excellent idea for teams where all devs may not have the same tools installed. – OldBuildingAndLoan Aug 07 '22 at 01:31
21

Building off Lenny's answer, we can use node's -p option, which evaluates the given script and writes the output to stdout.

Using the spread operator for easy modification gives:

node -p "JSON.stringify({...require('./data.json'), key4: 'value4'}, null, 2)" > data.json
Ben Chislett
  • 437
  • 6
  • 11
  • This follow-up to Lenny's answer did the trick in my case. I like that it is just a one-liner with no need to care for the shebang at the top of my script. However, for more complex manipulations it may be more appropriate to stay closer to Lenny's multi-line version. – Zaphod Beeblebrox Mar 16 '23 at 15:03
8

to change a file in place, use the sponge command

echo '{ "k": "old value" }' >f.json

cat f.json | jq '.k = $v' --arg v 'new value' | sponge f.json

see also: jq issue Edit files in place #105

alternative to jq: jaq

echo '{ "k": "old value" }' >f.json

jaq -i '.k = $v' --arg v 'new value' f.json

... but jaq has less features than jq

milahu
  • 2,447
  • 1
  • 18
  • 25
2

Here's a pure bash example, including the "comma issue".

#!/bin/bash
# This bash script just uses the sed command to 
#   replace/insert a new key at/before/after an 
#   existing key in a json file 
# The comma issue:
# - replace: with/without, as previous entry
# - before: always add
# - after: add before, if there was none
SED_CMD="/tmp/sed_cmd.tmp"
JSFILE1="./data1.json"
JSFILE2="./data2.json"
JSFILE3="./data3.json"
SEARCH_KEY="key3"
# create json input file
echo -e '{\n\t"key1": "value1",\n\t"key2": "value2",\n\t"key3": "value3"\n}' > $JSFILE1
echo -e "input:"
cat $JSFILE1
# duplicate twice
cp $JSFILE1 $JSFILE2 && cp $JSFILE1 $JSFILE3
# find the SEARCH_KEY and store the complete line to SEARCH_LINE 
SEARCH_LINE=`cat data.json | grep $SEARCH_KEY`
echo "SEARCH_LINE=>$SEARCH_LINE<"
# replace SEARCH_LINE
IS_COMMA=`echo $SEARCH_LINE | grep ","`
[ -z "$IS_COMMA" ] && \
    echo "s+$SEARCH_LINE+\t\"keyNew\": \"New\"+g" > $SED_CMD || \
    echo "s+$SEARCH_LINE+\t\"keyNew\": \"New\",+g" > $SED_CMD
sed -i -f $SED_CMD $JSFILE1
echo -e "replace:"
cat $JSFILE1
# insert before SEARCH_LINE
echo "s+$SEARCH_LINE+\t\"keyNew\": \"New\",\n$SEARCH_LINE+g" > $SED_CMD
sed -i -f $SED_CMD $JSFILE2
echo -e "before:"
cat $JSFILE2
# insert after SEARCH_LINE
IS_COMMA=`echo $SEARCH_LINE | grep ","`
[ -z "$IS_COMMA" ] && \
    echo "s+$SEARCH_LINE+$SEARCH_LINE,\n\t\"keyNew\": \"New\"+g" > $SED_CMD || \
    echo "s+$SEARCH_LINE+$SEARCH_LINE\n\t\"keyNew\": \"New\",+g" > $SED_CMD
sed -i -f $SED_CMD $JSFILE3
echo -e "after:"
cat $JSFILE3
exit 0
DekuDesu
  • 2,224
  • 1
  • 5
  • 19
  • Seems to work pretty well. Just as a hint this example script expects a data.json and generates data1.json data2.json and data3.json (one for each use case). – Saturas Apr 27 '22 at 11:34
-1

how can I add and remove a new key (i.e "key4": "value4") by bash script?

Using a dedicated JSON tool, like , would be a better idea than to use pure Bash functions.

Add a new attribute-value pair

xidel -s '{"a":1,"b":2,"c":3}' -e '($json).d:=4'                 # dot notation
xidel -s '{"a":1,"b":2,"c":3}' -e '{|$json,{"d":4}|}'            # JSONiq (deprecated)
xidel -s '{"a":1,"b":2,"c":3}' -e 'map:put($json,"d",4)'         # XQuery
xidel -s '{"a":1,"b":2,"c":3}' -e 'map:merge(($json,{"d":4}))'   # XQuery
{
  "a": 1,
  "b": 2,
  "c": 3,
  "d": 4
}

Remove the attribute-value pair "c":3

xidel -s '{"a":1,"b":2,"c":3}' --xmlns:jnlib="http://jsoniq.org/function-library" -e 'jnlib:remove-keys($json,"c")'   # JSONiq (deprecated)
xidel -s '{"a":1,"b":2,"c":3}' -e 'map:remove($json,"c")'   # XQuery
{
  "a": 1,
  "b": 2
}

Change value of "c" attribute to 4

xidel -s '{"a":1,"b":2,"c":3}' -e '($json).c:=4'
xidel -s '{"a":1,"b":2,"c":3}' -e 'map:put($json,"c",4)'
xidel -s '{"a":1,"b":2,"c":3}' -e 'map:merge(($json,{"c":4}),{"duplicates":"use-last"})'
{
  "a": 1,
  "b": 2,
  "c": 4
}
Reino
  • 3,203
  • 1
  • 13
  • 21