| 1 | 1 | def grep(pattern: str, flags: str, files: list[str]) -> str: |
| 2 | | pass |
| 2 | # Parse flags |
| 3 | line_numbers = '-n' in flags |
| 4 | files_only = '-l' in flags |
| 5 | case_insensitive = '-i' in flags |
| 6 | invert_match = '-v' in flags |
| 7 | exact_match = '-x' in flags |
| 8 | |
| 9 | # Edge Case: Empty pattern should match empty lines or be handled consistently |
| 10 | # Edge Case: Files list is empty - return empty string |
| 11 | if not files: |
| 12 | return "" |
| 13 | |
| 14 | # Prepare pattern for comparison |
| 15 | search_pattern = pattern.lower() if case_insensitive else pattern |
| 16 | |
| 17 | result_lines = [] |
| 18 | matched_files = set() |
| 19 | |
| 20 | # Edge Case: Single file vs multiple files affects output format |
| 21 | multiple_files = len(files) > 1 |
| 22 | |
| 23 | for file_name in files: |
| 24 | # Edge Case: File not found or inaccessible - let FileNotFoundError propagate |
| 25 | with open(file_name, 'r', encoding='utf-8') as f: |
| 26 | lines = f.readlines() |
| 27 | |
| 28 | file_has_match = False |
| 29 | |
| 30 | for i, line in enumerate(lines, 1): # 1-indexed line numbers |
| 31 | # Remove newline for processing but keep for output |
| 32 | line_content = line.rstrip('\n') |
| 33 | |
| 34 | # Determine if line matches |
| 35 | if case_insensitive: |
| 36 | line_to_check = line_content.lower() |
| 37 | else: |
| 38 | line_to_check = line_content |
| 39 | |
| 40 | if exact_match: |
| 41 | is_match = (line_to_check == search_pattern) |
| 42 | else: |
| 43 | is_match = (search_pattern in line_to_check) |
| 44 | |
| 45 | # Edge Case: Invert flag reverses match logic |
| 46 | if invert_match: |
| 47 | is_match = not is_match |
| 48 | |
| 49 | if is_match: |
| 50 | file_has_match = True |
| 51 | |
| 52 | # Edge Case: When -l flag is used, we only need to know which files match |
| 53 | if files_only: |
| 54 | matched_files.add(file_name) |
| 55 | # Optimization: break early if we only care about file names |
| 56 | break |
| 57 | |
| 58 | # Build output line |
| 59 | output_line = "" |
| 60 | |
| 61 | # Edge Case: Multiple files require filename prefix |
| 62 | if multiple_files: |
| 63 | output_line += f"{file_name}:" |
| 64 | |
| 65 | # Edge Case: Line numbers require number prefix |
| 66 | if line_numbers: |
| 67 | output_line += f"{i}:" |
| 68 | |
| 69 | output_line += line_content |
| 70 | result_lines.append(output_line) |
| 71 | |
| 72 | # Edge Case: -l flag with no matches in file |
| 73 | if files_only and file_has_match: |
| 74 | # Already added to matched_files above |
| 75 | pass |
| 76 | |
| 77 | # Edge Case: -l flag returns only filenames, one per line |
| 78 | if files_only: |
| 79 | # Return files in the order they were specified |
| 80 | output_files = [f for f in files if f in matched_files] |
| 81 | return "\n".join(output_files) + ("\n" if output_files else "") |
| 82 | |
| 83 | # Edge Case: No matches found at all |
| 84 | return "\n".join(result_lines) + ("\n" if result_lines else "") |
| 85 | |
| 86 | # Handled Edge Cases: Empty pattern, empty files list, file not found, single vs multiple files, invert flag, exact match, case insensitive matching, line numbers, files only flag, no matches found |