diff --git a/.gitignore b/.gitignore index 3c3629e64..274d04915 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.venv \ No newline at end of file diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py new file mode 100644 index 000000000..66c842667 --- /dev/null +++ b/implement-shell-tools/cat/cat.py @@ -0,0 +1,50 @@ +import argparse +import sys + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Reads file(s) and writes them to the standard output", + ) + parser.add_argument("paths", nargs="+", help="The file path(s) to process") + parser.add_argument( + "-n", + action="store_true", + dest="number_all", + help="Number the output lines, starting at 1.", + ) + parser.add_argument( + "-b", + action="store_true", + dest="number_nonblank", + help="Number only non-blank output lines, starting at 1.", + ) + return parser.parse_args() + + +def main(): + args = parse_args() + + try: + for path in args.paths: + line_num = 1 + + with open(path, "r", encoding="utf-8") as file: + for raw_line in file: + line = raw_line.rstrip("\n") + is_blank = line.strip() == "" + should_number = args.number_all or ( + args.number_nonblank and not is_blank) + + if should_number: + print(f"{line_num} {line}") + line_num += 1 + else: + print(line) + except OSError as err: + print(err, file=sys.stderr) + + return 0 + + +main() diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py new file mode 100644 index 000000000..88786fe4d --- /dev/null +++ b/implement-shell-tools/ls/ls.py @@ -0,0 +1,86 @@ +import argparse +import os +import stat +import sys + + +def parse_args(): + parser = argparse.ArgumentParser( + description="List directory contents", + ) + parser.add_argument( + "paths", + nargs="*", + help="The file path to process (defaults to current directory)", + ) + parser.add_argument( + "-a", + action="store_true", + dest="include_hidden", + help="Include directory entries whose names begin with a dot ('.').", + ) + parser.add_argument( + "-1", + action="store_true", + dest="one_per_line", + help="Force output to be one entry per line.", + ) + return parser.parse_args() + + +def filter_hidden(files: list[str]) -> list[str]: + return [name for name in files if not name.startswith(".")] + + +def get_visible_entries(files: list[str], include_hidden: bool): + return files if include_hidden else filter_hidden(files) + + +def format_entries(files: list[str], one_per_line: bool): + if len(files) == 0: + return + print(("\n" if one_per_line else "\t").join(files)) + + +def main(): + args = parse_args() + + try: + file_paths = args.paths if args.paths else ["."] + include_hidden = bool(args.include_hidden) + one_per_line = bool(args.one_per_line) + + result_files: list[str] = [] + result_dirs: dict[str, list[str]] = {} + + for file_path in file_paths: + st = os.stat(file_path) + # Is a file? + if stat.S_ISREG(st.st_mode): + result_files.append(file_path) + # Is a directory? + if stat.S_ISDIR(st.st_mode): + result_dirs[file_path] = os.listdir(file_path) + + result_files = get_visible_entries(result_files, include_hidden) + + if len(file_paths) == 1: + entries = list(result_files) + for contents in result_dirs.values(): + filtered = get_visible_entries(contents, include_hidden) + entries.extend(filtered) + format_entries(entries, one_per_line) + else: + format_entries(result_files, one_per_line) + + for directory, contents in result_dirs.items(): + print("\n" + directory + ":") + filtered = get_visible_entries(contents, include_hidden) + format_entries(filtered, one_per_line) + except OSError as err: + print(str(err), file=sys.stderr) + + return 0 + + +main() diff --git a/implement-shell-tools/wc/requirements.txt b/implement-shell-tools/wc/requirements.txt new file mode 100644 index 000000000..89f7140ec --- /dev/null +++ b/implement-shell-tools/wc/requirements.txt @@ -0,0 +1 @@ +tabulate \ 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..cd1f60eb9 --- /dev/null +++ b/implement-shell-tools/wc/wc.py @@ -0,0 +1,92 @@ +import argparse +import os +import sys + +from tabulate import tabulate + + +def parse_args(): + parser = argparse.ArgumentParser( + description="word, line and byte count", + ) + parser.add_argument("paths", nargs="+", + help="The file path(s) to process.") + parser.add_argument( + "-l", + "--lines", + action="store_true", + help="The number of lines in each input file is written to the standard output.", + ) + parser.add_argument( + "-w", + "--words", + action="store_true", + help="The number of words in each input file is written to the standard output.", + ) + parser.add_argument( + "-c", + "--bytes", + action="store_true", + dest="bytes", + help="The number of bytes in each input file is written to the standard output.", + ) + return parser.parse_args() + + +def main(): + args = parse_args() + + try: + file_paths: list[str] = args.paths + results: dict[str, dict[str, int]] = {} + + for file_path in file_paths: + stats = os.stat(file_path) + count = {"lines": 0, "words": 0, "bytes": stats.st_size} + + with open(file_path, "r", encoding="utf-8") as file: + for line in file: + count["lines"] += 1 + trimmed = line.strip() + if len(trimmed) > 0: + count["words"] += len(trimmed.split()) + + results[file_path] = count + + if len(file_paths) > 1: + total = {"lines": 0, "words": 0, "bytes": 0} + for file_count in results.values(): + total["lines"] += file_count["lines"] + total["words"] += file_count["words"] + total["bytes"] += file_count["bytes"] + results["total"] = total + + no_options_provided = not (args.lines or args.words or args.bytes) + selected_option_keys: list[str] = [] + + if args.lines: + selected_option_keys.append("lines") + if args.words: + selected_option_keys.append("words") + if args.bytes: + selected_option_keys.append("bytes") + + output_columns = [ + "lines", "words", "bytes"] if no_options_provided else selected_option_keys + rows: list[list[str | int]] = [] + for name, values in results.items(): + rows.append([name] + [values[column] for column in output_columns]) + + if no_options_provided: + print(tabulate(rows, headers=[ + "index"] + output_columns)) + else: + print(tabulate(rows, headers=[ + "index"] + output_columns)) + except OSError as err: + print(str(err), file=sys.stderr) + + return 0 + + +main()