Skip to content

LONDON SDC | Anna Fedyna | Module-Tools | Week 4 | Implement Shell Tools in Python #51

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
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
29 changes: 29 additions & 0 deletions implement-shell-tools/cat/cat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import argparse

parser = argparse.ArgumentParser(
prog='Cat Command',
description='Display and concatenate files contents')

parser.add_argument("-n", help="Number the output lines, starting at 1.", default = None, action="store_true")
parser.add_argument("-b", help="Number the non-blank output lines, starting at 1.", default = None, action="store_true")
Comment on lines +7 to +8

Choose a reason for hiding this comment

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

What happens if neither -n nor -b are used?
For instance, what will your solution print if we invoke it like this: python3 cat.py sample-files/1.txt?

parser.add_argument("paths", nargs="+", help="The file to search")

args = parser.parse_args()

for path in args.paths:
try:
with open(path, "r") as f:
content = f.read()
if args.n or args.b :
counter = 1
arr = content.split('\n')[:-1]
for line in arr:
if args.b and not line:
print(line)
continue
print(counter, line)
counter += 1
except FileNotFoundError:
print(f"Error: File '{path}' not found.")
Comment on lines +26 to +27

Choose a reason for hiding this comment

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

Very good catching of the error cases, this really improves the user's experience. Congrats!

except Exception as e:
print(f"An error occurred: {e}")
45 changes: 45 additions & 0 deletions implement-shell-tools/ls/ls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import argparse
import os

parser = argparse.ArgumentParser(
prog='ls command',
description='list directory contents')

parser.add_argument("-1", "--one", help="Force output to be one entry per line.", default = None, action="store_true")
parser.add_argument("-a", help="Include directory entries whose names begin with a dot (.).", default = None, action="store_true")

Choose a reason for hiding this comment

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

When I run the ls utility with -a, the current directory (.) and parent directory (..) are printed:

$ ls -a sample-files
.               ..              .hidden.txt     1.txt           2.txt           3.txt           dir

How could you change your solution to make sure they are printed?

parser.add_argument("paths", nargs="*", help="The file to search")

args = parser.parse_args()
cwd = os.getcwd()

def list_docs(dir_to_procccess):
try:
documents = [document for document in os.listdir(dir_to_procccess)]
doc_to_output = []
for doc in documents:
if not args.a and doc.startswith("."):
continue
else:
doc_to_output.append(doc)
doc_to_output.sort()
if args.one:
for doc in doc_to_output:
print(doc)
else:
print(' '.join(doc_to_output))

Choose a reason for hiding this comment

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

In ls, files are printed with a certain amount of padding, so that if we have many files, they appear to be in a table:

➜ /bin/ls
ls.js                   package-lock.json           README.md
node_modules            package.json                sample-files

This is not part of the assignment brief but, as a streacth goal, would you like to consider how to pad the outputs like that?

except FileNotFoundError:
print(f"Error: Directory '{dir_to_procccess}' not found.")
except NotADirectoryError:
print(f"Error: '{dir_to_procccess}' is not a directory.")
except Exception as e:
print(f"An unexpected error occurred: {e}")

if args.paths:
for directory in args.paths:
try:
dir_to_procccess = os.path.join(cwd, directory)
list_docs(dir_to_procccess)
except Exception as e:
print(f"An unexpected error occurred: {e}")
else:
list_docs(cwd)
Comment on lines +44 to +45

Choose a reason for hiding this comment

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

Good catch on the behaviour of ls without arguments! Well done.

72 changes: 72 additions & 0 deletions implement-shell-tools/wc/wc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import argparse
import os
import re

parser = argparse.ArgumentParser(
prog='wc command',
description='word, line, character, and byte count')

parser.add_argument("-c", "--bytes", help="The number of bytes in each input file is written to the standard output.", default = None, action="store_true")
parser.add_argument("-l", "--lines", help="The number of lines in each input file is written to the standard output.", default = None, action="store_true")
parser.add_argument("-w", "--words", help="The number of words in each input file is written to the standard output.", default = None, action="store_true")
parser.add_argument("paths", nargs="+", help="The file to search")

args = parser.parse_args()
cwd = os.getcwd()


def count_lines(content):
return len(content.splitlines())

def count_words(content):
return len(re.findall("[a-zA-Z\-\.'/]+", content))

Choose a reason for hiding this comment

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

Whenever I run this solution, I see the following warning:

implement-shell-tools/wc/wc.py:22: SyntaxWarning: invalid escape sequence '\-'
  return len(re.findall("[a-zA-Z\-\.'/]+", content))

While the code seems to work even with the warning, could you elaborate on what could be the reason for the warning?


def count_bytes(content):
return len(content.encode('utf-8'))

def process_file_content(content, path):
if not args.lines and not args.words and not args.bytes:
global no_options
no_options = True
lines_in_file = count_lines(content)
words_in_file = count_words(content)
bytes_in_file = count_bytes(content)
print(lines_in_file, words_in_file, bytes_in_file, path)
return {'lines': lines_in_file, 'words':words_in_file, 'bytes':bytes_in_file}
output = {}
if args.lines:
output['lines'] = count_lines(content)
if args.words:
output['words'] = count_words(content)
if args.bytes:
output['bytes'] = count_bytes(content)
print(' '.join([ str(val) for val in output.values()]), path)
return output

total_lines, total_words, total_bytes = 0, 0, 0
for path in args.paths:
try:
file_path_to_procccess = os.path.join(cwd, path)

Choose a reason for hiding this comment

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

Suggested change
file_path_to_procccess = os.path.join(cwd, path)
file_path_to_proccess = os.path.join(cwd, path)

Typo: There was an extra c :D

This is not relevant for the code working, but in a real code review it's likely to get pointed out, so I do it here as well. If you decide to change it, note that you'll have to change every place where that variable is used.

if os.path.isfile(file_path_to_procccess) :
with open(file_path_to_procccess, 'r') as f:
file_content = f.read()
dict_output = process_file_content(file_content, path)
total_lines += dict_output['lines'] if args.lines or no_options else 0
total_words += dict_output['words'] if args.words or no_options else 0
total_bytes += dict_output['bytes'] if args.bytes or no_options else 0
elif os.path.isdir(file_path_to_procccess):
print(f"{path}: Is a directory")
except FileNotFoundError:
print(f"Error: File '{file_path_to_procccess}' not found.")
Comment on lines +59 to +60

Choose a reason for hiding this comment

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

What happens if I pass three files, but the second one is missing? How will that affect the output?
For a stretch goal: How would you make it so that all errors are displayed at the end, instead of intermingled throughout the output?

except Exception as e:
print(f"An unexpected error occurred: {e}")

if len(args.paths) > 1:

Choose a reason for hiding this comment

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

When I pass more than one line and any flag, I get the following error: An unexpected error occurred: name 'no_options' is not defined.

For instance, with this invocation:

➜ python3  wc.py -l sample-files/*
1 sample-files/1.txt
An unexpected error occurred: name 'no_options' is not defined
1 sample-files/2.txt
An unexpected error occurred: name 'no_options' is not defined
5 sample-files/3.txt
An unexpected error occurred: name 'no_options' is not defined
Traceback (most recent call last):
  File "/Users/blorente/code/github.com/CodeYourFuture/Module-Tools/implement-shell-tools/wc/wc.py", line 65, in <module>
    if no_options:
       ^^^^^^^^^^
NameError: name 'no_options' is not defined

What could be the cause of this?

if no_options:
print(total_lines, total_words, total_bytes, 'total')
else:
total_output_line = []
if args.lines: total_output_line.append(str(total_lines))
if args.words: total_output_line.append(str(total_words))
if args.bytes: total_output_line.append(str(total_bytes))
print(' '.join(total_output_line), 'total')