From 38c96e95be43fb1616437b271f32e57c1140423b Mon Sep 17 00:00:00 2001 From: Raihan Sharif Date: Mon, 3 Aug 2026 13:30:34 +0100 Subject: [PATCH 01/14] set up argparse with -n, -n flags --- implement-shell-tools/cat/cat.py | 14 ++++++++++++++ implement-shell-tools/wc/requirements.txt | 1 + 2 files changed, 15 insertions(+) create mode 100644 implement-shell-tools/cat/cat.py create mode 100644 implement-shell-tools/wc/requirements.txt diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py new file mode 100644 index 000000000..2a6b69988 --- /dev/null +++ b/implement-shell-tools/cat/cat.py @@ -0,0 +1,14 @@ +import argparse + +parser = argparse.ArgumentParser( + prog="a simple cat implementation", + description="cat command line tool with the -n and -b flags" +) + +parser.add_argument("-n", action="store_true", help="number all output lines") +parser.add_argument("-b", action="store_true", help="number non-empty output lines") +parser.add_argument("paths", nargs="+", help="file path or paths", ) + +args = parser.parse_args(); + +print(args) \ No newline at end of file diff --git a/implement-shell-tools/wc/requirements.txt b/implement-shell-tools/wc/requirements.txt new file mode 100644 index 000000000..e9c6824d6 --- /dev/null +++ b/implement-shell-tools/wc/requirements.txt @@ -0,0 +1 @@ +argparse \ No newline at end of file From 0591f3f61428114bcf885c4905023cfeb2824aa5 Mon Sep 17 00:00:00 2001 From: Raihan Sharif Date: Mon, 3 Aug 2026 13:46:54 +0100 Subject: [PATCH 02/14] handle errors when reading file --- implement-shell-tools/cat/cat.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py index 2a6b69988..2335992fb 100644 --- a/implement-shell-tools/cat/cat.py +++ b/implement-shell-tools/cat/cat.py @@ -1,4 +1,5 @@ import argparse +import sys parser = argparse.ArgumentParser( prog="a simple cat implementation", @@ -11,4 +12,25 @@ args = parser.parse_args(); -print(args) \ No newline at end of file +# cat returns different error messages depending on the reason the path could be read +def cat_file(path): + try: + with open(path, "r",) as f: + content = f.read() + except FileNotFoundError: + print(f"cat: {path}: No such file or directory", file=sys.stderr) + return + except IsADirectoryError: + print(f"cat: {path}: Is a directory", file=sys.stderr) + return + except PermissionError: + print(f"cat: {path}: Permission denied", file=sys.stderr) + return + + print(content) + + + +for path in args.paths: + line_num = 1 + cat_file(path) \ No newline at end of file From 6020eef11e6daee88e9631e621a729e04ffe9e06 Mon Sep 17 00:00:00 2001 From: Raihan Sharif Date: Mon, 3 Aug 2026 13:55:46 +0100 Subject: [PATCH 03/14] return exit code 1 for any failed file read --- implement-shell-tools/cat/cat.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py index 2335992fb..32f66cfdd 100644 --- a/implement-shell-tools/cat/cat.py +++ b/implement-shell-tools/cat/cat.py @@ -19,18 +19,28 @@ def cat_file(path): content = f.read() except FileNotFoundError: print(f"cat: {path}: No such file or directory", file=sys.stderr) - return + return False except IsADirectoryError: print(f"cat: {path}: Is a directory", file=sys.stderr) - return + return False except PermissionError: print(f"cat: {path}: Permission denied", file=sys.stderr) - return + return False print(content) +# cat exits with error code 1 if any file read fails +file_error = False + for path in args.paths: line_num = 1 - cat_file(path) \ No newline at end of file + is_success = cat_file(path) + + if not is_success: + file_error = True + +# if at any point, file reading failed file error is set to True, +# and program exist with code 1 after all tasks completed +sys.exit(1 if file_error else 0) \ No newline at end of file From 108fef7cbe1ae35067cf1bc4db809f3dc238bde3 Mon Sep 17 00:00:00 2001 From: Raihan Sharif Date: Mon, 3 Aug 2026 14:19:19 +0100 Subject: [PATCH 04/14] seperate out file read and formatting --- implement-shell-tools/cat/cat.py | 64 ++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py index 32f66cfdd..8523a7301 100644 --- a/implement-shell-tools/cat/cat.py +++ b/implement-shell-tools/cat/cat.py @@ -13,34 +13,60 @@ args = parser.parse_args(); # cat returns different error messages depending on the reason the path could be read -def cat_file(path): +def read_file(path): + """Returns (content, error_message). error_message is None on success""" try: with open(path, "r",) as f: - content = f.read() + return f.read(), None except FileNotFoundError: - print(f"cat: {path}: No such file or directory", file=sys.stderr) - return False + return None, f"cat: {path}: No such file or directory" except IsADirectoryError: - print(f"cat: {path}: Is a directory", file=sys.stderr) - return False + return None, f"cat: {path}: Is a directory" except PermissionError: - print(f"cat: {path}: Permission denied", file=sys.stderr) - return False + return None, f"cat: {path}: Permission denied" - print(content) +# -b (number the non-empty lines) takes priority over -n (number all lines) +# if both are present +def format_lines(lines, number_all=False, number_non_empty=False): + """Returns a list of formatted output lines""" + output = [] + + if number_non_empty: + line_num = 0 + for line in lines: + if line == "": + output.append("") + else: + line_num += 1 + # {line_num:6} right justied number, length of at least 6 + # {some_str:6} left justifed string, length fo at least 6 + output.append(f"{line_num:6}\t{line}") + elif number_all: + for i, line in enumerate(lines, start=1): + output.append(f"{i:6}\t{line}") + else: + output = lines -# cat exits with error code 1 if any file read fails -file_error = False +# TODO: runner function to call read_file, and feed it into formatLines, then print -for path in args.paths: - line_num = 1 - is_success = cat_file(path) +def main(): + # cat exits with error code 1 if any file read fails + file_error = False - if not is_success: - file_error = True + for path in args.paths: + line_num = 1 + is_success = cat_file(path) -# if at any point, file reading failed file error is set to True, -# and program exist with code 1 after all tasks completed -sys.exit(1 if file_error else 0) \ No newline at end of file + if not is_success: + file_error = True + + # if at any point, file reading failed file error is set to True, + # and program exist with code 1 after all tasks completed + sys.exit(1 if file_error else 0) + +# ensures that main only runs when this file/module is directly executed +# not when it is imported, for example, for automated tests +if __name__ == "__main__": + main() \ No newline at end of file From 0c5aeb2d6be357cba8cb009176733e284cd081f6 Mon Sep 17 00:00:00 2001 From: Raihan Sharif Date: Mon, 3 Aug 2026 14:41:49 +0100 Subject: [PATCH 05/14] cat function to cat the formatted lines --- implement-shell-tools/cat/cat.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py index 8523a7301..b185e2471 100644 --- a/implement-shell-tools/cat/cat.py +++ b/implement-shell-tools/cat/cat.py @@ -28,11 +28,11 @@ def read_file(path): # -b (number the non-empty lines) takes priority over -n (number all lines) # if both are present -def format_lines(lines, number_all=False, number_non_empty=False): +def format_lines(lines, number_all=False, number_nonempty=False): """Returns a list of formatted output lines""" output = [] - if number_non_empty: + if number_nonempty: line_num = 0 for line in lines: if line == "": @@ -48,8 +48,29 @@ def format_lines(lines, number_all=False, number_non_empty=False): else: output = lines + return output + # TODO: runner function to call read_file, and feed it into formatLines, then print +def cat_file(path, number_all=False, number_nonempty=False): + """ + Calls read_file -> format_lines -> prints formatted line. + Returns True if file read successfully, else returns False + + If failed to read file, prints error to stderr + """ + content, error = read_file(path) + if (error): + print(error, file=sys.stderr) + return False + + # splitlines automatically trims trailing empty lines + lines = content.splitlines() + for line in format_lines(lines, number_all, number_nonempty): + print(line) + + return True + def main(): # cat exits with error code 1 if any file read fails @@ -57,7 +78,7 @@ def main(): for path in args.paths: line_num = 1 - is_success = cat_file(path) + is_success = cat_file(path, args.n, args.b) if not is_success: file_error = True From 9361dde008e9ed7e78f0bb76f0b1a0df7cdfb672 Mon Sep 17 00:00:00 2001 From: Raihan Sharif Date: Tue, 4 Aug 2026 22:47:46 +0100 Subject: [PATCH 06/14] remove stray semicolon --- implement-shell-tools/cat/cat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py index b185e2471..57ae39682 100644 --- a/implement-shell-tools/cat/cat.py +++ b/implement-shell-tools/cat/cat.py @@ -10,7 +10,7 @@ parser.add_argument("-b", action="store_true", help="number non-empty output lines") parser.add_argument("paths", nargs="+", help="file path or paths", ) -args = parser.parse_args(); +args = parser.parse_args() # cat returns different error messages depending on the reason the path could be read def read_file(path): From 36177423416a70edb0245cb839b1bca06695974f Mon Sep 17 00:00:00 2001 From: Raihan Sharif Date: Tue, 4 Aug 2026 22:57:44 +0100 Subject: [PATCH 07/14] set up parser --- implement-shell-tools/ls/ls.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 implement-shell-tools/ls/ls.py diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py new file mode 100644 index 000000000..40ab3a1ac --- /dev/null +++ b/implement-shell-tools/ls/ls.py @@ -0,0 +1,23 @@ +import argparse +import sys + +parser = argparse.ArgumentParser( + prog="a simple version of ls", + description="ls command line tool which can accept 0 or more arguements" \ + "and take -a and -1 flags") + +parser.add_argument("-a", action="store_true", help="show all files, including dot files") + +# can't store as an attribute of Namespace object, because 1 is not a valid python identifier +# but can store under the name given in the dest argument. When working with this +# parser, look for "opt_one", not "1". +parser.add_argument("-1", dest="opt_one", action="store_true", help="show one file/directory name per line") + +# takes 0 more arguments, if none are given, sets "." as default value +parser.add_argument("paths", nargs="*", help="file/directory path(s) to display", default=".") + +args = parser.parse_args() + +print(args) + +# def getDirectoryEntries(path, aFlag=args.a): From b28fa81af78f51ae003fd743ee1ecdaf2136e3e3 Mon Sep 17 00:00:00 2001 From: Raihan Sharif Date: Tue, 4 Aug 2026 23:36:46 +0100 Subject: [PATCH 08/14] read files/folders in a given folder --- implement-shell-tools/ls/ls.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py index 40ab3a1ac..c51152cd3 100644 --- a/implement-shell-tools/ls/ls.py +++ b/implement-shell-tools/ls/ls.py @@ -1,5 +1,6 @@ import argparse import sys +import os parser = argparse.ArgumentParser( prog="a simple version of ls", @@ -18,6 +19,16 @@ args = parser.parse_args() -print(args) -# def getDirectoryEntries(path, aFlag=args.a): +def getDirectoryEntries(path, aFlag=args.a): + # warning: listdir() prints current directory by default + entries = os.listdir(path) + entries = [".", ".."] + entries + entries.sort() + + if (not args.a): + entries = [entry for entry in entries if not entry.startswith(".")] + return entries + +for path in args.paths: + print(getDirectoryEntries(path)) From 4f62b37e13fad7e42421579589304ea235b01e0d Mon Sep 17 00:00:00 2001 From: Raihan Sharif Date: Wed, 5 Aug 2026 00:02:13 +0100 Subject: [PATCH 09/14] function to format and print a single directory content --- implement-shell-tools/ls/ls.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py index c51152cd3..3ca92d2f6 100644 --- a/implement-shell-tools/ls/ls.py +++ b/implement-shell-tools/ls/ls.py @@ -28,7 +28,19 @@ def getDirectoryEntries(path, aFlag=args.a): if (not args.a): entries = [entry for entry in entries if not entry.startswith(".")] + return entries +def printEntries(entries, onePerLineFlag = args.opt_one): + if (onePerLineFlag): + for entry in entries: + print(entry) + else: + for i in range(len(entries)-1): + print(f"{entries[i]}\t", end="") + print(entries[-1]) + + + for path in args.paths: - print(getDirectoryEntries(path)) + printEntries(getDirectoryEntries(path)) From 2ad95a6a25524a0c4f609f5f46da4f8d8a63be72 Mon Sep 17 00:00:00 2001 From: Raihan Sharif Date: Wed, 5 Aug 2026 00:17:37 +0100 Subject: [PATCH 10/14] snake case function names --- implement-shell-tools/ls/ls.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py index 3ca92d2f6..1afba00d5 100644 --- a/implement-shell-tools/ls/ls.py +++ b/implement-shell-tools/ls/ls.py @@ -20,7 +20,7 @@ args = parser.parse_args() -def getDirectoryEntries(path, aFlag=args.a): +def get_dir_entries(path, aFlag=args.a): # warning: listdir() prints current directory by default entries = os.listdir(path) entries = [".", ".."] + entries @@ -28,10 +28,10 @@ def getDirectoryEntries(path, aFlag=args.a): if (not args.a): entries = [entry for entry in entries if not entry.startswith(".")] - + return entries -def printEntries(entries, onePerLineFlag = args.opt_one): +def print_entries(entries, onePerLineFlag = args.opt_one): if (onePerLineFlag): for entry in entries: print(entry) @@ -41,6 +41,5 @@ def printEntries(entries, onePerLineFlag = args.opt_one): print(entries[-1]) - -for path in args.paths: - printEntries(getDirectoryEntries(path)) +# for path in args.paths: +# printEntries(getDirectoryEntries(path)) From 36c79a6e3793977222b9166e233e0ba92a508fd1 Mon Sep 17 00:00:00 2001 From: Raihan Sharif Date: Wed, 5 Aug 2026 00:35:02 +0100 Subject: [PATCH 11/14] print multiple files and folders --- implement-shell-tools/ls/ls.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py index 1afba00d5..0a614e6cf 100644 --- a/implement-shell-tools/ls/ls.py +++ b/implement-shell-tools/ls/ls.py @@ -31,15 +31,32 @@ def get_dir_entries(path, aFlag=args.a): return entries + def print_entries(entries, onePerLineFlag = args.opt_one): if (onePerLineFlag): for entry in entries: print(entry) - else: + elif (len(entries) > 0): for i in range(len(entries)-1): print(f"{entries[i]}\t", end="") - print(entries[-1]) + print(f"{entries[-1]}") + + +def main(): + # file and directory paths are processed separately + file_args = [arg for arg in args.paths if os.path.isfile(arg)] + dir_args = [arg for arg in args.paths if os.path.isdir(arg)] + + if (len(file_args) > 0): + print_entries(file_args) + + for index, path in enumerate(dir_args, start=0): + if (len(args.paths) > 1): + if (index > 0 or len(file_args) > 0): + print("") + print(f"{path}:") + print_entries(get_dir_entries(path)) -# for path in args.paths: -# printEntries(getDirectoryEntries(path)) +if __name__ == "__main__": + main() \ No newline at end of file From 34e629a847a706d8a48949655a8f95e0d0780d9e Mon Sep 17 00:00:00 2001 From: Raihan Sharif Date: Wed, 5 Aug 2026 00:52:57 +0100 Subject: [PATCH 12/14] add very basic error handling all paths that are not accessible as a file/folder throw same error --- implement-shell-tools/ls/ls.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py index 0a614e6cf..d1dab65ad 100644 --- a/implement-shell-tools/ls/ls.py +++ b/implement-shell-tools/ls/ls.py @@ -44,8 +44,26 @@ def print_entries(entries, onePerLineFlag = args.opt_one): def main(): # file and directory paths are processed separately - file_args = [arg for arg in args.paths if os.path.isfile(arg)] - dir_args = [arg for arg in args.paths if os.path.isdir(arg)] + # file_args = [arg for arg in args.paths if os.path.isfile(arg)] + # dir_args = [arg for arg in args.paths if os.path.isdir(arg)] + + file_args = [] + dir_args = [] + invalid_args = [] + + # this is a simplication, it groups all errors under "invalid file" + # real ls would have different messages things like permission denied + # also bad because it makes two syscalls + for arg in args.paths: + if (os.path.isfile(arg)): + file_args.append(arg) + elif (os.path.isdir(arg)): + dir_args.append(arg) + else: + invalid_args.append(arg) + + for arg in invalid_args: + print(f"ls: {arg}: No such file or directory", file=sys.stderr) if (len(file_args) > 0): print_entries(file_args) From 2d9a74081bdb5e4d54c28fa95244850a4b4d1615 Mon Sep 17 00:00:00 2001 From: Raihan Sharif Date: Wed, 5 Aug 2026 12:01:38 +0100 Subject: [PATCH 13/14] add a requirements file for argparse import --- implement-shell-tools/ls/requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 implement-shell-tools/ls/requirements.txt diff --git a/implement-shell-tools/ls/requirements.txt b/implement-shell-tools/ls/requirements.txt new file mode 100644 index 000000000..e9c6824d6 --- /dev/null +++ b/implement-shell-tools/ls/requirements.txt @@ -0,0 +1 @@ +argparse \ No newline at end of file From ef611c61a9569bbd465e264599f819febd78d134 Mon Sep 17 00:00:00 2001 From: Raihan Sharif Date: Wed, 5 Aug 2026 12:29:04 +0100 Subject: [PATCH 14/14] parser setup --- implement-shell-tools/wc/wc.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 implement-shell-tools/wc/wc.py diff --git a/implement-shell-tools/wc/wc.py b/implement-shell-tools/wc/wc.py new file mode 100644 index 000000000..de9f9c946 --- /dev/null +++ b/implement-shell-tools/wc/wc.py @@ -0,0 +1,15 @@ +import argparse +import sys +import os + +parser = argparse.ArgumentParser( + prog="a simple version of wc. Takes in one or more files.", + description="ls command line tool which can accept -l -w -c cflags") + +parser.add_argument("-l", action="store_true", help="show line count") +parser.add_argument("-w", action="store_true", help="show word count") +parser.add_argument("-c", action="store_true", help="show byte count") + +parser.add_argument("paths", nargs="*", help="file(s) for which to show data") + +args = parser.parse_args() \ No newline at end of file