1
0
mirror of https://github.com/sharkdp/bat.git synced 2026-02-08 00:32:08 +00:00

feat: add paging to '-h' and '--help' (#3478)

* feat: add paging to '-h' and '--help'

Fixes #1587
This commit is contained in:
Muntasir Mahmud
2025-11-24 02:10:56 +06:00
committed by GitHub
parent 1e4a4b765f
commit 70ec3fc24e
4 changed files with 184 additions and 19 deletions

View File

@@ -2,6 +2,8 @@
## Features
- Add paging to '-h' and '--help' see PR #3478 (@MuntasirSZN)
## Bugfixes
- Fix hang when using `--list-themes` with an explicit pager, see #3457 (@abhinavcool42)

View File

@@ -38,6 +38,11 @@ pub fn env_no_color() -> bool {
env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty())
}
enum HelpType {
Short,
Long,
}
pub struct App {
pub matches: ArgMatches,
interactive_output: bool,
@@ -49,38 +54,126 @@ impl App {
let _ = nu_ansi_term::enable_ansi_support();
let interactive_output = std::io::stdout().is_terminal();
let matches = Self::matches(interactive_output)?;
if matches.get_flag("help") {
let help_type = if wild::args_os().any(|arg| arg == "--help") {
HelpType::Long
} else {
HelpType::Short
};
let use_pager = match matches.get_one::<String>("paging").map(|s| s.as_str()) {
Some("never") => false,
_ => !matches.get_flag("no-paging"),
};
let use_color = match matches.get_one::<String>("color").map(|s| s.as_str()) {
Some("always") => true,
Some("never") => false,
_ => interactive_output, // auto: use color if interactive
};
let custom_pager = matches.get_one::<String>("pager").map(|s| s.to_string());
let theme_options = Self::theme_options_from_matches(&matches);
Self::display_help(
interactive_output,
help_type,
use_pager,
use_color,
custom_pager,
theme_options,
)?;
std::process::exit(0);
}
Ok(App {
matches: Self::matches(interactive_output)?,
matches,
interactive_output,
})
}
fn display_help(
interactive_output: bool,
help_type: HelpType,
use_pager: bool,
use_color: bool,
custom_pager: Option<String>,
theme_options: ThemeOptions,
) -> Result<()> {
use crate::assets::assets_from_cache_or_binary;
use crate::directories::PROJECT_DIRS;
use bat::{
config::Config,
controller::Controller,
input::Input,
style::{StyleComponent, StyleComponents},
theme::theme,
PagingMode,
};
let mut cmd = clap_app::build_app(interactive_output);
let help_text = match help_type {
HelpType::Short => cmd.render_help().to_string(),
HelpType::Long => cmd.render_long_help().to_string(),
};
let inputs: Vec<Input> = vec![Input::from_reader(Box::new(help_text.as_bytes()))];
let paging_mode = if use_pager {
PagingMode::QuitIfOneScreen
} else {
PagingMode::Never
};
let pager = bat::config::get_pager_executable(custom_pager.as_deref());
let help_config = Config {
style_components: StyleComponents::new(StyleComponent::Plain.components(false)),
paging_mode,
pager: pager.as_deref(),
colored_output: use_color,
true_color: use_color,
language: if use_color { Some("help") } else { None },
theme: theme(theme_options).to_string(),
..Default::default()
};
let cache_dir = PROJECT_DIRS.cache_dir();
let assets = assets_from_cache_or_binary(false, cache_dir)?;
Controller::new(&help_config, &assets)
.run(inputs, None)
.ok();
Ok(())
}
fn matches(interactive_output: bool) -> Result<ArgMatches> {
// Check if we should skip config file processing for special arguments
// that don't require full application setup (help, version, diagnostic)
// that don't require full application setup (version, diagnostic)
let should_skip_config = wild::args_os().any(|arg| {
matches!(
arg.to_str(),
Some("-h" | "--help" | "-V" | "--version" | "--diagnostic" | "--diagnostics")
Some("-V" | "--version" | "--diagnostic" | "--diagnostics")
)
});
// Check if help was requested - help should go through the same code path
// but be forgiving of config file errors
let help_requested =
wild::args_os().any(|arg| matches!(arg.to_str(), Some("-h" | "--help")));
let args = if wild::args_os().nth(1) == Some("cache".into()) {
// Skip the config file and env vars
wild::args_os().collect::<Vec<_>>()
} else if wild::args_os().any(|arg| arg == "--no-config") || should_skip_config {
// Skip the arguments in bats config file when --no-config is present
// or when user requests help, version, or diagnostic information
// or when user requests version or diagnostic information
let mut cli_args = wild::args_os();
let mut args = if should_skip_config {
// For special commands, don't even try to load env vars that might fail
vec![]
} else {
get_args_from_env_vars()
};
let mut args = get_args_from_env_vars();
// Put the zero-th CLI argument (program name) first
args.insert(0, cli_args.next().unwrap());
@@ -88,14 +181,28 @@ impl App {
// .. and the rest at the end
cli_args.for_each(|a| args.push(a));
args
} else if help_requested {
// Help goes through the normal config path but only uses env vars for themes
// to avoid failing on invalid config options
let mut cli_args = wild::args_os();
let mut args = get_args_from_env_vars();
// Put the zero-th CLI argument (program name) first
args.insert(0, cli_args.next().unwrap());
// .. and the rest at the end (includes --help and other CLI args)
cli_args.for_each(|a| args.push(a));
args
} else {
let mut cli_args = wild::args_os();
// Read arguments from bats config file
let mut args = get_args_from_env_opts_var()
.unwrap_or_else(get_args_from_config_file)
.map_err(|_| "Could not parse configuration file")?;
let mut args = match get_args_from_env_opts_var() {
Some(result) => result,
None => get_args_from_config_file(),
}
.map_err(|_| "Could not parse configuration file")?;
// Selected env vars supersede config vars
args.extend(get_args_from_env_vars());
@@ -462,17 +569,18 @@ impl App {
}
fn theme_options(&self) -> ThemeOptions {
let theme = self
.matches
Self::theme_options_from_matches(&self.matches)
}
fn theme_options_from_matches(matches: &ArgMatches) -> ThemeOptions {
let theme = matches
.get_one::<String>("theme")
.map(|t| ThemePreference::from_str(t).unwrap())
.unwrap_or_default();
let theme_dark = self
.matches
let theme_dark = matches
.get_one::<String>("theme-dark")
.map(|t| ThemeName::from_str(t).unwrap());
let theme_light = self
.matches
let theme_light = matches
.get_one::<String>("theme-light")
.map(|t| ThemeName::from_str(t).unwrap());
ThemeOptions {

View File

@@ -34,6 +34,8 @@ pub fn build_app(interactive_output: bool) -> Command {
.args_conflicts_with_subcommands(true)
.allow_external_subcommands(true)
.disable_help_subcommand(true)
.disable_help_flag(true)
.disable_version_flag(true)
.max_term_width(100)
.about("A cat(1) clone with wings.")
.long_about("A cat(1) clone with syntax highlighting and Git integration.")
@@ -654,6 +656,21 @@ pub fn build_app(interactive_output: bool) -> Command {
.action(ArgAction::SetTrue)
.hide_short_help(true)
.help("Sets terminal title to filenames when using a pager."),
)
.arg(
Arg::new("help")
.short('h')
.long("help")
.action(ArgAction::SetTrue)
.help("Print help (see more with '--help')")
.long_help("Print help (see a summary with '-h')"),
)
.arg(
Arg::new("version")
.long("version")
.short('V')
.action(ArgAction::Version)
.help("Print version"),
);
// Check if the current directory contains a file name cache. Otherwise,

View File

@@ -605,6 +605,44 @@ fn test_help(arg: &str, expect_file: &str) {
.assert_eq(&String::from_utf8_lossy(&assert.get_output().stdout));
}
#[test]
fn short_help_with_highlighting() {
bat()
.arg("-h")
.arg("--paging=never")
.arg("--color=always")
.assert()
.success()
.stdout(predicate::str::contains("\x1B["))
.stdout(predicate::str::contains("Usage:"))
.stdout(predicate::str::contains("Options:"));
}
#[test]
fn long_help_with_highlighting() {
bat()
.arg("--help")
.arg("--paging=never")
.arg("--color=always")
.assert()
.success()
.stdout(predicate::str::contains("\x1B["))
.stdout(predicate::str::contains("Usage:"))
.stdout(predicate::str::contains("Options:"));
}
#[test]
fn help_with_color_never() {
bat()
.arg("--help")
.arg("--color=never")
.arg("--paging=never")
.assert()
.success()
.stdout(predicate::str::contains("\x1B[").not())
.stdout(predicate::str::contains("Usage:"));
}
#[cfg(unix)]
fn setup_temp_file(content: &[u8]) -> io::Result<(PathBuf, tempfile::TempDir)> {
let dir = tempfile::tempdir().expect("Couldn't create tempdir");