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:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
- Add paging to '-h' and '--help' see PR #3478 (@MuntasirSZN)
|
||||||
|
|
||||||
## Bugfixes
|
## Bugfixes
|
||||||
|
|
||||||
- Fix hang when using `--list-themes` with an explicit pager, see #3457 (@abhinavcool42)
|
- Fix hang when using `--list-themes` with an explicit pager, see #3457 (@abhinavcool42)
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ pub fn env_no_color() -> bool {
|
|||||||
env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty())
|
env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum HelpType {
|
||||||
|
Short,
|
||||||
|
Long,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub matches: ArgMatches,
|
pub matches: ArgMatches,
|
||||||
interactive_output: bool,
|
interactive_output: bool,
|
||||||
@@ -49,38 +54,126 @@ impl App {
|
|||||||
let _ = nu_ansi_term::enable_ansi_support();
|
let _ = nu_ansi_term::enable_ansi_support();
|
||||||
|
|
||||||
let interactive_output = std::io::stdout().is_terminal();
|
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 {
|
Ok(App {
|
||||||
matches: Self::matches(interactive_output)?,
|
matches,
|
||||||
interactive_output,
|
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> {
|
fn matches(interactive_output: bool) -> Result<ArgMatches> {
|
||||||
// Check if we should skip config file processing for special arguments
|
// 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| {
|
let should_skip_config = wild::args_os().any(|arg| {
|
||||||
matches!(
|
matches!(
|
||||||
arg.to_str(),
|
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()) {
|
let args = if wild::args_os().nth(1) == Some("cache".into()) {
|
||||||
// Skip the config file and env vars
|
// Skip the config file and env vars
|
||||||
|
|
||||||
wild::args_os().collect::<Vec<_>>()
|
wild::args_os().collect::<Vec<_>>()
|
||||||
} else if wild::args_os().any(|arg| arg == "--no-config") || should_skip_config {
|
} 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
|
// 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 cli_args = wild::args_os();
|
||||||
let mut args = if should_skip_config {
|
let mut args = get_args_from_env_vars();
|
||||||
// For special commands, don't even try to load env vars that might fail
|
|
||||||
vec![]
|
|
||||||
} else {
|
|
||||||
get_args_from_env_vars()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Put the zero-th CLI argument (program name) first
|
// Put the zero-th CLI argument (program name) first
|
||||||
args.insert(0, cli_args.next().unwrap());
|
args.insert(0, cli_args.next().unwrap());
|
||||||
@@ -88,14 +181,28 @@ impl App {
|
|||||||
// .. and the rest at the end
|
// .. and the rest at the end
|
||||||
cli_args.for_each(|a| args.push(a));
|
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
|
args
|
||||||
} else {
|
} else {
|
||||||
let mut cli_args = wild::args_os();
|
let mut cli_args = wild::args_os();
|
||||||
|
|
||||||
// Read arguments from bats config file
|
// Read arguments from bats config file
|
||||||
let mut args = get_args_from_env_opts_var()
|
let mut args = match get_args_from_env_opts_var() {
|
||||||
.unwrap_or_else(get_args_from_config_file)
|
Some(result) => result,
|
||||||
.map_err(|_| "Could not parse configuration file")?;
|
None => get_args_from_config_file(),
|
||||||
|
}
|
||||||
|
.map_err(|_| "Could not parse configuration file")?;
|
||||||
|
|
||||||
// Selected env vars supersede config vars
|
// Selected env vars supersede config vars
|
||||||
args.extend(get_args_from_env_vars());
|
args.extend(get_args_from_env_vars());
|
||||||
@@ -462,17 +569,18 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn theme_options(&self) -> ThemeOptions {
|
fn theme_options(&self) -> ThemeOptions {
|
||||||
let theme = self
|
Self::theme_options_from_matches(&self.matches)
|
||||||
.matches
|
}
|
||||||
|
|
||||||
|
fn theme_options_from_matches(matches: &ArgMatches) -> ThemeOptions {
|
||||||
|
let theme = matches
|
||||||
.get_one::<String>("theme")
|
.get_one::<String>("theme")
|
||||||
.map(|t| ThemePreference::from_str(t).unwrap())
|
.map(|t| ThemePreference::from_str(t).unwrap())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let theme_dark = self
|
let theme_dark = matches
|
||||||
.matches
|
|
||||||
.get_one::<String>("theme-dark")
|
.get_one::<String>("theme-dark")
|
||||||
.map(|t| ThemeName::from_str(t).unwrap());
|
.map(|t| ThemeName::from_str(t).unwrap());
|
||||||
let theme_light = self
|
let theme_light = matches
|
||||||
.matches
|
|
||||||
.get_one::<String>("theme-light")
|
.get_one::<String>("theme-light")
|
||||||
.map(|t| ThemeName::from_str(t).unwrap());
|
.map(|t| ThemeName::from_str(t).unwrap());
|
||||||
ThemeOptions {
|
ThemeOptions {
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ pub fn build_app(interactive_output: bool) -> Command {
|
|||||||
.args_conflicts_with_subcommands(true)
|
.args_conflicts_with_subcommands(true)
|
||||||
.allow_external_subcommands(true)
|
.allow_external_subcommands(true)
|
||||||
.disable_help_subcommand(true)
|
.disable_help_subcommand(true)
|
||||||
|
.disable_help_flag(true)
|
||||||
|
.disable_version_flag(true)
|
||||||
.max_term_width(100)
|
.max_term_width(100)
|
||||||
.about("A cat(1) clone with wings.")
|
.about("A cat(1) clone with wings.")
|
||||||
.long_about("A cat(1) clone with syntax highlighting and Git integration.")
|
.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)
|
.action(ArgAction::SetTrue)
|
||||||
.hide_short_help(true)
|
.hide_short_help(true)
|
||||||
.help("Sets terminal title to filenames when using a pager."),
|
.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,
|
// Check if the current directory contains a file name cache. Otherwise,
|
||||||
|
|||||||
@@ -605,6 +605,44 @@ fn test_help(arg: &str, expect_file: &str) {
|
|||||||
.assert_eq(&String::from_utf8_lossy(&assert.get_output().stdout));
|
.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)]
|
#[cfg(unix)]
|
||||||
fn setup_temp_file(content: &[u8]) -> io::Result<(PathBuf, tempfile::TempDir)> {
|
fn setup_temp_file(content: &[u8]) -> io::Result<(PathBuf, tempfile::TempDir)> {
|
||||||
let dir = tempfile::tempdir().expect("Couldn't create tempdir");
|
let dir = tempfile::tempdir().expect("Couldn't create tempdir");
|
||||||
|
|||||||
Reference in New Issue
Block a user