From 3a31fbfcb8358f0e84af85cb2c25cc3b92a4d281 Mon Sep 17 00:00:00 2001 From: Luke-Manyamazi Date: Fri, 1 Aug 2025 05:15:58 +0200 Subject: [PATCH 01/12] added python the code for the cat command --- implement-shell-tools/cat/cat.py | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 implement-shell-tools/cat/cat.py diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py new file mode 100644 index 000000000..94b56ccb9 --- /dev/null +++ b/implement-shell-tools/cat/cat.py @@ -0,0 +1,38 @@ +import glob +import argparse + +def cat(filepath, n=False, b=False, line_counter=None): + try: + with open(filepath) as f: + for line in f: + if b: + if line.strip(): + print(f"{line_counter[0]:6}\t{line}", end='') + line_counter[0] += 1 + else: + print(line, end='') + elif n: + print(f"{line_counter[0]:6}\t{line}", end='') + line_counter[0] += 1 + else: + print(line, end='') + except FileNotFoundError: + print(f"cat: {filepath}: No such file or directory") + +def main(): + parser = argparse.ArgumentParser(description = "Concatenate files and print on the standard output.") + 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('files', nargs='+', help='files to concatenate') + args = parser.parse_args() + + files = [] + for pattern in args.files: + files.extend(glob.glob(pattern) or [pattern]) + + line_counter = [1] + for file in sorted(files): + cat(file, args.n, args.b, line_counter) + +if __name__ == "__main__": + main() \ No newline at end of file From d44366990a40765336038e8cbe6e10cdb3da109e Mon Sep 17 00:00:00 2001 From: Luke-Manyamazi Date: Fri, 1 Aug 2025 05:36:29 +0200 Subject: [PATCH 02/12] added python code for the ls command --- implement-shell-tools/ls/ls.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 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..2f4204bb5 --- /dev/null +++ b/implement-shell-tools/ls/ls.py @@ -0,0 +1,28 @@ +import os +import argparse + +def ls(path='.', one_column=False, show_hidden=False): + try: + files = os.listdir(path) + if not show_hidden: + files = [f for f in files if not f.startswith('.')] + files.sort() + + if one_column: + print(*files, sep='\n') + else: + print(*files) + except FileNotFoundError: + print(f"ls: cannot access '{path}': No such file or directory") + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-1', action='store_true', help='list one file per line') + parser.add_argument('-a', action='store_true', help='show hidden files') + parser.add_argument('path', nargs='?', default='.', help='directory to list') + args = parser.parse_args() + + ls(args.path, args.__dict__['1'], args.a) + +if __name__ == "__main__": + main() \ No newline at end of file From 1900a289319bc3d463c83f006576a2cedcf72956 Mon Sep 17 00:00:00 2001 From: Luke-Manyamazi Date: Sat, 2 Aug 2025 15:33:45 +0200 Subject: [PATCH 03/12] added a destination to handel the -1 as a string --- implement-shell-tools/ls/ls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py index 2f4204bb5..d7499b3a3 100644 --- a/implement-shell-tools/ls/ls.py +++ b/implement-shell-tools/ls/ls.py @@ -17,12 +17,12 @@ def ls(path='.', one_column=False, show_hidden=False): def main(): parser = argparse.ArgumentParser() - parser.add_argument('-1', action='store_true', help='list one file per line') + parser.add_argument('-1', dest='one_column', action='store_true', help='list one file per line') parser.add_argument('-a', action='store_true', help='show hidden files') parser.add_argument('path', nargs='?', default='.', help='directory to list') args = parser.parse_args() - ls(args.path, args.__dict__['1'], args.a) + ls(args.path, args.one_column, args.a) if __name__ == "__main__": main() \ No newline at end of file From ff86f10f0265a92313d00b3f1f0e012ebb3e86b5 Mon Sep 17 00:00:00 2001 From: Luke-Manyamazi Date: Tue, 5 Aug 2025 15:58:11 +0200 Subject: [PATCH 04/12] added python code for the wc exercise --- implement-shell-tools/wc/wc.py | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 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..a658c87f1 --- /dev/null +++ b/implement-shell-tools/wc/wc.py @@ -0,0 +1,37 @@ +import argparse + +def wc(path, count_lines, count_words, count_bytes): + try: + with open(path, 'r') as f: + content = f.read() + lines = content.splitlines() + words = content.split() + bytes_ = len(content.encode('utf-8')) + + parts = [] + if count_lines: parts.append(str(len(lines))) + if count_words: parts.append(str(len(words))) + if count_bytes: parts.append(str(bytes_)) + + if not parts: + parts = [str(len(lines)), str(len(words)), str(bytes_)] + print(' '.join(parts), path) + + except FileNotFoundError: + print(f"wc: {path}: No such file or directory") + except IsADirectoryError: + print(f"wc: {path}: Is a directory") + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-l', action='store_true', help='Count lines') + parser.add_argument('-w', action='store_true', help='Count words') + parser.add_argument('-c', action='store_true', help='Count bytes') + parser.add_argument('paths', nargs='+', help='Files to count') + args = parser.parse_args() + + for path in args.paths: + wc(path, args.l, args.w, args.c) + +if __name__ == "__main__": + main() \ No newline at end of file From fcac82c47e34fefba885d896876aeab526ea5af2 Mon Sep 17 00:00:00 2001 From: Luke Manyamazi Date: Mon, 13 Oct 2025 20:35:11 +0200 Subject: [PATCH 05/12] replace -n/-b flags with Numbering Enum --- implement-shell-tools/cat/cat.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py index 94b56ccb9..4eb2eab08 100644 --- a/implement-shell-tools/cat/cat.py +++ b/implement-shell-tools/cat/cat.py @@ -1,17 +1,23 @@ import glob import argparse +from enum import Enum -def cat(filepath, n=False, b=False, line_counter=None): +class Numbering(Enum): + NONE = 0 + ALL = 1 + NONEMPTY = 2 + +def cat(filepath, numbering=Numbering.NONE, line_counter=None): try: with open(filepath) as f: for line in f: - if b: + if numbering == Numbering.NONEMPTY: if line.strip(): print(f"{line_counter[0]:6}\t{line}", end='') line_counter[0] += 1 else: print(line, end='') - elif n: + elif numbering == Numbering.ALL: print(f"{line_counter[0]:6}\t{line}", end='') line_counter[0] += 1 else: @@ -20,7 +26,7 @@ def cat(filepath, n=False, b=False, line_counter=None): print(f"cat: {filepath}: No such file or directory") def main(): - parser = argparse.ArgumentParser(description = "Concatenate files and print on the standard output.") + parser = argparse.ArgumentParser(description="Concatenate files and print on the standard output.") 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('files', nargs='+', help='files to concatenate') @@ -31,8 +37,19 @@ def main(): files.extend(glob.glob(pattern) or [pattern]) line_counter = [1] + + # Determine numbering mode (mutually exclusive) + if args.n and args.b: + parser.error("options -n and -b are mutually exclusive") + elif args.n: + numbering = Numbering.ALL + elif args.b: + numbering = Numbering.NONEMPTY + else: + numbering = Numbering.NONE + for file in sorted(files): - cat(file, args.n, args.b, line_counter) + cat(file, numbering=numbering, line_counter=line_counter) if __name__ == "__main__": - main() \ No newline at end of file + main() From 5476f45a6737a8ec24be31d617bc7ef5ef810f26 Mon Sep 17 00:00:00 2001 From: Luke Manyamazi Date: Mon, 13 Oct 2025 21:16:03 +0200 Subject: [PATCH 06/12] refactored and corrected code to meet and answer the asked questions for all the files --- implement-shell-tools/cat/cat.py | 27 ++++++++++++--------------- implement-shell-tools/ls/ls.py | 7 +++++-- implement-shell-tools/wc/wc.py | 29 +++++++++++++++++++++++++---- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py index 4eb2eab08..58dccaee6 100644 --- a/implement-shell-tools/cat/cat.py +++ b/implement-shell-tools/cat/cat.py @@ -1,4 +1,3 @@ -import glob import argparse from enum import Enum @@ -7,23 +6,27 @@ class Numbering(Enum): ALL = 1 NONEMPTY = 2 -def cat(filepath, numbering=Numbering.NONE, line_counter=None): +def print_numbered_line(line, line_number, pad=6): + print(f"{line_number:{pad}}\t{line}", end='') + return line_number + 1 + +def cat(filepath, numbering, start_line): + line_number = start_line try: with open(filepath) as f: for line in f: if numbering == Numbering.NONEMPTY: if line.strip(): - print(f"{line_counter[0]:6}\t{line}", end='') - line_counter[0] += 1 + line_number = print_numbered_line(line, line_number) else: print(line, end='') elif numbering == Numbering.ALL: - print(f"{line_counter[0]:6}\t{line}", end='') - line_counter[0] += 1 + line_number = print_numbered_line(line, line_number) else: print(line, end='') except FileNotFoundError: print(f"cat: {filepath}: No such file or directory") + return line_number def main(): parser = argparse.ArgumentParser(description="Concatenate files and print on the standard output.") @@ -32,13 +35,6 @@ def main(): parser.add_argument('files', nargs='+', help='files to concatenate') args = parser.parse_args() - files = [] - for pattern in args.files: - files.extend(glob.glob(pattern) or [pattern]) - - line_counter = [1] - - # Determine numbering mode (mutually exclusive) if args.n and args.b: parser.error("options -n and -b are mutually exclusive") elif args.n: @@ -48,8 +44,9 @@ def main(): else: numbering = Numbering.NONE - for file in sorted(files): - cat(file, numbering=numbering, line_counter=line_counter) + line_number = 1 # start line numbering + for file in args.files: + line_number = cat(file, numbering=numbering, start_line=line_number) if __name__ == "__main__": main() diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py index d7499b3a3..bd3082765 100644 --- a/implement-shell-tools/ls/ls.py +++ b/implement-shell-tools/ls/ls.py @@ -1,7 +1,8 @@ import os import argparse -def ls(path='.', one_column=False, show_hidden=False): +def ls(path, one_column, show_hidden): + """List files in a directory, optionally in one column or including hidden files.""" try: files = os.listdir(path) if not show_hidden: @@ -14,6 +15,8 @@ def ls(path='.', one_column=False, show_hidden=False): print(*files) except FileNotFoundError: print(f"ls: cannot access '{path}': No such file or directory") + except NotADirectoryError: + print(f"ls: cannot access '{path}': Not a directory") def main(): parser = argparse.ArgumentParser() @@ -25,4 +28,4 @@ def main(): ls(args.path, args.one_column, args.a) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/implement-shell-tools/wc/wc.py b/implement-shell-tools/wc/wc.py index a658c87f1..23de65998 100644 --- a/implement-shell-tools/wc/wc.py +++ b/implement-shell-tools/wc/wc.py @@ -1,6 +1,7 @@ import argparse def wc(path, count_lines, count_words, count_bytes): + """Count lines, words, and bytes for a single file.""" try: with open(path, 'r') as f: content = f.read() @@ -8,19 +9,26 @@ def wc(path, count_lines, count_words, count_bytes): words = content.split() bytes_ = len(content.encode('utf-8')) + # Determine what to show + if not any([count_lines, count_words, count_bytes]): + count_lines = count_words = count_bytes = True + parts = [] if count_lines: parts.append(str(len(lines))) if count_words: parts.append(str(len(words))) if count_bytes: parts.append(str(bytes_)) - if not parts: - parts = [str(len(lines)), str(len(words)), str(bytes_)] print(' '.join(parts), path) + return (len(lines) if count_lines else 0, + len(words) if count_words else 0, + bytes_ if count_bytes else 0) except FileNotFoundError: print(f"wc: {path}: No such file or directory") + return (0, 0, 0) except IsADirectoryError: print(f"wc: {path}: Is a directory") + return (0, 0, 0) def main(): parser = argparse.ArgumentParser() @@ -30,8 +38,21 @@ def main(): parser.add_argument('paths', nargs='+', help='Files to count') args = parser.parse_args() + total_lines = total_words = total_bytes = 0 + multiple_files = len(args.paths) > 1 + for path in args.paths: - wc(path, args.l, args.w, args.c) + l, w, b = wc(path, args.l, args.w, args.c) + total_lines += l + total_words += w + total_bytes += b + + if multiple_files: + parts = [] + if args.l or not any([args.l, args.w, args.c]): parts.append(str(total_lines)) + if args.w or not any([args.l, args.w, args.c]): parts.append(str(total_words)) + if args.c or not any([args.l, args.w, args.c]): parts.append(str(total_bytes)) + print(' '.join(parts), 'total') if __name__ == "__main__": - main() \ No newline at end of file + main() From 84dcfb7169617293e7414da7fd890b1cd62365e7 Mon Sep 17 00:00:00 2001 From: Luke Manyamazi Date: Thu, 18 Jun 2026 13:37:18 +0200 Subject: [PATCH 07/12] Refactor wc function for clarity and error handling --- implement-shell-tools/wc/wc.py | 59 +++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/implement-shell-tools/wc/wc.py b/implement-shell-tools/wc/wc.py index 23de65998..0531b8e42 100644 --- a/implement-shell-tools/wc/wc.py +++ b/implement-shell-tools/wc/wc.py @@ -1,33 +1,51 @@ import argparse +import sys def wc(path, count_lines, count_words, count_bytes): """Count lines, words, and bytes for a single file.""" try: with open(path, 'r') as f: content = f.read() + lines = content.splitlines() words = content.split() - bytes_ = len(content.encode('utf-8')) - # Determine what to show + line_count = len(lines) + word_count = len(words) + byte_count = len(content.encode('utf-8')) + if not any([count_lines, count_words, count_bytes]): - count_lines = count_words = count_bytes = True + count_lines = True + count_words = True + count_bytes = True parts = [] - if count_lines: parts.append(str(len(lines))) - if count_words: parts.append(str(len(words))) - if count_bytes: parts.append(str(bytes_)) + + if count_lines: + parts.append(str(line_count)) + + if count_words: + parts.append(str(word_count)) + + if count_bytes: + parts.append(str(byte_count)) print(' '.join(parts), path) - return (len(lines) if count_lines else 0, - len(words) if count_words else 0, - bytes_ if count_bytes else 0) + return line_count, word_count, byte_count + except FileNotFoundError: - print(f"wc: {path}: No such file or directory") + print( + f"wc: {path}: No such file or directory", + file=sys.stderr + ) return (0, 0, 0) + except IsADirectoryError: - print(f"wc: {path}: Is a directory") + print( + f"wc: {path}: Is a directory", + file=sys.stderr + ) return (0, 0, 0) def main(): @@ -38,8 +56,12 @@ def main(): parser.add_argument('paths', nargs='+', help='Files to count') args = parser.parse_args() - total_lines = total_words = total_bytes = 0 + total_lines = 0 + total_words = 0 + total_bytes = 0 + multiple_files = len(args.paths) > 1 + show_all = not any([args.l, args.w, args.c]) for path in args.paths: l, w, b = wc(path, args.l, args.w, args.c) @@ -49,9 +71,16 @@ def main(): if multiple_files: parts = [] - if args.l or not any([args.l, args.w, args.c]): parts.append(str(total_lines)) - if args.w or not any([args.l, args.w, args.c]): parts.append(str(total_words)) - if args.c or not any([args.l, args.w, args.c]): parts.append(str(total_bytes)) + + if args.l or show_all: + parts.append(str(total_lines)) + + if args.w or show_all: + parts.append(str(total_words)) + + if args.c or show_all: + parts.append(str(total_bytes)) + print(' '.join(parts), 'total') if __name__ == "__main__": From d72fc0acf9dc7abf3ef483740f1396d162a297b6 Mon Sep 17 00:00:00 2001 From: Luke Manyamazi Date: Thu, 18 Jun 2026 13:37:52 +0200 Subject: [PATCH 08/12] Enhance ls function with error handling and formatting Added error handling for file access issues and improved output formatting. --- implement-shell-tools/ls/ls.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py index bd3082765..d94d2c48e 100644 --- a/implement-shell-tools/ls/ls.py +++ b/implement-shell-tools/ls/ls.py @@ -1,22 +1,38 @@ import os +import sys import argparse def ls(path, one_column, show_hidden): """List files in a directory, optionally in one column or including hidden files.""" try: + if os.path.isfile(path): + print(os.path.basename(path)) + return + files = os.listdir(path) - if not show_hidden: + + if show_hidden: + files = ['.', '..'] + files + else: files = [f for f in files if not f.startswith('.')] + files.sort() - + if one_column: print(*files, sep='\n') else: print(*files) + except FileNotFoundError: - print(f"ls: cannot access '{path}': No such file or directory") + print( + f"ls: cannot access '{path}': No such file or directory", + file=sys.stderr + ) except NotADirectoryError: - print(f"ls: cannot access '{path}': Not a directory") + print( + f"ls: cannot access '{path}': Not a directory", + file=sys.stderr + ) def main(): parser = argparse.ArgumentParser() @@ -24,7 +40,7 @@ def main(): parser.add_argument('-a', action='store_true', help='show hidden files') parser.add_argument('path', nargs='?', default='.', help='directory to list') args = parser.parse_args() - + ls(args.path, args.one_column, args.a) if __name__ == "__main__": From 575b3f96395972f76f2de67a15c8767d64b6090b Mon Sep 17 00:00:00 2001 From: Luke Manyamazi Date: Thu, 18 Jun 2026 13:38:30 +0200 Subject: [PATCH 09/12] Improve error handling and formatting in cat.py --- implement-shell-tools/cat/cat.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py index 58dccaee6..ee8ec04cb 100644 --- a/implement-shell-tools/cat/cat.py +++ b/implement-shell-tools/cat/cat.py @@ -1,4 +1,5 @@ import argparse +import sys from enum import Enum class Numbering(Enum): @@ -25,11 +26,16 @@ def cat(filepath, numbering, start_line): else: print(line, end='') except FileNotFoundError: - print(f"cat: {filepath}: No such file or directory") + print( + f"cat: {filepath}: No such file or directory", + file=sys.stderr + ) return line_number def main(): - parser = argparse.ArgumentParser(description="Concatenate files and print on the standard output.") + parser = argparse.ArgumentParser( + description="Concatenate files and print on the standard output." + ) 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('files', nargs='+', help='files to concatenate') @@ -44,7 +50,8 @@ def main(): else: numbering = Numbering.NONE - line_number = 1 # start line numbering + line_number = 1 + for file in args.files: line_number = cat(file, numbering=numbering, start_line=line_number) From a658d30be3e2997fba67f48efc69ed75b372fa71 Mon Sep 17 00:00:00 2001 From: Luke Manyamazi Date: Mon, 7 Sep 2026 15:40:43 +0200 Subject: [PATCH 10/12] Fix cat error handling and exit status --- implement-shell-tools/cat/cat.py | 90 +++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 20 deletions(-) diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py index ee8ec04cb..fab40e5d7 100644 --- a/implement-shell-tools/cat/cat.py +++ b/implement-shell-tools/cat/cat.py @@ -2,43 +2,84 @@ import sys from enum import Enum + class Numbering(Enum): NONE = 0 ALL = 1 NONEMPTY = 2 + def print_numbered_line(line, line_number, pad=6): - print(f"{line_number:{pad}}\t{line}", end='') - return line_number + 1 + print(f"{line_number:{pad}}\t{line}", end="") + def cat(filepath, numbering, start_line): line_number = start_line + try: - with open(filepath) as f: - for line in f: - if numbering == Numbering.NONEMPTY: - if line.strip(): - line_number = print_numbered_line(line, line_number) - else: - print(line, end='') - elif numbering == Numbering.ALL: - line_number = print_numbered_line(line, line_number) + with open(filepath) as file: + for line in file: + should_number = ( + numbering == Numbering.ALL + or ( + numbering == Numbering.NONEMPTY + and line.strip("\n") + ) + ) + + if should_number: + print_numbered_line(line, line_number) + line_number += 1 else: - print(line, end='') + print(line, end="") + except FileNotFoundError: print( f"cat: {filepath}: No such file or directory", - file=sys.stderr + file=sys.stderr, + ) + return line_number, False + + except IsADirectoryError: + print( + f"cat: {filepath}: Is a directory", + file=sys.stderr, + ) + return line_number, False + + except PermissionError: + print( + f"cat: {filepath}: Permission denied", + file=sys.stderr, ) - return line_number + return line_number, False + + return line_number, True + def main(): parser = argparse.ArgumentParser( description="Concatenate files and print on the standard output." ) - 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('files', nargs='+', help='files to concatenate') + + 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( + "files", + nargs="+", + help="files to concatenate", + ) + args = parser.parse_args() if args.n and args.b: @@ -51,9 +92,18 @@ def main(): numbering = Numbering.NONE line_number = 1 + success = True + + for filepath in args.files: + line_number, file_success = cat( + filepath, + numbering=numbering, + start_line=line_number, + ) + success = success and file_success + + return 0 if success else 1 - for file in args.files: - line_number = cat(file, numbering=numbering, start_line=line_number) if __name__ == "__main__": - main() + sys.exit(main()) \ No newline at end of file From d6641a7f8ed6c4a9bfe0bb2028ddb967d9233a5c Mon Sep 17 00:00:00 2001 From: Luke Manyamazi Date: Mon, 7 Sep 2026 16:07:59 +0200 Subject: [PATCH 11/12] Improve ls formatting and error handling --- implement-shell-tools/ls/ls.py | 69 +++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py index d94d2c48e..6c87a9103 100644 --- a/implement-shell-tools/ls/ls.py +++ b/implement-shell-tools/ls/ls.py @@ -1,47 +1,82 @@ +import argparse import os import sys -import argparse + def ls(path, one_column, show_hidden): - """List files in a directory, optionally in one column or including hidden files.""" try: if os.path.isfile(path): print(os.path.basename(path)) - return + return True files = os.listdir(path) if show_hidden: - files = ['.', '..'] + files + files = [".", ".."] + files else: - files = [f for f in files if not f.startswith('.')] + files = [file for file in files if not file.startswith(".")] files.sort() - if one_column: - print(*files, sep='\n') - else: - print(*files) + separator = "\n" if one_column else "\t" + print(*files, sep=separator) + + return True except FileNotFoundError: print( f"ls: cannot access '{path}': No such file or directory", - file=sys.stderr + file=sys.stderr, ) except NotADirectoryError: print( f"ls: cannot access '{path}': Not a directory", - file=sys.stderr + file=sys.stderr, + ) + except PermissionError: + print( + f"ls: cannot open directory '{path}': Permission denied", + file=sys.stderr, ) + return False + + def main(): - parser = argparse.ArgumentParser() - parser.add_argument('-1', dest='one_column', action='store_true', help='list one file per line') - parser.add_argument('-a', action='store_true', help='show hidden files') - parser.add_argument('path', nargs='?', default='.', help='directory to list') + parser = argparse.ArgumentParser( + description="List directory contents." + ) + + parser.add_argument( + "-1", + dest="one_column", + action="store_true", + help="list one file per line", + ) + + parser.add_argument( + "-a", + action="store_true", + help="show hidden files", + ) + + parser.add_argument( + "path", + nargs="?", + default=".", + help="directory to list", + ) + args = parser.parse_args() - ls(args.path, args.one_column, args.a) + success = ls( + path=args.path, + one_column=args.one_column, + show_hidden=args.a, + ) + + return 0 if success else 1 + if __name__ == "__main__": - main() + sys.exit(main()) \ No newline at end of file From 3fc838dcab5c3a703da725bb3c573f90a2ffc47a Mon Sep 17 00:00:00 2001 From: Luke Manyamazi Date: Mon, 7 Sep 2026 16:13:19 +0200 Subject: [PATCH 12/12] Refactor wc counting and error handling --- implement-shell-tools/wc/wc.py | 174 ++++++++++++++++++++++----------- 1 file changed, 117 insertions(+), 57 deletions(-) diff --git a/implement-shell-tools/wc/wc.py b/implement-shell-tools/wc/wc.py index 0531b8e42..d99d20a97 100644 --- a/implement-shell-tools/wc/wc.py +++ b/implement-shell-tools/wc/wc.py @@ -1,87 +1,147 @@ import argparse import sys -def wc(path, count_lines, count_words, count_bytes): - """Count lines, words, and bytes for a single file.""" - try: - with open(path, 'r') as f: - content = f.read() - - lines = content.splitlines() - words = content.split() - - line_count = len(lines) - word_count = len(words) - byte_count = len(content.encode('utf-8')) - - if not any([count_lines, count_words, count_bytes]): - count_lines = True - count_words = True - count_bytes = True - parts = [] - - if count_lines: - parts.append(str(line_count)) - - if count_words: - parts.append(str(word_count)) - - if count_bytes: - parts.append(str(byte_count)) +def wc(path): + try: + with open(path, "rb") as file: + content = file.read() - print(' '.join(parts), path) + line_count = content.count(b"\n") + word_count = len(content.split()) + byte_count = len(content) return line_count, word_count, byte_count except FileNotFoundError: print( f"wc: {path}: No such file or directory", - file=sys.stderr + file=sys.stderr, ) - return (0, 0, 0) - except IsADirectoryError: print( f"wc: {path}: Is a directory", - file=sys.stderr + file=sys.stderr, + ) + except PermissionError: + print( + f"wc: {path}: Permission denied", + file=sys.stderr, ) - return (0, 0, 0) + except OSError as error: + print( + f"wc: {path}: {error}", + file=sys.stderr, + ) + + return None + + +def print_stats( + line_count, + word_count, + byte_count, + filename, + show_lines, + show_words, + show_bytes, +): + parts = [] + + if show_lines: + parts.append(f"{line_count:7d}") + + if show_words: + parts.append(f"{word_count:7d}") + + if show_bytes: + parts.append(f"{byte_count:7d}") + + print("".join(parts), filename) + def main(): - parser = argparse.ArgumentParser() - parser.add_argument('-l', action='store_true', help='Count lines') - parser.add_argument('-w', action='store_true', help='Count words') - parser.add_argument('-c', action='store_true', help='Count bytes') - parser.add_argument('paths', nargs='+', help='Files to count') + parser = argparse.ArgumentParser( + description="Print newline, word, and byte counts for files." + ) + + parser.add_argument( + "-l", + action="store_true", + help="print the newline count", + ) + + parser.add_argument( + "-w", + action="store_true", + help="print the word count", + ) + + parser.add_argument( + "-c", + action="store_true", + help="print the byte count", + ) + + parser.add_argument( + "paths", + nargs="+", + help="files to count", + ) + args = parser.parse_args() + # If no options are supplied, wc prints all three counts. + show_lines = args.l + show_words = args.w + show_bytes = args.c + + if not any((show_lines, show_words, show_bytes)): + show_lines = True + show_words = True + show_bytes = True + total_lines = 0 total_words = 0 total_bytes = 0 - - multiple_files = len(args.paths) > 1 - show_all = not any([args.l, args.w, args.c]) + successful_files = 0 for path in args.paths: - l, w, b = wc(path, args.l, args.w, args.c) - total_lines += l - total_words += w - total_bytes += b - - if multiple_files: - parts = [] - - if args.l or show_all: - parts.append(str(total_lines)) + counts = wc(path) + + if counts is None: + continue + + line_count, word_count, byte_count = counts + + total_lines += line_count + total_words += word_count + total_bytes += byte_count + successful_files += 1 + + print_stats( + line_count, + word_count, + byte_count, + path, + show_lines, + show_words, + show_bytes, + ) - if args.w or show_all: - parts.append(str(total_words)) + if len(args.paths) > 1 and successful_files > 0: + print_stats( + total_lines, + total_words, + total_bytes, + "total", + show_lines, + show_words, + show_bytes, + ) - if args.c or show_all: - parts.append(str(total_bytes)) + return 0 if successful_files == len(args.paths) else 1 - print(' '.join(parts), 'total') if __name__ == "__main__": - main() + sys.exit(main()) \ No newline at end of file