commands: add support for paged output
Signed-off-by: Todd Gamblin <tgamblin@llnl.gov>
This commit is contained in:
parent
b932c14008
commit
81a1f97779
@ -178,6 +178,10 @@ config:
|
|||||||
package_lock_timeout: null
|
package_lock_timeout: null
|
||||||
|
|
||||||
|
|
||||||
|
# pager(s) to use for commands with potentially long output (e.g., spack info)
|
||||||
|
pager:
|
||||||
|
- less -FXRS
|
||||||
|
|
||||||
# Control how shared libraries are located at runtime on Linux. See the
|
# Control how shared libraries are located at runtime on Linux. See the
|
||||||
# the Spack documentation for details.
|
# the Spack documentation for details.
|
||||||
shared_linking:
|
shared_linking:
|
||||||
|
@ -4,12 +4,14 @@
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import difflib
|
import difflib
|
||||||
|
import functools
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from typing import List, Optional, Union
|
from typing import Callable, List, Optional, Union
|
||||||
|
|
||||||
import llnl.string
|
import llnl.string
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
@ -30,6 +32,7 @@
|
|||||||
import spack.store
|
import spack.store
|
||||||
import spack.traverse as traverse
|
import spack.traverse as traverse
|
||||||
import spack.user_environment as uenv
|
import spack.user_environment as uenv
|
||||||
|
import spack.util.executable as exe
|
||||||
import spack.util.spack_json as sjson
|
import spack.util.spack_json as sjson
|
||||||
import spack.util.spack_yaml as syaml
|
import spack.util.spack_yaml as syaml
|
||||||
|
|
||||||
@ -723,3 +726,123 @@ def __init__(self, cmd_name):
|
|||||||
long_msg += "\n ".join(similar)
|
long_msg += "\n ".join(similar)
|
||||||
|
|
||||||
super().__init__(msg, long_msg)
|
super().__init__(msg, long_msg)
|
||||||
|
|
||||||
|
|
||||||
|
def find_pager(pager_candidates: List[str]) -> Optional[List[str]]:
|
||||||
|
"""Find a pager from spack configuration.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
pager_candidates: list of candidate commands with optional arguments, e.g. "less -FXRS"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Arguments, including the found command, to launch the pager, or None if not found.
|
||||||
|
"""
|
||||||
|
for pager in pager_candidates:
|
||||||
|
# split each string in the list of pagers into args
|
||||||
|
argv = pager.split()
|
||||||
|
if not argv:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# try to find the requested pager command
|
||||||
|
command, *args = argv
|
||||||
|
path = exe.which_string(command)
|
||||||
|
if not path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# return the execv args we need to launch this thing
|
||||||
|
return [command] + args
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def spack_pager_candidates() -> List[str]:
|
||||||
|
"""Get a list of pager candidates by consulting environment and config.
|
||||||
|
|
||||||
|
Order of precedence is:
|
||||||
|
|
||||||
|
1. ``SPACK_PAGER``: pager just for spack
|
||||||
|
2. ``PAGER``: user's preferred pager, from their environment
|
||||||
|
3. ``config:pager``: list of pager candidates in config
|
||||||
|
|
||||||
|
"""
|
||||||
|
pager_candidates = []
|
||||||
|
|
||||||
|
spack_pager = os.environ.get("SPACK_PAGER")
|
||||||
|
if spack_pager:
|
||||||
|
pager_candidates.append(spack_pager)
|
||||||
|
|
||||||
|
pager = os.environ.get("PAGER")
|
||||||
|
if pager:
|
||||||
|
pager_candidates.append(pager)
|
||||||
|
|
||||||
|
config_pagers = spack.config.get("config:pager")
|
||||||
|
if config_pagers:
|
||||||
|
pager_candidates.extend(config_pagers)
|
||||||
|
|
||||||
|
return pager_candidates
|
||||||
|
|
||||||
|
|
||||||
|
def paged(command_function: Callable) -> Callable:
|
||||||
|
"""Decorator for commands whose output should be sent to a pager by default.
|
||||||
|
|
||||||
|
This will launch a subprocess for, e.g., ``less``, and will redirect ``stdout`` to it, as
|
||||||
|
``git`` does for commands like ``git log``.
|
||||||
|
|
||||||
|
The command will attempt to maintain colored output while paging, so you need a pager
|
||||||
|
that supports color, like ``less -R``. Spack defaults to using ``less -FXRS`` if it's
|
||||||
|
found, and nothing if not. You probably *do not* want to use ``more`` or any other
|
||||||
|
non-color-capable pager.
|
||||||
|
"""
|
||||||
|
pager_execv_args = find_pager(spack_pager_candidates())
|
||||||
|
if not pager_execv_args:
|
||||||
|
return command_function
|
||||||
|
|
||||||
|
@functools.wraps(command_function)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
# figure out if we're running the command with --help
|
||||||
|
is_help = False
|
||||||
|
if args and isinstance(args[-1], argparse.Namespace):
|
||||||
|
is_help = args[-1].help
|
||||||
|
|
||||||
|
# don't page if not a tty, and don't page help output
|
||||||
|
if not sys.stdout.isatty() or is_help:
|
||||||
|
return command_function(*args, **kwargs)
|
||||||
|
|
||||||
|
# Flush any buffered output before redirection.
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
# save original stdout and original color setting
|
||||||
|
original_stdout_fd = os.dup(sys.stdout.fileno())
|
||||||
|
original_stdout_isatty = sys.stdout.isatty
|
||||||
|
|
||||||
|
# launch the pager
|
||||||
|
proc = subprocess.Popen(pager_execv_args, stdin=subprocess.PIPE)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Redirect stdout's file descriptor to the pager's stdin.
|
||||||
|
os.dup2(proc.stdin.fileno(), sys.stdout.fileno())
|
||||||
|
|
||||||
|
# make spack think the pager is a tty
|
||||||
|
sys.stdout.isatty = lambda: True
|
||||||
|
|
||||||
|
# run the decorated function
|
||||||
|
result = command_function(*args, **kwargs)
|
||||||
|
|
||||||
|
# Flush any remaining output.
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# quit cheating on isatty
|
||||||
|
sys.stdout.isatty = original_stdout_isatty
|
||||||
|
|
||||||
|
# restore stdout
|
||||||
|
os.dup2(original_stdout_fd, sys.stdout.fileno())
|
||||||
|
os.close(original_stdout_fd)
|
||||||
|
|
||||||
|
# Close the pager's stdin and wait for it to finish.
|
||||||
|
proc.stdin.close()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
import llnl.util.filesystem as fs
|
import llnl.util.filesystem as fs
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
|
|
||||||
|
import spack.cmd
|
||||||
import spack.config
|
import spack.config
|
||||||
import spack.environment as ev
|
import spack.environment as ev
|
||||||
import spack.error
|
import spack.error
|
||||||
@ -169,6 +170,7 @@ def print_flattened_configuration(*, blame: bool) -> None:
|
|||||||
syaml.dump_config(flattened, stream=sys.stdout, default_flow_style=False, blame=blame)
|
syaml.dump_config(flattened, stream=sys.stdout, default_flow_style=False, blame=blame)
|
||||||
|
|
||||||
|
|
||||||
|
@spack.cmd.paged
|
||||||
def config_get(args):
|
def config_get(args):
|
||||||
"""Dump merged YAML configuration for a specific section.
|
"""Dump merged YAML configuration for a specific section.
|
||||||
|
|
||||||
@ -178,6 +180,7 @@ def config_get(args):
|
|||||||
print_configuration(args, blame=False)
|
print_configuration(args, blame=False)
|
||||||
|
|
||||||
|
|
||||||
|
@spack.cmd.paged
|
||||||
def config_blame(args):
|
def config_blame(args):
|
||||||
"""Print out line-by-line blame of merged YAML."""
|
"""Print out line-by-line blame of merged YAML."""
|
||||||
print_configuration(args, blame=True)
|
print_configuration(args, blame=True)
|
||||||
|
@ -363,6 +363,7 @@ def _find_query(args, env):
|
|||||||
return results, concretized_but_not_installed
|
return results, concretized_but_not_installed
|
||||||
|
|
||||||
|
|
||||||
|
@cmd.paged
|
||||||
def find(parser, args):
|
def find(parser, args):
|
||||||
env = ev.active_environment()
|
env = ev.active_environment()
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
from llnl.util.tty.colify import colify
|
from llnl.util.tty.colify import colify
|
||||||
|
|
||||||
import spack.builder
|
import spack.builder
|
||||||
|
import spack.cmd
|
||||||
import spack.deptypes as dt
|
import spack.deptypes as dt
|
||||||
import spack.fetch_strategy as fs
|
import spack.fetch_strategy as fs
|
||||||
import spack.install_test
|
import spack.install_test
|
||||||
@ -481,6 +482,7 @@ def print_licenses(pkg, args):
|
|||||||
color.cprint(line)
|
color.cprint(line)
|
||||||
|
|
||||||
|
|
||||||
|
@spack.cmd.paged
|
||||||
def info(parser, args):
|
def info(parser, args):
|
||||||
spec = spack.spec.Spec(args.package)
|
spec = spack.spec.Spec(args.package)
|
||||||
pkg_cls = spack.repo.PATH.get_pkg_class(spec.fullname)
|
pkg_cls = spack.repo.PATH.get_pkg_class(spec.fullname)
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
from llnl.util.tty.colify import colify
|
from llnl.util.tty.colify import colify
|
||||||
|
|
||||||
|
import spack.cmd
|
||||||
import spack.deptypes as dt
|
import spack.deptypes as dt
|
||||||
import spack.package_base
|
import spack.package_base
|
||||||
import spack.repo
|
import spack.repo
|
||||||
@ -315,6 +316,7 @@ def head(n, span_id, title, anchor=None):
|
|||||||
out.write("</div>\n")
|
out.write("</div>\n")
|
||||||
|
|
||||||
|
|
||||||
|
@spack.cmd.paged
|
||||||
def list(parser, args):
|
def list(parser, args):
|
||||||
# retrieve the formatter to use from args
|
# retrieve the formatter to use from args
|
||||||
formatter = formatters[args.format]
|
formatter = formatters[args.format]
|
||||||
|
@ -374,6 +374,12 @@ def make_argument_parser(**kwargs):
|
|||||||
choices=("always", "never", "auto"),
|
choices=("always", "never", "auto"),
|
||||||
help="when to colorize output (default: auto)",
|
help="when to colorize output (default: auto)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-pager",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="do not run any output through a pager",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-c",
|
"-c",
|
||||||
"--config",
|
"--config",
|
||||||
@ -536,6 +542,10 @@ def setup_main_options(args):
|
|||||||
if args.timestamp:
|
if args.timestamp:
|
||||||
tty.set_timestamp(True)
|
tty.set_timestamp(True)
|
||||||
|
|
||||||
|
# override pager configuration (note ::)
|
||||||
|
if args.no_pager:
|
||||||
|
spack.config.set("config::pager", [], scope="command_line")
|
||||||
|
|
||||||
# override lock configuration if passed on command line
|
# override lock configuration if passed on command line
|
||||||
if args.locks is not None:
|
if args.locks is not None:
|
||||||
if args.locks is False:
|
if args.locks is False:
|
||||||
|
@ -104,6 +104,7 @@
|
|||||||
"additional_external_search_paths": {"type": "array", "items": {"type": "string"}},
|
"additional_external_search_paths": {"type": "array", "items": {"type": "string"}},
|
||||||
"binary_index_ttl": {"type": "integer", "minimum": 0},
|
"binary_index_ttl": {"type": "integer", "minimum": 0},
|
||||||
"aliases": {"type": "object", "patternProperties": {r"\w[\w-]*": {"type": "string"}}},
|
"aliases": {"type": "object", "patternProperties": {r"\w[\w-]*": {"type": "string"}}},
|
||||||
|
"pager": {"type": "array", "items": {"type": "string"}},
|
||||||
},
|
},
|
||||||
"deprecatedProperties": [
|
"deprecatedProperties": [
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user