From 1bce56a1b1492cc805d1fce9fe56b531d25ca7ee Mon Sep 17 00:00:00 2001 From: ike-agu Date: Fri, 12 Dec 2025 01:15:57 +0000 Subject: [PATCH 1/8] Add python implementation of catCommand and .gitignore file --- implement-shell-tools/cat/README.md | 19 ++++++++++- implement-shell-tools/cat/cat.py | 50 +++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 implement-shell-tools/cat/cat.py diff --git a/implement-shell-tools/cat/README.md b/implement-shell-tools/cat/README.md index 7284a5e67..86291b406 100644 --- a/implement-shell-tools/cat/README.md +++ b/implement-shell-tools/cat/README.md @@ -1,4 +1,4 @@ -# Implement `cat` +# Implement `cat` in python You should already be familiar with the `cat` command line tool. @@ -15,3 +15,20 @@ It must act the same as `cat` would, if run from the directory containing this R Matching any additional behaviours or flags are optional stretch goals. We recommend you start off supporting no flags, then add support for `-n`, then add support for `-b`. + +*========commands for running the scripts======== + +* No flags +python cat.py sample-files/1.txt + +* Number every line (-n) +python cat.py -n sample-files/1.txt + +* Display all files in directory(shell expands *.txt) +python cat.py sample-files/*.txt + +* Number every line across multiple files(including empty ones) +python cat.py -n sample-files/*.txt + +* Number non-empty lines only (-b) +python cat.py -b sample-files/3.txt diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py new file mode 100644 index 000000000..96bbcdbc9 --- /dev/null +++ b/implement-shell-tools/cat/cat.py @@ -0,0 +1,50 @@ +import argparse +# ------------------------------------------- +# Set up the argument parser +# ------------------------------------------- + +parser = argparse.ArgumentParser( + prog ="display-file-content", + description = "Implement cat command with -n and -b flag support", + ) + +parser.add_argument("-n", "--number-all-lines", + action="store_true", + help="Number every line in the file" + ) + +parser.add_argument("-b", "--number-non-empty-lines", + action="store_true", + help="Number non empty lines in the file" + ) + +parser.add_argument("paths", nargs="+", help="File paths to process") + +args = parser.parse_args() + +# ------------------------------------------- +# Implement functionality +# ------------------------------------------- + +line_number = 1 + +for filepath in args.paths: + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + + lines = content.split("\n") + + for line in lines: + if args.number_all_lines: + print(f"{line_number} {line}") + line_number += 1 + + elif args.number_non_empty_lines: + if line.strip() == "": + print(line) + else: + print(f"{line_number} {line}") + line_number +=1 + + else: + print(line) From 4f141b32a5fc700f7dd7d0ddd74234f332b8ab0d Mon Sep 17 00:00:00 2001 From: ike-agu Date: Fri, 12 Dec 2025 02:28:56 +0000 Subject: [PATCH 2/8] Implement ls commands in python --- implement-shell-tools/ls/README.md | 5 ++ implement-shell-tools/ls/ls_py.py | 73 ++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100755 implement-shell-tools/ls/ls_py.py diff --git a/implement-shell-tools/ls/README.md b/implement-shell-tools/ls/README.md index edbfb811a..2190f6039 100644 --- a/implement-shell-tools/ls/README.md +++ b/implement-shell-tools/ls/README.md @@ -13,3 +13,8 @@ It must act the same as `ls` would, if run from the directory containing this RE Matching any additional behaviours or flags are optional stretch goals. We recommend you start off supporting just `-1`, then adding support for `-a`. + +*=======command for running the script=========== +python3 ls_py.py -1 +python3 ls_py.py -1 sample-files +python3 ls_py.py -1 -a sample-files diff --git a/implement-shell-tools/ls/ls_py.py b/implement-shell-tools/ls/ls_py.py new file mode 100755 index 000000000..82df96402 --- /dev/null +++ b/implement-shell-tools/ls/ls_py.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +import argparse +import os +import sys + + +# Set up the argument parser +def parse_args(): + parser = argparse.ArgumentParser( + prog="list_files_in_directory", + description="Implement ls commands to list files in directory" + ) + + parser.add_argument( + "-1","--file-per-line", + action="store_true", + dest="file_per_line", + help="list files one per line" + ) + + parser.add_argument( + "-a", + "--files-and-hidden-ones", + action="store_true", + dest="files_and_hidden_ones", + help="list all files including hidden ones", + ) + + parser.add_argument( + "paths", + nargs="*", + help="directories to list" + ) + + return parser.parse_args() + +# if no paths, default to current directory +def get_paths(args): + if len(args.paths) == 0: + return ["."] + return args.paths + +# list a single directory +def list_directory(directory_path, show_hidden, file_per_line): + try: + entries = os.listdir(directory_path) + except OSError as err: + print(f"ls: cannot access '{directory_path}': {err}", file=sys.stderr) + return + + if not show_hidden: + entries = [name for name in entries if not name.startswith(".")] + + if file_per_line: + for name in entries: + print(name) + else: + print(" ".join(entries)) + +def main(): + args = parse_args() + paths = get_paths(args) + + for directory_path in paths: + list_directory( + directory_path=directory_path, + show_hidden=args.files_and_hidden_ones, + file_per_line=args.file_per_line, + ) + +if __name__ == "__main__": + main() From 7a738aeff4c430a56e856a3e70d40a14f4713576 Mon Sep 17 00:00:00 2001 From: ike-agu Date: Fri, 12 Dec 2025 12:49:55 +0000 Subject: [PATCH 3/8] Implement wc command-line tool in python --- implement-shell-tools/wc/README.md | 19 +++++- implement-shell-tools/wc/wc.py | 93 ++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 implement-shell-tools/wc/wc.py diff --git a/implement-shell-tools/wc/README.md b/implement-shell-tools/wc/README.md index bd76b655a..7261aca7b 100644 --- a/implement-shell-tools/wc/README.md +++ b/implement-shell-tools/wc/README.md @@ -1,4 +1,4 @@ -# Implement `wc` +# Implement `wc` in python You should already be familiar with the `wc` command line tool. @@ -15,3 +15,20 @@ It must act the same as `wc` would, if run from the directory containing this RE Matching any additional behaviours or flags are optional stretch goals. We recommend you start off supporting no flags for one file, then add support for multiple files, then add support for the flags. + +*======Command for testing the script======== + +* All sample files +python wc.py sample-files/* + +* Just lines +python wc.py -l sample-files/3.txt + +* Just words +python wc.py -w sample-files/3.txt + +* Just characters +python wc.py -c sample-files/3.txt + +* Lines with multiple files (to see totals) +python wc.py -l sample-files/* diff --git a/implement-shell-tools/wc/wc.py b/implement-shell-tools/wc/wc.py new file mode 100644 index 000000000..70d83d21f --- /dev/null +++ b/implement-shell-tools/wc/wc.py @@ -0,0 +1,93 @@ +import argparse +import sys +import re + + +def main(): + # -------------------------------------------------------- + # 1. Set up argparse + # -------------------------------------------------------- + parser= argparse.ArgumentParser( + prog="wc", + description ="wc command implementation in python" + ) + + parser.add_argument("-l", action="store_true", help ="show number of lines only") + parser.add_argument("-w", action="store_true", help="show number of words only") + parser.add_argument("-c", action="store_true", help="show number of characters only") + + parser.add_argument("paths", nargs="*", help="file paths to process") + + args = parser.parse_args() + + # -------------------------------------------------------- + # 2. Ensures at least one path exists + # -------------------------------------------------------- + if len(args.paths) == 0: + print("wc: no file specified", file=sys.stderr) + sys.exit(1) + + totals= {"lines": 0, "words": 0, "chars": 0} + + # -------------------------------------------------------- + # 3. Loop over each file path and process it + # -------------------------------------------------------- + for file_path in args.paths: + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + except OSError as err: + print(f"wc: cannot read file'{file_path}': {err}", file=sys.stderr) + continue + + # -------------------------------------------------------- + # 4. Count values + # -------------------------------------------------------- + line_count = len(content.split("\n")) + + words = [w for w in re.split(r"\s+", content) if w] + word_count = len(words) + + char_count = len(content) + + totals["lines"] += line_count + totals["words"] += word_count + totals["chars"] +=char_count + + # -------------------------------------------------------- + # 5. Decide what to print based on flags + # -------------------------------------------------------- + no_flags = not args.l and not args.w and not args.c + + if no_flags: + print(f"{line_count} {word_count} {char_count} {file_path}") + continue + + if args.l: + print(f"{line_count} {file_path}" ) + + if args.w: + print(f"{word_count} {file_path}") + + if args.c: + print(f"{char_count} {file_path}") + + + # -------------------------------------------------------- + # 6. Print totals if there are multiple files + # -------------------------------------------------------- + if len(args.paths) > 1: + no_flags = not args.l and not args.w and not args.c + + if no_flags: + print(f"{totals['lines']} {totals['words']} {totals['chars']} total") + + if args.l: + print(f"{totals['lines']} total") + if args.w: + print(f"{totals['words']} total") + if args.c: + print(f"{totals['chars']} total") + +if __name__ == "__main__": + main() From da1d1cc152cdaab823722b22b82d0d0e7f558edb Mon Sep 17 00:00:00 2001 From: ike-agu Date: Fri, 12 Dec 2025 12:52:53 +0000 Subject: [PATCH 4/8] Fix comments --- implement-shell-tools/wc/wc.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/implement-shell-tools/wc/wc.py b/implement-shell-tools/wc/wc.py index 70d83d21f..2420f5d38 100644 --- a/implement-shell-tools/wc/wc.py +++ b/implement-shell-tools/wc/wc.py @@ -5,7 +5,7 @@ def main(): # -------------------------------------------------------- - # 1. Set up argparse + # Set up argparse # -------------------------------------------------------- parser= argparse.ArgumentParser( prog="wc", @@ -21,7 +21,7 @@ def main(): args = parser.parse_args() # -------------------------------------------------------- - # 2. Ensures at least one path exists + # Ensures at least one path exists # -------------------------------------------------------- if len(args.paths) == 0: print("wc: no file specified", file=sys.stderr) @@ -30,7 +30,7 @@ def main(): totals= {"lines": 0, "words": 0, "chars": 0} # -------------------------------------------------------- - # 3. Loop over each file path and process it + # Loop over each file path and process it # -------------------------------------------------------- for file_path in args.paths: try: @@ -41,7 +41,7 @@ def main(): continue # -------------------------------------------------------- - # 4. Count values + # Count values # -------------------------------------------------------- line_count = len(content.split("\n")) @@ -55,7 +55,7 @@ def main(): totals["chars"] +=char_count # -------------------------------------------------------- - # 5. Decide what to print based on flags + # Decide what to print based on flags # -------------------------------------------------------- no_flags = not args.l and not args.w and not args.c @@ -74,7 +74,7 @@ def main(): # -------------------------------------------------------- - # 6. Print totals if there are multiple files + # Print totals if there are multiple files # -------------------------------------------------------- if len(args.paths) > 1: no_flags = not args.l and not args.w and not args.c From 0a8482238652c9c003db2b81dc5d5036fd213dd9 Mon Sep 17 00:00:00 2001 From: ike-agu Date: Wed, 21 Jan 2026 18:31:11 +0000 Subject: [PATCH 5/8] Fix line numbering to match cat -n and -b behaviour --- implement-shell-tools/cat/cat.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py index 96bbcdbc9..d63825a45 100644 --- a/implement-shell-tools/cat/cat.py +++ b/implement-shell-tools/cat/cat.py @@ -30,21 +30,17 @@ for filepath in args.paths: with open(filepath, "r", encoding="utf-8") as f: - content = f.read() - - lines = content.split("\n") - - for line in lines: - if args.number_all_lines: - print(f"{line_number} {line}") - line_number += 1 - - elif args.number_non_empty_lines: + for line in f: + if args.number_non_empty_lines: if line.strip() == "": - print(line) + print(line, end="") else: - print(f"{line_number} {line}") + print(f"{line_number} {line}", end="") + line_number += 1 + + elif args.number_all_lines: + print(f"{line_number} {line}", end="") line_number +=1 else: - print(line) + print(line, end="") From 4471a05a0bc6a5b80177d9e329e867d35108f0cf Mon Sep 17 00:00:00 2001 From: ike-agu Date: Wed, 21 Jan 2026 19:36:54 +0000 Subject: [PATCH 6/8] Refactor cat implementationinto functions for readability --- implement-shell-tools/cat/cat.py | 87 +++++++++++++++++++------------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py index d63825a45..46c0ed6df 100644 --- a/implement-shell-tools/cat/cat.py +++ b/implement-shell-tools/cat/cat.py @@ -1,46 +1,63 @@ import argparse -# ------------------------------------------- -# Set up the argument parser -# ------------------------------------------- -parser = argparse.ArgumentParser( - prog ="display-file-content", - description = "Implement cat command with -n and -b flag support", - ) +def parse_args(): + parser = argparse.ArgumentParser( + prog ="display-file-content", + description = "Implement cat command with -n and -b flag support", + ) -parser.add_argument("-n", "--number-all-lines", - action="store_true", - help="Number every line in the file" - ) + parser.add_argument("-n", "--number-all-lines", + action="store_true", + help="Number every line in the file" + ) -parser.add_argument("-b", "--number-non-empty-lines", - action="store_true", - help="Number non empty lines in the file" - ) + parser.add_argument("-b", "--number-non-empty-lines", + action="store_true", + help="Number non empty lines in the file" + ) -parser.add_argument("paths", nargs="+", help="File paths to process") + parser.add_argument("paths", nargs="+", help="File paths to process") -args = parser.parse_args() + args = parser.parse_args() + return args -# ------------------------------------------- -# Implement functionality -# ------------------------------------------- -line_number = 1 +def print_line(line, line_number = None): + if line_number is None: + print(line, end="") + else: + print(f"{line_number} {line}", end="") -for filepath in args.paths: +def process_line(line, args, line_number): + if args.number_non_empty_lines: + if line.strip() == "": + print_line(line) + else: + print_line(line, line_number) + line_number += 1 + + elif args.number_all_lines: + print_line(line, line_number) + line_number +=1 + + else: + print_line(line) + + return line_number + +def process_file(filepath, args, line_number): + """Read a file and print its lines. Returns updated line_number.""" with open(filepath, "r", encoding="utf-8") as f: for line in f: - if args.number_non_empty_lines: - if line.strip() == "": - print(line, end="") - else: - print(f"{line_number} {line}", end="") - line_number += 1 - - elif args.number_all_lines: - print(f"{line_number} {line}", end="") - line_number +=1 - - else: - print(line, end="") + line_number = process_line(line, args, line_number) + return line_number + +def main(): + args = parse_args() + line_number = 1 + + for filepath in args.paths: + line_number = process_file(filepath, args, line_number) + +if __name__ == "__main__": + main() From ebd5f7154a7d3621e960f110d5d73a4d09d66402 Mon Sep 17 00:00:00 2001 From: ike-agu Date: Fri, 13 Feb 2026 19:56:53 +0000 Subject: [PATCH 7/8] Refactor wc into reusable function and fix line counting to match Unix wc --- implement-shell-tools/wc/wc.py | 73 ++++++++++++++-------------------- 1 file changed, 29 insertions(+), 44 deletions(-) diff --git a/implement-shell-tools/wc/wc.py b/implement-shell-tools/wc/wc.py index 2420f5d38..6b069e20e 100644 --- a/implement-shell-tools/wc/wc.py +++ b/implement-shell-tools/wc/wc.py @@ -4,9 +4,6 @@ def main(): - # -------------------------------------------------------- - # Set up argparse - # -------------------------------------------------------- parser= argparse.ArgumentParser( prog="wc", description ="wc command implementation in python" @@ -20,74 +17,62 @@ def main(): args = parser.parse_args() - # -------------------------------------------------------- - # Ensures at least one path exists - # -------------------------------------------------------- if len(args.paths) == 0: print("wc: no file specified", file=sys.stderr) sys.exit(1) totals= {"lines": 0, "words": 0, "chars": 0} - # -------------------------------------------------------- - # Loop over each file path and process it - # -------------------------------------------------------- + no_flags = not args.l and not args.w and not args.c + width = 7 + + def format_output(counts, label): + if no_flags: + return f"{counts['lines']:> {width}}{counts['words']:>{width}}{counts['chars']:>{width}} {label}" + + parts = [] + if args.l: + parts.append(f"{counts['lines']:>{width}}") + if args.w: + parts.append(f"{counts['words']:>{width}}") + if args.c: + parts.append(f"{counts['chars']:>{width}}") + + return f"{''.join(parts)} {label}" + had_error = False + for file_path in args.paths: try: - with open(file_path, "r", encoding="utf-8") as f: - content = f.read() + with open(file_path, "rb") as f: + data = f.read() except OSError as err: print(f"wc: cannot read file'{file_path}': {err}", file=sys.stderr) + had_error = True continue - # -------------------------------------------------------- - # Count values - # -------------------------------------------------------- - line_count = len(content.split("\n")) + content = data.decode("utf-8", errors="replace") + + line_count = content.count("\n") words = [w for w in re.split(r"\s+", content) if w] word_count = len(words) + char_count = len(data) - char_count = len(content) + counts = {"lines": line_count, "words": word_count, "chars":char_count} totals["lines"] += line_count totals["words"] += word_count totals["chars"] +=char_count - # -------------------------------------------------------- - # Decide what to print based on flags - # -------------------------------------------------------- - no_flags = not args.l and not args.w and not args.c + print(format_output(counts, file_path)) - if no_flags: - print(f"{line_count} {word_count} {char_count} {file_path}") - continue - - if args.l: - print(f"{line_count} {file_path}" ) - - if args.w: - print(f"{word_count} {file_path}") - - if args.c: - print(f"{char_count} {file_path}") - - - # -------------------------------------------------------- - # Print totals if there are multiple files - # -------------------------------------------------------- if len(args.paths) > 1: no_flags = not args.l and not args.w and not args.c if no_flags: - print(f"{totals['lines']} {totals['words']} {totals['chars']} total") + print(format_output(totals, "total")) - if args.l: - print(f"{totals['lines']} total") - if args.w: - print(f"{totals['words']} total") - if args.c: - print(f"{totals['chars']} total") + sys.exit(1 if had_error else 0) if __name__ == "__main__": main() From 62b3a4b68b8d4830e44ff2f3983c51e56adfa569 Mon Sep 17 00:00:00 2001 From: ike-agu Date: Wed, 18 Feb 2026 19:41:09 +0000 Subject: [PATCH 8/8] Fix total line behavior to match wc for flagged multi-file input Print totals when multiple files are provided, even if -l, -w, or -c flags are used.This matches wc real behaviour. --- implement-shell-tools/wc/wc.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/implement-shell-tools/wc/wc.py b/implement-shell-tools/wc/wc.py index 6b069e20e..8eddcde2a 100644 --- a/implement-shell-tools/wc/wc.py +++ b/implement-shell-tools/wc/wc.py @@ -24,11 +24,11 @@ def main(): totals= {"lines": 0, "words": 0, "chars": 0} no_flags = not args.l and not args.w and not args.c - width = 7 + width = 8 def format_output(counts, label): if no_flags: - return f"{counts['lines']:> {width}}{counts['words']:>{width}}{counts['chars']:>{width}} {label}" + return f"{counts['lines']:>{width}}{counts['words']:>{width}}{counts['chars']:>{width}} {label}" parts = [] if args.l: @@ -46,7 +46,7 @@ def format_output(counts, label): with open(file_path, "rb") as f: data = f.read() except OSError as err: - print(f"wc: cannot read file'{file_path}': {err}", file=sys.stderr) + print(f"wc: cannot read file' {file_path} ': {err}", file=sys.stderr) had_error = True continue @@ -67,12 +67,9 @@ def format_output(counts, label): print(format_output(counts, file_path)) if len(args.paths) > 1: - no_flags = not args.l and not args.w and not args.c + print(format_output(totals, "total")) - if no_flags: - print(format_output(totals, "total")) - - sys.exit(1 if had_error else 0) + sys.exit(1 if had_error else 0) if __name__ == "__main__": main()