diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py new file mode 100644 index 00000000..ef19d168 --- /dev/null +++ b/implement-shell-tools/cat/cat.py @@ -0,0 +1,62 @@ +import argparse +import sys +import glob # to find file paths and match patterns + + + +def parse_args(): + parser = argparse.ArgumentParser( + prog = "cat", + description= "copying cat commands behavior with -n and -b flags" + ) + parser.add_argument("-n", "--number", action="store_true", help="Number all output lines") #with store true argument behaves like a boolean, flag present returns true, not present = false. + parser.add_argument("-b", "--number-nonblank", action="store_true", help="Number non-empty output lines") + parser.add_argument("files", nargs="+", help="Files to read (supports shell globs)") # accepts one or more files names or glob patterns. + return parser.parse_args() + +#Uses glob to expand patterns like *.txt. +def expand_files(patterns): + expanded = [] + for p in patterns: + matches = glob.glob(p) + if matches: + expanded.extend(sorted(matches)) # predictable order + else: + print(f"cat: {p}: No such file", file=sys.stderr) #Prints an error if a pattern matches nothing. + return expanded + +def cat_file(filename, number_all=False, number_nonempty=False): + try: + with open(filename, 'r') as f: + lines = f.readlines() + except FileNotFoundError: + print(f"cat: {filename}: No such file") + return +#Applies numbering rules depending on flags. + line_num = 1 + for line in lines: + line = line.rstrip('\n') + if number_all: + print(f"{line_num:6} {line}") + line_num += 1 + elif number_nonempty: + if line.strip(): + print(f"{line_num:6} {line}") + line_num+= 1 + else: + print(line) + else: + print(line) + +def main(): + args = parse_args() #return an args obj + files = expand_files(args.files) #list of filenames or patterns user typed passed to expand_files helper that uses glob module to expand patterns to actual file paths + if not files: + sys.exit(1) #programs exits with status code 1- signals and error if no files were found + +#looping through each file path on expanded list + for file in files: + cat_file(file, number_all=args.number, number_nonempty=args.number_nonblank) + +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 00000000..ec15c6e3 --- /dev/null +++ b/implement-shell-tools/ls/ls.py @@ -0,0 +1,56 @@ +import argparse +import os #interacting with the filesystem (paths, directories) +import sys #access to system streams (stdout, stderr) and arguments + +def parseargs(): + parser = argparse.ArgumentParser( + prog = "ls", + description = "ls clone to -1 and -a flags" + ) + parser.add_argument("-1", dest = "one_per_line", action = "store_true",help = "List one file per line") #dest sets the attribute name in parsed argument. + parser.add_argument("-a", "--all", action ="store_true",help = "Include directory entries whose names begin with a dot (.)") + parser.add_argument("dirs", nargs = "*", default = ["."], help = "Directories to list (default is current directory)") + return parser.parse_args() + +def print_directory_entries(path, one_per_line=False, show_all=False): + try: + # Retrieve all entries (files and subdirectories) inside the given path + directory_entries = os.listdir(path) + except FileNotFoundError: + # If the directory doesn't exist, print an error to stderr + print(f"ls: cannot access '{path}': No such file or directory", file=sys.stderr) + return + except PermissionError: + # If the directory exists but we don't have permission, print an error + print(f"ls: cannot open directory '{path}': Permission denied", file=sys.stderr) + return + + # By default, ls hides "dotfiles" (names starting with "."). + # Only include them if the -a flag (show_all=True) is set. + if show_all: + directory_entries.extend([".",".."]) + else: + #Filter out hidden files + directory_entries = [entry for entry in directory_entries if not entry.startwith(".")] + #output consistent with the default ls behavior. + directory_entries.sort() + + if one_per_line: + # If -1 flag is set, print each entry on its own line + for entry_name in directory_entries: + print(entry_name) + else: + # Default behavior: print entries space-separated on one line + print(" ".join(directory_entries)) + +def main(): + args = parseargs() + # Loop through each directory provided + for directory in args.dirs: + print_directory_entries( + directory, + one_per_line = args.one_per_line, + show_all = args.all + ) +if __name__ == "__main__": + 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 00000000..d6531092 --- /dev/null +++ b/implement-shell-tools/wc/wc.py @@ -0,0 +1,82 @@ +import argparse +import sys +import glob # to find file paths and match patterns + +def parseargs(): + parser = argparse.ArgumentParser( + prog = "wc", + description = "wc clone supporting -l, -w, -c flags" + ) + parser.add_argument("-l", "--lines", action = "store_true",help = "print line counts") + parser.add_argument("-w", "--words", action ="store_true",help = "print word count") + parser.add_argument("-c", "--bytes", action ="store_true", help = "print byte count") + parser.add_argument("files", nargs="+", help="Files to read (shell globs allowed)") + + return parser.parse_args() + + #Uses glob to expand patterns like *.txt. +def expand_files(patterns): + expanded = [] + for p in patterns: + matches = glob.glob(p) + if matches: + expanded.extend(sorted(matches)) # predictable order + else: + print(f"wc: {p}: No such file", file=sys.stderr) #Prints an error if a pattern matches nothing. + return expanded + +def file_counts(filename): + try: + with open(filename, "rb") as f: #opening file in binary mode to be able to count bytes. + data = f.read() + except FileNotFoundError: + print(f"wc: {filename}: No such file", file=sys.stderr) + return None + + text = data.decode("utf-8", errors="ignore") + line_count = text.count("\n") + word_count = len(text.split()) + byte_count = len(data) + + return line_count, word_count, byte_count + +def print_counts(filename, counts, show_lines, show_words, show_bytes): #args.lines is passed into the parameter show_lines and same for other arguments. + line_count, word_count, byte_count = counts + + # If no flags are set, show all three + if not (show_lines or show_words or show_bytes): + print(f"{line_count:7} {word_count:7} {byte_count:7} {filename}") + return + + output = [] + if show_lines: + output.append(f"{line_count:7}") #:7 to ensure nums are rightaligned in a spaced column -just like GNU wc + if show_words: + output.append(f"{word_count:7}") + if show_bytes: + output.append(f"{byte_count:7}") + output.append(filename) + print(" ".join(output)) + +def main(): + args = parseargs() + files = expand_files(args.files) + + total_lines, total_words, total_bytes = 0, 0, 0 + + for file in files: + counts = file_counts(file) + if counts is None: + continue + print_counts(file, counts, args.lines, args.words, args.bytes) + total_lines += counts[0] + total_words += counts[1] + total_bytes += counts[2] + + # If multiple files, print totals + if len(files) > 1: + total_counts = (total_lines, total_words, total_bytes) + print_counts("total", total_counts, args.lines, args.words, args.bytes) + +if __name__ == "__main__": + main() \ No newline at end of file