1
0
mirror of https://github.com/BurntSushi/ripgrep.git synced 2025-04-14 00:58:43 +02:00

cli: clean-up crate

This does a variety of polishing.

1. Deprecate the tty methods in favor of std's IsTerminal trait.
2. Trim down un-needed dependencies.
3. Use bstr to implement escaping.
4. Various aesthetic polishing.

I'm doing this as prep work before adding more to this crate. And as
part of a general effort toward reducing ripgrep's dependencies.
This commit is contained in:
Andrew Gallant 2023-09-20 14:42:03 -04:00
parent 1a50324013
commit 19a08bee8a
11 changed files with 165 additions and 306 deletions

7
Cargo.lock generated
View File

@ -187,10 +187,7 @@ version = "0.1.9"
dependencies = [ dependencies = [
"bstr", "bstr",
"globset", "globset",
"lazy_static",
"log", "log",
"regex",
"same-file",
"termcolor", "termcolor",
"winapi-util", "winapi-util",
] ]
@ -612,9 +609,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.5" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [ dependencies = [
"winapi", "winapi",
] ]

View File

@ -11,16 +11,13 @@ repository = "https://github.com/BurntSushi/ripgrep/tree/master/crates/cli"
readme = "README.md" readme = "README.md"
keywords = ["regex", "grep", "cli", "utility", "util"] keywords = ["regex", "grep", "cli", "utility", "util"]
license = "Unlicense OR MIT" license = "Unlicense OR MIT"
edition = "2018" edition = "2021"
[dependencies] [dependencies]
bstr = "1.6.0" bstr = { version = "1.6.2", features = ["std"] }
globset = { version = "0.4.10", path = "../globset" } globset = { version = "0.4.10", path = "../globset" }
lazy_static = "1.1.0" log = "0.4.20"
log = "0.4.5" termcolor = "1.3.0"
regex = "1.1"
same-file = "1.0.4"
termcolor = "1.0.4"
[target.'cfg(windows)'.dependencies.winapi-util] [target.'cfg(windows)'.dependencies.winapi-util]
version = "0.1.1" version = "0.1.6"

View File

@ -1,8 +1,10 @@
use std::ffi::{OsStr, OsString}; use std::{
use std::fs::File; ffi::{OsStr, OsString},
use std::io; fs::File,
use std::path::{Path, PathBuf}; io,
use std::process::Command; path::{Path, PathBuf},
process::Command,
};
use globset::{Glob, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobSet, GlobSetBuilder};
@ -161,7 +163,7 @@ impl DecompressionMatcher {
/// Create a new matcher with default rules. /// Create a new matcher with default rules.
/// ///
/// To add more matching rules, build a matcher with /// To add more matching rules, build a matcher with
/// [`DecompressionMatcherBuilder`](struct.DecompressionMatcherBuilder.html). /// [`DecompressionMatcherBuilder`].
pub fn new() -> DecompressionMatcher { pub fn new() -> DecompressionMatcher {
DecompressionMatcherBuilder::new() DecompressionMatcherBuilder::new()
.build() .build()
@ -221,9 +223,8 @@ impl DecompressionReaderBuilder {
path: P, path: P,
) -> Result<DecompressionReader, CommandError> { ) -> Result<DecompressionReader, CommandError> {
let path = path.as_ref(); let path = path.as_ref();
let mut cmd = match self.matcher.command(path) { let Some(mut cmd) = self.matcher.command(path) else {
None => return DecompressionReader::new_passthru(path), return DecompressionReader::new_passthru(path);
Some(cmd) => cmd,
}; };
cmd.arg(path); cmd.arg(path);
@ -302,9 +303,7 @@ impl DecompressionReaderBuilder {
/// The default matching rules are probably good enough for most cases, and if /// The default matching rules are probably good enough for most cases, and if
/// they require revision, pull requests are welcome. In cases where they must /// they require revision, pull requests are welcome. In cases where they must
/// be changed or extended, they can be customized through the use of /// be changed or extended, they can be customized through the use of
/// [`DecompressionMatcherBuilder`](struct.DecompressionMatcherBuilder.html) /// [`DecompressionMatcherBuilder`] and [`DecompressionReaderBuilder`].
/// and
/// [`DecompressionReaderBuilder`](struct.DecompressionReaderBuilder.html).
/// ///
/// By default, this reader will asynchronously read the processes' stderr. /// By default, this reader will asynchronously read the processes' stderr.
/// This prevents subtle deadlocking bugs for noisy processes that write a lot /// This prevents subtle deadlocking bugs for noisy processes that write a lot
@ -320,15 +319,14 @@ impl DecompressionReaderBuilder {
/// matcher. /// matcher.
/// ///
/// ```no_run /// ```no_run
/// use std::io::Read; /// use std::{io::Read, process::Command};
/// use std::process::Command; ///
/// use grep_cli::DecompressionReader; /// use grep_cli::DecompressionReader;
/// ///
/// # fn example() -> Result<(), Box<::std::error::Error>> {
/// let mut rdr = DecompressionReader::new("/usr/share/man/man1/ls.1.gz")?; /// let mut rdr = DecompressionReader::new("/usr/share/man/man1/ls.1.gz")?;
/// let mut contents = vec![]; /// let mut contents = vec![];
/// rdr.read_to_end(&mut contents)?; /// rdr.read_to_end(&mut contents)?;
/// # Ok(()) } /// # Ok::<(), Box<dyn std::error::Error>>(())
/// ``` /// ```
#[derive(Debug)] #[derive(Debug)]
pub struct DecompressionReader { pub struct DecompressionReader {
@ -347,9 +345,7 @@ impl DecompressionReader {
/// ///
/// This uses the default matching rules for determining how to decompress /// This uses the default matching rules for determining how to decompress
/// the given file. To change those matching rules, use /// the given file. To change those matching rules, use
/// [`DecompressionReaderBuilder`](struct.DecompressionReaderBuilder.html) /// [`DecompressionReaderBuilder`] and [`DecompressionMatcherBuilder`].
/// and
/// [`DecompressionMatcherBuilder`](struct.DecompressionMatcherBuilder.html).
/// ///
/// When creating readers for many paths. it is better to use the builder /// When creating readers for many paths. it is better to use the builder
/// since it will amortize the cost of constructing the matcher. /// since it will amortize the cost of constructing the matcher.
@ -453,10 +449,7 @@ fn try_resolve_binary<P: AsRef<Path>>(
use std::env; use std::env;
fn is_exe(path: &Path) -> bool { fn is_exe(path: &Path) -> bool {
let md = match path.metadata() { let Ok(md) = path.metadata() else { return false };
Err(_) => return false,
Ok(md) => md,
};
!md.is_dir() !md.is_dir()
} }
@ -464,15 +457,12 @@ fn try_resolve_binary<P: AsRef<Path>>(
if prog.is_absolute() { if prog.is_absolute() {
return Ok(prog.to_path_buf()); return Ok(prog.to_path_buf());
} }
let syspaths = match env::var_os("PATH") { let Some(syspaths) = env::var_os("PATH") else {
Some(syspaths) => syspaths, let msg = "system PATH environment variable not found";
None => { return Err(CommandError::io(io::Error::new(
let msg = "system PATH environment variable not found"; io::ErrorKind::Other,
return Err(CommandError::io(io::Error::new( msg,
io::ErrorKind::Other, )));
msg,
)));
}
}; };
for syspath in env::split_paths(&syspaths) { for syspath in env::split_paths(&syspaths) {
if syspath.as_os_str().is_empty() { if syspath.as_os_str().is_empty() {

View File

@ -1,21 +1,7 @@
use std::ffi::OsStr; use std::ffi::OsStr;
use std::str;
use bstr::{ByteSlice, ByteVec}; use bstr::{ByteSlice, ByteVec};
/// A single state in the state machine used by `unescape`.
#[derive(Clone, Copy, Eq, PartialEq)]
enum State {
/// The state after seeing a `\`.
Escape,
/// The state after seeing a `\x`.
HexFirst,
/// The state after seeing a `\x[0-9A-Fa-f]`.
HexSecond(char),
/// Default state.
Literal,
}
/// Escapes arbitrary bytes into a human readable string. /// Escapes arbitrary bytes into a human readable string.
/// ///
/// This converts `\t`, `\r` and `\n` into their escaped forms. It also /// This converts `\t`, `\r` and `\n` into their escaped forms. It also
@ -38,17 +24,7 @@ enum State {
/// assert_eq!(r"foo\nbar\xFFbaz", escape(b"foo\nbar\xFFbaz")); /// assert_eq!(r"foo\nbar\xFFbaz", escape(b"foo\nbar\xFFbaz"));
/// ``` /// ```
pub fn escape(bytes: &[u8]) -> String { pub fn escape(bytes: &[u8]) -> String {
let mut escaped = String::new(); bytes.escape_bytes().to_string()
for (s, e, ch) in bytes.char_indices() {
if ch == '\u{FFFD}' {
for b in bytes[s..e].bytes() {
escape_byte(b, &mut escaped);
}
} else {
escape_char(ch, &mut escaped);
}
}
escaped
} }
/// Escapes an OS string into a human readable string. /// Escapes an OS string into a human readable string.
@ -89,76 +65,7 @@ pub fn escape_os(string: &OsStr) -> String {
/// assert_eq!(&b"foo\nbar\xFFbaz"[..], &*unescape(r"foo\nbar\xFFbaz")); /// assert_eq!(&b"foo\nbar\xFFbaz"[..], &*unescape(r"foo\nbar\xFFbaz"));
/// ``` /// ```
pub fn unescape(s: &str) -> Vec<u8> { pub fn unescape(s: &str) -> Vec<u8> {
use self::State::*; Vec::unescape_bytes(s)
let mut bytes = vec![];
let mut state = Literal;
for c in s.chars() {
match state {
Escape => match c {
'\\' => {
bytes.push(b'\\');
state = Literal;
}
'n' => {
bytes.push(b'\n');
state = Literal;
}
'r' => {
bytes.push(b'\r');
state = Literal;
}
't' => {
bytes.push(b'\t');
state = Literal;
}
'x' => {
state = HexFirst;
}
c => {
bytes.extend(format!(r"\{}", c).into_bytes());
state = Literal;
}
},
HexFirst => match c {
'0'..='9' | 'A'..='F' | 'a'..='f' => {
state = HexSecond(c);
}
c => {
bytes.extend(format!(r"\x{}", c).into_bytes());
state = Literal;
}
},
HexSecond(first) => match c {
'0'..='9' | 'A'..='F' | 'a'..='f' => {
let ordinal = format!("{}{}", first, c);
let byte = u8::from_str_radix(&ordinal, 16).unwrap();
bytes.push(byte);
state = Literal;
}
c => {
let original = format!(r"\x{}{}", first, c);
bytes.extend(original.into_bytes());
state = Literal;
}
},
Literal => match c {
'\\' => {
state = Escape;
}
c => {
bytes.extend(c.to_string().as_bytes());
}
},
}
}
match state {
Escape => bytes.push(b'\\'),
HexFirst => bytes.extend(b"\\x"),
HexSecond(c) => bytes.extend(format!("\\x{}", c).into_bytes()),
Literal => {}
}
bytes
} }
/// Unescapes an OS string. /// Unescapes an OS string.
@ -171,27 +78,6 @@ pub fn unescape_os(string: &OsStr) -> Vec<u8> {
unescape(&string.to_string_lossy()) unescape(&string.to_string_lossy())
} }
/// Adds the given codepoint to the given string, escaping it if necessary.
fn escape_char(cp: char, into: &mut String) {
if cp.is_ascii() {
escape_byte(cp as u8, into);
} else {
into.push(cp);
}
}
/// Adds the given byte to the given string, escaping it if necessary.
fn escape_byte(byte: u8, into: &mut String) {
match byte {
0x21..=0x5B | 0x5D..=0x7D => into.push(byte as char),
b'\n' => into.push_str(r"\n"),
b'\r' => into.push_str(r"\r"),
b'\t' => into.push_str(r"\t"),
b'\\' => into.push_str(r"\\"),
_ => into.push_str(&format!(r"\x{:02X}", byte)),
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{escape, unescape}; use super::{escape, unescape};
@ -215,7 +101,8 @@ mod tests {
#[test] #[test]
fn nul() { fn nul() {
assert_eq!(b(b"\x00"), unescape(r"\x00")); assert_eq!(b(b"\x00"), unescape(r"\x00"));
assert_eq!(r"\x00", escape(b"\x00")); assert_eq!(b(b"\x00"), unescape(r"\0"));
assert_eq!(r"\0", escape(b"\x00"));
} }
#[test] #[test]

View File

@ -1,10 +1,3 @@
use std::error;
use std::fmt;
use std::io;
use std::num::ParseIntError;
use regex::Regex;
/// An error that occurs when parsing a human readable size description. /// An error that occurs when parsing a human readable size description.
/// ///
/// This error provides an end user friendly message describing why the /// This error provides an end user friendly message describing why the
@ -18,7 +11,7 @@ pub struct ParseSizeError {
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
enum ParseSizeErrorKind { enum ParseSizeErrorKind {
InvalidFormat, InvalidFormat,
InvalidInt(ParseIntError), InvalidInt(std::num::ParseIntError),
Overflow, Overflow,
} }
@ -30,7 +23,7 @@ impl ParseSizeError {
} }
} }
fn int(original: &str, err: ParseIntError) -> ParseSizeError { fn int(original: &str, err: std::num::ParseIntError) -> ParseSizeError {
ParseSizeError { ParseSizeError {
original: original.to_string(), original: original.to_string(),
kind: ParseSizeErrorKind::InvalidInt(err), kind: ParseSizeErrorKind::InvalidInt(err),
@ -45,22 +38,18 @@ impl ParseSizeError {
} }
} }
impl error::Error for ParseSizeError { impl std::error::Error for ParseSizeError {}
fn description(&self) -> &str {
"invalid size"
}
}
impl fmt::Display for ParseSizeError { impl std::fmt::Display for ParseSizeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use self::ParseSizeErrorKind::*; use self::ParseSizeErrorKind::*;
match self.kind { match self.kind {
InvalidFormat => write!( InvalidFormat => write!(
f, f,
"invalid format for size '{}', which should be a sequence \ "invalid format for size '{}', which should be a non-empty \
of digits followed by an optional 'K', 'M' or 'G' \ sequence of digits followed by an optional 'K', 'M' or 'G' \
suffix", suffix",
self.original self.original
), ),
InvalidInt(ref err) => write!( InvalidInt(ref err) => write!(
@ -73,9 +62,9 @@ impl fmt::Display for ParseSizeError {
} }
} }
impl From<ParseSizeError> for io::Error { impl From<ParseSizeError> for std::io::Error {
fn from(size_err: ParseSizeError) -> io::Error { fn from(size_err: ParseSizeError) -> std::io::Error {
io::Error::new(io::ErrorKind::Other, size_err) std::io::Error::new(std::io::ErrorKind::Other, size_err)
} }
} }
@ -88,29 +77,24 @@ impl From<ParseSizeError> for io::Error {
/// ///
/// Additional suffixes may be added over time. /// Additional suffixes may be added over time.
pub fn parse_human_readable_size(size: &str) -> Result<u64, ParseSizeError> { pub fn parse_human_readable_size(size: &str) -> Result<u64, ParseSizeError> {
lazy_static::lazy_static! { let digits_end =
// Normally I'd just parse something this simple by hand to avoid the size.as_bytes().iter().take_while(|&b| b.is_ascii_digit()).count();
// regex dep, but we bring regex in any way for glob matching, so might let digits = &size[..digits_end];
// as well use it. if digits.is_empty() {
static ref RE: Regex = Regex::new(r"^([0-9]+)([KMG])?$").unwrap(); return Err(ParseSizeError::format(size));
} }
let value =
digits.parse::<u64>().map_err(|e| ParseSizeError::int(size, e))?;
let caps = match RE.captures(size) { let suffix = &size[digits_end..];
Some(caps) => caps, if suffix.is_empty() {
None => return Err(ParseSizeError::format(size)), return Ok(value);
}; }
let value: u64 =
caps[1].parse().map_err(|err| ParseSizeError::int(size, err))?;
let suffix = match caps.get(2) {
None => return Ok(value),
Some(cap) => cap.as_str(),
};
let bytes = match suffix { let bytes = match suffix {
"K" => value.checked_mul(1 << 10), "K" => value.checked_mul(1 << 10),
"M" => value.checked_mul(1 << 20), "M" => value.checked_mul(1 << 20),
"G" => value.checked_mul(1 << 30), "G" => value.checked_mul(1 << 30),
// Because if the regex matches this group, it must be [KMG]. _ => return Err(ParseSizeError::format(size)),
_ => unreachable!(),
}; };
bytes.ok_or_else(|| ParseSizeError::overflow(size)) bytes.ok_or_else(|| ParseSizeError::overflow(size))
} }

View File

@ -11,27 +11,11 @@ and Linux.
# Standard I/O # Standard I/O
The [`is_readable_stdin`] determines whether stdin can be usefully read from. It
[`is_readable_stdin`](fn.is_readable_stdin.html), is useful when writing an application that changes behavior based on whether
[`is_tty_stderr`](fn.is_tty_stderr.html), the application was invoked with data on stdin. For example, `rg foo` might
[`is_tty_stdin`](fn.is_tty_stdin.html) recursively search the current working directory for occurrences of `foo`, but
and `rg foo < file` might only search the contents of `file`.
[`is_tty_stdout`](fn.is_tty_stdout.html)
routines query aspects of standard I/O. `is_readable_stdin` determines whether
stdin can be usefully read from, while the `tty` methods determine whether a
tty is attached to stdin/stdout/stderr.
`is_readable_stdin` is useful when writing an application that changes behavior
based on whether the application was invoked with data on stdin. For example,
`rg foo` might recursively search the current working directory for
occurrences of `foo`, but `rg foo < file` might only search the contents of
`file`.
The `tty` methods are useful for similar reasons. Namely, commands like `ls`
will change their output depending on whether they are printing to a terminal
or not. For example, `ls` shows a file on each line when stdout is redirected
to a file or a pipe, but condenses the output to show possibly many files on
each line when stdout is connected to a tty.
# Coloring and buffering # Coloring and buffering
@ -165,21 +149,21 @@ mod pattern;
mod process; mod process;
mod wtr; mod wtr;
use std::io::IsTerminal; pub use crate::{
decompress::{
pub use crate::decompress::{ resolve_binary, DecompressionMatcher, DecompressionMatcherBuilder,
resolve_binary, DecompressionMatcher, DecompressionMatcherBuilder, DecompressionReader, DecompressionReaderBuilder,
DecompressionReader, DecompressionReaderBuilder, },
}; escape::{escape, escape_os, unescape, unescape_os},
pub use crate::escape::{escape, escape_os, unescape, unescape_os}; human::{parse_human_readable_size, ParseSizeError},
pub use crate::human::{parse_human_readable_size, ParseSizeError}; pattern::{
pub use crate::pattern::{ pattern_from_bytes, pattern_from_os, patterns_from_path,
pattern_from_bytes, pattern_from_os, patterns_from_path, patterns_from_reader, patterns_from_stdin, InvalidPatternError,
patterns_from_reader, patterns_from_stdin, InvalidPatternError, },
}; process::{CommandError, CommandReader, CommandReaderBuilder},
pub use crate::process::{CommandError, CommandReader, CommandReaderBuilder}; wtr::{
pub use crate::wtr::{ stdout, stdout_buffered_block, stdout_buffered_line, StandardStream,
stdout, stdout_buffered_block, stdout_buffered_line, StandardStream, },
}; };
/// Returns true if and only if stdin is believed to be readable. /// Returns true if and only if stdin is believed to be readable.
@ -189,34 +173,60 @@ pub use crate::wtr::{
/// might search the current directory for occurrences of `foo` where as /// might search the current directory for occurrences of `foo` where as
/// `command foo < some-file` or `cat some-file | command foo` might instead /// `command foo < some-file` or `cat some-file | command foo` might instead
/// only search stdin for occurrences of `foo`. /// only search stdin for occurrences of `foo`.
///
/// Note that this isn't perfect and essentially corresponds to a heuristic.
/// When things are unclear (such as if an error occurs during introspection to
/// determine whether stdin is readable), this prefers to return `false`. That
/// means it's possible for an end user to pipe something into your program and
/// have this return `false` and thus potentially lead to ignoring the user's
/// stdin data. While not ideal, this is perhaps better than falsely assuming
/// stdin is readable, which would result in blocking forever on reading stdin.
/// Regardless, commands should always provide explicit fallbacks to override
/// behavior. For example, `rg foo -` will explicitly search stdin and `rg foo
/// ./` will explicitly search the current working directory.
pub fn is_readable_stdin() -> bool { pub fn is_readable_stdin() -> bool {
use std::io::IsTerminal;
#[cfg(unix)] #[cfg(unix)]
fn imp() -> bool { fn imp() -> bool {
use same_file::Handle; use std::{
use std::os::unix::fs::FileTypeExt; fs::File,
os::{fd::AsFd, unix::fs::FileTypeExt},
let ft = match Handle::stdin().and_then(|h| h.as_file().metadata()) {
Err(_) => return false,
Ok(md) => md.file_type(),
}; };
let stdin = std::io::stdin();
let Ok(fd) = stdin.as_fd().try_clone_to_owned() else { return false };
let file = File::from(fd);
let Ok(md) = file.metadata() else { return false };
let ft = md.file_type();
ft.is_file() || ft.is_fifo() || ft.is_socket() ft.is_file() || ft.is_fifo() || ft.is_socket()
} }
#[cfg(windows)] #[cfg(windows)]
fn imp() -> bool { fn imp() -> bool {
use winapi_util as winutil; winapi_util::file::typ(winapi_util::HandleRef::stdin())
winutil::file::typ(winutil::HandleRef::stdin())
.map(|t| t.is_disk() || t.is_pipe()) .map(|t| t.is_disk() || t.is_pipe())
.unwrap_or(false) .unwrap_or(false)
} }
!is_tty_stdin() && imp() #[cfg(not(any(unix, windows)))]
fn imp() -> bool {
false
}
!std::io::stdin().is_terminal() && imp()
} }
/// Returns true if and only if stdin is believed to be connected to a tty /// Returns true if and only if stdin is believed to be connected to a tty
/// or a console. /// or a console.
///
/// Note that this is now just a wrapper around
/// [`std::io::IsTerminal`](https://doc.rust-lang.org/std/io/trait.IsTerminal.html).
/// Callers should prefer using the `IsTerminal` trait directly. This routine
/// is deprecated and will be removed in the next semver incompatible release.
#[deprecated(since = "0.1.10", note = "use std::io::IsTerminal instead")]
pub fn is_tty_stdin() -> bool { pub fn is_tty_stdin() -> bool {
use std::io::IsTerminal;
std::io::stdin().is_terminal() std::io::stdin().is_terminal()
} }
@ -228,12 +238,26 @@ pub fn is_tty_stdin() -> bool {
/// terminal or whether it's being redirected somewhere else. For example, /// terminal or whether it's being redirected somewhere else. For example,
/// implementations of `ls` will often show one item per line when stdout is /// implementations of `ls` will often show one item per line when stdout is
/// redirected, but will condensed output when printing to a tty. /// redirected, but will condensed output when printing to a tty.
///
/// Note that this is now just a wrapper around
/// [`std::io::IsTerminal`](https://doc.rust-lang.org/std/io/trait.IsTerminal.html).
/// Callers should prefer using the `IsTerminal` trait directly. This routine
/// is deprecated and will be removed in the next semver incompatible release.
#[deprecated(since = "0.1.10", note = "use std::io::IsTerminal instead")]
pub fn is_tty_stdout() -> bool { pub fn is_tty_stdout() -> bool {
use std::io::IsTerminal;
std::io::stdout().is_terminal() std::io::stdout().is_terminal()
} }
/// Returns true if and only if stderr is believed to be connected to a tty /// Returns true if and only if stderr is believed to be connected to a tty
/// or a console. /// or a console.
///
/// Note that this is now just a wrapper around
/// [`std::io::IsTerminal`](https://doc.rust-lang.org/std/io/trait.IsTerminal.html).
/// Callers should prefer using the `IsTerminal` trait directly. This routine
/// is deprecated and will be removed in the next semver incompatible release.
#[deprecated(since = "0.1.10", note = "use std::io::IsTerminal instead")]
pub fn is_tty_stderr() -> bool { pub fn is_tty_stderr() -> bool {
use std::io::IsTerminal;
std::io::stderr().is_terminal() std::io::stderr().is_terminal()
} }

View File

@ -1,10 +1,4 @@
use std::error; use std::{ffi::OsStr, io, path::Path};
use std::ffi::OsStr;
use std::fmt;
use std::fs::File;
use std::io;
use std::path::Path;
use std::str;
use bstr::io::BufReadExt; use bstr::io::BufReadExt;
@ -28,14 +22,10 @@ impl InvalidPatternError {
} }
} }
impl error::Error for InvalidPatternError { impl std::error::Error for InvalidPatternError {}
fn description(&self) -> &str {
"invalid pattern"
}
}
impl fmt::Display for InvalidPatternError { impl std::fmt::Display for InvalidPatternError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
f, f,
"found invalid UTF-8 in pattern at byte offset {}: {} \ "found invalid UTF-8 in pattern at byte offset {}: {} \
@ -77,7 +67,7 @@ pub fn pattern_from_os(pattern: &OsStr) -> Result<&str, InvalidPatternError> {
pub fn pattern_from_bytes( pub fn pattern_from_bytes(
pattern: &[u8], pattern: &[u8],
) -> Result<&str, InvalidPatternError> { ) -> Result<&str, InvalidPatternError> {
str::from_utf8(pattern).map_err(|err| InvalidPatternError { std::str::from_utf8(pattern).map_err(|err| InvalidPatternError {
original: escape(pattern), original: escape(pattern),
valid_up_to: err.valid_up_to(), valid_up_to: err.valid_up_to(),
}) })
@ -91,7 +81,7 @@ pub fn pattern_from_bytes(
/// path. /// path.
pub fn patterns_from_path<P: AsRef<Path>>(path: P) -> io::Result<Vec<String>> { pub fn patterns_from_path<P: AsRef<Path>>(path: P) -> io::Result<Vec<String>> {
let path = path.as_ref(); let path = path.as_ref();
let file = File::open(path).map_err(|err| { let file = std::fs::File::open(path).map_err(|err| {
io::Error::new( io::Error::new(
io::ErrorKind::Other, io::ErrorKind::Other,
format!("{}: {}", path.display(), err), format!("{}: {}", path.display(), err),
@ -135,7 +125,6 @@ pub fn patterns_from_stdin() -> io::Result<Vec<String>> {
/// ``` /// ```
/// use grep_cli::patterns_from_reader; /// use grep_cli::patterns_from_reader;
/// ///
/// # fn example() -> Result<(), Box<::std::error::Error>> {
/// let patterns = "\ /// let patterns = "\
/// foo /// foo
/// bar\\s+foo /// bar\\s+foo
@ -147,7 +136,7 @@ pub fn patterns_from_stdin() -> io::Result<Vec<String>> {
/// r"bar\s+foo", /// r"bar\s+foo",
/// r"[a-z]{3}", /// r"[a-z]{3}",
/// ]); /// ]);
/// # Ok(()) } /// # Ok::<(), Box<dyn std::error::Error>>(())
/// ``` /// ```
pub fn patterns_from_reader<R: io::Read>(rdr: R) -> io::Result<Vec<String>> { pub fn patterns_from_reader<R: io::Read>(rdr: R) -> io::Result<Vec<String>> {
let mut patterns = vec![]; let mut patterns = vec![];

View File

@ -1,9 +1,7 @@
use std::error; use std::{
use std::fmt; io::{self, Read},
use std::io::{self, Read}; process,
use std::iter; };
use std::process;
use std::thread::{self, JoinHandle};
/// An error that can occur while running a command and reading its output. /// An error that can occur while running a command and reading its output.
/// ///
@ -40,14 +38,10 @@ impl CommandError {
} }
} }
impl error::Error for CommandError { impl std::error::Error for CommandError {}
fn description(&self) -> &str {
"command error"
}
}
impl fmt::Display for CommandError { impl std::fmt::Display for CommandError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.kind { match self.kind {
CommandErrorKind::Io(ref e) => e.fmt(f), CommandErrorKind::Io(ref e) => e.fmt(f),
CommandErrorKind::Stderr(ref bytes) => { CommandErrorKind::Stderr(ref bytes) => {
@ -55,7 +49,7 @@ impl fmt::Display for CommandError {
if msg.trim().is_empty() { if msg.trim().is_empty() {
write!(f, "<stderr is empty>") write!(f, "<stderr is empty>")
} else { } else {
let div = iter::repeat('-').take(79).collect::<String>(); let div = "-".repeat(79);
write!( write!(
f, f,
"\n{div}\n{msg}\n{div}", "\n{div}\n{msg}\n{div}",
@ -161,18 +155,17 @@ impl CommandReaderBuilder {
/// is returned as an error. /// is returned as an error.
/// ///
/// ```no_run /// ```no_run
/// use std::io::Read; /// use std::{io::Read, process::Command};
/// use std::process::Command; ///
/// use grep_cli::CommandReader; /// use grep_cli::CommandReader;
/// ///
/// # fn example() -> Result<(), Box<::std::error::Error>> {
/// let mut cmd = Command::new("gzip"); /// let mut cmd = Command::new("gzip");
/// cmd.arg("-d").arg("-c").arg("/usr/share/man/man1/ls.1.gz"); /// cmd.arg("-d").arg("-c").arg("/usr/share/man/man1/ls.1.gz");
/// ///
/// let mut rdr = CommandReader::new(&mut cmd)?; /// let mut rdr = CommandReader::new(&mut cmd)?;
/// let mut contents = vec![]; /// let mut contents = vec![];
/// rdr.read_to_end(&mut contents)?; /// rdr.read_to_end(&mut contents)?;
/// # Ok(()) } /// # Ok::<(), Box<dyn std::error::Error>>(())
/// ``` /// ```
#[derive(Debug)] #[derive(Debug)]
pub struct CommandReader { pub struct CommandReader {
@ -279,7 +272,7 @@ impl io::Read for CommandReader {
/// stderr. /// stderr.
#[derive(Debug)] #[derive(Debug)]
enum StderrReader { enum StderrReader {
Async(Option<JoinHandle<CommandError>>), Async(Option<std::thread::JoinHandle<CommandError>>),
Sync(process::ChildStderr), Sync(process::ChildStderr),
} }
@ -287,7 +280,7 @@ impl StderrReader {
/// Create a reader for stderr that reads contents asynchronously. /// Create a reader for stderr that reads contents asynchronously.
fn r#async(mut stderr: process::ChildStderr) -> StderrReader { fn r#async(mut stderr: process::ChildStderr) -> StderrReader {
let handle = let handle =
thread::spawn(move || stderr_to_command_error(&mut stderr)); std::thread::spawn(move || stderr_to_command_error(&mut stderr));
StderrReader::Async(Some(handle)) StderrReader::Async(Some(handle))
} }

View File

@ -1,9 +1,6 @@
use std::io; use std::io::{self, IsTerminal};
use termcolor; use termcolor::{self, HyperlinkSpec};
use termcolor::HyperlinkSpec;
use crate::is_tty_stdout;
/// A writer that supports coloring with either line or block buffering. /// A writer that supports coloring with either line or block buffering.
pub struct StandardStream(StandardStreamKind); pub struct StandardStream(StandardStreamKind);
@ -23,7 +20,7 @@ pub struct StandardStream(StandardStreamKind);
/// The color choice given is passed along to the underlying writer. To /// The color choice given is passed along to the underlying writer. To
/// completely disable colors in all cases, use `ColorChoice::Never`. /// completely disable colors in all cases, use `ColorChoice::Never`.
pub fn stdout(color_choice: termcolor::ColorChoice) -> StandardStream { pub fn stdout(color_choice: termcolor::ColorChoice) -> StandardStream {
if is_tty_stdout() { if std::io::stdout().is_terminal() {
stdout_buffered_line(color_choice) stdout_buffered_line(color_choice)
} else { } else {
stdout_buffered_block(color_choice) stdout_buffered_block(color_choice)

View File

@ -2,7 +2,7 @@ use std::cmp;
use std::env; use std::env;
use std::ffi::{OsStr, OsString}; use std::ffi::{OsStr, OsString};
use std::fs; use std::fs;
use std::io::{self, Write}; use std::io::{self, IsTerminal, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process; use std::process;
use std::str::FromStr; use std::str::FromStr;
@ -976,7 +976,7 @@ impl ArgMatches {
} else if preference == "ansi" { } else if preference == "ansi" {
ColorChoice::AlwaysAnsi ColorChoice::AlwaysAnsi
} else if preference == "auto" { } else if preference == "auto" {
if cli::is_tty_stdout() || self.is_present("pretty") { if std::io::stdout().is_terminal() || self.is_present("pretty") {
ColorChoice::Auto ColorChoice::Auto
} else { } else {
ColorChoice::Never ColorChoice::Never
@ -1110,7 +1110,7 @@ impl ArgMatches {
if self.is_present("no-heading") || self.is_present("vimgrep") { if self.is_present("no-heading") || self.is_present("vimgrep") {
false false
} else { } else {
cli::is_tty_stdout() std::io::stdout().is_terminal()
|| self.is_present("heading") || self.is_present("heading")
|| self.is_present("pretty") || self.is_present("pretty")
} }
@ -1178,7 +1178,7 @@ impl ArgMatches {
// generally want to show line numbers by default when printing to a // generally want to show line numbers by default when printing to a
// tty for human consumption, except for one interesting case: when // tty for human consumption, except for one interesting case: when
// we're only searching stdin. This makes pipelines work as expected. // we're only searching stdin. This makes pipelines work as expected.
(cli::is_tty_stdout() && !self.is_only_stdin(paths)) (std::io::stdout().is_terminal() && !self.is_only_stdin(paths))
|| self.is_present("line-number") || self.is_present("line-number")
|| self.is_present("column") || self.is_present("column")
|| self.is_present("pretty") || self.is_present("pretty")

View File

@ -1,14 +1,15 @@
use std::env; use std::{env, error::Error, ffi::OsString, io::IsTerminal, process};
use std::error::Error;
use std::ffi::OsString;
use std::process;
use grep::cli; use {
use grep::printer::{ColorSpecs, StandardBuilder}; grep::{
use grep::regex::RegexMatcher; cli,
use grep::searcher::{BinaryDetection, SearcherBuilder}; printer::{ColorSpecs, StandardBuilder},
use termcolor::ColorChoice; regex::RegexMatcher,
use walkdir::WalkDir; searcher::{BinaryDetection, SearcherBuilder},
},
termcolor::ColorChoice,
walkdir::WalkDir,
};
fn main() { fn main() {
if let Err(err) = try_main() { if let Err(err) = try_main() {
@ -36,7 +37,7 @@ fn search(pattern: &str, paths: &[OsString]) -> Result<(), Box<dyn Error>> {
.build(); .build();
let mut printer = StandardBuilder::new() let mut printer = StandardBuilder::new()
.color_specs(ColorSpecs::default_with_color()) .color_specs(ColorSpecs::default_with_color())
.build(cli::stdout(if cli::is_tty_stdout() { .build(cli::stdout(if std::io::stdout().is_terminal() {
ColorChoice::Auto ColorChoice::Auto
} else { } else {
ColorChoice::Never ColorChoice::Never