From 5b484f155ebca85398d4005ff8341c5a0029f337 Mon Sep 17 00:00:00 2001 From: eyuell21 Date: Fri, 1 Aug 2025 22:47:42 +0100 Subject: [PATCH 1/6] Completed implementing shell tools in python. --- implement-shell-tools/cat/my_cat.py | 52 +++++++++++++++++++ implement-shell-tools/ls/my_ls.py | 53 ++++++++++++++++++++ implement-shell-tools/wc/my_wc.py | 78 +++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 implement-shell-tools/cat/my_cat.py create mode 100644 implement-shell-tools/ls/my_ls.py create mode 100644 implement-shell-tools/wc/my_wc.py diff --git a/implement-shell-tools/cat/my_cat.py b/implement-shell-tools/cat/my_cat.py new file mode 100644 index 000000000..f19fc2b2a --- /dev/null +++ b/implement-shell-tools/cat/my_cat.py @@ -0,0 +1,52 @@ +import argparse +import sys + +parser = argparse.ArgumentParser( + description="Reads and prints one or more files, optionally numbering lines continuously" +) +parser.add_argument("paths", nargs='+', help="One or more file paths") +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") + +args = parser.parse_args() + +file_paths = args.paths +number_nonblank = args.number_nonblank +number_all = args.number and not number_nonblank + +line_number = 1 + +for path in file_paths: + try: + with open(path, 'r', encoding='utf-8') as f: + lines = f.readlines() + + if number_nonblank: + nonblank_lines = [line for line in lines if line.strip()] + max_digits = len(str(len(nonblank_lines))) + + for line in lines: + if line.strip() == "": + print() + else: + num_str = str(line_number).rjust(max_digits) + print(f"{num_str}\t{line.rstrip()}") + line_number += 1 + + elif number_all: + max_digits = len(str(len(lines) * len(file_paths))) + + for line in lines: + num_str = str(line_number).rjust(max_digits) + print(f"{num_str}\t{line.rstrip()}") + line_number += 1 + + else: + for line in lines: + print(line, end='') + if not lines[-1].endswith('\n'): + print() + + except Exception as e: + print(f'Error reading file "{path}": {e}', file=sys.stderr) + sys.exit(1) diff --git a/implement-shell-tools/ls/my_ls.py b/implement-shell-tools/ls/my_ls.py new file mode 100644 index 000000000..189d05e83 --- /dev/null +++ b/implement-shell-tools/ls/my_ls.py @@ -0,0 +1,53 @@ +import os +import sys +import stat +import argparse + +parser = argparse.ArgumentParser( + description="List files in a directory (simplified ls implementation)" +) +parser.add_argument("paths", nargs="*", default=["."], help="One or more file or directory paths") +parser.add_argument("-l", "--longList", action="store_true", help="Long listing format") +parser.add_argument("-a", "--all", action="store_true", help="Include hidden files") + +args = parser.parse_args() + +file_paths = args.paths +show_long = args.longList +show_all = args.all + +def format_permissions(mode): + return stat.filemode(mode) + +for input_path in file_paths: + try: + if not os.path.exists(input_path): + raise FileNotFoundError(f'No such file or directory: {input_path}') + + if os.path.isfile(input_path): + if show_long: + file_stat = os.stat(input_path) + perms = format_permissions(file_stat.st_mode) + size = str(file_stat.st_size).rjust(6) + print(f"{perms} {size} {input_path}") + else: + print(input_path) + + elif os.path.isdir(input_path): + entries = os.listdir(input_path) + if not show_all: + entries = [e for e in entries if not e.startswith(".")] + + for entry in entries: + full_path = os.path.join(input_path, entry) + entry_stat = os.stat(full_path) + if show_long: + perms = format_permissions(entry_stat.st_mode) + size = str(entry_stat.st_size).rjust(6) + print(f"{perms} {size} {entry}") + else: + print(entry) + + except Exception as e: + print(f'Error reading "{input_path}": {e}', file=sys.stderr) + sys.exit(1) diff --git a/implement-shell-tools/wc/my_wc.py b/implement-shell-tools/wc/my_wc.py new file mode 100644 index 000000000..ffb6fb75d --- /dev/null +++ b/implement-shell-tools/wc/my_wc.py @@ -0,0 +1,78 @@ +import os +import sys +import argparse + +# CLI argument parsing +parser = argparse.ArgumentParser(description="Simplified implementation of wc") +parser.add_argument("paths", nargs="*", default=["."], help="One or more file or directory paths") +parser.add_argument("-l", "--line", action="store_true", help="Count lines") +parser.add_argument("-w", "--word", action="store_true", help="Count words") +parser.add_argument("-c", "--character", action="store_true", help="Count characters") + +args = parser.parse_args() +file_paths = args.paths + +# Fallback: if no options passed, show all +show_line = args.line +show_word = args.word +show_char = args.character +show_all = not (show_line or show_word or show_char) + +# Count content in a string +def count_content(content): + lines = content.splitlines() + words = content.strip().split() + characters = len(content) + return len(lines), len(words), characters + +# Totals for multiple files +total = { + "lines": 0, + "words": 0, + "characters": 0 +} + +file_count = 0 + +for input_path in file_paths: + try: + if os.path.isdir(input_path): + print(f"{input_path} is a directory. Skipping.") + continue + + with open(input_path, "r", encoding="utf-8") as f: + content = f.read() + + lines, words, characters = count_content(content) + + total["lines"] += lines + total["words"] += words + total["characters"] += characters + file_count += 1 + + # Prepare output per file + output_parts = [] + if show_line or show_all: + output_parts.append(f"{lines:8}") + if show_word or show_all: + output_parts.append(f"{words:8}") + if show_char or show_all: + output_parts.append(f"{characters:8}") + + output_parts.append(input_path) + print(" ".join(output_parts)) + + except Exception as e: + print(f'Error reading "{input_path}": {e}', file=sys.stderr) + +# Print totals if more than one file processed +if file_count > 1: + output_parts = [] + if show_line or show_all: + output_parts.append(f"{total['lines']:8}") + if show_word or show_all: + output_parts.append(f"{total['words']:8}") + if show_char or show_all: + output_parts.append(f"{total['characters']:8}") + output_parts.append("total") + print(" ".join(output_parts)) From a18c426cf9a02b0b5a1d576a05ae89870ab93d69 Mon Sep 17 00:00:00 2001 From: eyuell21 Date: Tue, 5 Aug 2025 09:24:43 +0100 Subject: [PATCH 2/6] added .venv files to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3c3629e64..3e5cc695e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.venv/ \ No newline at end of file From fded319de49792ebf0614925dc05919a3674ad5d Mon Sep 17 00:00:00 2001 From: eyuell21 Date: Wed, 15 Oct 2025 10:46:23 +0100 Subject: [PATCH 3/6] Fixes according to review --- implement-shell-tools/cat/my_cat.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/implement-shell-tools/cat/my_cat.py b/implement-shell-tools/cat/my_cat.py index f19fc2b2a..b5227615a 100644 --- a/implement-shell-tools/cat/my_cat.py +++ b/implement-shell-tools/cat/my_cat.py @@ -14,17 +14,35 @@ number_nonblank = args.number_nonblank number_all = args.number and not number_nonblank + +total_lines = 0 +total_nonblank_lines = 0 + +for path in file_paths: + try: + with open(path, 'r', encoding='utf-8') as f: + lines = f.readlines() + total_lines += len(lines) + total_nonblank_lines += sum(1 for line in lines if line.strip()) + except Exception as e: + print(f'Error reading file "{path}": {e}', file=sys.stderr) + sys.exit(1) + + +if number_nonblank: + max_digits = len(str(total_nonblank_lines)) +elif number_all: + max_digits = len(str(total_lines)) + line_number = 1 + for path in file_paths: try: with open(path, 'r', encoding='utf-8') as f: lines = f.readlines() if number_nonblank: - nonblank_lines = [line for line in lines if line.strip()] - max_digits = len(str(len(nonblank_lines))) - for line in lines: if line.strip() == "": print() @@ -34,8 +52,6 @@ line_number += 1 elif number_all: - max_digits = len(str(len(lines) * len(file_paths))) - for line in lines: num_str = str(line_number).rjust(max_digits) print(f"{num_str}\t{line.rstrip()}") @@ -44,7 +60,7 @@ else: for line in lines: print(line, end='') - if not lines[-1].endswith('\n'): + if lines and not lines[-1].endswith('\n'): print() except Exception as e: From 53a9aa923c903854cfd953074b3febd75778fded Mon Sep 17 00:00:00 2001 From: eyuell21 Date: Wed, 15 Oct 2025 10:53:22 +0100 Subject: [PATCH 4/6] Add support for -1 (single column) flag to ls implementation --- implement-shell-tools/ls/my_ls.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/implement-shell-tools/ls/my_ls.py b/implement-shell-tools/ls/my_ls.py index 189d05e83..341971e77 100644 --- a/implement-shell-tools/ls/my_ls.py +++ b/implement-shell-tools/ls/my_ls.py @@ -10,11 +10,13 @@ parser.add_argument("-l", "--longList", action="store_true", help="Long listing format") parser.add_argument("-a", "--all", action="store_true", help="Include hidden files") +parser.add_argument("-1", "--singleColumn", action="store_true", help="List one file per line") args = parser.parse_args() file_paths = args.paths show_long = args.longList show_all = args.all +force_single_column = args.singleColumn def format_permissions(mode): return stat.filemode(mode) @@ -38,6 +40,9 @@ def format_permissions(mode): if not show_all: entries = [e for e in entries if not e.startswith(".")] + # Optional: sort entries for consistent output + entries.sort() # (optional for predictable output) + for entry in entries: full_path = os.path.join(input_path, entry) entry_stat = os.stat(full_path) @@ -46,7 +51,7 @@ def format_permissions(mode): size = str(entry_stat.st_size).rjust(6) print(f"{perms} {size} {entry}") else: - print(entry) + print(entry) except Exception as e: print(f'Error reading "{input_path}": {e}', file=sys.stderr) From 5981cd76efab948d9043f8cc1a3b13e761ef3454 Mon Sep 17 00:00:00 2001 From: eyuell21 Date: Wed, 15 Oct 2025 10:58:36 +0100 Subject: [PATCH 5/6] Added a helper function to Format the output --- implement-shell-tools/wc/my_wc.py | 56 +++++++++++++------------------ 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/implement-shell-tools/wc/my_wc.py b/implement-shell-tools/wc/my_wc.py index ffb6fb75d..d4badc094 100644 --- a/implement-shell-tools/wc/my_wc.py +++ b/implement-shell-tools/wc/my_wc.py @@ -10,13 +10,9 @@ parser.add_argument("-c", "--character", action="store_true", help="Count characters") args = parser.parse_args() -file_paths = args.paths -# Fallback: if no options passed, show all -show_line = args.line -show_word = args.word -show_char = args.character -show_all = not (show_line or show_word or show_char) +# If no flags are set, show all +show_all = not (args.line or args.word or args.character) # Count content in a string def count_content(content): @@ -25,16 +21,23 @@ def count_content(content): characters = len(content) return len(lines), len(words), characters -# Totals for multiple files -total = { - "lines": 0, - "words": 0, - "characters": 0 -} +# Format output line based on flags +def format_output(lines, words, chars, label): + parts = [] + if args.line or show_all: + parts.append(f"{lines:8}") + if args.word or show_all: + parts.append(f"{words:8}") + if args.character or show_all: + parts.append(f"{chars:8}") + parts.append(label) + return " ".join(parts) +# Totals for multiple files +total = {"lines": 0, "words": 0, "characters": 0} file_count = 0 -for input_path in file_paths: +for input_path in args.paths: try: if os.path.isdir(input_path): print(f"{input_path} is a directory. Skipping.") @@ -50,29 +53,16 @@ def count_content(content): total["characters"] += characters file_count += 1 - # Prepare output per file - output_parts = [] - if show_line or show_all: - output_parts.append(f"{lines:8}") - if show_word or show_all: - output_parts.append(f"{words:8}") - if show_char or show_all: - output_parts.append(f"{characters:8}") - - output_parts.append(input_path) - print(" ".join(output_parts)) + print(format_output(lines, words, characters, input_path)) except Exception as e: print(f'Error reading "{input_path}": {e}', file=sys.stderr) # Print totals if more than one file processed if file_count > 1: - output_parts = [] - if show_line or show_all: - output_parts.append(f"{total['lines']:8}") - if show_word or show_all: - output_parts.append(f"{total['words']:8}") - if show_char or show_all: - output_parts.append(f"{total['characters']:8}") - output_parts.append("total") - print(" ".join(output_parts)) + print(format_output( + total["lines"], + total["words"], + total["characters"], + "total" + )) From b47f75e7aacf929659a90d4f8c7c3c72ff9f5f18 Mon Sep 17 00:00:00 2001 From: eyuell21 Date: Wed, 4 Mar 2026 15:23:15 +0000 Subject: [PATCH 6/6] Implement single column output for directory entries --- implement-shell-tools/ls/my_ls.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/implement-shell-tools/ls/my_ls.py b/implement-shell-tools/ls/my_ls.py index 341971e77..b88424746 100644 --- a/implement-shell-tools/ls/my_ls.py +++ b/implement-shell-tools/ls/my_ls.py @@ -50,6 +50,8 @@ def format_permissions(mode): perms = format_permissions(entry_stat.st_mode) size = str(entry_stat.st_size).rjust(6) print(f"{perms} {size} {entry}") + elif force_single_column: + print(entry) else: print(entry)