diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c0422e146..f00211a71d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Features - Add environment variable `BAT_PAGING`, see #2629 (@einfachIrgendwer0815) +- Add support for `LESSOPEN` and `LESSCLOSE`. See #1597, #1739, #2444, #2602, and #2662 (@Anomalocaridid) ## Bugfixes @@ -77,7 +78,6 @@ - Make the default macOS theme depend on Dark Mode. See #2197, #1746 (@Enselic) - Support for separate system and user config files. See #668 (@patrickpichler) -- Add support for $LESSOPEN and $LESSCLOSE. See #1597, #1739, and #2444 (@Anomalocaridid) ## Bugfixes diff --git a/assets/completions/_bat.ps1.in b/assets/completions/_bat.ps1.in index fe0b8b07c7..c0c151e1ed 100644 --- a/assets/completions/_bat.ps1.in +++ b/assets/completions/_bat.ps1.in @@ -59,7 +59,8 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script [CompletionResult]::new('--unbuffered', 'unbuffered', [CompletionResultType]::ParameterName, 'unbuffered') [CompletionResult]::new('--no-config', 'no-config', [CompletionResultType]::ParameterName, 'Do not use the configuration file') [CompletionResult]::new('--no-custom-assets', 'no-custom-assets', [CompletionResultType]::ParameterName, 'Do not load custom assets') - [CompletionResult]::new('--no-lessopen', 'no-lessopen', [CompletionResultType]::ParameterName, 'Do not use the $LESSOPEN preprocessor') + [CompletionResult]::new('--lessopen', 'lessopen', [CompletionResultType]::ParameterName, 'Enable the $LESSOPEN preprocessor') + [CompletionResult]::new('--no-lessopen', 'no-lessopen', [CompletionResultType]::ParameterName, 'Disable the $LESSOPEN preprocessor if enabled (overrides --lessopen)') [CompletionResult]::new('--config-file', 'config-file', [CompletionResultType]::ParameterName, 'Show path to the configuration file.') [CompletionResult]::new('--generate-config-file', 'generate-config-file', [CompletionResultType]::ParameterName, 'Generates a default configuration file.') [CompletionResult]::new('--config-dir', 'config-dir', [CompletionResultType]::ParameterName, 'Show bat''s configuration directory.') diff --git a/assets/completions/bat.bash.in b/assets/completions/bat.bash.in index e4292a7e84..de8651a807 100644 --- a/assets/completions/bat.bash.in +++ b/assets/completions/bat.bash.in @@ -78,6 +78,7 @@ _bat() { --list-themes | \ --line-range | \ -L | --list-languages | \ + --lessopen | \ --diagnostic | \ --acknowledgements | \ -h | --help | \ @@ -171,6 +172,7 @@ _bat() { --style --line-range --list-languages + --lessopen --diagnostic --acknowledgements --help diff --git a/assets/completions/bat.fish.in b/assets/completions/bat.fish.in index 54c8413a85..b0392dfdc8 100644 --- a/assets/completions/bat.fish.in +++ b/assets/completions/bat.fish.in @@ -163,6 +163,8 @@ complete -c $bat -l italic-text -x -a "$italic_text_opts" -d "When to use italic complete -c $bat -s l -l language -x -k -a "(__bat_complete_list_languages)" -d "Set the syntax highlighting language" -n __bat_no_excl_args +complete -c $bat -l lessopen -d "Enable the $LESSOPEN preprocessor" -n __fish_is_first_arg + complete -c $bat -s r -l line-range -x -d "Only print lines [M]:[N] (either optional)" -n __bat_no_excl_args complete -c $bat -l list-languages -f -d "List syntax highlighting languages" -n __fish_is_first_arg diff --git a/assets/completions/bat.zsh.in b/assets/completions/bat.zsh.in index 0939c6f23e..9acec1f26d 100644 --- a/assets/completions/bat.zsh.in +++ b/assets/completions/bat.zsh.in @@ -46,7 +46,8 @@ _{{PROJECT_EXECUTABLE}}_main() { '(: --list-themes --list-languages -L)'{-L,--list-languages}'[Display all supported languages]' '(: --no-config)'--no-config'[Do not use the configuration file]' '(: --no-custom-assets)'--no-custom-assets'[Do not load custom assets]' - '(: --no-lessopen)'--no-lessopen'[Do not use the $LESSOPEN preprocessor]' + '(: --lessopen)'--lessopen'[Enable the $LESSOPEN preprocessor]' + '(: --no-lessopen)'--no-lessopen'[Disable the $LESSOPEN preprocessor if enabled (overrides --lessopen)]' '(: --config-dir)'--config-dir'[Show bat'"'"'s configuration directory]' '(: --config-file)'--config-file'[Show path to the configuration file]' '(: --generate-config-file)'--generate-config-file'[Generates a default configuration file]' diff --git a/assets/manual/bat.1.in b/assets/manual/bat.1.in index 057cfc2131..b85520daa5 100644 --- a/assets/manual/bat.1.in +++ b/assets/manual/bat.1.in @@ -248,7 +248,13 @@ These can be installed to `\fB$({{PROJECT_EXECUTABLE}} --config-dir)/themes\fR`, Much like less(1) does, {{PROJECT_EXECUTABLE}} supports input preprocessors via the LESSOPEN and LESSCLOSE environment variables. In addition, {{PROJECT_EXECUTABLE}} attempts to be as compatible with less's preprocessor implementation as possible. -To run {{PROJECT_EXECUTABLE}} without using the preprocessor, call: +To use the preprocessor, call: + +\fB{{PROJECT_EXECUTABLE}} --lessopen\fR + +Alternatively, the preprocessor may be enabled by default by adding the '\-\-lessopen' option to the configuration file. + +To temporarily disable the preprocessor if it is enabled by default, call: \fB{{PROJECT_EXECUTABLE}} --no-lessopen\fR diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 95b66a9263..bb67fc3acb 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -282,7 +282,7 @@ impl App { .unwrap_or_default(), use_custom_assets: !self.matches.get_flag("no-custom-assets"), #[cfg(feature = "lessopen")] - use_lessopen: !self.matches.get_flag("no-lessopen"), + use_lessopen: self.matches.get_flag("lessopen"), }) } diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index f631853750..01f9d7549a 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -501,13 +501,21 @@ pub fn build_app(interactive_output: bool) -> Command { #[cfg(feature = "lessopen")] { - app = app.arg( - Arg::new("no-lessopen") - .long("no-lessopen") - .action(ArgAction::SetTrue) - .hide(true) - .help("Do not use the $LESSOPEN preprocessor"), - ) + app = app + .arg( + Arg::new("lessopen") + .long("lessopen") + .action(ArgAction::SetTrue) + .help("Enable the $LESSOPEN preprocessor"), + ) + .arg( + Arg::new("no-lessopen") + .long("no-lessopen") + .action(ArgAction::SetTrue) + .overrides_with("lessopen") + .hide(true) + .help("Disable the $LESSOPEN preprocessor if enabled (overrides --lessopen)"), + ) } app = app diff --git a/src/lessopen.rs b/src/lessopen.rs index 7c26e838a2..c8f5225d4d 100644 --- a/src/lessopen.rs +++ b/src/lessopen.rs @@ -12,7 +12,10 @@ use os_str_bytes::RawOsString; use run_script::{IoOptions, ScriptOptions}; use crate::error::Result; -use crate::input::{Input, InputKind, InputReader, OpenedInput, OpenedInputKind}; +use crate::{ + bat_warning, + input::{Input, InputKind, InputReader, OpenedInput, OpenedInputKind}, +}; /// Preprocess files and/or stdin using $LESSOPEN and $LESSCLOSE pub(crate) struct LessOpenPreprocessor { @@ -32,10 +35,18 @@ enum LessOpenKind { impl LessOpenPreprocessor { /// Create a new instance of LessOpenPreprocessor - /// Will return Ok if and only if $LESSOPEN is set + /// Will return Ok if and only if $LESSOPEN is set and contains exactly one %s pub(crate) fn new() -> Result { let lessopen = env::var("LESSOPEN")?; + // Ignore $LESSOPEN if it does not contains exactly one %s + // Note that $LESSCLOSE has no such requirement + if lessopen.match_indices("%s").count() != 1 { + let error_msg = "LESSOPEN ignored: must contain exactly one %s"; + bat_warning!("{}", error_msg); + return Err(error_msg.into()); + } + // "||" means pipe directly to bat without making a temporary file // Also, if preprocessor output is empty and exit code is zero, use the empty output // Otherwise, if output is empty and exit code is nonzero, use original file contents diff --git a/src/output.rs b/src/output.rs index 55ec1cefc7..dc75d6e7a5 100644 --- a/src/output.rs +++ b/src/output.rs @@ -114,6 +114,10 @@ impl OutputType { p.args(args); } p.env("LESSCHARSET", "UTF-8"); + + #[cfg(feature = "lessopen")] + // Ensures that 'less' does not preprocess input again if '$LESSOPEN' is set. + p.arg("--no-lessopen"); } else { p.args(args); }; diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 8fc2c30c95..0bf6437590 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -2032,6 +2032,7 @@ fn acknowledgements() { fn lessopen_file_piped() { bat() .env("LESSOPEN", "|echo File is %s") + .arg("--lessopen") .arg("test.txt") .assert() .success() @@ -2044,6 +2045,7 @@ fn lessopen_file_piped() { fn lessopen_stdin_piped() { bat() .env("LESSOPEN", "|cat") + .arg("--lessopen") .write_stdin("hello world\n") .assert() .success() @@ -2058,8 +2060,10 @@ fn lessopen_and_lessclose_file_temp() { // This is mainly to test that $LESSCLOSE gets passed the correct file paths // In this case, the original file and the temporary file returned by $LESSOPEN bat() - .env("LESSOPEN", "echo empty.txt") + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "echo empty.txt && echo %s >/dev/null") .env("LESSCLOSE", "echo lessclose: %s %s") + .arg("--lessopen") .arg("test.txt") .assert() .success() @@ -2075,16 +2079,18 @@ fn lessopen_and_lessclose_file_piped() { // In these cases, the original file and a dash bat() // This test will not work properly if $LESSOPEN does not output anything - .env("LESSOPEN", "|cat test.txt ") + .env("LESSOPEN", "|cat %s") .env("LESSCLOSE", "echo lessclose: %s %s") - .arg("empty.txt") + .arg("--lessopen") + .arg("test.txt") .assert() .success() - .stdout("hello world\nlessclose: empty.txt -\n"); + .stdout("hello world\nlessclose: test.txt -\n"); bat() - .env("LESSOPEN", "||cat empty.txt") + .env("LESSOPEN", "||cat %s") .env("LESSCLOSE", "echo lessclose: %s %s") + .arg("--lessopen") .arg("empty.txt") .assert() .success() @@ -2095,12 +2101,15 @@ fn lessopen_and_lessclose_file_piped() { #[cfg(feature = "lessopen")] #[test] #[serial] // Randomly fails otherwise +#[ignore = "randomly failing on some systems"] fn lessopen_and_lessclose_stdin_temp() { // This is mainly to test that $LESSCLOSE gets passed the correct file paths // In this case, a dash and the temporary file returned by $LESSOPEN bat() - .env("LESSOPEN", "-echo empty.txt") + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "-echo empty.txt && echo %s >/dev/null") .env("LESSCLOSE", "echo lessclose: %s %s") + .arg("--lessopen") .write_stdin("test.txt") .assert() .success() @@ -2116,16 +2125,20 @@ fn lessopen_and_lessclose_stdin_piped() { // In these cases, two dashes bat() // This test will not work properly if $LESSOPEN does not output anything - .env("LESSOPEN", "|-cat test.txt") + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "|-cat test.txt && echo %s >/dev/null") .env("LESSCLOSE", "echo lessclose: %s %s") + .arg("--lessopen") .write_stdin("empty.txt") .assert() .success() .stdout("hello world\nlessclose: - -\n"); bat() - .env("LESSOPEN", "||-cat empty.txt") + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "||-cat empty.txt && echo %s >/dev/null") .env("LESSCLOSE", "echo lessclose: %s %s") + .arg("--lessopen") .write_stdin("empty.txt") .assert() .success() @@ -2137,28 +2150,36 @@ fn lessopen_and_lessclose_stdin_piped() { #[test] fn lessopen_handling_empty_output_file() { bat() - .env("LESSOPEN", "|cat empty.txt") + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "|cat empty.txt && echo %s >/dev/null") + .arg("--lessopen") .arg("test.txt") .assert() .success() .stdout("hello world\n"); bat() - .env("LESSOPEN", "|cat nonexistent.txt") + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "|cat nonexistent.txt && echo %s >/dev/null") + .arg("--lessopen") .arg("test.txt") .assert() .success() .stdout("hello world\n"); bat() - .env("LESSOPEN", "||cat empty.txt") + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "||cat empty.txt && echo %s >/dev/null") + .arg("--lessopen") .arg("test.txt") .assert() .success() .stdout(""); bat() - .env("LESSOPEN", "||cat nonexistent.txt") + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "||cat nonexistent.txt && echo %s >/dev/null") + .arg("--lessopen") .arg("test.txt") .assert() .success() @@ -2168,30 +2189,39 @@ fn lessopen_handling_empty_output_file() { #[cfg(unix)] // Expected output assumed that tests are run on a Unix-like system #[cfg(feature = "lessopen")] #[test] +// FIXME fn lessopen_handling_empty_output_stdin() { bat() - .env("LESSOPEN", "|-cat empty.txt") + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "|-cat empty.txt && echo %s >/dev/null") + .arg("--lessopen") .write_stdin("hello world\n") .assert() .success() .stdout("hello world\n"); bat() - .env("LESSOPEN", "|-cat nonexistent.txt") + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "|-cat nonexistent.txt && echo %s >/dev/null") + .arg("--lessopen") .write_stdin("hello world\n") .assert() .success() .stdout("hello world\n"); bat() - .env("LESSOPEN", "||-cat empty.txt") + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "||-cat empty.txt && echo %s >/dev/null") + .arg("--lessopen") .write_stdin("hello world\n") .assert() .success() .stdout(""); bat() - .env("LESSOPEN", "||-cat nonexistent.txt") + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "||-cat nonexistent.txt && echo %s >/dev/null") + .arg("--lessopen") .write_stdin("hello world\n") .assert() .success() @@ -2204,6 +2234,19 @@ fn lessopen_handling_empty_output_stdin() { fn lessopen_uses_shell() { bat() .env("LESSOPEN", "|cat < %s") + .arg("--lessopen") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\n"); +} + +#[cfg(unix)] +#[cfg(feature = "lessopen")] +#[test] +fn do_not_use_lessopen_by_default() { + bat() + .env("LESSOPEN", "|echo File is %s") .arg("test.txt") .assert() .success() @@ -2213,12 +2256,49 @@ fn lessopen_uses_shell() { #[cfg(unix)] #[cfg(feature = "lessopen")] #[test] -fn do_not_use_lessopen() { +fn do_not_use_lessopen_if_overridden() { bat() .env("LESSOPEN", "|echo File is %s") + .arg("--lessopen") .arg("--no-lessopen") .arg("test.txt") .assert() .success() .stdout("hello world\n"); } + +#[cfg(unix)] +#[cfg(feature = "lessopen")] +#[test] +fn lessopen_validity() { + bat() + .env("LESSOPEN", "|echo File is test.txt") + .arg("--lessopen") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\n") + .stderr( + "\u{1b}[33m[bat warning]\u{1b}[0m: LESSOPEN ignored: must contain exactly one %s\n", + ); + + bat() + .env("LESSOPEN", "|echo File is %s") + .arg("--lessopen") + .arg("test.txt") + .assert() + .success() + .stdout("File is test.txt\n") + .stderr(""); + + bat() + .env("LESSOPEN", "|echo %s is %s") + .arg("--lessopen") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\n") + .stderr( + "\u{1b}[33m[bat warning]\u{1b}[0m: LESSOPEN ignored: must contain exactly one %s\n", + ); +}