diff --git a/.gitignore b/.gitignore index 3c3629e64..be3563007 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ node_modules +.venv +*.class \ 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..6c904d1aa --- /dev/null +++ b/implement-shell-tools/cat/cat.py @@ -0,0 +1,54 @@ +import sys +import argparse + +def read_and_output_files(): + # 1. Setup Argument Parser (Equivalent to 'commander') + parser = argparse.ArgumentParser(description="Python implementation of a basic cat-like utility") + parser.add_argument("-n", "--number", action="store_true", help="number all output lines") + parser.add_argument("-b", "--number-nonblank", action="store_true", help="number only non-empty lines") + parser.add_argument("files", nargs="+", help="files to read") + + args = parser.parse_args() + + try: + # 2. Read all file contents (Equivalent to Promise.all / fs.readFile) + file_contents = [] + for file_path in args.files: + with open(file_path, "r", encoding="utf-8") as f: + file_contents.append(f.read()) + + concatenated_content = "".join(file_contents) + + # 3. Process Logic + if args.number: + # -n logic: number all lines + lines = concatenated_content.split("\n") + output = [] + for index, line in enumerate(lines, start=1): + # rjust(6) is equivalent to padStart(6) + output.append(f"{str(index).rjust(6)} {line}") + sys.stdout.write("\n".join(output)) + + elif args.number_nonblank: + # -b logic: number only non-empty lines + lines = concatenated_content.split("\n") + output = [] + nonblank_line_number = 0 + for line in lines: + if line.strip() == "": + output.append(line) + else: + nonblank_line_number += 1 + output.append(f"{str(nonblank_line_number).rjust(6)} {line}") + sys.stdout.write("\n".join(output)) + + else: + # No flags: standard output + sys.stdout.write(concatenated_content) + + except Exception as err: + print(f"Error reading multiple files: {err}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + read_and_output_files() diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py new file mode 100644 index 000000000..967635a4c --- /dev/null +++ b/implement-shell-tools/ls/ls.py @@ -0,0 +1,58 @@ +import os +import sys +import argparse + +def run_ls_command(): + parser = argparse.ArgumentParser() + parser.add_argument("-1", "--one-per-line", action="store_true", dest="one") + parser.add_argument("-a", "--all", action="store_true") + parser.add_argument("path", nargs="?", default=".") + args = parser.parse_args() + + # ANSI Color Codes + BLUE = '\033[34m' + RESET = '\033[0m' + + try: + # 1. Get and sort entries + directory_entries = os.listdir(args.path) + directory_entries.sort(key=str.lower) + + # 2. Filter out dotfiles unless -a is used + visible_entries = [] + if args.all: + visible_entries = directory_entries + else: + for name in directory_entries: + if not name.startswith("."): + visible_entries.append(name) + + # 3. Apply colors to folders + colored_entries = [] + for name in visible_entries: + # We must join the path to the name to check if it's a folder correctly + full_path = os.path.join(args.path, name) + + if os.path.isdir(full_path): + # Wrap the name in Blue color codes + colored_entries.append(f"{BLUE}{name}{RESET}") + else: + # Keep regular file name as is + colored_entries.append(name) + + # 4. Build output string + output_string = "" + if args.one: + output_string = "\n".join(colored_entries) + "\n" + else: + output_string = " ".join(colored_entries) + "\n" + + if colored_entries: + sys.stdout.write(output_string) + + except Exception as err: + print(f"Error: {err}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + run_ls_command() \ 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..ea09e9c4d --- /dev/null +++ b/implement-shell-tools/wc/wc.py @@ -0,0 +1,98 @@ +import argparse +import sys +import os + + +def calculate_stats(content, display_name, original_bytes=None): + lines = content.count("\n") + words = len(content.split()) + # If we have the raw bytes, use that length. Otherwise, encode to get byte length. + byte_count = ( + original_bytes if original_bytes is not None else len(content.encode("utf-8")) + ) + + return { + "lineCount": lines, + "wordCount": words, + "byteCount": byte_count, + "displayName": display_name, + } + + +def print_formatted_report(stats, args, should_show_all_stats): + output_columns = [] + + def format_col(count): + return str(count).rjust(4) + + if should_show_all_stats: + output_columns.append(format_col(stats["lineCount"])) + output_columns.append(format_col(stats["wordCount"])) + output_columns.append(format_col(stats["byteCount"])) + else: + if args.lines: + output_columns.append(format_col(stats["lineCount"])) + if args.words: + output_columns.append(format_col(stats["wordCount"])) + if args.bytes: + output_columns.append(format_col(stats["byteCount"])) + + # Use a single space between the numbers and the name + print(f"{''.join(output_columns)} {stats['displayName']}") + + +def main(): + parser = argparse.ArgumentParser(description="A simple Python implementation of wc") + parser.add_argument("files", nargs="*", help="Files to process") + parser.add_argument( + "-l", "--lines", action="store_true", help="print the newline counts" + ) + parser.add_argument( + "-w", "--words", action="store_true", help="print the word counts" + ) + parser.add_argument( + "-c", "--bytes", action="store_true", help="print the byte counts" + ) + + args = parser.parse_args() + should_show_all_stats = not (args.lines or args.words or args.bytes) + all_file_stats = [] + exit_code = 0 + + # NEW: Handle Standard Input if no files are provided + if not args.files: + stdin_content = sys.stdin.read() + stats = calculate_stats(stdin_content, "") + print_formatted_report(stats, args, should_show_all_stats) + return + + # Process files + for file_path in args.files: + try: + with open(file_path, "rb") as f: + raw_bytes = f.read() + content = raw_bytes.decode("utf-8", errors="ignore") + stats = calculate_stats(content, file_path, len(raw_bytes)) + + all_file_stats.append(stats) + print_formatted_report(stats, args, should_show_all_stats) + except Exception: + print(f"wc: {file_path}: No such file or directory", file=sys.stderr) + exit_code = 1 + + # Print total if more than one file + if len(all_file_stats) > 1: + grand_totals = { + "lineCount": sum(s["lineCount"] for s in all_file_stats), + "wordCount": sum(s["wordCount"] for s in all_file_stats), + "byteCount": sum(s["byteCount"] for s in all_file_stats), + "displayName": "total", + } + print_formatted_report(grand_totals, args, should_show_all_stats) + + if exit_code != 0: + sys.exit(exit_code) + + +if __name__ == "__main__": + main()