diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py new file mode 100644 index 000000000..16fb42367 --- /dev/null +++ b/implement-shell-tools/cat/cat.py @@ -0,0 +1,93 @@ +import argparse +import sys + +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() + +# cat returns different error messages depending on the reason the path could be read +def read_file(path): + """Returns (content, error_message). error_message is None on success""" + try: + with open(path, "r", encoding="utf-8") as f: + return f.read(), None + except FileNotFoundError: + return None, f"cat: {path}: No such file or directory" + except IsADirectoryError: + return None, f"cat: {path}: Is a directory" + except PermissionError: + return None, f"cat: {path}: Permission denied" + + +# -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_nonempty=False): + """Returns a list of formatted output lines""" + output = [] + + if number_nonempty: + 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 + + 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 + file_error = False + + for path in args.paths: + line_num = 1 + is_success = cat_file(path, args.n, args.b) + + 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 diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py new file mode 100644 index 000000000..d1dab65ad --- /dev/null +++ b/implement-shell-tools/ls/ls.py @@ -0,0 +1,80 @@ +import argparse +import sys +import os + +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() + + +def get_dir_entries(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 + + +def print_entries(entries, onePerLineFlag = args.opt_one): + if (onePerLineFlag): + for entry in entries: + print(entry) + elif (len(entries) > 0): + for i in range(len(entries)-1): + print(f"{entries[i]}\t", end="") + 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)] + + 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) + + 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)) + + +if __name__ == "__main__": + main() \ No newline at end of file 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 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 diff --git a/implement-shell-tools/wc/wc.py b/implement-shell-tools/wc/wc.py new file mode 100644 index 000000000..b6ce983a3 --- /dev/null +++ b/implement-shell-tools/wc/wc.py @@ -0,0 +1,68 @@ +import argparse +import sys +import os + +# TODO: decompose into functions to make it more modular and reusable + +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", default="l") +parser.add_argument("-w", action="store_true", help="show word count", default='w') +parser.add_argument("-c", action="store_true", help="show byte count", default="c") + +parser.add_argument("paths", nargs="*", help="file(s) for which to show data") + +args = parser.parse_args() + +totals = {"l": 0, "w": 0, "c": 0} + +for path in args.paths: + if (not os.path.exists(path)): + print(f"wc: {path}: open: No such file or directory", file=sys.stderr) + elif (os.path.isdir(path)): + print(f"wc: {path}: read: Is a directory") + elif (os.path.isfile(path)): + output_str = "" + with open(path, "r", encoding="utf-8") as file: + lines = file.readlines() + + if (args.l): + if (len(lines) > 0 and lines[-1] == ""): + lines.pop() + + line_count = len(lines) + totals["l"] += line_count + output_str += f"{line_count:8}" + + if (args.w): + word_count = 0 + for line in lines: + # python string.split splits on any white space + word_count += len(line.split()) + totals["w"] += word_count + output_str += f"{word_count:8}" + + if (args.c): + bytes = os.path.getsize(path) + totals["c"] += bytes + output_str += f"{bytes:8}" + + output_str += f" {path}" + print(output_str) + +if (len(args.paths) > 1): + res = {key : val for key, val in totals.items() + if val != 0} + total_str = "" + for v in res.values(): + total_str += f"{v:8}" + + total_str += " total" + print(total_str) + + + + +