mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
tail: implement follow
for stdin (pipe, fifo, and redirects)
* implement behavior to pass `gnu/tests/tail-2/follow-stdin.sh` * add stdin redirect using the same /dev/stdin-workaround used by uu_stat * refactor
This commit is contained in:
parent
ede73745f5
commit
90cef98a14
1 changed files with 175 additions and 107 deletions
|
@ -47,7 +47,7 @@ use uucore::parse_size::{parse_size, ParseSizeError};
|
||||||
use uucore::ringbuffer::RingBuffer;
|
use uucore::ringbuffer::RingBuffer;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use crate::platform::stdin_is_pipe_or_fifo;
|
use crate::platform::{stdin_is_bad_fd, stdin_is_pipe_or_fifo};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::fs::metadata;
|
use std::fs::metadata;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
@ -65,9 +65,13 @@ const ABOUT: &str = "\
|
||||||
const USAGE: &str = "{} [FLAG]... [FILE]...";
|
const USAGE: &str = "{} [FLAG]... [FILE]...";
|
||||||
|
|
||||||
pub mod text {
|
pub mod text {
|
||||||
pub static STDIN_STR: &str = "standard input";
|
pub static DASH: &str = "-";
|
||||||
|
pub static DEV_STDIN: &str = "/dev/stdin";
|
||||||
|
pub static STDIN_HEADER: &str = "standard input";
|
||||||
pub static NO_FILES_REMAINING: &str = "no files remaining";
|
pub static NO_FILES_REMAINING: &str = "no files remaining";
|
||||||
pub static NO_SUCH_FILE: &str = "No such file or directory";
|
pub static NO_SUCH_FILE: &str = "No such file or directory";
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub static BAD_FD: &str = "Bad file descriptor";
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub static BACKEND: &str = "inotify";
|
pub static BACKEND: &str = "inotify";
|
||||||
#[cfg(all(unix, not(target_os = "linux")))]
|
#[cfg(all(unix, not(target_os = "linux")))]
|
||||||
|
@ -116,18 +120,18 @@ enum FollowMode {
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct Settings {
|
struct Settings {
|
||||||
mode: FilterMode,
|
|
||||||
sleep_sec: Duration,
|
|
||||||
max_unchanged_stats: u32,
|
|
||||||
beginning: bool,
|
beginning: bool,
|
||||||
follow: Option<FollowMode>,
|
follow: Option<FollowMode>,
|
||||||
|
max_unchanged_stats: u32,
|
||||||
|
mode: FilterMode,
|
||||||
|
paths: VecDeque<PathBuf>,
|
||||||
|
pid: platform::Pid,
|
||||||
|
retry: bool,
|
||||||
|
return_code: i32,
|
||||||
|
sleep_sec: Duration,
|
||||||
use_polling: bool,
|
use_polling: bool,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
retry: bool,
|
stdin_is_pipe_or_fifo: bool,
|
||||||
pid: platform::Pid,
|
|
||||||
paths: Vec<PathBuf>,
|
|
||||||
presume_input_pipe: bool,
|
|
||||||
return_code: i32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
|
@ -217,58 +221,15 @@ impl Settings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.presume_input_pipe = matches.is_present(options::PRESUME_INPUT_PIPE);
|
settings.stdin_is_pipe_or_fifo = matches.is_present(options::PRESUME_INPUT_PIPE);
|
||||||
|
|
||||||
let mut paths = matches
|
settings.paths = matches
|
||||||
.values_of(options::ARG_FILES)
|
.values_of(options::ARG_FILES)
|
||||||
.map(|v| v.map(PathBuf::from).collect())
|
.map(|v| v.map(PathBuf::from).collect())
|
||||||
.unwrap_or_else(|| vec![PathBuf::from("-")]);
|
.unwrap_or_default();
|
||||||
|
// .unwrap_or_else(|| {
|
||||||
// Filter out non tailable paths depending on `FollowMode`.
|
// vec![PathBuf::from("-")] // always follow stdin
|
||||||
paths.retain(|path| {
|
// });
|
||||||
if !path.is_stdin() {
|
|
||||||
if !(path.is_tailable()) {
|
|
||||||
if settings.follow == Some(FollowMode::Descriptor) && settings.retry {
|
|
||||||
show_warning!("--retry only effective for the initial open");
|
|
||||||
}
|
|
||||||
if !path.exists() {
|
|
||||||
settings.return_code = 1;
|
|
||||||
show_error!(
|
|
||||||
"cannot open {} for reading: {}",
|
|
||||||
path.quote(),
|
|
||||||
text::NO_SUCH_FILE
|
|
||||||
);
|
|
||||||
} else if path.is_dir() {
|
|
||||||
settings.return_code = 1;
|
|
||||||
show_error!("error reading {}: Is a directory", path.quote());
|
|
||||||
if settings.follow.is_some() && settings.retry {
|
|
||||||
let msg = if !settings.retry {
|
|
||||||
"; giving up on this name"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
show_error!(
|
|
||||||
"{}: cannot follow end of this type of file{}",
|
|
||||||
path.display(),
|
|
||||||
msg
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO: [2021-10; jhscheer] how to handle block device, socket, fifo?
|
|
||||||
todo!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if settings.follow == Some(FollowMode::Name) {
|
|
||||||
// Mimic GNU's tail; Exit immediately even though there might be other valid files.
|
|
||||||
crash!(1, "cannot follow '-' by name");
|
|
||||||
}
|
|
||||||
if settings.follow == Some(FollowMode::Name) && settings.retry {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
!path.is_dir() || path.is_stdin()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
settings.paths = paths.clone();
|
|
||||||
|
|
||||||
settings.verbose = (matches.is_present(options::verbosity::VERBOSE)
|
settings.verbose = (matches.is_present(options::verbosity::VERBOSE)
|
||||||
|| settings.paths.len() > 1)
|
|| settings.paths.len() > 1)
|
||||||
|
@ -280,16 +241,41 @@ impl Settings {
|
||||||
|
|
||||||
#[uucore::main]
|
#[uucore::main]
|
||||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let args = match Settings::get_from(args) {
|
let mut args = match Settings::get_from(args) {
|
||||||
Ok(o) => o,
|
Ok(o) => o,
|
||||||
Err(s) => {
|
Err(s) => {
|
||||||
return Err(USimpleError::new(1, s));
|
return Err(USimpleError::new(1, s));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// skip expansive fstat check if PRESUME_INPUT_PIPE is selected
|
||||||
|
if !args.stdin_is_pipe_or_fifo {
|
||||||
|
// FIXME windows has GetFileType which can determine if the file is a pipe/FIFO
|
||||||
|
// so this check can also be performed
|
||||||
|
if cfg!(unix) {
|
||||||
|
args.stdin_is_pipe_or_fifo = stdin_is_pipe_or_fifo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uu_tail(args)
|
uu_tail(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uu_tail(mut settings: Settings) -> UResult<()> {
|
fn uu_tail(mut settings: Settings) -> UResult<()> {
|
||||||
|
let dash = PathBuf::from(text::DASH);
|
||||||
|
|
||||||
|
// Mimic GNU's tail for `tail -F` and exit immediately
|
||||||
|
if (settings.paths.is_empty() || settings.paths.contains(&dash))
|
||||||
|
&& settings.follow == Some(FollowMode::Name)
|
||||||
|
{
|
||||||
|
crash!(1, "cannot follow {} by name", text::DASH.quote());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !settings.paths.contains(&dash) && settings.stdin_is_pipe_or_fifo
|
||||||
|
|| settings.paths.is_empty() && !settings.stdin_is_pipe_or_fifo
|
||||||
|
{
|
||||||
|
settings.paths.push_front(dash);
|
||||||
|
}
|
||||||
|
|
||||||
let mut first_header = true;
|
let mut first_header = true;
|
||||||
let mut files = FileHandling {
|
let mut files = FileHandling {
|
||||||
map: HashMap::with_capacity(settings.paths.len()),
|
map: HashMap::with_capacity(settings.paths.len()),
|
||||||
|
@ -299,45 +285,97 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
|
||||||
// Do an initial tail print of each path's content.
|
// Do an initial tail print of each path's content.
|
||||||
// Add `path` to `files` map if `--follow` is selected.
|
// Add `path` to `files` map if `--follow` is selected.
|
||||||
for path in &settings.paths {
|
for path in &settings.paths {
|
||||||
|
let mut path = path.to_path_buf();
|
||||||
|
let mut display_name = path.to_path_buf();
|
||||||
|
|
||||||
|
// Workaround to handle redirects, e.g. `touch f && tail -f - < f`
|
||||||
|
if cfg!(unix) && path.is_stdin() {
|
||||||
|
display_name = PathBuf::from(text::STDIN_HEADER);
|
||||||
|
if let Ok(p) = Path::new(text::DEV_STDIN).canonicalize() {
|
||||||
|
path = p.to_owned();
|
||||||
|
} else {
|
||||||
|
path = PathBuf::from(text::DEV_STDIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.is_stdin() && settings.follow == Some(FollowMode::Name) {
|
||||||
|
// TODO: still needed?
|
||||||
|
// Mimic GNU's tail; Exit immediately even though there might be other valid files.
|
||||||
|
crash!(1, "cannot follow {} by name", text::DASH.quote());
|
||||||
|
} else if !path.is_stdin() && !path.is_tailable() {
|
||||||
|
if settings.follow == Some(FollowMode::Descriptor) && settings.retry {
|
||||||
|
show_warning!("--retry only effective for the initial open");
|
||||||
|
}
|
||||||
|
if !path.exists() {
|
||||||
|
settings.return_code = 1;
|
||||||
|
show_error!(
|
||||||
|
"cannot open {} for reading: {}",
|
||||||
|
display_name.quote(),
|
||||||
|
text::NO_SUCH_FILE
|
||||||
|
);
|
||||||
|
} else if path.is_dir() {
|
||||||
|
settings.return_code = 1;
|
||||||
|
show_error!("error reading {}: Is a directory", display_name.quote());
|
||||||
|
if settings.follow.is_some() {
|
||||||
|
let msg = if !settings.retry {
|
||||||
|
"; giving up on this name"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
show_error!(
|
||||||
|
"{}: cannot follow end of this type of file{}",
|
||||||
|
display_name.display(),
|
||||||
|
msg
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !(settings.follow == Some(FollowMode::Name) && settings.retry) {
|
||||||
|
// skip directory if not retry
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: [2021-10; jhscheer] how to handle block device, socket, fifo?
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let md = path.metadata().ok();
|
let md = path.metadata().ok();
|
||||||
if path.is_stdin() || settings.presume_input_pipe {
|
|
||||||
|
if display_name.is_stdin() {
|
||||||
if settings.verbose {
|
if settings.verbose {
|
||||||
if !first_header {
|
if !first_header {
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
Path::new(text::STDIN_STR).print_header();
|
Path::new(text::STDIN_HEADER).print_header();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut reader = BufReader::new(stdin());
|
let mut reader = BufReader::new(stdin());
|
||||||
unbounded_tail(&mut reader, &settings)?;
|
if !stdin_is_bad_fd() {
|
||||||
|
unbounded_tail(&mut reader, &settings)?;
|
||||||
// Don't follow stdin since there are no checks for pipes/FIFOs
|
if settings.follow == Some(FollowMode::Descriptor) {
|
||||||
//
|
|
||||||
// FIXME windows has GetFileType which can determine if the file is a pipe/FIFO
|
|
||||||
// so this check can also be performed
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
POSIX specification regarding tail -f
|
|
||||||
If the input file is a regular file or if the file operand specifies a FIFO, do not
|
|
||||||
terminate after the last line of the input file has been copied, but read and copy
|
|
||||||
further bytes from the input file when they become available. If no file operand is
|
|
||||||
specified and standard input is a pipe or FIFO, the -f option shall be ignored. If
|
|
||||||
the input file is not a FIFO, pipe, or regular file, it is unspecified whether or
|
|
||||||
not the -f option shall be ignored.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if settings.follow == Some(FollowMode::Descriptor) && !stdin_is_pipe_or_fifo() {
|
|
||||||
// Insert `stdin` into `files.map`.
|
// Insert `stdin` into `files.map`.
|
||||||
files.map.insert(
|
files.insert(
|
||||||
PathBuf::from(text::STDIN_STR),
|
path.to_path_buf(),
|
||||||
PathData {
|
PathData {
|
||||||
reader: Some(Box::new(reader)),
|
reader: Some(Box::new(reader)),
|
||||||
metadata: None,
|
metadata: None,
|
||||||
display_name: PathBuf::from(text::STDIN_STR),
|
display_name: PathBuf::from(text::STDIN_HEADER),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
settings.return_code = 1;
|
||||||
|
show_error!(
|
||||||
|
"cannot fstat {}: {}",
|
||||||
|
text::STDIN_HEADER.quote(),
|
||||||
|
text::BAD_FD
|
||||||
|
);
|
||||||
|
if settings.follow.is_some() {
|
||||||
|
show_error!(
|
||||||
|
"error reading {}: {}",
|
||||||
|
text::STDIN_HEADER.quote(),
|
||||||
|
text::BAD_FD
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if path.is_tailable() {
|
} else if path.is_tailable() {
|
||||||
if settings.verbose {
|
if settings.verbose {
|
||||||
|
@ -359,44 +397,60 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
|
||||||
}
|
}
|
||||||
if settings.follow.is_some() {
|
if settings.follow.is_some() {
|
||||||
// Insert existing/file `path` into `files.map`.
|
// Insert existing/file `path` into `files.map`.
|
||||||
files.map.insert(
|
files.insert(
|
||||||
path.canonicalize().unwrap(),
|
path.canonicalize().unwrap(),
|
||||||
PathData {
|
PathData {
|
||||||
reader: Some(Box::new(reader)),
|
reader: Some(Box::new(reader)),
|
||||||
metadata: md,
|
metadata: md,
|
||||||
display_name: path.to_owned(),
|
display_name,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
files.last = Some(path.canonicalize().unwrap());
|
|
||||||
}
|
}
|
||||||
} else if settings.retry && settings.follow.is_some() {
|
} else if settings.retry && settings.follow.is_some() {
|
||||||
|
if path.is_relative() {
|
||||||
|
path = std::env::current_dir().unwrap().join(&path);
|
||||||
|
}
|
||||||
// Insert non-is_tailable() paths into `files.map`.
|
// Insert non-is_tailable() paths into `files.map`.
|
||||||
let key = if path.is_relative() {
|
files.insert(
|
||||||
std::env::current_dir().unwrap().join(path)
|
path.to_path_buf(),
|
||||||
} else {
|
|
||||||
path.to_path_buf()
|
|
||||||
};
|
|
||||||
files.map.insert(
|
|
||||||
key.to_path_buf(),
|
|
||||||
PathData {
|
PathData {
|
||||||
reader: None,
|
reader: None,
|
||||||
metadata: md,
|
metadata: md,
|
||||||
display_name: path.to_path_buf(),
|
display_name,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
files.last = Some(key);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dbg!(files.map.is_empty(), files.files_remaining(), files.only_stdin_remaining());
|
||||||
|
// for k in files.map.keys() {
|
||||||
|
// dbg!(k);
|
||||||
|
// }
|
||||||
|
|
||||||
if settings.follow.is_some() {
|
if settings.follow.is_some() {
|
||||||
|
/*
|
||||||
|
POSIX specification regarding tail -f
|
||||||
|
If the input file is a regular file or if the file operand specifies a FIFO, do not
|
||||||
|
terminate after the last line of the input file has been copied, but read and copy
|
||||||
|
further bytes from the input file when they become available. If no file operand is
|
||||||
|
specified and standard input is a pipe or FIFO, the -f option shall be ignored. If
|
||||||
|
the input file is not a FIFO, pipe, or regular file, it is unspecified whether or
|
||||||
|
not the -f option shall be ignored.
|
||||||
|
*/
|
||||||
if files.map.is_empty() || !files.files_remaining() && !settings.retry {
|
if files.map.is_empty() || !files.files_remaining() && !settings.retry {
|
||||||
show_error!("{}", text::NO_FILES_REMAINING);
|
if !files.only_stdin_remaining() {
|
||||||
} else {
|
show_error!("{}", text::NO_FILES_REMAINING);
|
||||||
|
}
|
||||||
|
} else if !(settings.stdin_is_pipe_or_fifo && settings.paths.len() == 1) {
|
||||||
follow(&mut files, &mut settings)?;
|
follow(&mut files, &mut settings)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.return_code > 0 {
|
if settings.return_code > 0 {
|
||||||
|
#[cfg(unix)]
|
||||||
|
if stdin_is_bad_fd() {
|
||||||
|
show_error!("-: {}", text::BAD_FD);
|
||||||
|
}
|
||||||
return Err(USimpleError::new(settings.return_code, ""));
|
return Err(USimpleError::new(settings.return_code, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -565,9 +619,9 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
|
||||||
// Fallback: polling (default delay is 30 seconds!)
|
// Fallback: polling (default delay is 30 seconds!)
|
||||||
|
|
||||||
// NOTE:
|
// NOTE:
|
||||||
// We force the use of kqueue with: features=["macos_kqueue"],
|
// We force the use of kqueue with: features=["macos_kqueue"].
|
||||||
// because macOS only `kqueue` is suitable for our use case since `FSEvents` waits until
|
// On macOS only `kqueue` is suitable for our use case because `FSEvents`
|
||||||
// file close util it delivers a modify event. See:
|
// waits for file close util it delivers a modify event. See:
|
||||||
// https://github.com/notify-rs/notify/issues/240
|
// https://github.com/notify-rs/notify/issues/240
|
||||||
|
|
||||||
let mut watcher: Box<dyn Watcher>;
|
let mut watcher: Box<dyn Watcher>;
|
||||||
|
@ -600,7 +654,7 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
|
||||||
// If there is no parent, add `path` to `orphans`.
|
// If there is no parent, add `path` to `orphans`.
|
||||||
let mut orphans = Vec::new();
|
let mut orphans = Vec::new();
|
||||||
for path in files.map.keys() {
|
for path in files.map.keys() {
|
||||||
if path.is_file() {
|
if path.is_tailable() {
|
||||||
let path = get_path(path, settings);
|
let path = get_path(path, settings);
|
||||||
watcher
|
watcher
|
||||||
.watch(&path.canonicalize().unwrap(), RecursiveMode::NonRecursive)
|
.watch(&path.canonicalize().unwrap(), RecursiveMode::NonRecursive)
|
||||||
|
@ -616,7 +670,8 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: [2021-10; jhscheer] does this case need handling?
|
// TODO: [2021-10; jhscheer] do we need to handle non-is_tailable without follow/retry?
|
||||||
|
todo!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -934,7 +989,7 @@ fn handle_event(
|
||||||
let new_path = event.paths.last().unwrap().canonicalize().unwrap();
|
let new_path = event.paths.last().unwrap().canonicalize().unwrap();
|
||||||
// Open new file and seek to End:
|
// Open new file and seek to End:
|
||||||
let mut file = File::open(&new_path).unwrap();
|
let mut file = File::open(&new_path).unwrap();
|
||||||
let _ = file.seek(SeekFrom::End(0));
|
file.seek(SeekFrom::End(0)).unwrap();
|
||||||
// Add new reader and remove old reader:
|
// Add new reader and remove old reader:
|
||||||
files.map.insert(
|
files.map.insert(
|
||||||
new_path.to_owned(),
|
new_path.to_owned(),
|
||||||
|
@ -1006,9 +1061,20 @@ struct FileHandling {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileHandling {
|
impl FileHandling {
|
||||||
|
fn insert(&mut self, k: PathBuf, v: PathData) -> Option<PathData> {
|
||||||
|
self.last = Some(k.to_owned());
|
||||||
|
self.map.insert(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn only_stdin_remaining(&self) -> bool {
|
||||||
|
self.map.len() == 1
|
||||||
|
&& (self.map.contains_key(Path::new(text::DASH))
|
||||||
|
|| self.map.contains_key(Path::new(text::DEV_STDIN))) // TODO: still needed?
|
||||||
|
}
|
||||||
|
|
||||||
fn files_remaining(&self) -> bool {
|
fn files_remaining(&self) -> bool {
|
||||||
for path in self.map.keys() {
|
for path in self.map.keys() {
|
||||||
if path.is_tailable() {
|
if path.is_tailable() || path.is_stdin() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1349,7 +1415,9 @@ trait PathExt {
|
||||||
|
|
||||||
impl PathExt for Path {
|
impl PathExt for Path {
|
||||||
fn is_stdin(&self) -> bool {
|
fn is_stdin(&self) -> bool {
|
||||||
self.to_str() == Some("-")
|
self.eq(Path::new(text::DASH))
|
||||||
|
|| self.eq(Path::new(text::DEV_STDIN))
|
||||||
|
|| self.eq(Path::new(text::STDIN_HEADER))
|
||||||
}
|
}
|
||||||
fn print_header(&self) {
|
fn print_header(&self) {
|
||||||
println!("==> {} <==", self.display());
|
println!("==> {} <==", self.display());
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue