Skip to content
Open
93 changes: 93 additions & 0 deletions implement-shell-tools/cat/cat.py
Original file line numberDiff line numberDiff line change
@@ -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",) 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()
80 changes: 80 additions & 0 deletions implement-shell-tools/ls/ls.py
Original file line numberDiff line numberDiff line change
@@ -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()
1 change: 1 addition & 0 deletions implement-shell-tools/wc/requirements.txt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
argparse
Loading