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