1use assert_cmd::assert::OutputAssertExt;
2use assert_cmd::cargo::CommandCargoExt;
3use predicates::{prelude::predicate, str::PredicateStrExt};
4use serial_test::serial;
7use std::process::{Command, Stdio};
8use std::str::from_utf8;
11use std::time::Duration;
14use utils::mocked_pagers;
16const EXAMPLES_DIR: &str = "tests/examples";
19const SAFE_CHILD_PROCESS_CREATION_TIME: Duration = Duration::from_millis(100);
22const CHILD_WAIT_TIMEOUT: Duration = Duration::from_secs(15);
24fn bat_raw_command() -> Command {
25 let mut cmd = Command::cargo_bin("bat").unwrap();
26 cmd.current_dir("tests/examples");
27 cmd.env_remove("PAGER");
28 cmd.env_remove("BAT_PAGER");
29 cmd.env_remove("BAT_CONFIG_PATH");
30 cmd.env_remove("BAT_STYLE");
31 cmd.env_remove("BAT_THEME");
32 cmd.env_remove("BAT_TABS");
36fn bat_with_config() -> assert_cmd::Command {
37 assert_cmd::Command::from_std(bat_raw_command())
40fn bat() -> assert_cmd::Command {
41 let mut cmd = bat_with_config();
42 cmd.arg("--no-config");
52 .stdout("hello world\n")
59 .write_stdin("foo\nbar\n")
62 .stdout("foo\nbar\n");
72 .stdout("hello world\nhello world\n");
76fn concatenate_stdin() {
81 .write_stdin("stdin\n")
84 .stdout("hello world\nstdin\nhello world\n");
88fn concatenate_empty_first() {
94 .stdout("hello world\n");
98fn concatenate_empty_last() {
104 .stdout("hello world\n");
108fn concatenate_empty_both() {
118fn concatenate_empty_between() {
125 .stdout("hello world\nhello world\n");
129fn concatenate_empty_first_and_last() {
136 .stdout("hello world\n");
140fn concatenate_single_line() {
142 .arg("single-line.txt")
143 .arg("single-line.txt")
146 .stdout("Single LineSingle Line");
150fn concatenate_single_line_empty() {
152 .arg("single-line.txt")
154 .arg("single-line.txt")
157 .stdout("Single LineSingle Line");
163 .arg("multiline.txt")
164 .arg("--style=numbers")
165 .arg("--decorations=always")
168 .stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n");
174 .arg("multiline.txt")
175 .arg("--line-range=2:3")
178 .stdout("line 2\nline 3\n");
182fn line_range_first_two() {
184 .arg("multiline.txt")
185 .arg("--line-range=:2")
188 .stdout("line 1\nline 2\n");
192fn line_range_last_3() {
194 .arg("multiline.txt")
195 .arg("--line-range=2:")
198 .stdout("line 2\nline 3\nline 4\n");
202fn line_range_multiple() {
204 .arg("multiline.txt")
205 .arg("--line-range=1:2")
206 .arg("--line-range=4:4")
209 .stdout("line 1\nline 2\nline 4\n");
214 let file_out = Stdio::from(File::open("tests/examples/cycle.txt").unwrap());
224fn stdin_to_stdout_cycle() {
225 let file_out = Stdio::from(File::open("tests/examples/cycle.txt").unwrap());
226 let file_in = Stdio::from(File::open("tests/examples/cycle.txt").unwrap());
238fn no_args_doesnt_break() {
240 use std::os::unix::io::FromRawFd;
243 use clircle::nix::pty::{openpty, OpenptyResult};
244 use wait_timeout::ChildExt;
246 // To simulate bat getting started from the shell, a process is created with stdin and stdout
247 // as the slave end of a pseudo terminal. Although both point to the same "file", bat should
248 // not exit, because in this case it is safe to read and write to the same fd, which is why
250 let OpenptyResult { master, slave } = openpty(None, None).expect("Couldn't open pty.");
251 let mut master = unsafe { File::from_raw_fd(master) };
252 let stdin = unsafe { Stdio::from_raw_fd(slave) };
253 let stdout = unsafe { Stdio::from_raw_fd(slave) };
255 let mut child = bat_raw_command()
259 .expect("Failed to start.");
261 // Some time for the child process to start and to make sure, that we can poll the exit status.
262 // Although this waiting period is not necessary, it is best to keep it in and be absolutely
263 // sure, that the try_wait does not error later.
264 thread::sleep(SAFE_CHILD_PROCESS_CREATION_TIME);
266 // The child process should be running and waiting for input,
267 // therefore no exit status should be available.
268 let exit_status = child
270 .expect("Error polling exit status, this should never happen.");
271 assert!(exit_status.is_none());
273 // Write Ctrl-D (end of transmission) to the pty.
276 .expect("Couldn't write EOT character to master end.");
278 let exit_status = child
279 .wait_timeout(CHILD_WAIT_TIMEOUT)
280 .expect("Error polling exit status, this should never happen.")
281 .expect("Exit status not set, but the child should have exited already.");
283 assert!(exit_status.success());
291 .arg("--style=numbers")
292 .arg("--decorations=always")
310fn tabs_passthrough_wrapped() {
314 .arg("--style=plain")
315 .arg("--decorations=always")
337 .arg("--style=plain")
338 .arg("--decorations=always")
360 .arg("--style=plain")
361 .arg("--decorations=always")
379fn tabs_passthrough() {
383 .arg("--style=plain")
384 .arg("--decorations=always")
406 .arg("--style=plain")
407 .arg("--decorations=always")
429 .arg("--style=plain")
430 .arg("--decorations=always")
448fn fail_non_existing() {
449 bat().arg("non-existing-file").assert().failure();
454 bat().arg("sub_directory").assert().failure();
458fn do_not_exit_directory() {
460 .arg("sub_directory")
463 .stdout("hello world\n")
470 .env("PAGER", "echo pager-output")
471 .arg("--paging=always")
475 .stdout(predicate::eq("pager-output\n").normalize());
479fn pager_overwrite() {
481 .env("PAGER", "echo other-pager")
482 .env("BAT_PAGER", "echo pager-output")
483 .arg("--paging=always")
487 .stdout(predicate::eq("pager-output\n").normalize());
493 .env("PAGER", "echo other-pager")
494 .env("BAT_PAGER", "")
495 .arg("--paging=always")
499 .stdout(predicate::eq("hello world\n").normalize());
503fn env_var_pager_value_bat() {
506 .arg("--paging=always")
510 .stdout(predicate::eq("hello world\n").normalize());
514fn env_var_bat_pager_value_bat() {
516 .env("BAT_PAGER", "bat")
517 .arg("--paging=always")
521 .stderr(predicate::str::contains("bat as a pager is disallowed"));
525fn pager_value_bat() {
528 .arg("--paging=always")
532 .stderr(predicate::str::contains("bat as a pager is disallowed"));
535/// We shall use less instead of most if PAGER is used since PAGER
536/// is a generic env var
538#[serial] // Because of PATH
539fn pager_most_from_pager_env_var() {
540 mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| {
541 // If the output is not "I am most" then we know 'most' is not used
543 .env("PAGER", mocked_pagers::from("most"))
544 .arg("--paging=always")
548 .stdout(predicate::eq("hello world\n").normalize());
552/// If the bat-specific BAT_PAGER is used, obey the wish of the user
555#[serial] // Because of PATH
556fn pager_most_from_bat_pager_env_var() {
557 mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| {
559 .env("BAT_PAGER", mocked_pagers::from("most"))
560 .arg("--paging=always")
564 .stdout(predicate::str::contains("I am most"));
568/// Same reasoning with --pager as with BAT_PAGER
570#[serial] // Because of PATH
571fn pager_most_from_pager_arg() {
572 mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| {
574 .arg("--paging=always")
575 .arg(format!("--pager={}", mocked_pagers::from("most")))
579 .stdout(predicate::str::contains("I am most"));
583/// Make sure the logic for 'most' applies even if an argument is passed
585#[serial] // Because of PATH
586fn pager_most_with_arg() {
587 mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| {
589 .env("PAGER", format!("{} -w", mocked_pagers::from("most")))
590 .arg("--paging=always")
594 .stdout(predicate::eq("hello world\n").normalize());
598/// Sanity check that 'more' is treated like 'most'
600#[serial] // Because of PATH
602 mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| {
604 .env("PAGER", mocked_pagers::from("more"))
605 .arg("--paging=always")
609 .stdout(predicate::eq("hello world\n").normalize());
614fn alias_pager_disable() {
616 .env("PAGER", "echo other-pager")
621 .stdout(predicate::eq("hello world\n").normalize());
625fn alias_pager_disable_long_overrides_short() {
627 .env("PAGER", "echo pager-output")
629 .arg("--paging=always")
633 .stdout(predicate::eq("pager-output\n").normalize());
637fn pager_failed_to_parse() {
639 .env("BAT_PAGER", "mismatched-quotes 'a")
640 .arg("--paging=always")
644 .stderr(predicate::str::contains("Could not parse pager command"));
648fn config_location_test() {
650 .env("BAT_CONFIG_PATH", "bat.conf")
651 .arg("--config-file")
654 .stdout("bat.conf\n");
658fn config_read_arguments_from_file() {
660 .env("BAT_CONFIG_PATH", "bat.conf")
664 .stdout(predicate::eq("dummy-pager-from-config\n").normalize());
669 // The output will be converted to UTF-8 with a leading UTF-8 BOM
672 .arg("--decorations=always")
673 .arg("test_UTF-16LE.txt")
676 .stdout(std::str::from_utf8(b"\xEF\xBB\xBFhello world\n").unwrap());
680fn can_print_file_named_cache() {
690fn can_print_file_named_cache_with_additional_argument() {
696 .stdout("test\nhello world\n")
701fn can_print_file_starting_with_cache() {
711fn does_not_print_unwanted_file_named_cache() {
712 bat_with_config().arg("cach").assert().failure();
718 .arg("unicode-wrap.txt")
719 .arg("--style=numbers,snip")
720 .arg("--decorations=always")
721 .arg("--terminal-width=40")
725 " 1 ビタミンA ビタミンD ビタミンE ビ
731 3 고양이 고양이 고양이 고양이 고양이
741 5 1 บวก 2 บวก 3 บวก 4 บวก 5 บวก 6 บวก
742 7 บวก 8 บวก 9 บวก 10 บวก 11 บวก 12
743 บวก 13 บวก 14 บวก 15 บวก 16 บวก 17
746 7 Бельгия Болгария Чехия Дания Герман
747 ия Эстония Ирландия Греция Испания
748 Франция Хорватия Италия Кипр Латвия
749 Литва Люксембург Венгрия Мальта Ни
750 дерланды Австрия Польша Португалия
751 Румыния Словения Словакия Финляндия
752 Швеция Великобритания
760 .arg("multiline.txt")
761 .arg("--style=numbers,snip")
762 .arg("--decorations=always")
763 .arg("--line-range=1:2")
764 .arg("--line-range=4:")
765 .arg("--terminal-width=80")
771 ...─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ 8< ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
778fn empty_file_leads_to_empty_output_with_grid_enabled() {
782 .arg("--decorations=always")
783 .arg("--terminal-width=80")
790fn empty_file_leads_to_empty_output_with_rule_enabled() {
794 .arg("--decorations=always")
795 .arg("--terminal-width=80")
805 .arg("--decorations=always")
806 .arg("--style=header")
808 .arg("--file-name=foo")
811 .stdout("File: foo\n")
816fn filename_binary() {
819 .arg("--decorations=always")
820 .arg("--style=header")
822 .arg("--file-name=foo")
825 .stdout("File: foo <BINARY>\n")
832 .arg("--decorations=always")
833 .arg("--style=header")
836 .write_stdin("stdin\n")
837 .arg("--file-name=foo")
840 .stdout("File: foo\n")
845fn filename_stdin_binary() {
846 let vec = vec![0; 1];
848 .arg("--decorations=always")
849 .arg("--style=header")
851 .arg("--file-name=foo")
854 .stdout("File: foo <BINARY>\n")
859fn filename_multiple_ok() {
861 .arg("--decorations=always")
862 .arg("--style=header")
865 .arg("--file-name=foo")
866 .arg("single-line.txt")
867 .arg("--file-name=bar")
870 .stdout("File: foo\n\nFile: bar\n")
875fn filename_multiple_err() {
877 .arg("--decorations=always")
878 .arg("--style=header")
881 .arg("--file-name=foo")
882 .arg("single-line.txt")
890 .arg("--decorations=always")
891 .arg("--style=header")
893 .arg("single-line.txt")
895 .stdout("File: test.txt\nhello world\n\nFile: single-line.txt\nSingle Line\n")
900fn header_padding_rule() {
902 .arg("--decorations=always")
903 .arg("--style=header,rule")
904 .arg("--terminal-width=80")
906 .arg("single-line.txt")
911────────────────────────────────────────────────────────────────────────────────
920fn grid_overrides_rule() {
922 .arg("--decorations=always")
923 .arg("--style=grid,rule")
924 .arg("--terminal-width=80")
926 .arg("single-line.txt")
930────────────────────────────────────────────────────────────────────────────────
932────────────────────────────────────────────────────────────────────────────────
933────────────────────────────────────────────────────────────────────────────────
935────────────────────────────────────────────────────────────────────────────────
938 .stderr("\x1b[33m[bat warning]\x1b[0m: Style 'rule' is a subset of style 'grid', 'rule' will not be visible.\n");
941#[cfg(target_os = "linux")]
943fn file_with_invalid_utf8_filename() {
947 use std::os::unix::ffi::OsStrExt;
949 use tempdir::TempDir;
951 let tmp_dir = TempDir::new("bat_test").expect("can create temporary directory");
952 let file_path = tmp_dir
954 .join(OsStr::from_bytes(b"test-invalid-utf8-\xC3(.rs"));
956 let mut file = File::create(&file_path).expect("can create temporary file");
957 writeln!(file, "dummy content").expect("can write to file");
961 .arg(file_path.as_os_str())
964 .stdout("dummy content\n");
968fn do_not_panic_regression_tests() {
977 .arg("--color=always")
978 .arg(&format!("regression_tests/{}", filename))
985fn do_not_detect_different_syntax_for_stdin_and_files() {
986 let file = "regression_tests/issue_985.js";
988 let cmd_for_file = bat()
989 .arg("--color=always")
990 .arg("--map-syntax=*.js:Markdown")
991 .arg(&format!("--file-name={}", file))
992 .arg("--style=plain")
997 let cmd_for_stdin = bat()
998 .arg("--color=always")
999 .arg("--map-syntax=*.js:Markdown")
1000 .arg("--style=plain")
1001 .arg(&format!("--file-name={}", file))
1002 .pipe_stdin(Path::new(EXAMPLES_DIR).join(file))
1008 from_utf8(&cmd_for_file.get_output().stdout).expect("output is valid utf-8"),
1009 from_utf8(&cmd_for_stdin.get_output().stdout).expect("output is valid utf-8")
1017 .arg("nonprintable.txt")
1019 .stdout("hello·world␊\n├──┤␍␀␇␈␛")
1024fn plain_mode_does_not_add_nonexisting_newline() {
1026 .arg("--paging=never")
1027 .arg("--color=never")
1028 .arg("--decorations=always")
1029 .arg("--style=plain")
1030 .arg("single-line.txt")
1033 .stdout("Single Line");
1036// Regression test for https://github.com/sharkdp/bat/issues/299
1038fn grid_for_file_without_newline() {
1040 .arg("--paging=never")
1041 .arg("--color=never")
1042 .arg("--terminal-width=80")
1043 .arg("--wrap=never")
1044 .arg("--decorations=always")
1045 .arg("--style=full")
1046 .arg("single-line.txt")
1051───────┬────────────────────────────────────────────────────────────────────────
1052 │ File: single-line.txt
1053───────┼────────────────────────────────────────────────────────────────────────
1055───────┴────────────────────────────────────────────────────────────────────────
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list