diff --git a/src/args.rs b/src/args.rs index 526a105..85bfd28 100644 --- a/src/args.rs +++ b/src/args.rs @@ -2,228 +2,205 @@ use std::ffi::OsString; use std::path::PathBuf; use clap::{self, App, Arg, ArgMatches}; -use clap::{crate_authors, crate_version, crate_name, crate_description}; - +use clap::{crate_authors, crate_description, crate_name, crate_version}; /// The output mode. #[derive(Debug)] pub enum Output { - FileName, - Bytes, - Offset + FileName, + Bytes, + Offset, } impl Default for Output { - fn default() -> Output { Output::FileName } + fn default() -> Output { + Output::FileName + } } - /// The values of all flags, except help and version. #[derive(Default, Debug)] pub struct Options { - pub inverse: bool, - pub case_insensitive: bool, - pub trim_ending_newline: bool, - pub non_matching: bool, // Whether to print non matching files. Only true when (-L). - pub print_filename: bool, - pub output: Output + pub inverse: bool, + pub case_insensitive: bool, + pub trim_ending_newline: bool, + pub non_matching: bool, // Whether to print non matching files. Only true when (-L). + pub print_filename: bool, + pub output: Output, } - /// The arguments when the action is grep. #[derive(Default, Debug)] pub struct Args { - pub options: Options, - pub pattern: String, - pub files: Box<[PathBuf]> + pub options: Options, + pub pattern: String, + pub files: Box<[PathBuf]>, } - /// The action to be executed. #[derive(Debug)] pub enum Command { - Help(String), - Version(String), - Grep(Args) + Help(String), + Version(String), + Grep(Args), } - /// The error type for the argument parser. Contains only the error message. #[derive(Debug)] pub struct Error { - pub message: String + pub message: String, } - - /// The path used to denote reading from stdin. pub const STDIN: &str = "-"; - - /// Build clap's `App`. This specifies all arguments and metadata. fn build_app() -> App<'static, 'static> { - App::new(crate_name!()) - .about(crate_description!()) - .author(crate_authors!()) - .version(crate_version!()) - .template("{bin} {version}\nMade by {author}\n{about}\n\n{usage}\n\nFLAGS:\n{flags}") - // Positional arguments: - .arg( - Arg::with_name("pattern") - .required(true) - .index(1) - ) - .arg( - Arg::with_name("files") - .multiple(true) - .default_value(STDIN) - .index(2) - ) - // Matching flags: - .arg( - Arg::with_name("invert-match") - .short("v") - .long("invert-match") - .help("Invert the sense of matching, to select non matching slices") - ) - .arg( - Arg::with_name("ignore-case") - .short("i") - .long("ignore-case") - .help("Case insensitive matching for ASCII alphabetic characters") - ) - // Input flags: - .arg( - Arg::with_name("trim-ending-newline") - .short("n") - .long("trim-ending-newline") - .help("If the file ends with a newline, disconsider the last byte") - ) - // Output flags: - .arg( - Arg::with_name("with-filename") - .short("H") - .long("with-filename") - .help("Print the file name for each match (default when there are multiple files).") - .overrides_with("no-filename") - ) - .arg( - Arg::with_name("no-filename") - .short("h") - .long("no-filename") - .help("Suppress the file names on output (default when there is a single file).") - .overrides_with("with-filename") - .conflicts_with_all(&[ - "files-with-matches", - "files-without-matches", - ]) - ) - .arg( - Arg::with_name("only-matching") - .short("o") - .long("only-matching") - .help("Prints the matched bytes of each match") - .overrides_with_all(&[ - "byte-offset", - "files-with-matches", - "files-without-matches", - ]) - ) - .arg( - Arg::with_name("byte-offset") - .short("b") - .long("byte-offset") - .help("Prints the byte offset of each match") - .overrides_with_all(&[ - "only-matching", - "files-with-matches", - "files-without-matches", - ]) - ) - .arg( - Arg::with_name("files-with-matches") - .short("l") - .long("files-with-matches") - .help("Prints the name of the matched files (default output mode)") - .overrides_with_all(&[ - "only-matching", - "byte-offset", - "files-without-matches", - ]) - ) - .arg( - Arg::with_name("files-without-matches") - .short("L") - .long("files-without-matches") - .help("Prints the name of non-matched files") - .overrides_with_all(&[ - "only-matching", - "byte-offset", - "files-with-matches", - ]) - ) + App::new(crate_name!()) + .about(crate_description!()) + .author(crate_authors!()) + .version(crate_version!()) + .template("{bin} {version}\nMade by {author}\n{about}\n\n{usage}\n\nFLAGS:\n{flags}") + // Positional arguments: + .arg(Arg::with_name("pattern").required(true).index(1)) + .arg( + Arg::with_name("files") + .multiple(true) + .default_value(STDIN) + .index(2), + ) + // Matching flags: + .arg( + Arg::with_name("invert-match") + .short("v") + .long("invert-match") + .help("Invert the sense of matching, to select non matching slices"), + ) + .arg( + Arg::with_name("ignore-case") + .short("i") + .long("ignore-case") + .help("Case insensitive matching for ASCII alphabetic characters"), + ) + // Input flags: + .arg( + Arg::with_name("trim-ending-newline") + .short("n") + .long("trim-ending-newline") + .help("If the file ends with a newline, disconsider the last byte"), + ) + // Output flags: + .arg( + Arg::with_name("with-filename") + .short("H") + .long("with-filename") + .help("Print the file name for each match (default when there are multiple files).") + .overrides_with("no-filename"), + ) + .arg( + Arg::with_name("no-filename") + .short("h") + .long("no-filename") + .help("Suppress the file names on output (default when there is a single file).") + .overrides_with("with-filename") + .conflicts_with_all(&["files-with-matches", "files-without-matches"]), + ) + .arg( + Arg::with_name("only-matching") + .short("o") + .long("only-matching") + .help("Prints the matched bytes of each match") + .overrides_with_all(&[ + "byte-offset", + "files-with-matches", + "files-without-matches", + ]), + ) + .arg( + Arg::with_name("byte-offset") + .short("b") + .long("byte-offset") + .help("Prints the byte offset of each match") + .overrides_with_all(&[ + "only-matching", + "files-with-matches", + "files-without-matches", + ]), + ) + .arg( + Arg::with_name("files-with-matches") + .short("l") + .long("files-with-matches") + .help("Prints the name of the matched files (default output mode)") + .overrides_with_all(&["only-matching", "byte-offset", "files-without-matches"]), + ) + .arg( + Arg::with_name("files-without-matches") + .short("L") + .long("files-without-matches") + .help("Prints the name of non-matched files") + .overrides_with_all(&["only-matching", "byte-offset", "files-with-matches"]), + ) } - /// Build an `Args` from clap's `ArgMatches`. /// The matches are supposed to be valid, therefore there is no error handling/reporting. fn build_args(args: ArgMatches) -> Args { - let pattern = args.value_of("pattern") - .expect(" not in ArgMatches") // pattern is required. - .to_owned(); - - let files: Box<[PathBuf]> = args.values_of_os("files") - .expect(" not in ArgMatches") - .map(PathBuf::from) - .collect(); - - let flag = |f| args.is_present(f); - - let output_flags = ( - flag("only-matching"), - flag("byte-offset"), - flag("files-with-matches"), - flag("files-without-matches") - ); - - let output = match output_flags { - (true, _, _, _) => Output::Bytes, - (_, true, _, _) => Output::Offset, - (_, _, true, _) => Output::FileName, - (_, _, _, true) => Output::FileName, - (_, _, _, _) => Default::default(), - }; - - Args { - options: Options { - inverse: flag("invert-match"), - case_insensitive: flag("ignore-case"), - trim_ending_newline: flag("trim-ending-newline"), - non_matching: flag("files-without-matches"), - print_filename: flag("with-filename") || !(flag("no-filename") || files.len() == 1), - output - }, - pattern, - files - } + let pattern = args + .value_of("pattern") + .expect(" not in ArgMatches") // pattern is required. + .to_owned(); + + let files: Box<[PathBuf]> = args + .values_of_os("files") + .expect(" not in ArgMatches") + .map(PathBuf::from) + .collect(); + + let flag = |f| args.is_present(f); + + let output_flags = ( + flag("only-matching"), + flag("byte-offset"), + flag("files-with-matches"), + flag("files-without-matches"), + ); + + let output = match output_flags { + (true, _, _, _) => Output::Bytes, + (_, true, _, _) => Output::Offset, + (_, _, true, _) => Output::FileName, + (_, _, _, true) => Output::FileName, + (_, _, _, _) => Default::default(), + }; + + Args { + options: Options { + inverse: flag("invert-match"), + case_insensitive: flag("ignore-case"), + trim_ending_newline: flag("trim-ending-newline"), + non_matching: flag("files-without-matches"), + print_filename: flag("with-filename") || !(flag("no-filename") || files.len() == 1), + output, + }, + pattern, + files, + } } - /// Parse the arguments from `std::env::args_os`. /// Returns the command to be executed, or the error message. -pub fn parse< - T: Into + Clone, - A: IntoIterator ->(args: A) -> Result { - let app = build_app(); - - match app.get_matches_from_safe(args) { - Ok(arg_matches) => Ok(Command::Grep(build_args(arg_matches))), - Err(e) => match e.kind { - clap::ErrorKind::HelpDisplayed => Ok(Command::Help(e.message)), - clap::ErrorKind::VersionDisplayed => Ok(Command::Version(e.message)), - _ => Err(Error { message: e.message }) +pub fn parse + Clone, A: IntoIterator>( + args: A, +) -> Result { + let app = build_app(); + + match app.get_matches_from_safe(args) { + Ok(arg_matches) => Ok(Command::Grep(build_args(arg_matches))), + Err(e) => match e.kind { + clap::ErrorKind::HelpDisplayed => Ok(Command::Help(e.message)), + clap::ErrorKind::VersionDisplayed => Ok(Command::Version(e.message)), + _ => Err(Error { message: e.message }), + }, } - } } diff --git a/src/grep.rs b/src/grep.rs index 60d6c97..6733e93 100644 --- a/src/grep.rs +++ b/src/grep.rs @@ -1,318 +1,301 @@ +use std::fmt::Display; +use std::fs::File; use std::io; use std::io::{Read, Write}; -use std::fs::File; use std::path::{Path, PathBuf}; -use std::fmt::Display; use regex::bytes::{Regex, RegexBuilder}; use crate::args::{self, Args}; - /// Build the regex pattern with the given options. /// By default, the `unicode` flag is set to false, and `dot_matches_new_line` set to true. fn build_pattern>( - pattern: &P, - options: &args::Options + pattern: &P, + options: &args::Options, ) -> Result { - let mut builder = RegexBuilder::new(pattern.as_ref()); + let mut builder = RegexBuilder::new(pattern.as_ref()); - builder.unicode(false); - builder.dot_matches_new_line(true); - builder.case_insensitive(options.case_insensitive); + builder.unicode(false); + builder.dot_matches_new_line(true); + builder.case_insensitive(options.case_insensitive); - builder.build() + builder.build() } - /// Run bgrep, outputting `path` to the given `out` if there is a match. /// Returns whether there was a match. fn grep_filename>( - out: &mut O, - options: &args::Options, - pattern: &Regex, - path: P, - buffer: B + out: &mut O, + options: &args::Options, + pattern: &Regex, + path: P, + buffer: B, ) -> io::Result { - let buffer = buffer.as_ref(); + let buffer = buffer.as_ref(); - // When inverse matching, matches must be checked until a "hole" is found. - // Otherwise, the more performant `Regex::is_match` can be used. - if options.inverse { - // if the pattern matches multiple times, comprising the entire buffer, then no - // inverse match is present. - let mut matches = pattern.find_iter(buffer); + // When inverse matching, matches must be checked until a "hole" is found. + // Otherwise, the more performant `Regex::is_match` can be used. + if options.inverse { + // if the pattern matches multiple times, comprising the entire buffer, then no + // inverse match is present. + let mut matches = pattern.find_iter(buffer); - let mut end = 0; // Start from the beginning of the buffer. + let mut end = 0; // Start from the beginning of the buffer. - // Try to find a "hole" between matches: - let inverse_match = matches.find( - |m| { - let matched = m.start() > end; + // Try to find a "hole" between matches: + let inverse_match = matches.find(|m| { + let matched = m.start() > end; - end = m.end(); + end = m.end(); - matched - } - ); + matched + }); - // Also check for a "hole" after the last match. - let matched = (inverse_match.is_some() || end < buffer.len()) - ^ options.non_matching; // List non matching files. + // Also check for a "hole" after the last match. + let matched = (inverse_match.is_some() || end < buffer.len()) ^ options.non_matching; // List non matching files. - if matched { - writeln!(out, "{}", path)?; - } + if matched { + writeln!(out, "{}", path)?; + } - Ok(matched) - } - else { - let matched = pattern.is_match(buffer) ^ options.non_matching; + Ok(matched) + } else { + let matched = pattern.is_match(buffer) ^ options.non_matching; - if matched { - writeln!(out, "{}", path)?; - } + if matched { + writeln!(out, "{}", path)?; + } - Ok(matched) - } + Ok(matched) + } } - /// Run bgrep, outputting the matched bytes to the given `out`. /// Returns whether there was a match. fn grep_bytes>( - out: &mut O, - options: &args::Options, - pattern: &Regex, - path: P, - buffer: B, + out: &mut O, + options: &args::Options, + pattern: &Regex, + path: P, + buffer: B, ) -> io::Result { - let buffer = buffer.as_ref(); + let buffer = buffer.as_ref(); - let mut write_bytes = |bs| { - if options.print_filename { - write!(out, "{}: ", path)?; - } - - out.write_all(bs)?; - writeln!(out) - }; - - - let mut matched = false; - - if options.inverse { - // `Regex::split` yields the slices outside the matches. - let mut matches = pattern.split(buffer); + let mut write_bytes = |bs| { + if options.print_filename { + write!(out, "{}: ", path)?; + } - // Set `matched` if there is a first occurrence: - if let Some(bs) = matches.next() { - if !bs.is_empty() { // A regex may have a empty match, but when inverse matching - write_bytes(bs)?; // we disconsider empty intervals. - matched = true; - } + out.write_all(bs)?; + writeln!(out) }; - // Iterate the remaining matches: - for bs in matches { - if !bs.is_empty() { - write_bytes(bs)?; - } - } - } - else { - let mut matches = pattern.find_iter(buffer); - - // Set `matched` if there is a first occurrence: - if let Some(m) = matches.next() { - write_bytes(m.as_bytes())?; - matched = true; - } + let mut matched = false; + + if options.inverse { + // `Regex::split` yields the slices outside the matches. + let mut matches = pattern.split(buffer); + + // Set `matched` if there is a first occurrence: + if let Some(bs) = matches.next() { + if !bs.is_empty() { + // A regex may have a empty match, but when inverse matching + write_bytes(bs)?; // we disconsider empty intervals. + matched = true; + } + }; + + // Iterate the remaining matches: + for bs in matches { + if !bs.is_empty() { + write_bytes(bs)?; + } + } + } else { + let mut matches = pattern.find_iter(buffer); - // Iterate the remaining matches: - for m in matches { - write_bytes(m.as_bytes())?; - } - }; + // Set `matched` if there is a first occurrence: + if let Some(m) = matches.next() { + write_bytes(m.as_bytes())?; + matched = true; + } + // Iterate the remaining matches: + for m in matches { + write_bytes(m.as_bytes())?; + } + }; - Ok(matched) + Ok(matched) } - /// Run bgrep, outputting the matche's offset in hex to the given `out`. /// Returns whether there was a match. fn grep_offset>( - out: &mut O, - options: &args::Options, - pattern: &Regex, - path: P, - buffer: B + out: &mut O, + options: &args::Options, + pattern: &Regex, + path: P, + buffer: B, ) -> io::Result { - let buffer = buffer.as_ref(); - - let mut write_hex = |x| { - if options.print_filename { - writeln!(out, "{}: 0x{:x}", path, x) - } else { - writeln!(out, "0x{:x}", x) - } - }; + let buffer = buffer.as_ref(); + let mut write_hex = |x| { + if options.print_filename { + writeln!(out, "{}: 0x{:x}", path, x) + } else { + writeln!(out, "0x{:x}", x) + } + }; - let mut matches = pattern.find_iter(buffer); + let mut matches = pattern.find_iter(buffer); - let mut matched = false; + let mut matched = false; - if options.inverse { - // if the pattern matches multiple times, comprising the entire buffer, then no - // inverse match is present. - let mut end = 0; // Start from the beginning of the buffer. + if options.inverse { + // if the pattern matches multiple times, comprising the entire buffer, then no + // inverse match is present. + let mut end = 0; // Start from the beginning of the buffer. - for m in matches { - if m.start() > end { - write_hex(end)?; - matched = true; - } + for m in matches { + if m.start() > end { + write_hex(end)?; + matched = true; + } - end = m.end() - } + end = m.end() + } - if end < buffer.len() { // Also check for a "hole" after the last match. - write_hex(end)?; - matched = true; - } - } - else { - // Set `matched` if there is a first occurrence: - if let Some(m) = matches.next() { - write_hex(m.start())?; - matched = true; - } + if end < buffer.len() { + // Also check for a "hole" after the last match. + write_hex(end)?; + matched = true; + } + } else { + // Set `matched` if there is a first occurrence: + if let Some(m) = matches.next() { + write_hex(m.start())?; + matched = true; + } - // Iterate the remaining matches: - for m in matches { - write_hex(m.start())?; + // Iterate the remaining matches: + for m in matches { + write_hex(m.start())?; + } } - } - - Ok(matched) + Ok(matched) } - /// Run bgrep with the given options, outputting to the given `out`. /// Error detail may be outputted to stderr. /// Returns whether there was a match. fn run_file, B: AsMut>>( - out: &mut O, - options: &args::Options, - pattern: &Regex, - path: P, - buffer: &mut B + out: &mut O, + options: &args::Options, + pattern: &Regex, + path: P, + buffer: &mut B, ) -> io::Result { - let buffer = buffer.as_mut(); - let path = path.as_ref(); + let buffer = buffer.as_mut(); + let path = path.as_ref(); - buffer.clear(); + buffer.clear(); - let (read_result, path) = - if path == Path::new(args::STDIN) { // Path::new is cost-free. - (io::stdin().lock().read_to_end(buffer), Path::new("").display()) - } - else { - let mut file = File::open(path) - .map_err(|e| { - eprintln!("Error: failed to open file '{}'", path.display()); - e - })?; - - // Resize buffer to the file size if it exceeds the current size. - // Currently, the strategy is to grow if needed, and otherwise do nothing. - // Considering we never shrink the buffer, this can be bad if the first file - // is huge and the others are small. - let file_size = file.metadata() - .map(|m| m.len()) - .unwrap_or(0) as usize; - buffer.reserve( - file_size.saturating_sub(buffer.len()) - ); - - (file.read_to_end(buffer), path.display()) + let (read_result, path) = if path == Path::new(args::STDIN) { + // Path::new is cost-free. + ( + io::stdin().lock().read_to_end(buffer), + Path::new("").display(), + ) + } else { + let mut file = File::open(path).map_err(|e| { + eprintln!("Error: failed to open file '{}'", path.display()); + e + })?; + + // Resize buffer to the file size if it exceeds the current size. + // Currently, the strategy is to grow if needed, and otherwise do nothing. + // Considering we never shrink the buffer, this can be bad if the first file + // is huge and the others are small. + let file_size = file.metadata().map(|m| m.len()).unwrap_or(0) as usize; + buffer.reserve(file_size.saturating_sub(buffer.len())); + + (file.read_to_end(buffer), path.display()) }; - if let Err(e) = read_result { - eprintln!("Error: failed to read file '{}'", path); - return Err(e); - } - - - // Trim the ending newline if requested and present: - if options.trim_ending_newline && buffer.last() == Some(&b'\n') { - buffer.pop(); - }; + if let Err(e) = read_result { + eprintln!("Error: failed to read file '{}'", path); + return Err(e); + } + // Trim the ending newline if requested and present: + if options.trim_ending_newline && buffer.last() == Some(&b'\n') { + buffer.pop(); + }; - let matched = match options.output { - args::Output::FileName => grep_filename (out, options, pattern, path, buffer), - args::Output::Bytes => grep_bytes (out, options, pattern, path, buffer), - args::Output::Offset => grep_offset (out, options, pattern, path, buffer) - }?; + let matched = match options.output { + args::Output::FileName => grep_filename(out, options, pattern, path, buffer), + args::Output::Bytes => grep_bytes(out, options, pattern, path, buffer), + args::Output::Offset => grep_offset(out, options, pattern, path, buffer), + }?; - Ok(matched) + Ok(matched) } /// Run bgrep with the given args, outputting to stdout. /// Error detail may be outputted to stderr. /// Returns whether there was a match. pub fn run(args: Args, out: &mut O) -> io::Result { - // Deconstruct to split ownership: - let Args { options, pattern, files } = args; - - - let pattern = build_pattern(&pattern, &options).map_err( - |e| { - eprintln!("Error: invalid pattern '{}', {}", pattern, e); - io::ErrorKind::InvalidInput - } - )?; - - - // Reuse the same buffer for all the files, minimizing allocations. - let mut buffer = Vec::::new(); - - // The next part is a bit complicated: - // Bgrep must return: - // - // 0 if there was any match, and no errors. `BrokenPipe` is not considered an error, - // but a signal to stop processing. - // - // 1 if there was no match, and no errors. `BrokenPipe` cannot happen in this case, - // because it should only happen when outputting, and no matches means no output. - // - // An error code corresponding to the last error. Common errors are `NotFound` and - // `PermissionDenied`. - - // We need to store the last generated error if any, or whether there was a match: - let mut result = Ok(false); - - // Converting to vec to use the owned iterator. Box<[T]> has no owned iterator. - for file in files.to_vec() { - let file: PathBuf = file; // Make sure we are using an owned iterator. - - match run_file(out, &options, &pattern, &file, &mut buffer) { - Ok(false) => (), - Ok(true) => result = result.map(|_| true), // Set to true if there was no error. - Err(e) => - if e.kind() == io::ErrorKind::BrokenPipe { - // Bail early on `BronkenPipe`, conserving the previous error if any. - result = result.map(|_| true); // `BrokenPipe` only happens when outputting, - break; // and that means there was a match. - } else { - result = Err(e) // Store the error and move on. + // Deconstruct to split ownership: + let Args { + options, + pattern, + files, + } = args; + + let pattern = build_pattern(&pattern, &options).map_err(|e| { + eprintln!("Error: invalid pattern '{}', {}", pattern, e); + io::ErrorKind::InvalidInput + })?; + + // Reuse the same buffer for all the files, minimizing allocations. + let mut buffer = Vec::::new(); + + // The next part is a bit complicated: + // Bgrep must return: + // + // 0 if there was any match, and no errors. `BrokenPipe` is not considered an error, + // but a signal to stop processing. + // + // 1 if there was no match, and no errors. `BrokenPipe` cannot happen in this case, + // because it should only happen when outputting, and no matches means no output. + // + // An error code corresponding to the last error. Common errors are `NotFound` and + // `PermissionDenied`. + + // We need to store the last generated error if any, or whether there was a match: + let mut result = Ok(false); + + // Converting to vec to use the owned iterator. Box<[T]> has no owned iterator. + for file in files.to_vec() { + let file: PathBuf = file; // Make sure we are using an owned iterator. + + match run_file(out, &options, &pattern, &file, &mut buffer) { + Ok(false) => (), + Ok(true) => result = result.map(|_| true), // Set to true if there was no error. + Err(e) => { + if e.kind() == io::ErrorKind::BrokenPipe { + // Bail early on `BronkenPipe`, conserving the previous error if any. + result = result.map(|_| true); // `BrokenPipe` only happens when outputting, + break; // and that means there was a match. + } else { + result = Err(e) // Store the error and move on. + } + } } } - } - result + result } diff --git a/src/main.rs b/src/main.rs index 76debee..11c5c70 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,48 +7,42 @@ use std::process; use args::Command; - /// Main does not return because we use `process::exit`. fn main() -> ! { - /// Run bgrep with `std::env::args_os`, outputting to stdout. - /// Error detail may be outputted to stderr. - /// Returns whether there was a match. - fn run() -> io::Result { // Returns whether there was a match. - let args = env::args_os(); - - let command = args::parse(args).map_err( - |e| { - eprintln!("{}", e.message); - io::ErrorKind::InvalidInput - } - )?; - - - // Lock stdout to prevent repetitive locking. - let stdout = io::stdout(); - let mut stdout = stdout.lock(); - - - match command { - Command::Grep(args) => grep::run(args, &mut stdout), - Command::Help(msg) | Command::Version(msg) => { - writeln!(stdout, "{}", msg)?; - Ok(true) - } + /// Run bgrep with `std::env::args_os`, outputting to stdout. + /// Error detail may be outputted to stderr. + /// Returns whether there was a match. + fn run() -> io::Result { + // Returns whether there was a match. + let args = env::args_os(); + + let command = args::parse(args).map_err(|e| { + eprintln!("{}", e.message); + io::ErrorKind::InvalidInput + })?; + + // Lock stdout to prevent repetitive locking. + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + + match command { + Command::Grep(args) => grep::run(args, &mut stdout), + Command::Help(msg) | Command::Version(msg) => { + writeln!(stdout, "{}", msg)?; + Ok(true) + } + } } - } - process::exit( - match run() { - Ok(true) => 0, // There was at least one match. - Ok(false) => 1, // There was no match. - Err(e) => match e.kind() { - io::ErrorKind::InvalidInput => 3, - io::ErrorKind::NotFound => 4, - io::ErrorKind::PermissionDenied => 5, - io::ErrorKind::Interrupted => 130, - _ => 2 - } - } - ) + process::exit(match run() { + Ok(true) => 0, // There was at least one match. + Ok(false) => 1, // There was no match. + Err(e) => match e.kind() { + io::ErrorKind::InvalidInput => 3, + io::ErrorKind::NotFound => 4, + io::ErrorKind::PermissionDenied => 5, + io::ErrorKind::Interrupted => 130, + _ => 2, + }, + }) }