1"""YANG output plugin""" 2 3import optparse 4 5from .. import plugin 6from .. import util 7from .. import grammar 8 9def pyang_plugin_init(): 10 plugin.register_plugin(YANGPlugin()) 11 12class YANGPlugin(plugin.PyangPlugin): 13 def add_output_format(self, fmts): 14 fmts['yang'] = self 15 self.handle_comments = True 16 17 def add_opts(self, optparser): 18 optlist = [ 19 optparse.make_option("--yang-canonical", 20 dest="yang_canonical", 21 action="store_true", 22 help="Print in canonical order"), 23 optparse.make_option("--yang-remove-unused-imports", 24 dest="yang_remove_unused_imports", 25 action="store_true"), 26 optparse.make_option("--yang-line-length", 27 type="int", 28 dest="yang_line_length", 29 help="Maximum line length"), 30 ] 31 g = optparser.add_option_group("YANG output specific options") 32 g.add_options(optlist) 33 34 def setup_fmt(self, ctx): 35 ctx.implicit_errors = False 36 ctx.keep_arg_substrings = True 37 38 def emit(self, ctx, modules, fd): 39 module = modules[0] 40 emit_yang(ctx, module, fd) 41 42def emit_yang(ctx, module, fd): 43 emit_stmt(ctx, module, fd, 0, None, None, False, '', ' ') 44 45# always add newline between keyword and argument 46_force_newline_arg = ('description', 'reference', 'contact', 'organization') 47 48# do not quote these arguments 49_non_quote_arg_type = ('identifier', 'identifier-ref', 'boolean', 'integer', 50 'non-negative-integer', 'max-value', 51 'date', 'ordered-by-arg', 52 'fraction-digits-arg', 'deviate-arg', 'version', 53 'status-arg') 54 55_maybe_quote_arg_type = ('enum-arg', ) 56 57# add extra blank line after these, when they occur on the top level 58_keyword_with_trailing_blank_line_toplevel = ( 59 'identity', 60 'feature', 61 'extension', 62 'rpc', 63 'notification', 64 'augment', 65 'deviation', 66 ) 67 68# always add extra blank line after these 69_keyword_with_trailing_blank_line = ( 70 'typedef', 71 'grouping', 72 ) 73 74# use single quote for the arguments to these keywords (if possible) 75_keyword_prefer_single_quote_arg = ( 76 'must', 77 'when', 78 'pattern', 79) 80 81_keyword_with_path_arg = ( 82## FIXME: change these when emit_path_arg does a good job 83# 'augment', 84# 'refine', 85# 'deviation', 86## FIXME: uncomment this and run tests/test_yang/g--yang-line-length_50 87## it doesn't look good 88# 'path', 89) 90 91_kwd_class = { 92 'yang-version': 'header', 93 'namespace': 'header', 94 'prefix': 'header', 95 'belongs-to': 'header', 96 'organization': 'meta', 97 'contact': 'meta', 98 'description': 'meta', 99 'reference': 'meta', 100 'import': 'linkage', 101 'include': 'linkage', 102 'revision': 'revision', 103 'typedef': 'defs', 104 'grouping': 'defs', 105 'identity': 'defs', 106 'feature': 'defs', 107 'extension': 'defs', 108 '_comment': 'comment', 109 'augment': 'augment', 110 'rpc': 'rpc', 111 'notification': 'notification', 112 'deviation': 'deviation', 113 'module': None, 114 'submodule': None, 115} 116def get_kwd_class(keyword): 117 if util.is_prefixed(keyword): 118 return 'extension' 119 else: 120 try: 121 return _kwd_class[keyword] 122 except KeyError: 123 return 'body' 124 125_need_quote = ( 126 " ", "}", "{", ";", '"', "'", 127 "\n", "\t", "\r", "//", "/*", "*/", 128 ) 129 130def emit_stmt(ctx, stmt, fd, level, prev_kwd, prev_kwd_class, islast, 131 indent, indentstep): 132 if ctx.opts.yang_remove_unused_imports and stmt.keyword == 'import': 133 for p in stmt.parent.i_unused_prefixes: 134 if stmt.parent.i_unused_prefixes[p] == stmt: 135 return 136 137 max_line_len = ctx.opts.yang_line_length 138 if util.is_prefixed(stmt.raw_keyword): 139 (prefix, identifier) = stmt.raw_keyword 140 keywordstr = prefix + ':' + identifier 141 else: 142 keywordstr = stmt.keyword 143 144 kwd_class = get_kwd_class(stmt.keyword) 145 if ((level == 1 and 146 kwd_class != prev_kwd_class and kwd_class != 'extension') and 147 not ((level == 1 and prev_kwd in 148 _keyword_with_trailing_blank_line_toplevel) or 149 prev_kwd in _keyword_with_trailing_blank_line)): 150 fd.write('\n') 151 152 if stmt.keyword == '_comment': 153 emit_comment(stmt.arg, fd, indent) 154 return 155 156 fd.write(indent + keywordstr) 157 arg_on_new_line = False 158 if len(stmt.substmts) == 0: 159 eol = ';' 160 else: 161 eol = ' {' 162 if stmt.arg is not None: 163 # line_len is length of line w/o arg but with quotes and space before 164 # the arg 165 line_len = len(indent) + len(keywordstr) + 1 + 2 + len(eol) 166 if (stmt.keyword in _keyword_prefer_single_quote_arg and 167 stmt.arg.find("'") == -1): 168 # print with single quotes 169 if hasattr(stmt, 'arg_substrings') and len(stmt.arg_substrings) > 1: 170 # the arg was already split into multiple lines, keep them 171 emit_multi_str_arg(keywordstr, stmt.arg_substrings, fd, "'", 172 indent, indentstep, max_line_len, line_len) 173 elif not(need_new_line(max_line_len, line_len, stmt.arg)): 174 # fits into a single line 175 fd.write(" '" + stmt.arg + "'") 176 else: 177 # otherwise, print on new line, don't check line length 178 # since we can't break the string into multiple lines 179 fd.write('\n' + indent + indentstep) 180 fd.write("'" + stmt.arg + "'") 181 arg_on_new_line = True 182 elif hasattr(stmt, 'arg_substrings') and len(stmt.arg_substrings) > 1: 183 # the arg was already split into multiple lines, keep them 184 emit_multi_str_arg(keywordstr, stmt.arg_substrings, fd, '"', 185 indent, indentstep, max_line_len, line_len) 186 elif '\n' in stmt.arg: 187 # the arg string contains newlines; print it as double quoted 188 arg_on_new_line = emit_arg(keywordstr, stmt, fd, indent, indentstep, 189 max_line_len, line_len) 190 elif stmt.keyword in _keyword_with_path_arg: 191 # special code for path argument; pretty-prints a long path with 192 # line breaks 193 arg_on_new_line = emit_path_arg(keywordstr, stmt.arg, fd, 194 indent, max_line_len, line_len, eol) 195 elif stmt.keyword in grammar.stmt_map: 196 (arg_type, _subspec) = grammar.stmt_map[stmt.keyword] 197 if (arg_type in _non_quote_arg_type or 198 (arg_type in _maybe_quote_arg_type and 199 not need_quote(stmt.arg))): 200 # minus 2 since we don't quote 201 if not(need_new_line(max_line_len, line_len-2, stmt.arg)): 202 fd.write(' ' + stmt.arg) 203 else: 204 fd.write('\n' + indent + indentstep + stmt.arg) 205 arg_on_new_line = True 206 else: 207 arg_on_new_line = emit_arg(keywordstr, stmt, fd, 208 indent, indentstep, 209 max_line_len, line_len) 210 else: 211 arg_on_new_line = emit_arg(keywordstr, stmt, fd, indent, indentstep, 212 max_line_len, line_len) 213 fd.write(eol + '\n') 214 215 if len(stmt.substmts) > 0: 216 if ctx.opts.yang_canonical: 217 substmts = grammar.sort_canonical(stmt.keyword, stmt.substmts) 218 else: 219 substmts = stmt.substmts 220 if level == 0: 221 kwd_class = 'header' 222 prev_kwd = None 223 for i, s in enumerate(substmts, start=1): 224 n = 1 225 if arg_on_new_line: 226 # arg was printed on a new line, increase indentation 227 n = 2 228 emit_stmt(ctx, s, fd, level + 1, prev_kwd, kwd_class, 229 i == len(substmts), 230 indent + (indentstep * n), indentstep) 231 kwd_class = get_kwd_class(s.keyword) 232 prev_kwd = s.keyword 233 fd.write(indent + '}\n') 234 235 if (not(islast) and 236 ((level == 1 and stmt.keyword in 237 _keyword_with_trailing_blank_line_toplevel) or 238 stmt.keyword in _keyword_with_trailing_blank_line)): 239 fd.write('\n') 240 241def need_new_line(max_line_len, line_len, arg): 242 eol = arg.find('\n') 243 if eol == -1: 244 eol = len(arg) 245 if (max_line_len is not None and line_len + eol > max_line_len): 246 return True 247 else: 248 return False 249 250def emit_multi_str_arg(keywordstr, strs, fd, pref_q, 251 indent, indentstep, max_line_len, 252 line_len): 253 # we want to align all strings on the same column; check if 254 # we can print w/o a newline 255 need_new_line = False 256 if max_line_len is not None: 257 for (s, q) in strs: 258 q = select_quote(s, q, pref_q) 259 if q == '"': 260 s = escape_str(s) 261 if line_len + len(s) > max_line_len: 262 need_new_line = True 263 break 264 if need_new_line: 265 fd.write('\n' + indent + indentstep) 266 prefix = (len(indent) - 2) * ' ' + indentstep + '+ ' 267 else: 268 fd.write(' ') 269 prefix = indent + ((len(keywordstr) - 1) * ' ') + '+ ' 270 # print first substring 271 (s, q) = strs[0] 272 q = select_quote(s, q, pref_q) 273 if q == '"': 274 s = escape_str(s) 275 fd.write("%s%s%s\n" % (q, s, q)) 276 # then print the rest with the prefix and a newline at the end 277 for (s, q) in strs[1:-1]: 278 q = select_quote(s, q, pref_q) 279 if q == '"': 280 s = escape_str(s) 281 fd.write("%s%s%s%s\n" % (prefix, q, s, q)) 282 # then print last substring with prefix but no newline 283 (s, q) = strs[-1] 284 q = select_quote(s, q, pref_q) 285 if q == '"': 286 s = escape_str(s) 287 fd.write("%s%s%s%s" % (prefix, q, s, q)) 288 289 return need_new_line 290 291def select_quote(s, q, pref_q): 292 if pref_q == q: 293 return q 294 elif pref_q == "'": 295 if s.find("'") == -1: 296 # the string was double quoted, but it wasn't necessary, 297 # use preferred single quote 298 return "'" 299 else: 300 # the string was double quoted for a reason, keep it 301 return '"' 302 elif q == "'": 303 if need_quote(s): 304 # the string was single quoted for a reason, keep it 305 return "'" 306 else: 307 # the string was single quoted but it wasn't necessary, 308 # use preferred double quote 309 return '"' 310 311def escape_str(s): 312 s = s.replace('\\', r'\\') 313 s = s.replace('"', r'\"') 314 s = s.replace('\t', r'\t') 315 return s 316 317def emit_path_arg(keywordstr, arg, fd, indent, max_line_len, line_len, eol): 318 """Heuristically pretty print a path argument""" 319 320 quote = '"' 321 322 arg = escape_str(arg) 323 324 if not(need_new_line(max_line_len, line_len, arg)): 325 fd.write(" " + quote + arg + quote) 326 return False 327 328 ## FIXME: we should split the path on '/' and '[]' into multiple lines 329 ## and then print each line 330 331 num_chars = max_line_len - line_len 332 if num_chars <= 0: 333 # really small max_line_len; we give up 334 fd.write(" " + quote + arg + quote) 335 return False 336 337 while num_chars > 2 and arg[num_chars - 1:num_chars].isalnum(): 338 num_chars -= 1 339 fd.write(" " + quote + arg[:num_chars] + quote) 340 arg = arg[num_chars:] 341 keyword_cont = ((len(keywordstr) - 1) * ' ') + '+' 342 while arg != '': 343 line_len = len( 344 "%s%s %s%s%s%s" % (indent, keyword_cont, quote, arg, quote, eol)) 345 num_chars = len(arg) - (line_len - max_line_len) 346 while num_chars > 2 and arg[num_chars - 1:num_chars].isalnum(): 347 num_chars -= 1 348 fd.write('\n' + indent + keyword_cont + " " + 349 quote + arg[:num_chars] + quote) 350 arg = arg[num_chars:] 351 352def emit_arg(keywordstr, stmt, fd, indent, indentstep, max_line_len, line_len): 353 """Heuristically pretty print the argument string with double quotes""" 354 arg = escape_str(stmt.arg) 355 lines = arg.splitlines(True) 356 if len(lines) <= 1: 357 if len(arg) > 0 and arg[-1] == '\n': 358 arg = arg[:-1] + r'\n' 359 if (stmt.keyword in _force_newline_arg or 360 need_new_line(max_line_len, line_len, arg)): 361 fd.write('\n' + indent + indentstep + '"' + arg + '"') 362 return True 363 else: 364 fd.write(' "' + arg + '"') 365 return False 366 else: 367 need_nl = False 368 if stmt.keyword in _force_newline_arg: 369 need_nl = True 370 elif len(keywordstr) > 8: 371 # Heuristics: multi-line after a "long" keyword looks better 372 # than after a "short" keyword (compare 'when' and 'error-message') 373 need_nl = True 374 else: 375 for line in lines: 376 if need_new_line(max_line_len, line_len + 1, line): 377 need_nl = True 378 break 379 if need_nl: 380 fd.write('\n' + indent + indentstep) 381 prefix = indent + indentstep 382 else: 383 fd.write(' ') 384 prefix = indent + len(keywordstr) * ' ' + ' ' 385 fd.write('"' + lines[0]) 386 for line in lines[1:-1]: 387 if line[0] == '\n': 388 fd.write('\n') 389 else: 390 fd.write(prefix + ' ' + line) 391 # write last line 392 fd.write(prefix + ' ' + lines[-1]) 393 if lines[-1][-1] == '\n': 394 # last line ends with a newline, indent the ending quote 395 fd.write(prefix + '"') 396 else: 397 fd.write('"') 398 return True 399 400def emit_comment(comment, fd, indent): 401 lines = comment.splitlines(True) 402 for x in lines: 403 if x[0] == '*': 404 fd.write(indent + ' ' + x) 405 else: 406 fd.write(indent + x) 407 fd.write('\n') 408 409def need_quote(arg): 410 for ch in _need_quote: 411 if arg.find(ch) != -1: 412 return True 413 return False 414