scripts: gerrit: Send output thru the pager
We can convenience the user by automatically paging long output when
we're attached to a TTY.
BUG=none
TEST=* gerrit todo ~> goes thru pager and has colors
* gerrit --nocolor todo ~> goes thru pager and has no colors
* gerrit todo | cat ~> no pager
* Press Ctrl+C while viewing output, only gerrit subprocess gets
terminated (pager stays running)
* When pressing Ctrl+C while viewing output, exit status reflects
failure.
* "gerrit help" and "gerrit help-all" show in pager,
"gerrit --help" does not.
* Press Ctrl+Z while viewing output, restart pager with fg.
* Press q on a long results. Gerrit terminated with signal 15
after pager exits.
* gerrit --no-pager todo ~> shows with no pager
Change-Id: Idf532f929971452999631ceccb686d9012d35c09
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/3739142
Tested-by: Jack Rosenthal <jrosenth@chromium.org>
Commit-Queue: Jack Rosenthal <jrosenth@chromium.org>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
diff --git a/scripts/gerrit.py b/scripts/gerrit.py
index b49bc81..f530f49 100644
--- a/scripts/gerrit.py
+++ b/scripts/gerrit.py
@@ -17,9 +17,12 @@
import inspect
import json
import logging
+import os
from pathlib import Path
import re
import shlex
+import signal
+import subprocess
import sys
from chromite.lib import chromite_config
@@ -63,6 +66,9 @@
# The name of the command the user types in.
COMMAND = None
+ # Should output be paged?
+ USE_PAGER = False
+
@staticmethod
def init_subparser(parser):
"""Add arguments to this action's subparser."""
@@ -331,6 +337,8 @@
class _ActionSearchQuery(UserAction):
"""Base class for actions that perform searches."""
+ USE_PAGER = True
+
@staticmethod
def init_subparser(parser):
"""Add arguments to this action's subparser."""
@@ -1008,6 +1016,7 @@
"""Get user account information"""
COMMAND = 'account'
+ USE_PAGER = True
@staticmethod
def init_subparser(parser):
@@ -1064,6 +1073,7 @@
"""An alias to --help for CLI symmetry"""
COMMAND = 'help'
+ USE_PAGER = True
@staticmethod
def init_subparser(parser):
@@ -1086,6 +1096,7 @@
"""Show all actions help output at once."""
COMMAND = 'help-all'
+ USE_PAGER = True
@staticmethod
def __call__(opts):
@@ -1234,6 +1245,20 @@
const=OutputFormat.JSON,
help='Alias for --format=json.',
)
+
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument(
+ '--pager',
+ action='store_true',
+ default=sys.stdout.isatty(),
+ help='Enable pager.',
+ )
+ group.add_argument(
+ '--no-pager',
+ action='store_false',
+ dest='pager',
+ help='Disable pager.'
+ )
return parser
@@ -1260,6 +1285,39 @@
return parser
+def start_pager():
+ """Re-spawn ourselves attached to a pager."""
+ pager = os.environ.get('PAGER', 'less')
+ os.environ.setdefault('LESS', 'FRX')
+ with subprocess.Popen(
+ # sys.argv can have some edge cases: we may not necessarily use
+ # sys.executable if the script is executed as "python path/to/script".
+ # If we upgrade to Python 3.10+, this should be changed to sys.orig_argv
+ # for full accuracy.
+ sys.argv,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ env={'GERRIT_RESPAWN_FOR_PAGER': '1', **os.environ},
+ ) as gerrit_proc:
+ with subprocess.Popen(
+ pager,
+ shell=True,
+ stdin=gerrit_proc.stdout,
+ ) as pager_proc:
+ # Send SIGINT to just the gerrit process, not the pager too.
+ def _sighandler(signum, _frame):
+ gerrit_proc.send_signal(signum)
+
+ signal.signal(signal.SIGINT, _sighandler)
+
+ pager_proc.communicate()
+ # If the pager exits, and the gerrit process is still running, we
+ # must terminate it.
+ if gerrit_proc.poll() is None:
+ gerrit_proc.terminate()
+ sys.exit(gerrit_proc.wait())
+
+
def main(argv):
base_parser = GetBaseParser()
opts, subargs = base_parser.parse_known_args(argv)
@@ -1276,6 +1334,13 @@
parser = GetParser(parser=base_parser)
opts = parser.parse_args(argv)
+ # If we're running as a re-spawn for the pager, from this point on
+ # we'll pretend we're attached to a TTY. This will give us colored
+ # output when requested.
+ if os.environ.pop('GERRIT_RESPAWN_FOR_PAGER', None) is not None:
+ opts.pager = False
+ sys.stdout.isatty = lambda: True
+
# In case the action wants to throw a parser error.
opts.parser = parser
@@ -1293,7 +1358,10 @@
# Now look up the requested user action and run it.
actions = _GetActions()
- obj = actions[opts.action]()
+ action_class = actions[opts.action]
+ if action_class.USE_PAGER and opts.pager:
+ start_pager()
+ obj = action_class()
try:
obj(opts)
except (cros_build_lib.RunCommandError, gerrit.GerritException,