» Make grep CLI App in Rust » 2. Development » 2.7 Support Pipes

Support Pipes

In Unix-like operating systems (such as Linux and macOS), a pipe is a mechanism for interprocess communication (IPC) that allows the output of one process to be used as the input of another.

Pipes are represented by the | symbol in shell commands. When you use a command like command1 | command2, it means that the standard output of command1 is connected to the standard input of command2.

For example:

cat file.txt | grep "pattern"

In this command, the content of file.txt is piped as input to the grep command, which searches for the specified pattern.

Modify src/main.rs to make file_path argument optional:

@@ -8,7 +8,7 @@ fn main() {
         .author("literank")
         .about("A grep-like utility in Rust")
         .arg(Arg::with_name("pattern").required(true).index(1).help("The pattern to search for"))
-        .arg(Arg::with_name("file_path").required(true).index(2).help("The file to search in"))
+        .arg(Arg::with_name("file_path").required(false).index(2).help("The file to search in"))
         .arg(Arg::with_name("count").short("c").long("count").help("Only a count of selected lines is written to standard output"))
         .arg(Arg::with_name("ignore-case").short("i").long("ignore-case").help("Perform case-insensitive matching"))
         .arg(Arg::with_name("line-number").short("n").long("line-number").help("Each output line is preceded by its relative line number in the file, starting at line 1"))
@@ -18,13 +18,13 @@ fn main() {
 
     // Extract command-line arguments
     let pattern = matches.value_of("pattern").unwrap();
-    let file_path = matches.value_of("file_path").unwrap();
+    let file_path = matches.value_of("file_path").unwrap_or("");
     let options = GrepOptions {
         ignore_case: matches.is_present("ignore-case"),
         invert_match: matches.is_present("invert-match"),
     };

You cannot do a recursive search when there's no file paths, modify src/main.rs:

-    let result = if matches.is_present("recursive") {
+    let result = if matches.is_present("recursive") && !file_path.is_empty() {
         grep_recursive(pattern, file_path.as_ref(), &options)
     } else {
         grep(pattern, file_path.as_ref(), &options)

Read the content from standard input when file_path is empty in src/lib.rs:

@@ -66,8 +66,12 @@ pub fn grep_recursive(
 }
 
 fn read_file_lines(file_path: &Path) -> Result<Vec<String>, io::Error> {
-    let file = fs::File::open(file_path)?;
-    let reader = io::BufReader::new(file);
+    let reader: Box<dyn BufRead> = if file_path.components().count() == 0 {
+        Box::new(io::BufReader::new(io::stdin()))
+    } else {
+        let file = fs::File::open(file_path)?;
+        Box::new(io::BufReader::new(file))
+    };
     Ok(reader.lines().map_while(Result::ok).collect())
 }

Now you can do this:

cat src/main.rs |  cargo run -- -n line

Its result:

5: // Define command-line arguments using clap
12: .arg(Arg::with_name("count").short("c").long("count").help("Only a count of selected lines is written to standard output"))
14: .arg(Arg::with_name("line-number").short("n").long("line-number").help("Each output line is preceded by its relative line number in the file, starting at line 1"))
16: .arg(Arg::with_name("invert-match").short("v").long("invert-match").help("Selected lines are those not matching any of the specified patterns"))
19: // Extract command-line arguments
38: print_result(&result, matches.is_present("line-number"))
47: fn print_result(result: &MatchResult, show_line_number: bool) {
57: if show_line_number {
58: println!("{}: {}", item.line_number, item.line);
60: println!("{}", item.line);

After re-installation, your grustep command supports pipes as well.

cat src/main.rs | grustep -n line

It produces the same result as above.