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