Split grep and rg into independent builtins
ober
20ca8afdd35aaa93554f71b334d94b4a70756031
--- a/jerboa-src/src/jsh/coreutils.ss +++ b/jerboa-src/src/jsh/coreutils.ss @@ -268,8 +268,9 @@ (define-coreutil-ffi ffi-tr "jsh_tr") (define-coreutil-ffi ffi-numfmt "jsh_numfmt") -;; Search (ripgrep) -(define-coreutil-ffi ffi-grep "jsh_grep") +;; Search +(define-coreutil-ffi ffi-grep "jsh_grep") ; uu_grep — real GNU grep semantics +(define-coreutil-ffi ffi-rg "jsh_rg") ; ripgrep, untouched (define-coreutil-ffi ffi-id "jsh_id") (define-coreutil-ffi ffi-whoami "jsh_whoami") @@ -387,8 +388,9 @@ ("uniq" . ,ffi-uniq) ("tr" . ,ffi-tr) ("numfmt" . ,ffi-numfmt) - ;; --- Search (ripgrep) --- + ;; --- Search --- ("grep" . ,ffi-grep) + ("rg" . ,ffi-rg) ;; --- Identity & system info --- ("id" . ,ffi-id) ("whoami" . ,ffi-whoami) --- a/rust-coreutils/Cargo.lock +++ b/rust-coreutils/Cargo.lock @@ -1444,6 +1444,7 @@ dependencies = [ "uu_factor", "uu_fmt", "uu_fold", + "uu_grep", "uu_groups", "uu_head", "uu_hostid", @@ -2999,6 +3000,19 @@ dependencies = [ ] [[package]] +name = "uu_grep" +version = "0.1.0" +dependencies = [ + "clap", + "glob", + "memchr", + "onig", + "onig_sys", + "uucore", + "walkdir", +] + +[[package]] name = "uu_groups" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" --- a/rust-coreutils/Cargo.toml +++ b/rust-coreutils/Cargo.toml @@ -31,7 +31,9 @@ uu_uniq = "0.7.0" uu_cut = "0.7.0" uu_paste = "0.7.0" uu_tr = "0.7.0" -# grep: ripgrep embedded as library (not part of GNU coreutils) +# grep: real GNU-compatible grep (uutils/grep, not part of GNU coreutils proper) +uu_grep = { path = "uu-grep" } +# rg: ripgrep embedded as library, exposed as its own separate builtin ripgrep-core = { path = "ripgrep-core" } uu_tee = "0.7.0" uu_basename = "0.7.0" --- a/rust-coreutils/src/lib.rs +++ b/rust-coreutils/src/lib.rs @@ -126,64 +126,24 @@ coreutil!(jsh_uniq, "uniq", uu_uniq); coreutil!(jsh_tr, "tr", uu_tr); coreutil!(jsh_numfmt, "numfmt", uu_numfmt); -// --- Search (ripgrep, not part of GNU coreutils) --- +// --- Search --- // -// Translate POSIX/GNU grep flags into ripgrep equivalents so common usage -// works without surprises. Notable collisions: -// -E (POSIX: extended regex) → ripgrep -E means "specify encoding". -// ripgrep is ERE by default — drop the flag. -// -G (POSIX: basic regex) → ripgrep has no BRE mode; drop, runs as ERE. -fn translate_grep_args(mut args: Vec<String>) -> Vec<String> { - let mut out = Vec::with_capacity(args.len()); - let mut drain = args.drain(..); - // First element is the program name. - if let Some(prog) = drain.next() { - out.push(prog); - } - let mut after_double_dash = false; - for a in drain { - if after_double_dash { - out.push(a); - continue; - } - if a == "--" { - after_double_dash = true; - out.push(a); - continue; - } - if a == "-E" || a == "-G" { - // Drop: ripgrep is already ERE. - continue; - } - // Bundled short flags like -En, -Gv, -Ein etc.: strip E/G from the bundle. - if a.len() > 1 && a.starts_with('-') && !a.starts_with("--") { - let bytes = a.as_bytes(); - let has_collision = bytes[1..].iter().any(|&b| b == b'E' || b == b'G'); - if has_collision { - let mut cleaned = String::with_capacity(a.len()); - cleaned.push('-'); - for &b in &bytes[1..] { - if b != b'E' && b != b'G' { - cleaned.push(b as char); - } - } - if cleaned.len() > 1 { - out.push(cleaned); - } - continue; - } - } - out.push(a); - } - out -} +// grep and rg used to be the same builtin (grep aliased straight to +// ripgrep), which silently broke GNU-style flag bundling: ripgrep +// repurposes several GNU grep short flags for unrelated options — e.g. +// -r/--replace takes a VALUE, so "-ri" got parsed as -r with value "i", +// silently dropping -i and replacing every match with the literal "i". +// Now they're separate builtins with no translation layer: +// grep -> uu_grep (vendored in ./uu-grep) — real GNU grep semantics. +// rg -> ripgrep, untouched. +coreutil!(jsh_grep, "grep", uu_grep); #[no_mangle] -pub unsafe extern "C" fn jsh_grep( +pub unsafe extern "C" fn jsh_rg( argc: libc::c_int, argv: *const *const libc::c_char, ) -> libc::c_int { - let args = translate_grep_args(args_from_c("grep", argc, argv)); + let args = args_from_c("rg", argc, argv); let os_args: Vec<std::ffi::OsString> = args.into_iter().map(|s| s.into()).collect(); ripgrep_core::rg_main(os_args) as libc::c_int } new file mode 100644 --- /dev/null +++ b/rust-coreutils/uu-grep/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "uu_grep" +description = "A Rust implementation of GNU Grep" +repository = "https://github.com/uutils/grep" +edition = "2024" +rust-version = "1.88.0" +version = "0.1.0" +license = "MIT" +homepage = "https://github.com/uutils/grep" +keywords = ["grep", "uutils", "cross-platform", "cli", "utility"] +categories = ["command-line-utilities"] + +[lib] +name = "uu_grep" +path = "src/lib.rs" + +[dependencies] +clap = { version = "4.5", features = ["wrap_help", "cargo", "color"] } +glob = "0.3.1" +memchr = "2.7.2" +onig = { version = "~6.5.1", default-features = false } +onig_sys = { version = "*", default-features = false } +# Pinned to match the project's local uucore-patch (0.7.0); upstream uu_grep +# targets uucore 0.8.0. See ../uucore-patch/Cargo.toml. +uucore = "0.7.0" +walkdir = "2.5" new file mode 100644 --- /dev/null +++ b/rust-coreutils/uu-grep/LICENSE @@ -0,0 +1,18 @@ +Copyright (c) uutils developers + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. new file mode 100644 --- /dev/null +++ b/rust-coreutils/uu-grep/src/context_buffer.rs @@ -0,0 +1,107 @@ +// This file is part of the uutils grep package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +pub struct LineView<'a> { + /// Line content (without the terminator). + pub line: &'a [u8], + /// 1-based line number. + pub line_number: u64, + /// Byte offset of this line in the input stream. + pub byte_offset: u64, + /// Whether this line matched (vs. being a context line). + pub is_match: bool, + /// Match positions within the line (start, end). + /// Empty for context lines or for matching lines we don't need to highlight. + pub match_positions: &'a [(usize, usize)], +} + +#[derive(Clone)] +pub struct BufferedLine { + pub line: Vec<u8>, + pub line_number: u64, + pub byte_offset: u64, +} + +impl BufferedLine { + pub fn view(&self) -> LineView<'_> { + LineView { + line: &self.line, + line_number: self.line_number, + byte_offset: self.byte_offset, + is_match: false, + match_positions: &[], + } + } +} + +/// A fixed-capacity ring buffer of context lines. +/// +/// TODO: Ideally, this would be integrated into `LineBuffer`, which can then +/// provide a more optimized `ContextBuffer` when `mmap()` is available. +pub struct ContextBuffer { + slots: Vec<BufferedLine>, + head: usize, + len: usize, +} + +impl ContextBuffer { + pub fn new(capacity: usize) -> Self { + let len = if capacity == 0 { + 0 + } else { + capacity.next_power_of_two() + }; + Self { + slots: vec![ + BufferedLine { + line: Vec::new(), + line_number: 0, + byte_offset: 0, + }; + len + ], + head: 0, + len: 0, + } + } + + pub fn clear(&mut self) { + self.head = 0; + self.len = 0; + } + + pub fn push(&mut self, line: &[u8], line_number: u64, byte_offset: u64) { + debug_assert!( + !self.slots.is_empty(), + "push on zero-capacity ContextBuffer" + ); + + let mask = self.slots.len() - 1; + let slot = &mut self.slots[self.head & mask]; + + slot.line.clear(); + if slot.line.capacity() / 2 > line.len() { + slot.line.shrink_to(line.len()); + } + slot.line.extend_from_slice(line); + slot.line_number = line_number; + slot.byte_offset = byte_offset; + + self.head = self.head.wrapping_add(1); + self.len = (self.len + 1).min(self.slots.len()); + } + + pub fn drain_iter(&mut self) -> impl Iterator<Item = &BufferedLine> { + let len = self.len; + let slots = &self.slots[..]; + let mask = self.slots.len().wrapping_sub(1); + let start = self.head.wrapping_sub(len); + + self.head = 0; + self.len = 0; + + (0..len).map(move |i| &slots[start.wrapping_add(i) & mask]) + } +} new file mode 100644 --- /dev/null +++ b/rust-coreutils/uu-grep/src/lib.rs @@ -0,0 +1,999 @@ +// This file is part of the uutils grep package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +#[doc(hidden)] +pub mod context_buffer; +#[doc(hidden)] +pub mod line_buffer; +#[doc(hidden)] +pub mod matcher; +mod output; +mod searcher; + +use crate::line_buffer::LineBuffer; +use crate::matcher::Matcher; +use crate::output::OutputWriter; +use crate::searcher::Searcher; +use clap::{Arg, ArgAction, Command}; +use std::ffi::{OsStr, OsString}; +use std::io::{IsTerminal as _, Read}; +use std::path::Path; +use uucore::error::{ExitCode, FromIo, UResult, USimpleError}; +use uucore::show_warning; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[doc(hidden)] +pub enum RegexMode { + Fixed, + Basic, + Extended, + Perl, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +#[doc(hidden)] +pub enum BinaryMode { + Binary, + Text, + WithoutMatch, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum ColorMode { + Always, + Never, + Auto, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +#[doc(hidden)] +pub enum DirectoryMode { + Read, + Skip, + Recurse, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +#[doc(hidden)] +pub enum DeviceMode { + Default, + Read, + Skip, +} + +#[doc(hidden)] +pub struct ColorConfig<'a> { + pub matched_selected: &'a str, + pub matched_context: &'a str, + pub filename: &'a str, + pub line_number: &'a str, + pub byte_offset: &'a str, + pub separator: &'a str, + pub selected_line: &'a str, + pub context_line: &'a str, + + pub reverse_video: bool, + pub no_erase: bool, +} + +#[doc(hidden)] +pub struct GlobSet { + patterns: Vec<glob::Pattern>, +} + +#[doc(hidden)] +pub struct Config<'a> { + // Searcher + pub directory_mode: DirectoryMode, + pub device_mode: DeviceMode, + pub follow_symlinks: bool, + pub include_globs: GlobSet, + pub exclude_globs: GlobSet, + pub exclude_dir_globs: GlobSet, + pub label: &'a str, + #[cfg(windows)] + pub strip_cr: bool, + pub binary_mode: BinaryMode, + pub max_count: Option<u64>, + pub before_context: usize, + pub after_context: usize, + pub has_context: bool, + + // Matcher + pub patterns: &'a [&'a str], + pub regex_mode: RegexMode, + pub ignore_case: bool, + pub invert_match: bool, + pub word_regexp: bool, + pub line_regexp: bool, + + // Output + pub quiet: bool, + pub count: bool, + pub show_filename: bool, + pub files_with_matches: bool, + pub files_without_match: bool, + pub only_matching: bool, + pub byte_offset: bool, + pub line_number: bool, + pub initial_tab: bool, + pub null_separator: bool, + pub null_data: bool, + pub line_buffered: bool, + pub no_messages: bool, + pub group_separator: Option<&'a str>, + pub use_color: bool, + pub color_config: ColorConfig<'a>, +} + +#[uucore::main(no_signals)] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let args = expand_num_shorthand(args); + let matches = uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 2)?; + + let grep_color = std::env::var("GREP_COLOR").unwrap_or_default(); + let grep_colors = std::env::var("GREP_COLORS").unwrap_or_default(); + + let patterns_or_files: Vec<_> = matches + .get_many::<OsString>("patterns_or_files") + .map_or(Default::default(), |v| v.collect()); + let extended_regexp = matches.get_flag("extended_regexp"); + let fixed_strings = matches.get_flag("fixed_strings"); + let basic_regexp = matches.get_flag("basic_regexp"); + let perl_regexp = matches.get_flag("perl_regexp"); + let regexp = matches.get_many::<String>("regexp").unwrap_or_default(); + let file_pattern = matches + .get_many::<String>("file_pattern") + .unwrap_or_default(); + let ignore_case = matches.get_flag("ignore_case"); + let word_regexp = matches.get_flag("word_regexp"); + let line_regexp = matches.get_flag("line_regexp"); + let null_data = matches.get_flag("null_data"); + let no_messages = matches.get_flag("no_messages"); + let invert_match = matches.get_flag("invert_match"); + let max_count = matches.get_one::<u64>("max_count").copied(); + let byte_offset = matches.get_flag("byte_offset"); + let line_number = matches.get_flag("line_number"); + let line_buffered = matches.get_flag("line_buffered"); + let with_filename = matches.get_flag("with_filename"); + let no_filename = matches.get_flag("no_filename"); + let label = matches + .get_one::<String>("label") + .map_or("(standard input)", |s| s.as_str()); + let only_matching = matches.get_flag("only_matching"); + let quiet = matches.get_flag("quiet"); + let binary_files = matches + .get_one::<String>("binary_files") + .map(String::as_str); + let text = matches.get_flag("text"); + let skip_binary = matches.get_flag("skip_binary"); + let directories = matches.get_one::<String>("directories").map(String::as_str); + let devices = matches.get_one::<String>("devices").map(String::as_str); + let recursive = matches.get_flag("recursive"); + let dereference_recursive = matches.get_flag("dereference_recursive"); + let include = matches.get_many::<String>("include").unwrap_or_default(); + let exclude = matches.get_many::<String>("exclude").unwrap_or_default(); + let exclude_from = matches + .get_many::<String>("exclude_from") + .unwrap_or_default(); + let exclude_dir = matches + .get_many::<String>("exclude_dir") + .unwrap_or_default(); + let files_without_match = matches.get_flag("files_without_match"); + let files_with_matches = matches.get_flag("files_with_matches"); + let count = matches.get_flag("count"); + let initial_tab = matches.get_flag("initial_tab"); + let null = matches.get_flag("null"); + let before_context = matches.get_one::<usize>("before_context").copied(); + let after_context = matches.get_one::<usize>("after_context").copied(); + let context = matches.get_one::<usize>("context").copied(); + let group_separator = matches + .get_one::<String>("group_separator") + .map_or("--", |s| s.as_str()); + let no_group_separator = matches.get_flag("no_group_separator"); + let color = matches + .get_one::<String>("color") + .map_or("", |s| s.as_str()); + #[cfg(windows)] + let binary = matches.get_flag("binary"); + + let matcher_mode_count = [extended_regexp, fixed_strings, basic_regexp, perl_regexp] + .into_iter() + .filter(|matched| *matched) + .count(); + if matcher_mode_count > 1 { + return Err(USimpleError::new( + 2, + "conflicting matchers specified".to_string(), + )); + } + + // With -e/-f given, ALL positionals are files. + let has_explicit_patterns = regexp.len() != 0 || file_pattern.len() != 0; + let (positional_pattern, file_args) = if has_explicit_patterns { + (None, &patterns_or_files[..]) + } else { + patterns_or_files + .split_first() + .map_or((None, &[][..]), |(p, rest)| (Some(*p), rest)) + }; + + // An empty pattern set is a usage error only when no explicit pattern source was + // given (`-e` / `-f`). An empty `-f` file is legitimate and simply matches nothing. + let mut pattern_strings = Vec::new(); + let mut patterns = Vec::new(); + { + for expr in regexp { + for line in expr.split('\n') { + patterns.push(line); + } + } + + for path in file_pattern { + let contents = if *path == "-" { + let mut buf = String::new(); + std::io::stdin() + .read_to_string(&mut buf) + .map_err_context(|| "(standard input)".to_string())?; + buf + } else { + std::fs::read_to_string(path).map_err_context(|| path.to_string())? + }; + pattern_strings.push(contents); + } + for contents in &pattern_strings { + if !contents.is_empty() { + let body = contents.strip_suffix('\n').unwrap_or(contents); + for line in body.split('\n') { + patterns.push(line); + } + } + } + + if let Some(pos) = positional_pattern { + let pat = pos + .to_str() + .ok_or_else(|| USimpleError::new(2, "pattern must be valid UTF-8".to_string()))?; + for line in pat.split('\n') { + patterns.push(line); + } + } + } + if patterns.is_empty() && !has_explicit_patterns { + return Err(USimpleError::new( + 2, + "no PATTERN specified. Try 'grep --help' for more information.".to_string(), + )); + } + + // GNU grep's PCRE backend (-P) supports only a single pattern. + if perl_regexp && patterns.len() > 1 { + return Err(USimpleError::new( + 2, + "the -P option only supports a single pattern".to_string(), + )); + } + + // Decoded options into enums + let regex_mode = if fixed_strings { + RegexMode::Fixed + } else if extended_regexp { + RegexMode::Extended + } else if perl_regexp { + RegexMode::Perl + } else { + RegexMode::Basic + }; + let directory_mode = if recursive || dereference_recursive { + DirectoryMode::Recurse + } else { + match directories { + Some("skip") => DirectoryMode::Skip, + Some("recurse") => DirectoryMode::Recurse, + _ => DirectoryMode::Read, + } + }; + let binary_mode = if text { + BinaryMode::Text + } else if skip_binary { + BinaryMode::WithoutMatch + } else { + match binary_files { + Some("text") => BinaryMode::Text, + Some("without-match") => BinaryMode::WithoutMatch, + _ => BinaryMode::Binary, + } + }; + let device_mode = match devices { + Some("read") => DeviceMode::Read, + Some("skip") => DeviceMode::Skip, + _ => DeviceMode::Default, + }; + let color = match color { + "always" => ColorMode::Always, + "never" => ColorMode::Never, + _ => ColorMode::Auto, + }; + let (before_context, after_context, has_context) = { + // "-o" overrides any context arguments + if only_matching { + (0, 0, false) + } else { + let fallback = context.unwrap_or(0); + let before = before_context.unwrap_or(fallback); + let after = after_context.unwrap_or(fallback); + let has = context.is_some() || before_context.is_some() || after_context.is_some(); + + (before, after, has) + } + }; + let include_globs = { + let mut patterns = GlobSet::with_capacity(include.len()); + for pattern in include { + patterns.add(pattern)?; + } + patterns + }; + let exclude_globs = { + let mut patterns = GlobSet::with_capacity(exclude.len()); + for pattern in exclude { + patterns.add(pattern)?; + } + for path in exclude_from { + let contents = std::fs::read_to_string(path).map_err_context(|| path.to_string())?; + for line in contents.lines() { + let trimmed = line.trim(); + if !trimmed.is_empty() { + patterns.add(trimmed)?; + } + } + } + patterns + }; + let exclude_dir_globs = { + let mut patterns = GlobSet::with_capacity(exclude_dir.len()); + for pattern in exclude_dir { + patterns.add(pattern)?; + } + patterns + }; + let show_filename = if with_filename { + true + } else if no_filename { + false + } else { + match file_args { + [] => directory_mode == DirectoryMode::Recurse, + [one] if one.to_str() != Some("-") => { + directory_mode == DirectoryMode::Recurse && Path::new(one).is_dir() + } + [_] => false, + _ => true, + } + }; + let group_separator = (!no_group_separator).then_some(group_separator); + let use_color = match color { + ColorMode::Always => true, + ColorMode::Never => false, + ColorMode::Auto => std::io::stdout().is_terminal(), + }; + // GNU grep treats GREP_COLOR as deprecated: when it is set and color output + // is active, warn and point users at the GREP_COLORS 'mt' capability. + if use_color && !grep_color.is_empty() { + show_warning!("GREP_COLOR='{grep_color}' is deprecated; use GREP_COLORS='mt={grep_color}'"); + } + let color_config = ColorConfig::from_env(&grep_color, &grep_colors); + + let config = Config { + // Searcher + directory_mode, + device_mode, + follow_symlinks: dereference_recursive, + include_globs, + exclude_globs, + exclude_dir_globs, + label, + #[cfg(windows)] + strip_cr: !binary, + binary_mode, + max_count, + before_context, + after_context, + has_context, + + // Matcher + patterns: &patterns, + regex_mode, + ignore_case, + invert_match, + word_regexp, + line_regexp, + + // Output + quiet, + count, + show_filename, + files_with_matches, + files_without_match, + only_matching, + byte_offset, + line_number, + initial_tab, + null_separator: null, + null_data, + line_buffered, + no_messages, + group_separator, + use_color, + color_config, + }; + + let matcher = Matcher::compile(&config)?; + + // grep with -m 0 should not open any file + if config.max_count == Some(0) { + return Err(ExitCode::new(1)); + } + + // An empty pattern matches every line; with `-v`, GNU grep selects no lines + // and exits as "no match" without reading any input files. + if invert_match && patterns.iter().any(|pattern| pattern.is_empty()) { + return Err(ExitCode::new(1)); + } + + let writer = OutputWriter::new(&config); + let mut searcher = Searcher::new(&config, matcher, writer); + let mut lb = LineBuffer::new(if config.null_data { b'\0' } else { b'\n' }); + + if file_args.is_empty() { + if directory_mode != DirectoryMode::Recurse { + _ = searcher.process_stdin(&mut lb); + } else { + _ = searcher.process_implicit_cwd(&mut lb); + } + } else { + for f in file_args { + let cf = if f.to_str() == Some("-") { + searcher.process_stdin(&mut lb) + } else { + searcher.process_path(&mut lb, Path::new(f)) + }; + if cf.is_break() { + break; + } + } + } + + searcher.finish() +} + +pub fn uu_app() -> Command { + Command::new("grep") + .version(env!("CARGO_PKG_VERSION")) + .about("Search for PATTERNS in each FILE.") + .disable_help_flag(true) + .disable_version_flag(true) + // GNU grep accepts repeated options (booleans are idempotent, value + // options take the last); make clap replace rather than error. Args + // with ArgAction::Append (e.g. -e/-f/--include) still accumulate. + .args_override_self(true) + .after_help( + "When FILE is '-', read standard input. If no FILE is given, read standard \ + input, but with -r, recursively search the working directory instead. With \ + fewer than two FILEs, assume -h. Exit status is 0 if any line is selected, \ + 1 otherwise; if any error occurs and -q is not given, the exit status is 2.", + ) + .arg( + Arg::new("patterns_or_files") + .help("Pattern (if no -e/-f) followed by files to search") + .index(1) + .num_args(0..) + .value_parser(clap::value_parser!(OsString)), + ) + .arg( + Arg::new("extended_regexp") + .short('E') + .long("extended-regexp") + .help("PATTERNS are extended regular expressions") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("fixed_strings") + .short('F') + .long("fixed-strings") + .help("PATTERNS are strings") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("basic_regexp") + .short('G') + .long("basic-regexp") + .help("PATTERNS are basic regular expressions") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("perl_regexp") + .short('P') + .long("perl-regexp") + .help("PATTERNS are Perl regular expressions") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("regexp") + .short('e') + .long("regexp") + .value_name("PATTERNS") + .help("use PATTERNS for matching") + .action(ArgAction::Append) + .allow_hyphen_values(true), + ) + .arg( + Arg::new("file_pattern") + .short('f') + .long("file") + .value_name("FILE") + .help("take PATTERNS from FILE") + .action(ArgAction::Append), + ) + .arg( + Arg::new("ignore_case") + .short('i') + .long("ignore-case") + .short_alias('y') + .help("ignore case distinctions in patterns and data") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("no_ignore_case") + .long("no-ignore-case") + .help("do not ignore case distinctions (default)") + .action(ArgAction::SetTrue) + .overrides_with("ignore_case"), + ) + .arg( + Arg::new("word_regexp") + .short('w') + .long("word-regexp") + .help("match only whole words") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("line_regexp") + .short('x') + .long("line-regexp") + .help("match only whole lines") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("null_data") + .short('z') + .long("null-data") + .help("a data line ends in 0 byte, not newline") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("no_messages") + .short('s') + .long("no-messages") + .help("suppress error messages") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("invert_match") + .short('v') + .long("invert-match") + .help("select non-matching lines") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("version") + .short('V') + .long("version") + .help("display version information and exit") + .action(ArgAction::Version), + ) + .arg( + Arg::new("help") + .long("help") + .help("display this help text and exit") + .action(ArgAction::Help), + ) + .arg( + Arg::new("max_count") + .short('m') + .long("max-count") + .value_name("NUM") + .help("stop after NUM selected lines") + .value_parser(clap::value_parser!(u64)), + ) + .arg( + Arg::new("byte_offset") + .short('b') + .long("byte-offset") + .help("print the byte offset with output lines") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("line_number") + .short('n') + .long("line-number") + .help("print line number with output lines") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("line_buffered") + .long("line-buffered") + .help("flush output on every line") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("with_filename") + .short('H') + .long("with-filename") + .help("print file name with output lines") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("no_filename") + .short('h') + .long("no-filename") + .help("suppress the file name prefix on output") + .action(ArgAction::SetTrue) + .overrides_with("with_filename"), + ) + .arg( + Arg::new("label") + .long("label") + .value_name("LABEL") + .help("use LABEL as the standard input file name prefix"), + ) + .arg( + Arg::new("only_matching") + .short('o') + .long("only-matching") + .help("show only nonempty parts of lines that match") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("quiet") + .short('q') + .long("quiet") + .alias("silent") + .help("suppress all normal output") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("binary_files") + .long("binary-files") + .value_name("TYPE") + .help("assume that binary files are TYPE; TYPE is 'binary', 'text', or 'without-match'") + .value_parser(["binary", "text", "without-match"]), + ) + .arg( + Arg::new("text") + .short('a') + .long("text") + .help("equivalent to --binary-files=text") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("skip_binary") + .short('I') + .help("equivalent to --binary-files=without-match") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("directories") + .short('d') + .long("directories") + .value_name("ACTION") + .help("how to handle directories; ACTION is 'read', 'recurse', or 'skip'") + .value_parser(["read", "skip", "recurse"]), + ) + .arg( + Arg::new("devices") + .short('D')