on master help sign in
Merge remote-tracking branch 'origin/master' into fix-1063
46487b2Martin Nordholts
Cargo.lock+26
@@ -99,6 +99,7 @@ dependencies = [
99 "assert_cmd",
99 "assert_cmd",
100 "atty",
100 "atty",
101 "clap",
101 "clap",
102+ "clircle",
102 "console",
103 "console",
103 "content_inspector",
104 "content_inspector",
104 "dirs",
105 "dirs",
@@ -117,6 +118,7 @@ dependencies = [
117 "syntect",
118 "syntect",
118 "tempdir",
119 "tempdir",
119 "unicode-width",
120 "unicode-width",
121+ "wait-timeout",
120 "wild",
122 "wild",
121]
123]
122
124
@@ -251,6 +253,18 @@ dependencies = [
251 "vec_map",
253 "vec_map",
252]
254]
253
255
256+[[package]]
257+name = "clircle"
258+version = "0.2.0"
259+source = "registry+https://github.com/rust-lang/crates.io-index"
260+checksum = "e27a01e782190a8314e65cc94274d9bbcd52e05a9d15b437fe2b31259b854b0d"
261+dependencies = [
262+ "cfg-if 1.0.0",
263+ "nix",
264+ "serde",
265+ "winapi",
266+]
267+
254[[package]]
268[[package]]
255name = "console"
269name = "console"
256version = "0.14.0"
270version = "0.14.0"
@@ -691,6 +705,18 @@ dependencies = [
691 "autocfg",
705 "autocfg",
692]
706]
693
707
708+[[package]]
709+name = "nix"
710+version = "0.19.1"
711+source = "registry+https://github.com/rust-lang/crates.io-index"
712+checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2"
713+dependencies = [
714+ "bitflags",
715+ "cc",
716+ "cfg-if 1.0.0",
717+ "libc",
718+]
719+
694[[package]]
720[[package]]
695name = "normalize-line-endings"
721name = "normalize-line-endings"
696version = "0.3.0"
722version = "0.3.0"
Cargo.toml+2
@@ -50,6 +50,7 @@ serde = { version = "1.0", features = ["derive"] }
50serde_yaml = "0.8"
50serde_yaml = "0.8"
51semver = "0.11"
51semver = "0.11"
52path_abs = { version = "0.5", default-features = false }
52path_abs = { version = "0.5", default-features = false }
53+clircle = "0.2.0"
53
54
54[dependencies.git2]
55[dependencies.git2]
55version = "0.13"
56version = "0.13"
@@ -76,6 +77,7 @@ tempdir = "0.3"
76assert_cmd = "1.0.2"
77assert_cmd = "1.0.2"
77serial_test = "0.5.1"
78serial_test = "0.5.1"
78predicates = "1.0.6"
79predicates = "1.0.6"
80+wait-timeout = "0.2.0"
79
81
80[build-dependencies]
82[build-dependencies]
81clap = { version = "2.33", optional = true }
83clap = { version = "2.33", optional = true }
src/controller.rs+14
@@ -1,3 +1,4 @@
1+use std::convert::TryFrom;
1use std::io::{self, Write};
2use std::io::{self, Write};
2
3
3use crate::assets::HighlightingAssets;
4use crate::assets::HighlightingAssets;
@@ -66,6 +67,14 @@ impl<'b> Controller<'b> {
66 }
67 }
67
68
68 let attached_to_pager = output_type.is_pager();
69 let attached_to_pager = output_type.is_pager();
70+ let rw_cycle_detected = !attached_to_pager && {
71+ let identifiers: Vec<_> = inputs
72+ .iter()
73+ .flat_map(clircle::Identifier::try_from)
74+ .collect();
75+ clircle::stdout_among_inputs(&identifiers)
76+ };
77+
69 let writer = output_type.handle()?;
78 let writer = output_type.handle()?;
70 let mut no_errors: bool = true;
79 let mut no_errors: bool = true;
71
80
@@ -78,6 +87,11 @@ impl<'b> Controller<'b> {
78 }
87 }
79 };
88 };
80
89
90+ if rw_cycle_detected {
91+ print_error(&"The output file is also an input!".into(), writer);
92+ return Ok(false);
93+ }
94+
81 for (index, input) in inputs.into_iter().enumerate() {
95 for (index, input) in inputs.into_iter().enumerate() {
82 match input.open(io::stdin().lock()) {
96 match input.open(io::stdin().lock()) {
83 Err(error) => {
97 Err(error) => {
src/input.rs+14
@@ -1,6 +1,8 @@
1+use std::convert::TryFrom;
1use std::ffi::{OsStr, OsString};
2use std::ffi::{OsStr, OsString};
2use std::fs::File;
3use std::fs::File;
3use std::io::{self, BufRead, BufReader, Read};
4use std::io::{self, BufRead, BufReader, Read};
5+use std::path::Path;
4
6
5use content_inspector::{self, ContentType};
7use content_inspector::{self, ContentType};
6
8
@@ -191,6 +193,18 @@ impl<'a> Input<'a> {
191 }
193 }
192}
194}
193
195
196+impl TryFrom<&'_ Input<'_>> for clircle::Identifier {
197+ type Error = ();
198+
199+ fn try_from(input: &Input) -> std::result::Result<clircle::Identifier, ()> {
200+ match input.kind {
201+ InputKind::OrdinaryFile(ref path) => Self::try_from(Path::new(path)).map_err(|_| ()),
202+ InputKind::StdIn => Self::try_from(clircle::Stdio::Stdin).map_err(|_| ()),
203+ InputKind::CustomReader(_) => Err(()),
204+ }
205+ }
206+}
207+
194pub(crate) struct InputReader<'a> {
208pub(crate) struct InputReader<'a> {
195 inner: Box<dyn BufRead + 'a>,
209 inner: Box<dyn BufRead + 'a>,
196 pub(crate) first_line: Vec<u8>,
210 pub(crate) first_line: Vec<u8>,
tests/examples/cycle.txtnew file

No changes to the content.

tests/integration_tests.rs+87−3
@@ -1,13 +1,19 @@
1−use assert_cmd::Command;
1+use assert_cmd::assert::OutputAssertExt;
2+use assert_cmd::cargo::CommandCargoExt;
2use predicates::{prelude::predicate, str::PredicateStrExt};
3use predicates::{prelude::predicate, str::PredicateStrExt};
3use serial_test::serial;
4use serial_test::serial;
4use std::env;
5use std::env;
6+use std::fs::File;
5use std::path::{Path, PathBuf};
7use std::path::{Path, PathBuf};
8+use std::process::{Command, Stdio};
6use std::str::from_utf8;
9use std::str::from_utf8;
10+use std::time::Duration;
7
11
8const EXAMPLES_DIR: &str = "tests/examples";
12const EXAMPLES_DIR: &str = "tests/examples";
13+const SAFE_CHILD_PROCESS_CREATION_TIME: Duration = Duration::from_millis(100);
14+const CHILD_WAIT_TIMEOUT: Duration = Duration::from_secs(15);
9
15
10−fn bat_with_config() -> Command {
16+fn bat_raw_command() -> Command {
11 let mut cmd = Command::cargo_bin("bat").unwrap();
17 let mut cmd = Command::cargo_bin("bat").unwrap();
12 cmd.current_dir("tests/examples");
18 cmd.current_dir("tests/examples");
13 cmd.env_remove("PAGER");
19 cmd.env_remove("PAGER");
@@ -19,7 +25,11 @@ fn bat_with_config() -> Command {
19 cmd
25 cmd
20}
26}
21
27
28+fn bat_with_config() -> assert_cmd::Command {
29+ assert_cmd::Command::from_std(bat_raw_command())
30+}
31+
22−fn bat() -> Command {
32+fn bat() -> assert_cmd::Command {
23 let mut cmd = bat_with_config();
33 let mut cmd = bat_with_config();
24 cmd.arg("--no-config");
34 cmd.arg("--no-config");
25 cmd
35 cmd
@@ -258,6 +268,80 @@ fn line_range_multiple() {
258 .stdout("line 1\nline 2\nline 4\n");
268 .stdout("line 1\nline 2\nline 4\n");
259}
269}
260
270
271+#[test]
272+fn basic_io_cycle() {
273+ let file_out = Stdio::from(File::open("tests/examples/cycle.txt").unwrap());
274+ bat_raw_command()
275+ .arg("test.txt")
276+ .arg("cycle.txt")
277+ .stdout(file_out)
278+ .assert()
279+ .failure();
280+}
281+
282+#[test]
283+fn stdin_to_stdout_cycle() {
284+ let file_out = Stdio::from(File::open("tests/examples/cycle.txt").unwrap());
285+ let file_in = Stdio::from(File::open("tests/examples/cycle.txt").unwrap());
286+ bat_raw_command()
287+ .stdin(file_in)
288+ .arg("test.txt")
289+ .arg("-")
290+ .stdout(file_out)
291+ .assert()
292+ .failure();
293+}
294+
295+#[cfg(unix)]
296+#[test]
297+fn no_args_doesnt_break() {
298+ use std::io::Write;
299+ use std::os::unix::io::FromRawFd;
300+ use std::thread;
301+
302+ use clircle::nix::pty::{openpty, OpenptyResult};
303+ use wait_timeout::ChildExt;
304+
305+ // To simulate bat getting started from the shell, a process is created with stdin and stdout
306+ // as the slave end of a pseudo terminal. Although both point to the same "file", bat should
307+ // not exit, because in this case it is safe to read and write to the same fd, which is why
308+ // this test exists.
309+ let OpenptyResult { master, slave } = openpty(None, None).expect("Couldn't open pty.");
310+ let mut master = unsafe { File::from_raw_fd(master) };
311+ let stdin = unsafe { Stdio::from_raw_fd(slave) };
312+ let stdout = unsafe { Stdio::from_raw_fd(slave) };
313+
314+ let mut child = bat_raw_command()
315+ .stdin(stdin)
316+ .stdout(stdout)
317+ .spawn()
318+ .expect("Failed to start.");
319+
320+ // Some time for the child process to start and to make sure, that we can poll the exit status.
321+ // Although this waiting period is not necessary, it is best to keep it in and be absolutely
322+ // sure, that the try_wait does not error later.
323+ thread::sleep(SAFE_CHILD_PROCESS_CREATION_TIME);
324+
325+ // The child process should be running and waiting for input,
326+ // therefore no exit status should be available.
327+ let exit_status = child
328+ .try_wait()
329+ .expect("Error polling exit status, this should never happen.");
330+ assert!(exit_status.is_none());
331+
332+ // Write Ctrl-D (end of transmission) to the pty.
333+ master
334+ .write_all(&[0x04])
335+ .expect("Couldn't write EOT character to master end.");
336+
337+ let exit_status = child
338+ .wait_timeout(CHILD_WAIT_TIMEOUT)
339+ .expect("Error polling exit status, this should never happen.")
340+ .expect("Exit status not set, but the child should have exited already.");
341+
342+ assert!(exit_status.success());
343+}
344+
261#[test]
345#[test]
262fn tabs_numbers() {
346fn tabs_numbers() {
263 bat()
347 bat()