» Make grep CLI App in Python » 2. Development » 2.3 Add Functionalities

Add Functionalities

You need to support these functionalities as shown in the project objectives section:

-c, --count
-i, --ignore-case
-n, --line-number
-r, --recursive
-v, --invert-match

grepy_cli.py:

import argparse

from grepy.grep import grep, grep_recursive, grep_count

def main():
    parser = argparse.ArgumentParser(description='''A grep-like command-line utility from LiteRank, 
                                     see https://literank.com/project/9/intro''')
    parser.add_argument('pattern', type=str, help='The pattern to search for')
    parser.add_argument('file_path', type=str, help='The path to the file to search in')

    # Optional arguments
    parser.add_argument('-c', '--count', action='store_true', help='Only a count of selected lines is written to standard output.')
    parser.add_argument('-i', '--ignore-case', action='store_true', help='Perform case insensitive matching. By default, it is case sensitive.')
    parser.add_argument('-n', '--line-number', action='store_true', help='Each output line is preceded by its relative line number in the file, starting at line 1. This option is ignored if -c is specified.')
    parser.add_argument('-r', '--recursive', action='store_true', help='Recursively search subdirectories listed.')
    parser.add_argument('-v', '--invert-match', action='store_true', help='Selected lines are those not matching any of the specified patterns.')

    args = parser.parse_args()

    if args.recursive:
        result = grep_recursive(args.pattern, args.file_path, get_options(args))
    else:
        result = grep(args.pattern, args.file_path, get_options(args))

    if args.count:
        print(grep_count(result))
    else:
        print_result(result, args.line_number)

def get_options(args):
    options = []
    if args.ignore_case:
        options.append('i')
    if args.invert_match:
        options.append('v')
    return options

def print_result(result, line_number_option):
    current_file = None
    file_count = len(result)
    for file_path, lines in result.items():
        for (line_number, line) in lines:
            if file_count > 1 and file_path != current_file:
                current_file = file_path
                print(f"\n{file_path}:")
            if line_number_option:
                print(f"{line_number}: {line}")
            else:
                print(line)

if __name__ == '__main__':
    main()

argparse adds all the optional arguments into the app. Function print_result parses the result dictionary and prints the file_path and line_number if it's needed.

grepy/grep.py:

import re
import os

def _filter_lines(pattern, lines, flag):
    return [(line_number, line.strip()) for line_number, line in enumerate(lines, start=1) if bool(re.search(pattern, line)) == flag]

def grep(pattern, file_path, options=None):
    with open(file_path, 'r') as file:
        try:
            lines = file.readlines()
        except UnicodeDecodeError: # filter out binary files
            return {file_path: []}

        if options:
            if 'i' in options:
                pattern = re.compile(pattern, re.IGNORECASE)
            if 'v' in options:
                matching_lines = _filter_lines(pattern, lines, False)
            else:
                matching_lines = _filter_lines(pattern, lines, True)
        else:
            matching_lines = _filter_lines(pattern, lines, True)

    return {file_path: matching_lines}

def grep_count(result):
    return sum([len(v) for v in result.values()])

def grep_recursive(pattern, directory_path, options=None):
    results = {}

    for root, _, files in os.walk(directory_path):
        for file in files:
            file_path = os.path.join(root, file)
            results.update(grep(pattern, file_path, options))

    return results

Function grep adds logic for "case-insensitive match" and "invert match." Function grep_recursive lists all files recursively, greps them, and puts the results into the big dictionary.

Run the normal way:

python3 -m grepy_cli result grepy_cli.py

Result lines:

result = grep_recursive(args.pattern, args.file_path, get_options(args))
result = grep(args.pattern, args.file_path, get_options(args))
print(grep_count(result))
print_result(result, args.line_number)
def print_result(result, line_number_option):
file_count = len(result)
for file_path, lines in result.items():

Do the count:

python3 -m grepy_cli -c result grepy_cli.py

Result number is: 7.

Show the line numbers:

python3 -m grepy_cli -n result grepy_cli.py

Result lines:

21: result = grep_recursive(args.pattern, args.file_path, get_options(args))
23: result = grep(args.pattern, args.file_path, get_options(args))
26: print(grep_count(result))
28: print_result(result, args.line_number)
38: def print_result(result, line_number_option):
40: file_count = len(result)
41: for file_path, lines in result.items():

Use regex pattern

python3 -m grepy_cli -n "\br[a-z]+t" grepy_cli.py

Result lines:

15: parser.add_argument('-n', '--line-number', action='store_true', help='Each output line is preceded by its relative line number in the file, starting at line 1. This option is ignored if -c is specified.')
22: result = grep_recursive(args.pattern, args.file_path, get_options(args))
24: result = grep(args.pattern, args.file_path, get_options(args))
27: print(grep_count(result))
29: print_result(result, args.line_number)
37: return options
39: def print_result(result: Dict[str, MatchResults], line_number_option: bool):
41: file_count = len(result)
42: for file_path, lines in result.items():

Do the invert match:

python3 -m grepy_cli -vn result grepy_cli.py

Result is something like this:

1: import argparse
2: 
3: from grepy.grep import grep, grep_recursive, grep_count
4: 
5: def main():

...

8: parser.add_argument('pattern', type=str, help='The pattern to search for')
9: parser.add_argument('file_path', type=str, help='The path to the file to search in')

...

50: 
51: if __name__ == '__main__':
52: main()

Do the case insensitive match:

python3 -m grepy_cli -i only grepy_cli.py

Result is:

parser.add_argument('-c', '--count', action='store_true', help='Only a count of selected lines is written to standard output.')

Do the big recursive match:

python3 -m grepy_cli -r count .

Result lines should be like:

./grepy_cli.py:
from grepy.grep import grep, grep_recursive, grep_count
parser.add_argument('-c', '--count', action='store_true', help='Only a count of selected lines is written to standard output.')
if args.count:
print(grep_count(result))
file_count = len(result)
if file_count > 1 and file_path != current_file:

./grepy/grep.py:
def grep_count(result):
PrevNext