File: bj.py 1 #!/usr/bin/python 2 3 # The MIT License (MIT) 4 # 5 # Copyright (c) 2026 pacman64 6 # 7 # Permission is hereby granted, free of charge, to any person obtaining a copy 8 # of this software and associated documentation files (the "Software"), to deal 9 # in the Software without restriction, including without limitation the rights 10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 # copies of the Software, and to permit persons to whom the Software is 12 # furnished to do so, subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be included in 15 # all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 # SOFTWARE. 24 25 26 from curses import ( 27 cbreak, curs_set, endwin, initscr, noecho, resetty, savetty, set_escdelay, 28 A_NORMAL, A_REVERSE, 29 ) 30 from json import dumps, load, loads 31 from os import dup2 32 from sys import argv, stderr, stdin 33 34 35 info = ''' 36 bj [options...] [file...] 37 38 Browse Json is a text user-interface (TUI) which lets interactively pick/zoom 39 JSON input data. Picking anything is optional, so the final result on stdout 40 is either nothing or valid JSON, assuming the input is valid JSON. 41 42 All (optional) leading options start with either single or double-dash: 43 44 -c, -compact, -json0 emit compact JSON output 45 -d, -default [json] default JSON string, when not picking anything 46 -f, -fallback [json] fallback JSON string, same as `-d`, or `-default` 47 -h, -help show this help message 48 -v, -verbose also show gron-style path on the standard error 49 50 You can also view this help message by pressing the F1 key while browsing 51 folders: you can exit the help viewer via any of F1, F10, F12, or Escape. 52 ''' 53 54 55 class SimpleTUI: 56 ''' 57 Manager to start/stop a no-color text user-interface (TUI), allowing for 58 standard input/output to be used normally before method `start` is called 59 and after method `stop` is called. After calling is method `start`, its 60 field `screen` has the ncurses value for all the interactive input-output. 61 ''' 62 63 def __init__(self): 64 self.screen = None 65 66 def start(self, out_fd = -1, esc_delay = -1): 67 ''' 68 Start interactive-mode: the first optional argument should be more 69 than 2, if given, since it would mess with stdio, which is precisely 70 what it's meant to avoid doing. 71 ''' 72 73 if out_fd >= 0: 74 from os import dup2 75 76 # keep original stdout as /dev/fd/... 77 dup2(1, out_fd) 78 # separate live output from final (optional) result on stdout 79 with open('/dev/tty', 'rb') as inp, open('/dev/tty', 'wb') as out: 80 dup2(inp.fileno(), 0) 81 dup2(out.fileno(), 1) 82 83 self.screen = initscr() 84 savetty() 85 noecho() 86 cbreak() 87 self.screen.keypad(True) 88 curs_set(0) 89 if esc_delay >= 0: 90 set_escdelay(esc_delay) 91 92 def stop(self): 93 'Stop interactive-mode.' 94 if self.screen: 95 resetty() 96 endwin() 97 98 99 class ValueBrowserTUI: 100 ''' 101 This is a scrollable viewer to browse JSON fields. After initializing it 102 with a TUI screen value, you can call its method `run`. 103 ''' 104 105 def __init__(self, screen, quit_set = ('KEY_F(10)', 'KEY_F(12)', '\x1b')): 106 'Optional argument controls which ncurses keys quit the viewer.' 107 108 self.data = None 109 self.max_view_size = -1 110 self.side_step = 1 111 self.handlers = { 112 'KEY_RESIZE': lambda: self._on_resize(), 113 'KEY_UP': lambda: self._on_up(), 114 'KEY_DOWN': lambda: self._on_down(), 115 'KEY_NPAGE': lambda: self._on_page_down(), 116 'KEY_PPAGE': lambda: self._on_page_up(), 117 'KEY_HOME': lambda: self._on_home(), 118 'KEY_END': lambda: self._on_end(), 119 'KEY_LEFT': lambda: self._on_left(), 120 'KEY_RIGHT': lambda: self._on_right(), 121 'KEY_BACKSPACE': lambda: self._on_left(), 122 '\t': lambda: self._on_right(), 123 } 124 if quit_set: 125 for k in quit_set: 126 self.handlers[k] = None 127 128 self._screen = screen 129 self._inner_width = 0 130 self._inner_height = 0 131 self._max_line_width = 0 132 self._pick = 0 133 self._max_top = 0 134 self._max_left = 0 135 self._path = [] 136 self._stack = [] 137 self._values = [] 138 self.pick = None 139 140 def run(self): 141 'Interactively view/browse the fields/items in the value given.' 142 143 self._path = [] 144 self._stack = [] 145 self._values = [] 146 res = self._run() 147 self.data = None 148 self._path = [] 149 self._stack = [] 150 self._values = [] 151 return res 152 153 def _run(self): 154 self._on_resize() 155 self._cache_entries() 156 157 while True: 158 self._redraw() 159 k = self._screen.getkey() 160 if k == '\n': 161 if len(self._stack) == 0: 162 return ('data', self.data, k) 163 _, pick = self._get() 164 165 from io import StringIO 166 167 sb = StringIO() 168 self._gron(sb) 169 return (sb.getvalue(), pick, k) 170 171 if k in self.handlers: 172 h = self.handlers[k] 173 if h is None or h() is False: 174 return (None, None, None) 175 elif len(k) == 1: 176 i = self._seek(k, self._pick + 1) 177 if i < 0: 178 i = self._seek(k, 0) 179 if i >= 0: 180 self._pick = i 181 182 def _fit_string(self, s): 183 maxlen = max(self._inner_width, 0) 184 return s if len(s) <= maxlen else s[:maxlen] 185 186 def _is_ident(self, s: str): 187 if len(s) == 0 or '0' <= s[0] <= '9': 188 return False 189 return all(e.isalnum() or e == '_' for e in s) 190 191 def _gron(self, sb): 192 from itertools import islice 193 from json import dumps 194 195 sb.write('data') 196 for k in islice(self._path, 1, None): 197 if k is None: 198 continue 199 if isinstance(k, int): 200 sb.write('[') 201 sb.write(str(k)) 202 sb.write(']') 203 else: 204 if self._is_ident(k): 205 sb.write('.') 206 sb.write(k) 207 else: 208 sb.write('[') 209 sb.write(dumps(k)) 210 sb.write(']') 211 212 def _redraw(self): 213 screen = self._screen 214 iw = self._inner_width 215 ih = self._inner_height 216 217 if iw < 10 or ih < 10: 218 return 219 220 from io import StringIO 221 222 sb = StringIO() 223 if self.title: 224 sb.write(self.title) 225 sb.write(': ') 226 self._gron(sb) 227 228 if len(self._stack) > 1: 229 title = sb.getvalue() 230 elif self.title: 231 title = f'{self.title}: {self._describe_type(self.data)}' 232 else: 233 title = self._describe_type(self.data) 234 235 screen.erase() 236 237 if not isinstance(self.data, (dict, list, tuple)): 238 if title: 239 screen.addstr(0, 0, f'{title:<{iw}}') 240 screen.addstr(1, 2, self._values[0], A_REVERSE) 241 screen.refresh() 242 return 243 244 if title: 245 style = A_NORMAL if len(self._stack) > 0 else A_REVERSE 246 screen.addstr(0, 0, f'{title:<{iw}}', style) 247 248 if len(self._stack) == 0: 249 screen.refresh() 250 return 251 252 end = self._current_value() 253 n = max(self._count_entries(), 1) 254 start = self._pick - (self._pick % ih) 255 stop = min(start + ih, n) 256 257 if isinstance(end, (list, tuple)): 258 n = len(end) 259 if n > 0: 260 from math import ceil, log10 261 w = int(ceil(log10(n - 1))) if n > 1 else 1 262 msg = f'({self._pick:>{w},} < {n:,})' 263 style = A_NORMAL 264 else: 265 style = A_REVERSE 266 msg = '[]' 267 screen.addstr(0, iw - len(msg), self._fit_string(msg), style) 268 self._redraw_array_entries(end) 269 elif isinstance(end, dict): 270 n = len(end) 271 msg = f'({n:,} keys)' if n > 0 else '{}' 272 style = A_NORMAL if n > 0 else A_REVERSE 273 screen.addstr(0, iw - len(msg), self._fit_string(msg), style) 274 self._redraw_object_entries(end) 275 else: 276 self._redraw_value(end) 277 278 # show up/down arrows 279 if start > 0 and n > 0: 280 self._screen.addstr(1, iw - 1, '▲') 281 if stop < n and n > 0: 282 self._screen.addstr(ih, iw - 1, '▼') 283 284 screen.refresh() 285 286 def _redraw_array_entries(self, data, indent=2): 287 screen = self._screen 288 iw = self._inner_width 289 ih = self._inner_height 290 start = self._pick - (self._pick % ih) 291 stop = min(start + ih, len(data)) 292 293 for i in range(start, stop): 294 j = i - start 295 try: 296 style = A_REVERSE if j == self._pick % ih else A_NORMAL 297 s = self._values[j] 298 screen.addnstr(j + 1, indent, s, iw - indent, style) 299 except Exception as _: 300 # some utf-8 files have lines which upset func addstr 301 screen.addnstr(j + 1, 0, '?' * 20, iw, style) 302 303 def _redraw_object_entries(self, data, indent=2): 304 from itertools import islice 305 306 screen = self._screen 307 iw = self._inner_width 308 ih = self._inner_height 309 start = self._pick - (self._pick % ih) 310 stop = min(start + ih, len(data)) 311 maxw = 0 312 for k in islice(data.keys(), start, stop): 313 maxw = max(maxw, len(k)) 314 315 for i, k in enumerate(islice(data.keys(), start, stop), start): 316 j = i - start 317 try: 318 style = A_REVERSE if j == self._pick % ih else A_NORMAL 319 screen.addnstr(j + 1, indent, k, iw - indent, A_NORMAL) 320 v = self._values[j] 321 x = indent + maxw + 2 322 screen.addnstr(j + 1, x, v, iw - x, style) 323 except Exception as _: 324 # some utf-8 files have lines which upset func addstr 325 screen.addnstr(j + 1, 0, '?' * 20, iw, style) 326 327 def _redraw_value(self, data, indent=2): 328 screen = self._screen 329 iw = self._inner_width 330 style = A_REVERSE 331 332 try: 333 screen.addnstr(1, indent, self._values[0], iw - indent, style) 334 except Exception as _: 335 # some utf-8 files have lines which upset func addstr 336 screen.addnstr(1, 0, '?' * 20, iw) 337 338 def _cache_entries(self): 339 from itertools import islice 340 341 iw = self._inner_width 342 ih = self._inner_height 343 end = self._current_value() 344 345 if len(self._stack) == 0 and len(self._values) == 0: 346 self._values = tuple([self._json0(self.data)[:self._inner_width]]) 347 return 348 349 if isinstance(end, (list, tuple)): 350 start = self._pick - (self._pick % ih) 351 stop = min(start + ih, len(end)) 352 self._values = tuple( 353 self._json0(e)[:iw] for e in islice(end, start, stop) 354 ) 355 elif isinstance(end, dict): 356 start = self._pick - (self._pick % ih) 357 stop = min(start + ih, len(end)) 358 self._values = tuple( 359 self._json0(v)[:iw] for v in islice(end.values(), start, stop) 360 ) 361 else: 362 self._values = tuple([self._json0(end)[:iw]]) 363 364 def _change_selection(self, new): 365 new = max(new, 0) 366 limit = max(self._count_entries() - 1, 0) 367 new = min(new, limit) 368 369 if not self._in_page(new): 370 self._pick = new 371 self._cache_entries() 372 else: 373 self._pick = new 374 375 def _in_page(self, new): 376 end = self._current_value() 377 378 if isinstance(end, (dict, list, tuple)): 379 ih = self._inner_height 380 start = self._pick - (self._pick % ih) 381 stop = min(start + ih, len(end)) 382 return start <= new < stop 383 else: 384 return new == 0 385 386 def _count_entries(self): 387 end = self._current_value() 388 n = len(end) if isinstance(end, (dict, list, tuple)) else -1 389 return n 390 391 def _current_value(self): 392 return self._stack[-1] if len(self._stack) > 0 else self.data 393 394 def _describe_type(self, data): 395 kind = { 396 bool: 'boolean', 397 int: 'number', 398 float: 'number', 399 str: 'string', 400 list: 'array', 401 tuple: 'array', 402 dict: 'object', 403 }.get(type(data), '?') 404 return f'{kind} ({len(data)})' if kind in ('array', 'object') else kind 405 406 def _json0(self, data): 407 from json import dumps 408 return dumps(data, indent=None, separators=(', ', ': '), 409 allow_nan=False, check_circular=False) 410 411 def _seek(self, k, start): 412 from itertools import islice 413 414 end = self._current_value() 415 if not isinstance(end, dict): 416 return -1 417 if len(k) != 1: 418 return -1 419 420 k = k.lower() 421 for i, e in enumerate(islice(end.keys(), start, None)): 422 name = e[0] 423 if name.startswith(k) or name.lower().startswith(k): 424 return start + i 425 return -1 426 427 def _on_resize(self): 428 height, width = self._screen.getmaxyx() 429 self._inner_width = width - 1 430 self._inner_height = height - 1 431 n = max(self._count_entries(), 1) 432 self._max_top = max(n - self._inner_height, 0) 433 ss = self.side_step 434 self._max_left = self._max_line_width - self._inner_width - 1 + ss 435 self._max_left = max(self._max_left, 0) 436 if self._max_left >= self._inner_width - 1 + ss: 437 self._max_left = 0 438 self._cache_entries() 439 440 def _on_up(self): 441 if len(self._stack) == 0: 442 self._on_right() 443 else: 444 self._change_selection(self._pick - 1) 445 446 def _on_down(self): 447 if len(self._stack) == 0: 448 self._on_right() 449 else: 450 self._change_selection(self._pick + 1) 451 452 def _on_page_up(self): 453 if len(self._stack) == 0: 454 self._on_right() 455 else: 456 self._change_selection(self._pick - self._inner_height) 457 458 def _on_page_down(self): 459 if len(self._stack) == 0: 460 self._on_right() 461 else: 462 self._change_selection(self._pick + self._inner_height) 463 464 def _on_home(self): 465 if len(self._stack) == 0: 466 self._on_right() 467 else: 468 self._change_selection(0) 469 470 def _on_end(self): 471 if len(self._stack) == 0: 472 self._on_right() 473 else: 474 self._change_selection(self._count_entries() - 1) 475 476 def _on_left(self): 477 if len(self._stack) > 0: 478 pick = self._path.pop() 479 self._stack.pop() 480 self._pick = 0 481 if isinstance(pick, int): 482 self._pick = pick 483 elif len(self._stack) > 0: 484 end = self._stack[-1] 485 for i, k in enumerate(end.keys()): 486 if k == pick: 487 self._pick = i 488 break 489 self._cache_entries() 490 491 def _on_right(self): 492 if len(self._stack) == 0: 493 self._path.append(None) 494 self._stack.append(self.data) 495 self._pick = 0 496 self._cache_entries() 497 return 498 499 end = self._current_value() 500 if isinstance(end, (dict, list, tuple)): 501 k, v = self._get() 502 self._path.append(k) 503 self._stack.append(v) 504 self._pick = 0 505 self._cache_entries() 506 507 def _get(self): 508 end = self._current_value() 509 if len(self._stack) == 0: 510 return (None, end) 511 512 if isinstance(end, (list, tuple)): 513 i = self._pick 514 return (i, end[i]) if 0 <= i < len(end) else (None, None) 515 516 if isinstance(end, dict): 517 for i, k in enumerate(end.keys()): 518 if i == self._pick: 519 return (k, end[k]) 520 return (None, None) 521 522 return (0, end) 523 524 525 class TextViewerTUI: 526 ''' 527 This is a scrollable viewer for plain-text content. After initializing it 528 with a TUI screen value, you can configure various fields, before running 529 it by calling method `run`: 530 - title, which is shown at the top in reverse-style 531 - tab_stop, which controls how tabs are turned into spaces 532 - side_step, which controls the speed of lateral side-scrolling 533 - handlers, which has all ncurses key-bindings for the viewer 534 ''' 535 536 def __init__(self, screen, quit_set = ('KEY_F(10)', 'KEY_F(12)', '\x1b')): 537 'Optional argument controls which ncurses keys quit the viewer.' 538 539 self.title = '' 540 self.tab_stop = 4 541 self.side_step = 1 542 self.handlers = { 543 'KEY_RESIZE': lambda: self._on_resize(), 544 'KEY_UP': lambda: self._on_up(), 545 'KEY_DOWN': lambda: self._on_down(), 546 'KEY_NPAGE': lambda: self._on_page_down(), 547 'KEY_PPAGE': lambda: self._on_page_up(), 548 'KEY_HOME': lambda: self._on_home(), 549 'KEY_END': lambda: self._on_end(), 550 'KEY_LEFT': lambda: self._on_left(), 551 'KEY_RIGHT': lambda: self._on_right(), 552 } 553 if quit_set: 554 for k in quit_set: 555 self.handlers[k] = None 556 557 self._screen = screen 558 self._inner_width = 0 559 self._inner_height = 0 560 self._max_line_width = 0 561 self._top = 0 562 self._left = 0 563 self._max_top = 0 564 self._max_left = 0 565 self._lines = tuple() 566 567 def run(self, content): 568 'Interactively view/browse the string/strings given.' 569 570 if isinstance(content, BaseException): 571 self._on_resize() 572 self._show_error(content) 573 self._screen.getkey() 574 return 575 576 ts = self.tab_stop 577 if isinstance(content, str): 578 self._lines = tuple(l.expandtabs(ts) for l in content.splitlines()) 579 else: 580 self._lines = tuple(l.expandtabs(ts) for l in content) 581 content = '' # try to deallocate a few MBs when viewing big files 582 583 if len(self._lines) == 0: 584 self._max_line_width = 0 585 else: 586 self._max_line_width = max(len(l) for l in self._lines) 587 self._on_resize() 588 589 iw = self._inner_width 590 ih = self._inner_height 591 592 if iw < 10 or ih < 10: 593 return 594 595 while True: 596 self._redraw() 597 k = self._screen.getkey() 598 if self.handlers and (k in self.handlers): 599 h = self.handlers[k] 600 if (h is None) or (h() is False): 601 self._lines = tuple() 602 return k 603 604 def _fit_string(self, s): 605 maxlen = max(self._inner_width, 0) 606 return s if len(s) <= maxlen else s[:maxlen] 607 608 def _redraw(self): 609 title = self._fit_string(self.title) 610 lines = self._lines 611 screen = self._screen 612 iw = self._inner_width 613 ih = self._inner_height 614 615 if iw < 10 or ih < 10: 616 return 617 618 screen.erase() 619 620 if title: 621 screen.addstr(0, 0, f'{title:<{iw}}', A_REVERSE) 622 623 at_bottom = len(self._lines) - self._top <= ih 624 if at_bottom: 625 msg = f'END ({len(lines):,})' if len(lines) > 0 else '(empty)' 626 else: 627 from math import ceil, log10 628 n = max(len(lines) - 1, 0) 629 w = int(ceil(log10(n))) if n > 0 else 1 630 msg = f'({self._top + 1:>{w},} / {n:,})' 631 screen.addstr(0, iw - len(msg), self._fit_string(msg), A_REVERSE) 632 633 from itertools import islice 634 635 for i, l in enumerate(islice(lines, self._top, self._top + ih)): 636 if self._left > 0: 637 l = l[self._left:] 638 try: 639 screen.addnstr(i + 1, 0, l, iw) 640 except Exception: 641 # some utf-8 files have lines which upset func addstr 642 screen.addnstr(i + 1, 0, '?' * len(l), iw) 643 644 # show up/down arrows 645 if self._top > 0: 646 self._screen.addstr(1, iw - 1, '▲') 647 if self._top < self._max_top: 648 self._screen.addstr(ih, iw - 1, '▼') 649 650 screen.refresh() 651 652 def _show_error(self, err): 653 title = self._fit_string(self.title) 654 screen = self._screen 655 iw = self._inner_width 656 ih = self._inner_height 657 658 if iw < 10 or ih < 10: 659 return 660 661 screen.erase() 662 if title: 663 screen.addstr(0, 0, f'{title:<{iw}}', A_REVERSE) 664 screen.addstr(2, 0, self._fit_string(str(err)), A_REVERSE) 665 screen.refresh() 666 667 def _on_resize(self): 668 height, width = self._screen.getmaxyx() 669 self._inner_width = width - 1 670 self._inner_height = height - 1 671 self._max_top = max(len(self._lines) - self._inner_height, 0) 672 ss = self.side_step 673 self._max_left = self._max_line_width - self._inner_width - 1 + ss 674 self._max_left = max(self._max_left, 0) 675 676 def _on_up(self): 677 self._top = max(self._top - 1, 0) 678 679 def _on_down(self): 680 self._top = min(self._top + 1, self._max_top) 681 682 def _on_page_up(self): 683 self._top = max(self._top - self._inner_height, 0) 684 685 def _on_page_down(self): 686 self._top = min(self._top + self._inner_height, self._max_top) 687 688 def _on_home(self): 689 self._top = 0 690 691 def _on_end(self): 692 self._top = self._max_top 693 694 def _on_left(self): 695 self._left = max(self._left - self.side_step, 0) 696 697 def _on_right(self): 698 self._left = min(self._left + self.side_step, self._max_left) 699 700 701 def browse_file(name, screen): 702 tv = TextViewerTUI(screen) 703 tv.title = name 704 tv.side_step = 4 705 tv.handlers['KEY_F(1)'] = lambda: show_help(screen) 706 tv.handlers['kLFT5'] = None 707 tv.handlers['KEY_BACKSPACE'] = None 708 return tv.run(slurp(name)) 709 710 711 def run(name, fallback, verbose, compact): 712 try: 713 tui = None 714 # can read piped input only before entering the `ui-mode` 715 if name == '-': 716 data = loads(stdin.read()) 717 else: 718 with open(name, 'r') as inp: 719 data = load(inp) 720 721 tui = SimpleTUI() 722 tui.start(3, 10) 723 vb = ValueBrowserTUI(tui.screen) 724 vb.title = '<stdin>' if name == '-' else name 725 vb.side_step = 4 726 vb.handlers['KEY_F(1)'] = lambda: show_help(tui.screen) 727 vb.data = data 728 729 path, pick, last = vb.run() 730 tui.stop() 731 dup2(3, 1) 732 733 indent = None if compact else 2 734 seps = (',', ':' if compact else ': ') 735 736 if last is None or last in ('\x1b', 'KEY_F(10)', 'KEY_F(12)'): 737 if not fallback: 738 return 1 739 pick = loads(fallback) 740 verbose = False 741 742 if verbose: 743 print(path, file=stderr) 744 print(dumps(pick, indent=indent, separators=seps, 745 allow_nan=False, check_circular=False)) 746 return 0 747 except KeyboardInterrupt: 748 if tui: 749 tui.stop() 750 dup2(3, 1) 751 return 1 752 except Exception as e: 753 if tui: 754 tui.stop() 755 dup2(3, 1) 756 # raise e 757 print(str(e), file=stderr) 758 return 1 759 760 761 def show_help(screen): 762 # quit_set = ('KEY_F(10)', 'KEY_F(12)', '\x1b', 'KEY_F(1)') 763 quit_set = ('\x1b', 'KEY_F(1)', 'KEY_F(10)', 'KEY_F(12)', 'KEY_BACKSPACE') 764 h = TextViewerTUI(screen, quit_set) 765 h.title = 'Help for Browse JSON (bj)' 766 return h.run(info) != '\x1b' 767 768 769 def slurp(name): 770 try: 771 with open(name, 'r') as inp: 772 return inp.read() 773 except Exception as e: 774 return e 775 776 777 if len(argv) > 1 and argv[1] in ('-h', '--h', '-help', '--help'): 778 print(info.strip()) 779 exit(0) 780 781 compact_output_opts = ( 782 '-c', '--c', '-compact', '--compact', '-j0', '--j0', '-json0', '--json0', 783 ) 784 fallback_opts = ( 785 '-d', '--d', '-f', '--f', '-fallback', '--fallback', '-default', 786 '--default', 787 ) 788 verbose_output_opts = ( 789 '-v', '--v', '-verbose', '--verbose', '-gron-path', '--gron-path', 790 ) 791 792 args = argv[1:] 793 compact = False 794 verbose = False 795 fallback = '' 796 797 while len(args) > 0: 798 if args[0] in compact_output_opts: 799 compact = True 800 args = args[1:] 801 continue 802 803 if args[0] in fallback_opts: 804 if len(args) < 2: 805 print('forgot the JSON fallback value', file=stderr) 806 exit(1) 807 try: 808 fallback = args[1] 809 loads(fallback) 810 except Exception as e: 811 print(str(e), file=stderr) 812 exit(1) 813 args = args[2:] 814 continue 815 816 if args[0] in verbose_output_opts: 817 verbose = True 818 args = args[1:] 819 continue 820 821 break 822 823 if len(args) > 0 and args[0] == '--': 824 args = args[1:] 825 826 name = args[0] if len(args) > 0 else '-' 827 828 if len(args) > 1: 829 msg = 'there can only be one (optional) filename argument' 830 print(msg, file=stderr) 831 exit(4) 832 833 # avoid func curses.wrapper, since it calls func curses.start_color, which in 834 # turn forces a black background no matter the terminal configuration 835 836 exit(run(name, fallback, verbose, compact))