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
52 changes: 52 additions & 0 deletions implement-shell-tools/cat/cat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import argparse
from enum import Enum

class Numbering(Enum):
NONE = 0
ALL = 1
NONEMPTY = 2

def print_numbered_line(line, line_number, pad=6):
print(f"{line_number:{pad}}\t{line}", end='')
return line_number + 1

def cat(filepath, numbering, start_line):
line_number = start_line
try:
with open(filepath) as f:
for line in f:
if numbering == Numbering.NONEMPTY:
if line.strip():
line_number = print_numbered_line(line, line_number)
else:
print(line, end='')
elif numbering == Numbering.ALL:
line_number = print_numbered_line(line, line_number)
else:
print(line, end='')
except FileNotFoundError:
print(f"cat: {filepath}: No such file or directory")
Copy link
Member

Choose a reason for hiding this comment

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

Does this print to stdout or stderr? Which should it print to? Why?

return line_number

def main():
Copy link
Member

Choose a reason for hiding this comment

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

How did you test this implementation?

I created two files and tried using cat -n /file/1 /file/2 and cat -b /file/1 /file/2 and compared the output with using your script, and didn't always get the same results

Copy link
Member

Choose a reason for hiding this comment

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

This comment still stands - I get different results between your program and the builtin cat.

parser = argparse.ArgumentParser(description="Concatenate files and print on the standard output.")
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('files', nargs='+', help='files to concatenate')
args = parser.parse_args()

if args.n and args.b:
parser.error("options -n and -b are mutually exclusive")
elif args.n:
numbering = Numbering.ALL
elif args.b:
numbering = Numbering.NONEMPTY
else:
numbering = Numbering.NONE

line_number = 1 # start line numbering
for file in args.files:
line_number = cat(file, numbering=numbering, start_line=line_number)

if __name__ == "__main__":
main()
31 changes: 31 additions & 0 deletions implement-shell-tools/ls/ls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import os
import argparse

def ls(path, one_column, show_hidden):
"""List files in a directory, optionally in one column or including hidden files."""
try:
files = os.listdir(path)
if not show_hidden:
files = [f for f in files if not f.startswith('.')]
files.sort()

if one_column:
print(*files, sep='\n')
else:
print(*files)
except FileNotFoundError:
print(f"ls: cannot access '{path}': No such file or directory")
except NotADirectoryError:
print(f"ls: cannot access '{path}': Not a directory")
Comment on lines +17 to +19
Copy link
Member

Choose a reason for hiding this comment

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

Same question about stdout vs stderr

Comment on lines +17 to +19
Copy link
Member

Choose a reason for hiding this comment

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

Stretch goal: What happens if you run ls /some/file for a file not a directory? What does your program do?


def main():
Copy link
Member

Choose a reason for hiding this comment

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

Same question about testing here - If I run ls -a I get different files listed than if I run python3 ls.py -a.

parser = argparse.ArgumentParser()
parser.add_argument('-1', dest='one_column', action='store_true', help='list one file per line')
parser.add_argument('-a', action='store_true', help='show hidden files')
parser.add_argument('path', nargs='?', default='.', help='directory to list')
args = parser.parse_args()

ls(args.path, args.one_column, args.a)

if __name__ == "__main__":
main()
58 changes: 58 additions & 0 deletions implement-shell-tools/wc/wc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import argparse

def wc(path, count_lines, count_words, count_bytes):
"""Count lines, words, and bytes for a single file."""
try:
with open(path, 'r') as f:
content = f.read()
lines = content.splitlines()
words = content.split()
bytes_ = len(content.encode('utf-8'))

# Determine what to show
if not any([count_lines, count_words, count_bytes]):
count_lines = count_words = count_bytes = True
Copy link
Member

Choose a reason for hiding this comment

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

Again, I'd generally avoid putting this all on one line


parts = []
if count_lines: parts.append(str(len(lines)))
if count_words: parts.append(str(len(words)))
if count_bytes: parts.append(str(bytes_))
Comment on lines +17 to +19
Copy link
Member

Choose a reason for hiding this comment

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

It's a little unusual that two of these have len calls and the last doesn't - not a big deal, but a bit oddly inconsistent - I would maybe make these consistent.


print(' '.join(parts), path)

return (len(lines) if count_lines else 0,
Copy link
Member

Choose a reason for hiding this comment

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

It's interesting that you're doing this filtering twice - you're checking whether we should output lines both inside this function and when printing the totals.

Personally, I would probably just always return the values here, and let the caller decide whether to show them.

len(words) if count_words else 0,
bytes_ if count_bytes else 0)
except FileNotFoundError:
print(f"wc: {path}: No such file or directory")
return (0, 0, 0)
except IsADirectoryError:
print(f"wc: {path}: Is a directory")
return (0, 0, 0)

def main():
parser = argparse.ArgumentParser()
parser.add_argument('-l', action='store_true', help='Count lines')
parser.add_argument('-w', action='store_true', help='Count words')
parser.add_argument('-c', action='store_true', help='Count bytes')
parser.add_argument('paths', nargs='+', help='Files to count')
args = parser.parse_args()

total_lines = total_words = total_bytes = 0
Copy link
Member

Choose a reason for hiding this comment

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

It's generally pretty unusual to initialise multiple variables like this in one go, because it makes it harder to skim the code to see where a particular variable was set/initialised than initialising each on its own line.

multiple_files = len(args.paths) > 1

for path in args.paths:
Copy link
Member

Choose a reason for hiding this comment

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

If I pass multiple files, the real wc outlines a line that you don't - can you add that too?

l, w, b = wc(path, args.l, args.w, args.c)
total_lines += l
total_words += w
total_bytes += b

if multiple_files:
parts = []
if args.l or not any([args.l, args.w, args.c]): parts.append(str(total_lines))
Copy link
Member

Choose a reason for hiding this comment

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

You're repeating this not any([args.l, args.w, args.c]) three times - can you think of a way to avoid that duplication?

if args.w or not any([args.l, args.w, args.c]): parts.append(str(total_words))
if args.c or not any([args.l, args.w, args.c]): parts.append(str(total_bytes))
print(' '.join(parts), 'total')

if __name__ == "__main__":
main()