1#!/usr/bin/env python
2#
3# Copyright (C) 2019 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17
18import argparse
19import collections
20import json
21import sys
22
23def follow_path(obj, path):
24  cur = obj
25  last_key = None
26  for key in path.split('.'):
27    if last_key:
28      if last_key not in cur:
29        return None,None
30      cur = cur[last_key]
31    last_key = key
32  if last_key not in cur:
33    return None,None
34  return cur, last_key
35
36
37def ensure_path(obj, path):
38  cur = obj
39  last_key = None
40  for key in path.split('.'):
41    if last_key:
42      if last_key not in cur:
43        cur[last_key] = dict()
44      cur = cur[last_key]
45    last_key = key
46  return cur, last_key
47
48
49class SetValue(str):
50  def apply(self, obj, val):
51    cur, key = ensure_path(obj, self)
52    cur[key] = val
53
54
55class Replace(str):
56  def apply(self, obj, val):
57    cur, key = follow_path(obj, self)
58    if cur:
59      cur[key] = val
60
61
62class ReplaceIfEqual(str):
63  def apply(self, obj, old_val, new_val):
64    cur, key = follow_path(obj, self)
65    if cur and cur[key] == int(old_val):
66      cur[key] = new_val
67
68
69class Remove(str):
70  def apply(self, obj):
71    cur, key = follow_path(obj, self)
72    if cur:
73      del cur[key]
74
75
76class AppendList(str):
77  def apply(self, obj, *args):
78    cur, key = ensure_path(obj, self)
79    if key not in cur:
80      cur[key] = list()
81    if not isinstance(cur[key], list):
82      raise ValueError(self + " should be a array.")
83    cur[key].extend(args)
84
85# A JSONDecoder that supports line comments start with //
86class JSONWithCommentsDecoder(json.JSONDecoder):
87  def __init__(self, **kw):
88    super().__init__(**kw)
89
90  def decode(self, s: str):
91    s = '\n'.join(l for l in s.split('\n') if not l.lstrip(' ').startswith('//'))
92    return super().decode(s)
93
94def main():
95  parser = argparse.ArgumentParser()
96  parser.add_argument('-o', '--out',
97                      help='write result to a file. If omitted, print to stdout',
98                      metavar='output',
99                      action='store')
100  parser.add_argument('input', nargs='?', help='JSON file')
101  parser.add_argument("-v", "--value", type=SetValue,
102                      help='set value of the key specified by path. If path doesn\'t exist, creates new one.',
103                      metavar=('path', 'value'),
104                      nargs=2, dest='patch', default=[], action='append')
105  parser.add_argument("-s", "--replace", type=Replace,
106                      help='replace value of the key specified by path. If path doesn\'t exist, no op.',
107                      metavar=('path', 'value'),
108                      nargs=2, dest='patch', action='append')
109  parser.add_argument("-se", "--replace-if-equal", type=ReplaceIfEqual,
110                      help='replace value of the key specified by path to new_value if it\'s equal to old_value.' +
111                      'If path doesn\'t exist or the value is not equal to old_value, no op.',
112                      metavar=('path', 'old_value', 'new_value'),
113                      nargs=3, dest='patch', action='append')
114  parser.add_argument("-r", "--remove", type=Remove,
115                      help='remove the key specified by path. If path doesn\'t exist, no op.',
116                      metavar='path',
117                      nargs=1, dest='patch', action='append')
118  parser.add_argument("-a", "--append_list", type=AppendList,
119                      help='append values to the list specified by path. If path doesn\'t exist, creates new list for it.',
120                      metavar=('path', 'value'),
121                      nargs='+', dest='patch', default=[], action='append')
122  args = parser.parse_args()
123
124  if args.input:
125    with open(args.input) as f:
126      obj = json.load(f, object_pairs_hook=collections.OrderedDict, cls=JSONWithCommentsDecoder)
127  else:
128    obj = json.load(sys.stdin, object_pairs_hook=collections.OrderedDict, cls=JSONWithCommentsDecoder)
129
130  for p in args.patch:
131    p[0].apply(obj, *p[1:])
132
133  if args.out:
134    with open(args.out, "w") as f:
135      json.dump(obj, f, indent=2, separators=(',', ': '))
136      f.write('\n')
137  else:
138    print(json.dumps(obj, indent=2, separators=(',', ': ')))
139
140
141if __name__ == '__main__':
142  main()
143