Skip to content

Commit 70ec3fc

Browse files
authored
feat: add paging to '-h' and '--help' (#3478)
* feat: add paging to '-h' and '--help' Fixes #1587
1 parent 1e4a4b7 commit 70ec3fc

File tree

4 files changed

+184
-19
lines changed

4 files changed

+184
-19
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Features
44

5+
- Add paging to '-h' and '--help' see PR #3478 (@MuntasirSZN)
6+
57
## Bugfixes
68

79
- Fix hang when using `--list-themes` with an explicit pager, see #3457 (@abhinavcool42)

src/bin/bat/app.rs

Lines changed: 127 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ pub fn env_no_color() -> bool {
3838
env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty())
3939
}
4040

41+
enum HelpType {
42+
Short,
43+
Long,
44+
}
45+
4146
pub struct App {
4247
pub matches: ArgMatches,
4348
interactive_output: bool,
@@ -49,53 +54,155 @@ impl App {
4954
let _ = nu_ansi_term::enable_ansi_support();
5055

5156
let interactive_output = std::io::stdout().is_terminal();
57+
let matches = Self::matches(interactive_output)?;
58+
59+
if matches.get_flag("help") {
60+
let help_type = if wild::args_os().any(|arg| arg == "--help") {
61+
HelpType::Long
62+
} else {
63+
HelpType::Short
64+
};
65+
66+
let use_pager = match matches.get_one::<String>("paging").map(|s| s.as_str()) {
67+
Some("never") => false,
68+
_ => !matches.get_flag("no-paging"),
69+
};
70+
71+
let use_color = match matches.get_one::<String>("color").map(|s| s.as_str()) {
72+
Some("always") => true,
73+
Some("never") => false,
74+
_ => interactive_output, // auto: use color if interactive
75+
};
76+
77+
let custom_pager = matches.get_one::<String>("pager").map(|s| s.to_string());
78+
let theme_options = Self::theme_options_from_matches(&matches);
79+
80+
Self::display_help(
81+
interactive_output,
82+
help_type,
83+
use_pager,
84+
use_color,
85+
custom_pager,
86+
theme_options,
87+
)?;
88+
std::process::exit(0);
89+
}
5290

5391
Ok(App {
54-
matches: Self::matches(interactive_output)?,
92+
matches,
5593
interactive_output,
5694
})
5795
}
5896

97+
fn display_help(
98+
interactive_output: bool,
99+
help_type: HelpType,
100+
use_pager: bool,
101+
use_color: bool,
102+
custom_pager: Option<String>,
103+
theme_options: ThemeOptions,
104+
) -> Result<()> {
105+
use crate::assets::assets_from_cache_or_binary;
106+
use crate::directories::PROJECT_DIRS;
107+
use bat::{
108+
config::Config,
109+
controller::Controller,
110+
input::Input,
111+
style::{StyleComponent, StyleComponents},
112+
theme::theme,
113+
PagingMode,
114+
};
115+
116+
let mut cmd = clap_app::build_app(interactive_output);
117+
let help_text = match help_type {
118+
HelpType::Short => cmd.render_help().to_string(),
119+
HelpType::Long => cmd.render_long_help().to_string(),
120+
};
121+
122+
let inputs: Vec<Input> = vec![Input::from_reader(Box::new(help_text.as_bytes()))];
123+
124+
let paging_mode = if use_pager {
125+
PagingMode::QuitIfOneScreen
126+
} else {
127+
PagingMode::Never
128+
};
129+
130+
let pager = bat::config::get_pager_executable(custom_pager.as_deref());
131+
132+
let help_config = Config {
133+
style_components: StyleComponents::new(StyleComponent::Plain.components(false)),
134+
paging_mode,
135+
pager: pager.as_deref(),
136+
colored_output: use_color,
137+
true_color: use_color,
138+
language: if use_color { Some("help") } else { None },
139+
theme: theme(theme_options).to_string(),
140+
..Default::default()
141+
};
142+
143+
let cache_dir = PROJECT_DIRS.cache_dir();
144+
let assets = assets_from_cache_or_binary(false, cache_dir)?;
145+
Controller::new(&help_config, &assets)
146+
.run(inputs, None)
147+
.ok();
148+
149+
Ok(())
150+
}
151+
59152
fn matches(interactive_output: bool) -> Result<ArgMatches> {
60153
// Check if we should skip config file processing for special arguments
61-
// that don't require full application setup (help, version, diagnostic)
154+
// that don't require full application setup (version, diagnostic)
62155
let should_skip_config = wild::args_os().any(|arg| {
63156
matches!(
64157
arg.to_str(),
65-
Some("-h" | "--help" | "-V" | "--version" | "--diagnostic" | "--diagnostics")
158+
Some("-V" | "--version" | "--diagnostic" | "--diagnostics")
66159
)
67160
});
68161

162+
// Check if help was requested - help should go through the same code path
163+
// but be forgiving of config file errors
164+
let help_requested =
165+
wild::args_os().any(|arg| matches!(arg.to_str(), Some("-h" | "--help")));
166+
69167
let args = if wild::args_os().nth(1) == Some("cache".into()) {
70168
// Skip the config file and env vars
71169

72170
wild::args_os().collect::<Vec<_>>()
73171
} else if wild::args_os().any(|arg| arg == "--no-config") || should_skip_config {
74172
// Skip the arguments in bats config file when --no-config is present
75-
// or when user requests help, version, or diagnostic information
173+
// or when user requests version or diagnostic information
76174

77175
let mut cli_args = wild::args_os();
78-
let mut args = if should_skip_config {
79-
// For special commands, don't even try to load env vars that might fail
80-
vec![]
81-
} else {
82-
get_args_from_env_vars()
83-
};
176+
let mut args = get_args_from_env_vars();
84177

85178
// Put the zero-th CLI argument (program name) first
86179
args.insert(0, cli_args.next().unwrap());
87180

88181
// .. and the rest at the end
89182
cli_args.for_each(|a| args.push(a));
90183

184+
args
185+
} else if help_requested {
186+
// Help goes through the normal config path but only uses env vars for themes
187+
// to avoid failing on invalid config options
188+
let mut cli_args = wild::args_os();
189+
let mut args = get_args_from_env_vars();
190+
191+
// Put the zero-th CLI argument (program name) first
192+
args.insert(0, cli_args.next().unwrap());
193+
194+
// .. and the rest at the end (includes --help and other CLI args)
195+
cli_args.for_each(|a| args.push(a));
91196
args
92197
} else {
93198
let mut cli_args = wild::args_os();
94199

95200
// Read arguments from bats config file
96-
let mut args = get_args_from_env_opts_var()
97-
.unwrap_or_else(get_args_from_config_file)
98-
.map_err(|_| "Could not parse configuration file")?;
201+
let mut args = match get_args_from_env_opts_var() {
202+
Some(result) => result,
203+
None => get_args_from_config_file(),
204+
}
205+
.map_err(|_| "Could not parse configuration file")?;
99206

100207
// Selected env vars supersede config vars
101208
args.extend(get_args_from_env_vars());
@@ -462,17 +569,18 @@ impl App {
462569
}
463570

464571
fn theme_options(&self) -> ThemeOptions {
465-
let theme = self
466-
.matches
572+
Self::theme_options_from_matches(&self.matches)
573+
}
574+
575+
fn theme_options_from_matches(matches: &ArgMatches) -> ThemeOptions {
576+
let theme = matches
467577
.get_one::<String>("theme")
468578
.map(|t| ThemePreference::from_str(t).unwrap())
469579
.unwrap_or_default();
470-
let theme_dark = self
471-
.matches
580+
let theme_dark = matches
472581
.get_one::<String>("theme-dark")
473582
.map(|t| ThemeName::from_str(t).unwrap());
474-
let theme_light = self
475-
.matches
583+
let theme_light = matches
476584
.get_one::<String>("theme-light")
477585
.map(|t| ThemeName::from_str(t).unwrap());
478586
ThemeOptions {

src/bin/bat/clap_app.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ pub fn build_app(interactive_output: bool) -> Command {
3434
.args_conflicts_with_subcommands(true)
3535
.allow_external_subcommands(true)
3636
.disable_help_subcommand(true)
37+
.disable_help_flag(true)
38+
.disable_version_flag(true)
3739
.max_term_width(100)
3840
.about("A cat(1) clone with wings.")
3941
.long_about("A cat(1) clone with syntax highlighting and Git integration.")
@@ -654,6 +656,21 @@ pub fn build_app(interactive_output: bool) -> Command {
654656
.action(ArgAction::SetTrue)
655657
.hide_short_help(true)
656658
.help("Sets terminal title to filenames when using a pager."),
659+
)
660+
.arg(
661+
Arg::new("help")
662+
.short('h')
663+
.long("help")
664+
.action(ArgAction::SetTrue)
665+
.help("Print help (see more with '--help')")
666+
.long_help("Print help (see a summary with '-h')"),
667+
)
668+
.arg(
669+
Arg::new("version")
670+
.long("version")
671+
.short('V')
672+
.action(ArgAction::Version)
673+
.help("Print version"),
657674
);
658675

659676
// Check if the current directory contains a file name cache. Otherwise,

tests/integration_tests.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,44 @@ fn test_help(arg: &str, expect_file: &str) {
605605
.assert_eq(&String::from_utf8_lossy(&assert.get_output().stdout));
606606
}
607607

608+
#[test]
609+
fn short_help_with_highlighting() {
610+
bat()
611+
.arg("-h")
612+
.arg("--paging=never")
613+
.arg("--color=always")
614+
.assert()
615+
.success()
616+
.stdout(predicate::str::contains("\x1B["))
617+
.stdout(predicate::str::contains("Usage:"))
618+
.stdout(predicate::str::contains("Options:"));
619+
}
620+
621+
#[test]
622+
fn long_help_with_highlighting() {
623+
bat()
624+
.arg("--help")
625+
.arg("--paging=never")
626+
.arg("--color=always")
627+
.assert()
628+
.success()
629+
.stdout(predicate::str::contains("\x1B["))
630+
.stdout(predicate::str::contains("Usage:"))
631+
.stdout(predicate::str::contains("Options:"));
632+
}
633+
634+
#[test]
635+
fn help_with_color_never() {
636+
bat()
637+
.arg("--help")
638+
.arg("--color=never")
639+
.arg("--paging=never")
640+
.assert()
641+
.success()
642+
.stdout(predicate::str::contains("\x1B[").not())
643+
.stdout(predicate::str::contains("Usage:"));
644+
}
645+
608646
#[cfg(unix)]
609647
fn setup_temp_file(content: &[u8]) -> io::Result<(PathBuf, tempfile::TempDir)> {
610648
let dir = tempfile::tempdir().expect("Couldn't create tempdir");

0 commit comments

Comments
 (0)