» Make grep CLI App in Node.js » 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 bin/cmd.ts to make file argument optional:

 // Parse command-line options
 const argv = await yargs(process.argv.slice(2))
   .locale('en')
-  .usage('Usage: $0 [options] <pattern> <file>')
+  .usage('Usage: $0 [options] <pattern> [<file>]')
   .option('c', {
     alias: 'count',
     describe: 'Only a count of selected lines is written to standard output.',
@@ -48,10 +48,10 @@ const argv = await yargs(process.argv.slice(2))
     type: 'boolean',
     default: false
   })
-  .demandCommand(2, 'Please provide both pattern and file arguments.').argv
+  .demandCommand(1, 'Please provide pattern to search for.').argv
 
 const pattern = argv._[0] as string
-const filePath = argv._[1] as string
+const filePath = argv._[1] !== undefined ? argv._[1] as string : ''

This makes the argument optional, allowing it to be provided or omitted on the command line.

You cannot do a recursive search when there's no file paths, modify bin/cmd.ts:

-const result = argv.recursive as boolean
+const result = (argv.recursive as boolean && filePath !== '')
   ? grepRecursive(pattern, filePath, options)
   : grep(pattern, filePath, options)

Read the content from standard input when filePath is empty in lib/grep.ts:

 export async function grep (pattern: string, filePath: string, options: Options): Promise<MatchResult> {
   const { ignoreCase, invertMatch } = options
-  const lines = await _readFileLines(filePath)
+  const lines = filePath === '' ? await _readStdinLines() : await _readFileLines(filePath)
   const regexFlags = ignoreCase ? 'gi' : 'g'
   const regex = new RegExp(pattern, regexFlags)
   let matchingLines: MatchItem[]
@@ -65,3 +66,22 @@ async function _readFileLines (filePath: string): Promise<string[]> {
   }
   return []
 }
+
+async function _readStdinLines (): Promise<string[]> {
+  const rl = readline.createInterface({
+    input: process.stdin,
+    output: process.stdout,
+    terminal: false
+  })
+
+  return await new Promise((resolve) => {
+    const lines: string[] = []
+    rl.on('line', (line) => {
+      lines.push(line)
+    })
+
+    rl.on('close', () => {
+      resolve(lines)
+    })
+  })
+}

Now after re-compilation, you can do this:

cat lib/grep.ts | node dist/bin/cmd.js -in line

Its result:

3: import * as readline from 'readline'
16: const lines = filePath === '' ? await _readStdinLines() : await _readFileLines(filePath)
19: let matchingLines: MatchItem[]
21: matchingLines = _filterLines(regex, lines, false)
23: matchingLines = _filterLines(regex, lines, true)
25: return { [filePath]: matchingLines }
48: (count, lines) => count + lines.length,
53: function _filterLines (regexPattern: RegExp, lines: string[], flag: boolean): MatchItem[] {
54: const candidates: MatchItem[] = lines.map((line, index) => [index + 1, line.trim()])
56: .filter(([_, line]) => regexPattern.test(line) === flag)
59: async function _readFileLines (filePath: string): Promise<string[]> {
70: async function _readStdinLines (): Promise<string[]> {
78: const lines: string[] = []
79: rl.on('line', (line) => {
84: resolve(lines)
PrevNext