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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
venv
testoutput.txt
1 change: 1 addition & 0 deletions implement-shell-tools/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.venv/
89 changes: 89 additions & 0 deletions implement-shell-tools/cat/mycat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env python3

import sys
import glob

def main():
args = sys.argv[1:]

if not args:
print("Usage: cat [-n|-b] file...", file=sys.stderr)
sys.exit(1)

number_all = False
number_nonempty = False
paths = []

for a in args:
if a == "-n":
number_all = True
elif a == "-b":
number_nonempty = True
else:
paths.append(a)

if number_nonempty:
number_all = False
Comment on lines +17 to +26

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This kind of relationship where different options/states are related but exclusive can be hard to follow. (Here, it should never be the case that number_all and number_nonempty are both True - it's an invalid state in the program).

Instead of this, we sometimes use enums for this (or can use strings as enums). Consider a single variable number_mode which could be set to either "none", "non_empty" or "all" - here we don't need to think about what both True mean - we just have one variable which could have one of three variables. What do you think about this?


files = expand_paths(paths)

had_error = print_lines(
files,
number_all=number_all,
number_nonempty=number_nonempty,
)

if had_error:
sys.exit(1)


def expand_paths(paths):
"""Expand glob patterns and return sorted unique file list."""
files = []
for p in paths:
matches = glob.glob(p)
if matches:
files.extend(matches)
else:
files.append(p) # keep as-is (will error later if missing)
return sorted(files)

def read_lines(file):
with open(file, "r", encoding="utf-8") as f:
return f.readlines()

def print_lines(files, number_all=False, number_nonempty=False):
had_error = False
line_no = 1
for file in files:
try:
lines = read_lines(file)
except FileNotFoundError:
print(f"cat: {file}: No such file or directory", file=sys.stderr)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good job printing to stderr not stdout :)

However! If you ran into an issue here, I think your program will still exit with exit code 0. What exit code do you think it should exit with?

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.

You're right. A missing file should result in a non-zero exit code to indicate an error. I've updated the code to exit with code 1 if any file cannot be read.

had_error = True
continue

for line in lines:
is_empty = (line.strip() == "")

if number_nonempty:
if not is_empty:
prefix = f"{line_no:6}\t"
line_no += 1
else:
prefix = ""
elif number_all:
prefix = f"{line_no:6}\t"
line_no += 1
else:
prefix = ""

# avoid double newlines: line already includes '\n'
sys.stdout.write(prefix + line)

return had_error

if __name__ == "__main__":
main()


65 changes: 65 additions & 0 deletions implement-shell-tools/ls/my-ls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env python3

import sys
import os



def main():
args = sys.argv[1:]

show_all = False
one_per_line = False
paths = []

for a in args:
if a == "-a":
show_all = True
elif a == "-1":
one_per_line = True
else:
paths.append(a)

if not paths:
paths = ["."]

had_error = False

for path in paths:
if list_dir(
path,
show_all=show_all,
one_per_line=one_per_line,
):
had_error = True

if had_error:
sys.exit(1)


def list_dir(path, show_all=False, one_per_line=False):
try:
entries = os.listdir(path)
except FileNotFoundError:
print(f"ls: cannot access '{path}': No such file or directory",
file=sys.stderr,
)
return True

entries = sorted(entries)

if show_all:
normal = sorted([e for e in entries if not e.startswith('.')])
hidden = sorted([e for e in entries if e.startswith('.')])

entries = [".", ".."] + normal + hidden
else:
entries = sorted([e for e in entries if not e.startswith('.')])

for entry in entries:
print(entry)

return False

if __name__ == "__main__":
main()
104 changes: 104 additions & 0 deletions implement-shell-tools/wc/my-wc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env python3

import sys
import glob

def count_file(path):
with open(path, "rb") as f:
content = f.read()

byte_count = len(content)
text = content.decode("utf-8", errors="ignore")

line_count = text.count("\n")
word_count = len(text.split())

return line_count, word_count, byte_count

def expand(paths):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In general you shouldn't need to do this kind of glob expansion yourself - the shell should do this for you and expand the globs into different file names when calling your program. You can test this out by printing what sys.argv is when you run python3 my-wc.py some*glob - look at what arguments you actually get :)

files = []
for p in paths:
matches = glob.glob(p)
if matches:
files.extend(matches)
else:
files.append(p)
return files

def main():
args = sys.argv[1:]

show_l = False
show_w = False
show_c = False

paths = []


# parse args
for a in args:
if a == "-l":
show_l = True
elif a == "-w":
show_w = True
elif a == "-c":
show_c = True
else:
paths.append(a)

# default: show all
if not (show_l or show_w or show_c):
show_l = show_w = show_c = True

files = expand(paths)

total_l = 0
total_w = 0
total_c = 0

results = []

for file in files:
try:
l, w, c = count_file(file)
except FileNotFoundError:
print(f"wc: {file}: No such file or directory", file=sys.stderr)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What exit code will you exit with if you failed to read one file?

continue

total_l += l
total_w += w
total_c += c

results.append((l,w,c, file))

# print per-file results (GNU-aligned formatting)
for l, w, c, file in results:

parts = []
if show_l:
parts.append(f"{l:3}")
if show_w:
parts.append(f"{w:4}")
if show_c:
parts.append(f"{c:4}")


print("".join(parts) + " " + file)

# print total if multiple files
if len(results) > 1:
parts = []

if show_l:
parts.append(f"{total_l:3}")
if show_w:
parts.append(f"{total_w:4}")
if show_c:
parts.append(f"{total_c:4}")


print("".join(parts) + " total")


if __name__ == "__main__":
main()
Loading