diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py new file mode 100644 index 000000000..fab40e5d7 --- /dev/null +++ b/implement-shell-tools/cat/cat.py @@ -0,0 +1,109 @@ +import argparse +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="") + + +def cat(filepath, numbering, start_line): + line_number = start_line + + try: + 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="") + + except FileNotFoundError: + print( + f"cat: {filepath}: No such file or directory", + 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, 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", + ) + + args = parser.parse_args() + + 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 + + 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 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py new file mode 100644 index 000000000..6c87a9103 --- /dev/null +++ b/implement-shell-tools/ls/ls.py @@ -0,0 +1,82 @@ +import argparse +import os +import sys + + +def ls(path, one_column, show_hidden): + try: + if os.path.isfile(path): + print(os.path.basename(path)) + return True + + files = os.listdir(path) + + if show_hidden: + files = [".", ".."] + files + else: + files = [file for file in files if not file.startswith(".")] + + files.sort() + + 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, + ) + except NotADirectoryError: + print( + f"ls: cannot access '{path}': Not a directory", + file=sys.stderr, + ) + except PermissionError: + print( + f"ls: cannot open directory '{path}': Permission denied", + file=sys.stderr, + ) + + return False + + +def main(): + 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() + + success = ls( + path=args.path, + one_column=args.one_column, + show_hidden=args.a, + ) + + return 0 if success else 1 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/implement-shell-tools/wc/wc.py b/implement-shell-tools/wc/wc.py new file mode 100644 index 000000000..d99d20a97 --- /dev/null +++ b/implement-shell-tools/wc/wc.py @@ -0,0 +1,147 @@ +import argparse +import sys + + +def wc(path): + try: + with open(path, "rb") as file: + content = file.read() + + 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, + ) + except IsADirectoryError: + print( + f"wc: {path}: Is a directory", + file=sys.stderr, + ) + except PermissionError: + print( + f"wc: {path}: Permission denied", + file=sys.stderr, + ) + 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( + 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 + successful_files = 0 + + for path in args.paths: + 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 len(args.paths) > 1 and successful_files > 0: + print_stats( + total_lines, + total_words, + total_bytes, + "total", + show_lines, + show_words, + show_bytes, + ) + + return 0 if successful_files == len(args.paths) else 1 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file