Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions implement-shell-tools/cat/cat.py
Original file line number Diff line number Diff 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):
Comment thread
RaihanSharif marked this conversation as resolved.
"""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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

format_lines numbers with enumerate(..., start=1) and is called fresh for each file in cat_file. Run cat -n
sample-files/*.txt against the real cat - does the count reset to 1 at each file, or keep climbing? Where would the counter need to live to carry across files?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the numbering starts from 1 for each file in the cat input

cat -n sample-files/*
     1  Once upon a time...
     1  There was a house made of gingerbread.
     1  It looked delicious.
     2  I was tempted to take a bite of it.
     3  But this seemed like a bad idea...
     4
     5  There's more to come, though...

python cat:

python3 cat.py -n sample-files/*
     1  Once upon a time...
     1  There was a house made of gingerbread.
     1  It looked delicious.
     2  I was tempted to take a bite of it.
     3  But this seemed like a bad idea...
     4
     5  There's more to come, though...

You asked this in the nodeJS version of cat as well. Are you hinting at something, and I'm just not getting it?

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 number Diff line number Diff 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()
71 changes: 71 additions & 0 deletions implement-shell-tools/wc/wc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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")
parser.add_argument("-w", action="store_true", help="show word count")
parser.add_argument("-c", action="store_true", help="show byte count")

parser.add_argument("paths", nargs="*", help="file(s) for which to show data")

args = parser.parse_args()

totals = {"l": 0, "w": 0, "c": 0}

# if no flags then set all flags to true, same as in real wc
if (not args.l and not args.w and not args.c):
args.l = args.w = args.c = True

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", file=sys.stderr)
elif (os.path.isfile(path)):
output_str = ""
with open(path, "r", encoding="utf-8") as file:
content = file.read()

lines = content.split('\n')

if (args.l):
#if (len(lines)) > 0:
# lines[-1].strip()

line_count = content.count('\n')
totals["l"] += line_count
output_str += f"{line_count:8}"

if (args.w):
word_count = len(content.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)