diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index f881cada..61844a84 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -457,7 +457,7 @@ jobs: needs: build if: startsWith(github.ref, 'refs/tags/v') steps: - - uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f + - uses: vedantmgoyal9/winget-releaser@a8fff44b123f115ce4c21543edf8b677376fc7e0 with: identifier: sharkdp.bat installers-regex: '-pc-windows-msvc\.zip$' diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cce4d5c..1c2caf3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## Other - Fix dead Visual C++ Redistributable link in README, see #3895 (@1t1sCooL) +- Document `StripAnsiMode` and its variants in rustdoc, see #4067 (@ump45nose) - Update Cargo dependencies to resolve current RustSec advisories, see #3861 (@TyceHerrman) - Add instructions for removing fish help abbreviations to README, see #3655 (@claw-explorer). Closes #3536 - Add .NET slnx extension, see #3682 (@ltrzesniewski) @@ -26,6 +27,9 @@ - Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres) ## Bugfixes +- Fix the first line of UTF-16BE input swallowing the second line, which broke line numbers, `--line-range` and highlighting, see #XXXX (@cindykrafft) +- Exit quietly with status 0 instead of panicking when stdout is closed early (broken pipe) on auxiliary output paths such as `--completion`, `--config-file`, `--diagnostic`, `--generate-config-file`, `--set-terminal-title` and `cache --build`/`--clear`, matching the main output path since #232, closes #4034, see #4035 (@Mathjk) +- Track strikethrough (SGR 9 / 29) in the ANSI style tracker so it is re-emitted like bold, dim, italic, and underline, and is no longer dropped after the first highlighted token when displaying ANSI input with syntax highlighting, see #4015 (@mikehasa) - Allow boolean flags to be given more than once, so that a flag set in the config file can also be passed on the command line without erroring, see #3912 (@logarithmone1128) - Use parsed CLI arguments to detect number flags, see #3908 (@cuishuang) - Detect binary content beyond the first line, preventing encrypted files with early line breaks from being treated as text. Closes #3554, see #3877 (@Matei02355) @@ -85,6 +89,7 @@ - Add syntax highlighting for `Caddyfile` #3789 (@CosmicHorrorDev) - Include `.code-workspace` as a JSON extension #3809 (@dhruvkb) - Add syntax mapping for DNF repo configuration files, see #3814 (@injust) +- Add syntax mapping for Podman `.quadlets` files, see #4069 (@injust) ## Themes diff --git a/src/assets/build_assets.rs b/src/assets/build_assets.rs index 6d9c8e59..a05d3721 100644 --- a/src/assets/build_assets.rs +++ b/src/assets/build_assets.rs @@ -1,4 +1,5 @@ use std::convert::TryInto; +use std::io::{self, Write}; use std::path::Path; use syntect::highlighting::ThemeSet; @@ -24,7 +25,7 @@ pub fn build( let acknowledgements = build_acknowledgements(source_dir, include_acknowledgements)?; - print_unlinked_contexts(&syntax_set); + print_unlinked_contexts(&syntax_set)?; write_assets( &theme_set, @@ -46,16 +47,18 @@ fn build_theme_set(source_dir: &Path, include_integrated_assets: bool) -> Result if theme_dir.exists() { let res = theme_set.add_from_folder(&theme_dir); if let Err(err) = res { - println!( + writeln!( + io::stdout(), "Failed to load one or more themes from '{}' (reason: '{err}')", theme_dir.to_string_lossy(), - ); + )?; } } else { - println!( + writeln!( + io::stdout(), "No themes were found in '{}', using the default set", theme_dir.to_string_lossy() - ); + )?; } theme_set.try_into() @@ -78,23 +81,25 @@ fn build_syntax_set_builder( if syntax_dir.exists() { syntax_set_builder.add_from_folder(syntax_dir, true)?; } else { - println!( + writeln!( + io::stdout(), "No syntaxes were found in '{}', using the default set.", syntax_dir.to_string_lossy() - ); + )?; } Ok(syntax_set_builder) } -fn print_unlinked_contexts(syntax_set: &SyntaxSet) { +fn print_unlinked_contexts(syntax_set: &SyntaxSet) -> Result<()> { let missing_contexts = syntax_set.find_unlinked_contexts(); if !missing_contexts.is_empty() { - println!("Some referenced contexts could not be found!"); + writeln!(io::stdout(), "Some referenced contexts could not be found!")?; for context in missing_contexts { - println!("- {context}"); + writeln!(io::stdout(), "- {context}")?; } } + Ok(()) } fn write_assets( @@ -127,12 +132,13 @@ fn write_assets( )?; } - print!( + write!( + io::stdout(), "Writing metadata to folder {} ... ", target_dir.to_string_lossy() - ); + )?; crate::assets_metadata::AssetsMetadata::new(current_version).save_to_folder(target_dir)?; - println!("okay"); + writeln!(io::stdout(), "okay")?; Ok(()) } @@ -161,10 +167,14 @@ fn asset_to_cache( description: &str, compressed: bool, ) -> Result<()> { - print!("Writing {description} to {} ... ", path.to_string_lossy()); + write!( + io::stdout(), + "Writing {description} to {} ... ", + path.to_string_lossy() + )?; let contents = asset_to_contents(asset, description, compressed)?; std::fs::write(path, &contents[..]) .map_err(|_| format!("Could not save {description} to {}", path.to_string_lossy()))?; - println!("okay"); + writeln!(io::stdout(), "okay")?; Ok(()) } diff --git a/src/bin/bat/assets.rs b/src/bin/bat/assets.rs index 7f917088..94c858b5 100644 --- a/src/bin/bat/assets.rs +++ b/src/bin/bat/assets.rs @@ -1,5 +1,5 @@ use std::fs; -use std::io; +use std::io::{self, Write}; use std::path::Path; use std::path::PathBuf; @@ -9,10 +9,11 @@ use bat::assets::HighlightingAssets; use bat::assets_metadata::AssetsMetadata; use bat::error::*; -pub fn clear_assets(cache_dir: &Path) { - clear_asset(cache_dir.join("themes.bin"), "theme set cache"); - clear_asset(cache_dir.join("syntaxes.bin"), "syntax set cache"); - clear_asset(cache_dir.join("metadata.yaml"), "metadata file"); +pub fn clear_assets(cache_dir: &Path) -> Result<()> { + clear_asset(cache_dir.join("themes.bin"), "theme set cache")?; + clear_asset(cache_dir.join("syntaxes.bin"), "syntax set cache")?; + clear_asset(cache_dir.join("metadata.yaml"), "metadata file")?; + Ok(()) } pub fn assets_from_cache_or_binary( @@ -43,15 +44,19 @@ pub fn assets_from_cache_or_binary( Ok(custom_assets.unwrap_or_else(HighlightingAssets::from_binary)) } -fn clear_asset(path: PathBuf, description: &str) { - print!("Clearing {description} ... "); +fn clear_asset(path: PathBuf, description: &str) -> Result<()> { + write!(io::stdout(), "Clearing {description} ... ")?; match fs::remove_file(&path) { Err(err) if err.kind() == io::ErrorKind::NotFound => { - println!("skipped (not present)"); + writeln!(io::stdout(), "skipped (not present)")?; } Err(err) => { - println!("could not remove the cache file {path:?}: {err}"); + writeln!( + io::stdout(), + "could not remove the cache file {path:?}: {err}" + )?; } - Ok(_) => println!("okay"), + Ok(_) => writeln!(io::stdout(), "okay")?, } + Ok(()) } diff --git a/src/bin/bat/config.rs b/src/bin/bat/config.rs index 3e2fb72e..863aa3d7 100644 --- a/src/bin/bat/config.rs +++ b/src/bin/bat/config.rs @@ -32,12 +32,13 @@ pub fn config_file() -> PathBuf { pub fn generate_config_file() -> bat::error::Result<()> { let config_file = config_file(); if config_file.is_file() { - println!( + writeln!( + io::stdout(), "A config file already exists at: {}", config_file.to_string_lossy() - ); + )?; - print!("Overwrite? (y/N): "); + write!(io::stdout(), "Overwrite? (y/N): ")?; io::stdout().flush()?; let mut decision = String::new(); io::stdin().read_line(&mut decision)?; @@ -93,10 +94,11 @@ pub fn generate_config_file() -> bat::error::Result<()> { ) })?; - println!( + writeln!( + io::stdout(), "Success! Config file written to {}", config_file.to_string_lossy() - ); + )?; Ok(()) } diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 705a28b9..359219be 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -74,9 +74,9 @@ fn run_cache_subcommand( #[cfg(feature = "build-assets")] build_assets(matches, config_dir, cache_dir)?; #[cfg(not(feature = "build-assets"))] - println!("bat has been built without the 'build-assets' feature. The 'cache --build' option is not available."); + writeln!(io::stdout(), "bat has been built without the 'build-assets' feature. The 'cache --build' option is not available.")?; } else if matches.get_flag("clear") { - clear_assets(cache_dir); + clear_assets(cache_dir)?; } Ok(()) @@ -262,13 +262,17 @@ pub fn list_themes( Ok(()) } -fn set_terminal_title_to(new_terminal_title: String) { +fn set_terminal_title_to(new_terminal_title: String) -> Result<()> { let osc_command_for_setting_terminal_title = "\x1b]0;"; let osc_end_command = "\x07"; // Prevent BEL/ESC/C1 bytes in the title from terminating or nesting the OSC. let safe_title = bat::sanitize_for_terminal(&new_terminal_title); - print!("{osc_command_for_setting_terminal_title}{safe_title}{osc_end_command}"); - io::stdout().flush().unwrap(); + write!( + io::stdout(), + "{osc_command_for_setting_terminal_title}{safe_title}{osc_end_command}" + )?; + io::stdout().flush()?; + Ok(()) } fn get_new_terminal_title(inputs: &Vec) -> String { @@ -286,13 +290,13 @@ fn run_controller(inputs: Vec, config: &Config, cache_dir: &Path) -> Resu let assets = assets_from_cache_or_binary(config.use_custom_assets, cache_dir)?; let controller = Controller::new(config, &assets); if config.paging_mode != PagingMode::Never && config.set_terminal_title { - set_terminal_title_to(get_new_terminal_title(&inputs)); + set_terminal_title_to(get_new_terminal_title(&inputs))?; } controller.run(inputs, None) } #[cfg(feature = "bugreport")] -fn invoke_bugreport(app: &App, cache_dir: &Path) { +fn invoke_bugreport(app: &App, cache_dir: &Path) -> Result<()> { use bugreport::{bugreport, collector::*, format::Markdown, report::ReportEntry}; struct ColorSchemeCollector; @@ -370,7 +374,8 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) { )) }; - report.print::(); + writeln!(io::stdout(), "{}", report.format::())?; + Ok(()) } /// Returns `Err(..)` upon fatal errors. Otherwise, returns `Ok(true)` on full success and @@ -382,21 +387,22 @@ fn run() -> Result { if app.matches.get_flag("diagnostic") { #[cfg(feature = "bugreport")] - invoke_bugreport(&app, cache_dir); + invoke_bugreport(&app, cache_dir)?; #[cfg(not(feature = "bugreport"))] - println!("bat has been built without the 'bugreport' feature. The '--diagnostic' option is not available."); + writeln!(io::stdout(), "bat has been built without the 'bugreport' feature. The '--diagnostic' option is not available.")?; return Ok(true); } #[cfg(feature = "application")] if let Some(shell) = app.matches.get_one::("completion") { - match shell.as_str() { - "bash" => println!("{}", completions::BASH_COMPLETION), - "fish" => println!("{}", completions::FISH_COMPLETION), - "ps1" => println!("{}", completions::PS1_COMPLETION), - "zsh" => println!("{}", completions::ZSH_COMPLETION), + let completion = match shell.as_str() { + "bash" => completions::BASH_COMPLETION, + "fish" => completions::FISH_COMPLETION, + "ps1" => completions::PS1_COMPLETION, + "zsh" => completions::ZSH_COMPLETION, _ => unreachable!("No completion for shell '{shell}' available."), - } + }; + writeln!(io::stdout(), "{completion}")?; return Ok(true); } @@ -435,7 +441,7 @@ fn run() -> Result { list_themes(&config, config_dir, cache_dir, app.theme_options())?; Ok(true) } else if app.matches.get_flag("config-file") { - println!("{}", config_file().to_string_lossy()); + writeln!(io::stdout(), "{}", config_file().to_string_lossy())?; Ok(true) } else if app.matches.get_flag("generate-config-file") { generate_config_file()?; diff --git a/src/config.rs b/src/config.rs index 9f78244e..4b47852a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -98,20 +98,20 @@ pub struct Config<'a> { /// cached assets) are not available, assets from the binary will be used instead. pub use_custom_assets: bool, - // Whether or not to use $LESSOPEN if set + /// Whether or not to use $LESSOPEN if set #[cfg(feature = "lessopen")] pub use_lessopen: bool, - // Whether or not to set terminal title when using a pager + /// Whether or not to set terminal title when using a pager pub set_terminal_title: bool, /// The maximum number of consecutive empty lines to display pub squeeze_lines: Option, - // Whether or not to strip ANSI escape codes from the input + /// Whether or not to strip ANSI escape codes from the input pub strip_ansi: StripAnsiMode, - // Substitute terminal-active and spoofing-relevant bytes; implies strip_ansi. + /// Substitute terminal-active and spoofing-relevant bytes; implies strip_ansi. pub sanitize: StripAnsiMode, /// Whether or not to produce no output when input is empty diff --git a/src/input.rs b/src/input.rs index d79c8198..7fa2bdff 100644 --- a/src/input.rs +++ b/src/input.rs @@ -291,7 +291,12 @@ impl<'a> InputReader<'a> { if content_type == Some(ContentType::UTF_16LE) { read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A)?; - } else if content_type == Some(ContentType::UTF_16BE) { + } else if content_type == Some(ContentType::UTF_16BE) + // In UTF-16BE a newline is encoded as 0x00 0x0A, so `read_until(b'\n')` above + // may already have read a complete first line. Only keep reading if it did not + // (e.g. the 0x0A byte was part of another character such as U+4E0A). + && !(first_line.len() % 2 == 0 && first_line.ends_with(&[0x00, 0x0A])) + { read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00)?; } diff --git a/src/line_range.rs b/src/line_range.rs index 3d15c607..9e4f64e6 100644 --- a/src/line_range.rs +++ b/src/line_range.rs @@ -355,13 +355,13 @@ fn test_parse_context_fail() { #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum RangeCheckResult { - // Within one of the given ranges + /// Within one of the given ranges InRange, - // Before the first range or within two ranges + /// Before the first range or within two ranges BeforeOrBetweenRanges, - // Line number is outside of all ranges and larger than the last range. + /// Line number is outside of all ranges and larger than the last range. AfterLastRange, } diff --git a/src/preprocessor.rs b/src/preprocessor.rs index a9b8e712..c19d0092 100644 --- a/src/preprocessor.rs +++ b/src/preprocessor.rs @@ -302,11 +302,21 @@ pub fn strip_overstrike(line: &str, first_backspace: usize) -> String { output } +/// Specify when to strip ANSI escape sequences from the input. +/// +/// Used for both the `--strip-ansi` and the `--sanitize` option, the latter +/// implying the former with the same value. #[derive(Debug, PartialEq, Clone, Copy, Default)] pub enum StripAnsiMode { + /// Keep escape sequences in the output #[default] Never, + + /// Strip escape sequences from the output Always, + + /// Strip escape sequences unless the syntax highlighting language is plain + /// text, in which case they may be part of what the input wants to display Auto, } diff --git a/src/syntax_mapping/builtins/linux/50-podman-quadlet.toml b/src/syntax_mapping/builtins/linux/50-podman-quadlet.toml index 1544b52f..a744a69e 100644 --- a/src/syntax_mapping/builtins/linux/50-podman-quadlet.toml +++ b/src/syntax_mapping/builtins/linux/50-podman-quadlet.toml @@ -1,5 +1,6 @@ # see `man quadlet` [mappings] "INI" = [ + "*.quadlets", "**/containers/systemd/**/*.{container,volume,network,kube,image,build,pod,artifact}", ] diff --git a/src/vscreen.rs b/src/vscreen.rs index 6478f577..b5a8134a 100644 --- a/src/vscreen.rs +++ b/src/vscreen.rs @@ -173,8 +173,10 @@ impl Attributes { 2 => self.dim = "\x1B[2m".to_owned(), 3 => self.italic = "\x1B[3m".to_owned(), 4 => self.underline = "\x1B[4m".to_owned(), + 9 => self.strike = "\x1B[9m".to_owned(), 23 => self.italic.clear(), 24 => self.underline.clear(), + 29 => self.strike.clear(), 22 => { self.bold.clear(); self.dim.clear(); @@ -1112,10 +1114,10 @@ mod tests { fn test_sgr_attributes_do_not_leak_into_wrong_field() { let mut attrs = crate::vscreen::Attributes::new(); - // Bold, Dim, Italic, Underline, Foreground, Background + // Bold, Dim, Italic, Underline, Strike, Foreground, Background attrs.update(EscapeSequence::CSI { - raw_sequence: "\x1B[1;2;3;4;31;41m", - parameters: "1;2;3;4;31;41", + raw_sequence: "\x1B[1;2;3;4;9;31;41m", + parameters: "1;2;3;4;9;31;41", intermediates: "", final_byte: "m", }); @@ -1124,6 +1126,7 @@ mod tests { assert_eq!(attrs.dim, "\x1B[2m"); assert_eq!(attrs.italic, "\x1B[3m"); assert_eq!(attrs.underline, "\x1B[4m"); + assert_eq!(attrs.strike, "\x1B[9m"); assert_eq!(attrs.foreground, "\x1B[31m"); assert_eq!(attrs.background, "\x1B[41m"); @@ -1140,4 +1143,27 @@ mod tests { assert_eq!(attrs.foreground, "\x1B[94m"); assert_eq!(attrs.background, "\x1B[103m"); } + + #[test] + fn test_sgr_strike_is_tracked_and_cleared() { + let mut attrs = crate::vscreen::Attributes::new(); + + // Strikethrough on (SGR 9) must be tracked so it can be re-emitted. + attrs.update(EscapeSequence::CSI { + raw_sequence: "\x1B[9m", + parameters: "9", + intermediates: "", + final_byte: "m", + }); + assert_eq!(attrs.strike, "\x1B[9m"); + + // Strikethrough off (SGR 29) must clear it, just like italic (23) and underline (24). + attrs.update(EscapeSequence::CSI { + raw_sequence: "\x1B[29m", + parameters: "29", + intermediates: "", + final_byte: "m", + }); + assert_eq!(attrs.strike, ""); + } } diff --git a/src/wrapping.rs b/src/wrapping.rs index 3a5fa457..7b24b558 100644 --- a/src/wrapping.rs +++ b/src/wrapping.rs @@ -2,7 +2,7 @@ pub enum WrappingMode { Character, Word, - // The bool specifies whether wrapping has been explicitly disabled by the user via --wrap=never + /// The bool specifies whether wrapping has been explicitly disabled by the user via --wrap=never NoWrapping(bool), } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 3f15f281..a418ce70 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -25,10 +25,8 @@ mod unix { use unix::*; mod utils; -use utils::command::{bat, bat_with_config}; +use utils::command::{bat, bat_raw_command, bat_raw_command_with_config, bat_with_config}; -#[cfg(unix)] -use utils::command::bat_raw_command; use utils::mocked_pagers; const EXAMPLES_DIR: &str = "tests/examples"; @@ -60,6 +58,67 @@ fn stdin() { .stdout("foo\nbar\n"); } +/// Runs `bat` with `args` while its stdout is a pipe whose read end has +/// already been closed, so the very first write fails with `BrokenPipe` +/// (EPIPE on Unix, `ERROR_NO_DATA` on Windows). +fn assert_quiet_exit_when_stdout_is_closed(command: &mut std::process::Command, args: &[&str]) { + use std::process::Stdio; + + let (pipe_reader, pipe_writer) = std::io::pipe().expect("could not create a pipe"); + drop(pipe_reader); + + let output = command + .args(args) + .stdin(Stdio::null()) + .stdout(pipe_writer) + .stderr(Stdio::piped()) + .spawn() + .and_then(|child| child.wait_with_output()) + .expect("could not run bat"); + + assert!( + output.status.success(), + "bat {args:?} should exit quietly when stdout is closed, status: {}, stderr: {}", + output.status, + String::from_utf8_lossy(&output.stderr), + ); + assert!( + output.stderr.is_empty(), + "bat {args:?} should not print to stderr when stdout is closed, stderr: {}", + String::from_utf8_lossy(&output.stderr), + ); +} + +#[test] +fn auxiliary_output_does_not_panic_on_closed_stdout() { + // Auxiliary outputs used to be written via `println!`, which panics when + // the reader of a piped stdout is already gone (e.g. `bat --config-file | + // head -n 0`). The main output path has exited quietly on `BrokenPipe` + // since #232; auxiliary outputs should behave the same way. + assert_quiet_exit_when_stdout_is_closed(&mut bat_raw_command(), &["--config-file"]); + assert_quiet_exit_when_stdout_is_closed(&mut bat_raw_command(), &["--diagnostic"]); + + // The `cache` subcommand is only enabled when the working directory does + // not contain a file named `cache` (see `cache_clear`), and it conflicts + // with top-level flags like `--no-config`. Run it from a temporary + // directory that doubles as `BAT_CACHE_PATH`, leaving the real cache alone. + let temp_dir = tempdir().expect("could not create a temporary directory"); + let mut command = bat_raw_command_with_config(); + command + .current_dir(temp_dir.path()) + .env("BAT_CONFIG_PATH", temp_dir.path().join("config")) + .env("BAT_CACHE_PATH", temp_dir.path()); + assert_quiet_exit_when_stdout_is_closed(&mut command, &["cache", "--clear"]); +} + +#[cfg(feature = "application")] +#[test] +fn completion_does_not_panic_on_closed_stdout() { + for shell in ["bash", "fish", "ps1", "zsh"] { + assert_quiet_exit_when_stdout_is_closed(&mut bat_raw_command(), &["--completion", shell]); + } +} + #[test] fn concatenate() { bat() @@ -2042,6 +2101,19 @@ fn utf16be() { .stdout(" 1 上一伊刀\n 2 foo bar\n 3 hello world\n"); } +#[test] +fn utf16be_first_line_not_merged_with_second() { + // The first line of a UTF-16BE file must not swallow the second line + bat() + .arg("--decorations=always") + .arg("--style=numbers") + .arg("--color=never") + .arg("test_UTF-16BE.txt") + .assert() + .success() + .stdout(" 1 hello world\n 2 this is a test\n"); +} + // Regression test for https://github.com/sharkdp/bat/issues/1922 #[test] fn bom_not_stripped_in_loop_through_mode() { @@ -3355,6 +3427,25 @@ fn ansi_sgr_joins_attributes_when_wrapped() { .stderr(""); } +// Ensure that strikethrough (SGR 9) is tracked like the other attributes, so it is +// re-emitted (and therefore preserved) across a wrap boundary instead of being dropped. +#[test] +fn ansi_sgr_strike_joins_attributes_when_wrapped() { + bat() + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=20") + .arg("--wrap=character") + .arg("--decorations=always") + .arg("--style=plain") + .write_stdin("\x1B[33mColor. \x1B[9mStrike.......Struck+color.\n") + .assert() + .success() + .stdout("\x1B[33m\x1B[33mColor. \x1B[9m\x1B[33m\x1B[9mStrike.......\n\x1B[33m\x1B[9mStruck+color.\n") + // FIXME: ~~~~~~~~ ~~~~~~~~~~~~~~~ should not be emitted twice. + .stderr(""); +} + #[test] fn ignored_suffix_arg() { bat()