From 074a085d92837857af2b628e8cecc0f4ff47bb95 Mon Sep 17 00:00:00 2001 From: Nitkarsh Chourasia Date: Sat, 20 Jun 2026 13:29:56 +0530 Subject: [PATCH] update: cat unix like command --- Cat/README.md | 117 ++++++++++++++++++++++++++++ Cat/cat.py | 201 ++++++++++++++++++++++++++++++++++-------------- Cat/test_cat.py | 143 ++++++++++++++++++++++++++++++++++ 3 files changed, 402 insertions(+), 59 deletions(-) create mode 100644 Cat/README.md create mode 100644 Cat/test_cat.py diff --git a/Cat/README.md b/Cat/README.md new file mode 100644 index 00000000000..dc9039f0685 --- /dev/null +++ b/Cat/README.md @@ -0,0 +1,117 @@ +# Python Cat + +Author: Nitkarsh Chourasia + +A small Python implementation of the Unix `cat` command. It reads text from files or standard input and writes the result to standard output. + +## Usage + +```powershell +python cat.py [options] [files...] +``` + +If no files are provided, the program reads from standard input. + +## Basic Examples + +Read one file: + +```powershell +python cat.py text_a.txt +``` + +Read multiple files in order: + +```powershell +python cat.py text_a.txt text_b.txt text_c.txt +``` + +Read from standard input: + +```powershell +Get-Content text_a.txt | python cat.py +``` + +Use `-` to read standard input between files: + +```powershell +Get-Content text_b.txt | python cat.py text_a.txt - text_c.txt +``` + +## Options + +Number all lines: + +```powershell +python cat.py -n text_a.txt +``` + +Number only non-empty lines: + +```powershell +python cat.py -b text_a.txt +``` + +Squeeze repeated blank lines: + +```powershell +python cat.py -s text_a.txt +``` + +Show line endings with `$`: + +```powershell +python cat.py -E text_a.txt +``` + +Combine options: + +```powershell +python cat.py -n -E text_a.txt +``` + +```powershell +python cat.py -b -s text_a.txt text_b.txt +``` + +## Error Handling + +If a file cannot be read, the error is printed to standard error and the program continues with the next file. + +```powershell +python cat.py text_a.txt missing.txt text_b.txt +``` + +The program exits with: + +- `0` when all input is processed successfully +- `1` when one or more files cannot be read + +## Test Cases + +Run the automated test suite: + +```powershell +python -m unittest test_cat.py +``` + +The tests cover: + +- reading one file +- reading multiple files in order +- reading from standard input +- using `-` for standard input between files +- continuing after a missing file +- `-n` line numbering +- `-b` non-empty line numbering +- `-b` priority over `-n` +- `-s` repeated blank-line squeezing +- `-E` visible line endings +- combined options + +## Design Notes + +- Files are processed one at a time instead of loading everything into memory. +- `sys.stdin` and opened files are both treated as streams. +- Line numbering continues across multiple files. +- `-b` takes priority over `-n` when both are used. diff --git a/Cat/cat.py b/Cat/cat.py index 3024b35ef0a..53271e8e28a 100644 --- a/Cat/cat.py +++ b/Cat/cat.py @@ -1,65 +1,148 @@ -""" -The 'cat' Program Implemented in Python 3 - -The Unix 'cat' utility reads the contents -of file(s) specified through stdin and 'conCATenates' -into stdout. If it is run without any filename(s) given, -then the program reads from standard input itself, -which means it simply copies stdin to stdout. - -It is fairly easy to implement such a program -in Python, and as a result countless examples -exist online. This particular implementation -focuses on the basic functionality of the cat -utility. Compatible with Python 3.6 or higher. - -Syntax: -python3 cat.py [filename1] [filename2] etc... -Separate filenames with spaces. - -David Costell (DontEatThemCookies on GitHub) -v2 - 03/12/2022 -""" - -import sys - - -def with_files(files): - """Executes when file(s) is/are specified.""" - try: - # Read each file's contents and store them - file_contents = [contents for contents in [open(file).read() for file in files]] - except OSError as err: - # This executes when there's an error (e.g. FileNotFoundError) - exit(print(f"cat: error reading files ({err})")) - - # Write all file contents into the standard output stream - for contents in file_contents: - sys.stdout.write(contents) - - -def no_files(): - """Executes when no file(s) is/are specified.""" - try: - # Get input, output the input, repeat - while True: - print(input()) - # Graceful exit for Ctrl + C, Ctrl + D - except KeyboardInterrupt: - exit() - # exit when no data found in file - except EOFError: - exit() +""" +A simple Python implementation of the Unix cat command. + +Author: +- Nitkarsh Chourasia + +Features: +- Reads one or more files +- Reads from stdin when no files are given +- Supports "-" as stdin +- Prints errors to stderr +- Continues after file errors +- Uses proper exit codes +- Supports: + -n : number all lines + -b : number non-empty lines + -s : squeeze repeated blank lines + -E : show $ at end of each line + +Design notes: +- Files and stdin are both handled as streams. +- Line numbering state is shared across files, matching cat-style behavior. +- File errors are reported to stderr while processing continues. +""" + +import argparse +import sys + +__author__ = "Nitkarsh Chourasia" + + +def process_stream(stream, args, state): + """Read from a stream and write processed output to stdout.""" + for line in stream: + is_blank = line == "\n" + + if args.squeeze_blank and is_blank and state["previous_was_blank"]: + continue + + state["previous_was_blank"] = is_blank + + if args.show_ends: + if line.endswith("\n"): + line = line[:-1] + "$\n" + else: + line = line + "$" + + if args.number_nonblank: + if not is_blank: + sys.stdout.write(f"{state['line_number']:6}\t") + state["line_number"] += 1 + elif args.number: + sys.stdout.write(f"{state['line_number']:6}\t") + state["line_number"] += 1 + + sys.stdout.write(line) + + +def process_file(filename, args, state): + """Open one file and process its contents.""" + with open(filename, "r", encoding="utf-8") as file: + process_stream(file, args, state) + + +def process_files(files, args): + """Process all given filenames.""" + had_error = False + + state = { + "line_number": 1, + "previous_was_blank": False, + } + + for filename in files: + try: + if filename == "-": + process_stream(sys.stdin, args, state) + else: + process_file(filename, args, state) + except OSError as err: + print(f"cat: {filename}: {err}", file=sys.stderr) + had_error = True + + return had_error + + +def parse_arguments(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="A simple Python cat command.") + + parser.add_argument( + "files", + nargs="*", + help="Files to read. Use '-' to read from standard input.", + ) + + parser.add_argument( + "-n", + "--number", + action="store_true", + help="Number all output lines.", + ) + + parser.add_argument( + "-b", + "--number-nonblank", + action="store_true", + help="Number non-empty output lines.", + ) + + parser.add_argument( + "-s", + "--squeeze-blank", + action="store_true", + help="Suppress repeated empty output lines.", + ) + + parser.add_argument( + "-E", + "--show-ends", + action="store_true", + help="Display $ at the end of each line.", + ) + + return parser.parse_args() def main(): - """Entry point of the cat program.""" - # Read the arguments passed to the program - if not sys.argv[1:]: - no_files() - else: - with_files(sys.argv[1:]) + args = parse_arguments() + + if not args.files: + state = { + "line_number": 1, + "previous_was_blank": False, + } + process_stream(sys.stdin, args, state) + sys.exit(0) + + had_error = process_files(args.files, args) + + if had_error: + sys.exit(1) + + sys.exit(0) if __name__ == "__main__": - main() + main() diff --git a/Cat/test_cat.py b/Cat/test_cat.py new file mode 100644 index 00000000000..47e3fe2c370 --- /dev/null +++ b/Cat/test_cat.py @@ -0,0 +1,143 @@ +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +CAT_SCRIPT = Path(__file__).with_name("cat.py") + + +class CatProgramTests(unittest.TestCase): + def run_cat(self, *args, input_text=""): + return subprocess.run( + [sys.executable, str(CAT_SCRIPT), *map(str, args)], + input=input_text, + capture_output=True, + text=True, + check=False, + ) + + def test_reads_single_file(self): + with tempfile.TemporaryDirectory() as temp_dir: + file_path = Path(temp_dir) / "one.txt" + file_path.write_text("Alpha\nBeta\n", encoding="utf-8") + + result = self.run_cat(file_path) + + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, "Alpha\nBeta\n") + self.assertEqual(result.stderr, "") + + def test_reads_multiple_files_in_order(self): + with tempfile.TemporaryDirectory() as temp_dir: + first = Path(temp_dir) / "first.txt" + second = Path(temp_dir) / "second.txt" + first.write_text("First\n", encoding="utf-8") + second.write_text("Second\n", encoding="utf-8") + + result = self.run_cat(first, second) + + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, "First\nSecond\n") + + def test_reads_from_stdin_without_files(self): + result = self.run_cat(input_text="From stdin\nSecond line\n") + + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, "From stdin\nSecond line\n") + + def test_dash_reads_stdin_between_files(self): + with tempfile.TemporaryDirectory() as temp_dir: + first = Path(temp_dir) / "first.txt" + second = Path(temp_dir) / "second.txt" + first.write_text("Before\n", encoding="utf-8") + second.write_text("After\n", encoding="utf-8") + + result = self.run_cat(first, "-", second, input_text="Middle\n") + + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, "Before\nMiddle\nAfter\n") + + def test_missing_file_reports_error_and_continues(self): + with tempfile.TemporaryDirectory() as temp_dir: + first = Path(temp_dir) / "first.txt" + missing = Path(temp_dir) / "missing.txt" + second = Path(temp_dir) / "second.txt" + first.write_text("Before\n", encoding="utf-8") + second.write_text("After\n", encoding="utf-8") + + result = self.run_cat(first, missing, second) + + self.assertEqual(result.returncode, 1) + self.assertEqual(result.stdout, "Before\nAfter\n") + self.assertIn("cat:", result.stderr) + self.assertIn("missing.txt", result.stderr) + + def test_number_all_lines(self): + with tempfile.TemporaryDirectory() as temp_dir: + file_path = Path(temp_dir) / "lines.txt" + file_path.write_text("Alpha\n\nBeta\n", encoding="utf-8") + + result = self.run_cat("-n", file_path) + + expected = " 1\tAlpha\n 2\t\n 3\tBeta\n" + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, expected) + + def test_number_nonblank_lines(self): + with tempfile.TemporaryDirectory() as temp_dir: + file_path = Path(temp_dir) / "lines.txt" + file_path.write_text("Alpha\n\nBeta\n", encoding="utf-8") + + result = self.run_cat("-b", file_path) + + expected = " 1\tAlpha\n\n 2\tBeta\n" + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, expected) + + def test_number_nonblank_takes_priority_over_number(self): + with tempfile.TemporaryDirectory() as temp_dir: + file_path = Path(temp_dir) / "lines.txt" + file_path.write_text("Alpha\n\nBeta\n", encoding="utf-8") + + result = self.run_cat("-n", "-b", file_path) + + expected = " 1\tAlpha\n\n 2\tBeta\n" + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, expected) + + def test_squeeze_repeated_blank_lines(self): + with tempfile.TemporaryDirectory() as temp_dir: + file_path = Path(temp_dir) / "blanks.txt" + file_path.write_text("Alpha\n\n\nBeta\n\n\n", encoding="utf-8") + + result = self.run_cat("-s", file_path) + + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, "Alpha\n\nBeta\n\n") + + def test_show_ends(self): + with tempfile.TemporaryDirectory() as temp_dir: + file_path = Path(temp_dir) / "lines.txt" + file_path.write_text("Alpha\nBeta", encoding="utf-8") + + result = self.run_cat("-E", file_path) + + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, "Alpha$\nBeta$") + + def test_combines_options(self): + with tempfile.TemporaryDirectory() as temp_dir: + file_path = Path(temp_dir) / "lines.txt" + file_path.write_text("Alpha\n\n\nBeta\n", encoding="utf-8") + + result = self.run_cat("-b", "-s", "-E", file_path) + + expected = " 1\tAlpha$\n$\n 2\tBeta$\n" + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, expected) + + +if __name__ == "__main__": + unittest.main()