ZA | 25-SDC-July | Luke Manyamazi | Sprint 4 | Python Implement Shell Tools Exercises#148
Conversation
illicitonion
left a comment
There was a problem hiding this comment.
This is generally looking good, but I left a few things to think about :)
| import glob | ||
| import argparse | ||
|
|
||
| def cat(filepath, n=False, b=False, line_counter=None): |
There was a problem hiding this comment.
n and b are both:
- Not very expressive names
- Seem mutually exclusive - it doesn't make sense for someone to pass both.
Can you think of a way of passing this information into the function which is easier to read, and makes more clear that only one of these should be set?
| else: | ||
| print(line, end='') | ||
| elif n: | ||
| print(f"{line_counter[0]:6}\t{line}", end='') |
There was a problem hiding this comment.
There's some repetition here between the b case and n case - imagine we wanted to start padding the line number by 8 instead of 6 characters - we'd need to change that in two places. Can you think how to avoid that?
There was a problem hiding this comment.
We can create a single helper function for printing a numbered line. This avoids duplicating the f"{line_counter[0]:6}" formatting. Then in cat(), both the “all lines” and “non-empty lines” cases can just call this helper.
|
|
||
| files = [] | ||
| for pattern in args.files: | ||
| files.extend(glob.glob(pattern) or [pattern]) |
There was a problem hiding this comment.
It's interesting you're globbing here - I would expect a shell to handle this. What would break if you removed the glob expansion yourself?
| if b: | ||
| if line.strip(): | ||
| print(f"{line_counter[0]:6}\t{line}", end='') | ||
| line_counter[0] += 1 |
There was a problem hiding this comment.
Why is this value in an array? You always seem to look at exactly one value in the array?
| import glob | ||
| import argparse | ||
|
|
||
| def cat(filepath, n=False, b=False, line_counter=None): |
There was a problem hiding this comment.
It looks like you always pass values for these arguments - why are you supplying default values for them? Particularly for line_counter where if None is used your code will actually break.
| except FileNotFoundError: | ||
| print(f"cat: {filepath}: No such file or directory") | ||
|
|
||
| def main(): |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
This comment still stands - I get different results between your program and the builtin cat.
| import os | ||
| import argparse | ||
|
|
||
| def ls(path='.', one_column=False, show_hidden=False): |
There was a problem hiding this comment.
Same question as above about default args
| if count_words: parts.append(str(len(words))) | ||
| if count_bytes: parts.append(str(bytes_)) | ||
|
|
||
| if not parts: |
There was a problem hiding this comment.
This works, but it feels like it's testing for a side-effect ("We didn't add any paths") rather than the cause ("you didn't ask for any of lines, words, or bytes") - I'd maybe rewrite this if to be framed in what the user requested.
(But the code works fine as it is, too)
| parser.add_argument('paths', nargs='+', help='Files to count') | ||
| args = parser.parse_args() | ||
|
|
||
| for path in args.paths: |
There was a problem hiding this comment.
If I pass multiple files, the real wc outlines a line that you don't - can you add that too?
35a4349 to
ff86f10
Compare
…for all the files
|
cat.py Removed the mutable list [1] hack for line numbering; now cat() returns the updated line number. ls.py wc.py |
|
Hi @illicitonion, please kindly review and see if all the requirements were met. If so please mark the PR as complete. Thank you. |
illicitonion
left a comment
There was a problem hiding this comment.
This is generally looking good, but there are a bunch of small details and consistencies to look at :)
| else: | ||
| print(line, end='') | ||
| except FileNotFoundError: | ||
| print(f"cat: {filepath}: No such file or directory") |
There was a problem hiding this comment.
Does this print to stdout or stderr? Which should it print to? Why?
| except FileNotFoundError: | ||
| print(f"cat: {filepath}: No such file or directory") | ||
|
|
||
| def main(): |
There was a problem hiding this comment.
This comment still stands - I get different results between your program and the builtin cat.
| except NotADirectoryError: | ||
| print(f"ls: cannot access '{path}': Not a directory") | ||
|
|
||
| def main(): |
There was a problem hiding this comment.
Same question about testing here - If I run ls -a I get different files listed than if I run python3 ls.py -a.
| print(f"ls: cannot access '{path}': No such file or directory") | ||
| except NotADirectoryError: | ||
| print(f"ls: cannot access '{path}': Not a directory") |
There was a problem hiding this comment.
Same question about stdout vs stderr
| print(f"ls: cannot access '{path}': No such file or directory") | ||
| except NotADirectoryError: | ||
| print(f"ls: cannot access '{path}': Not a directory") |
There was a problem hiding this comment.
Stretch goal: What happens if you run ls /some/file for a file not a directory? What does your program do?
| parser.add_argument('paths', nargs='+', help='Files to count') | ||
| args = parser.parse_args() | ||
|
|
||
| total_lines = total_words = total_bytes = 0 |
There was a problem hiding this comment.
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.
|
|
||
| if multiple_files: | ||
| parts = [] | ||
| if args.l or not any([args.l, args.w, args.c]): parts.append(str(total_lines)) |
There was a problem hiding this comment.
You're repeating this not any([args.l, args.w, args.c]) three times - can you think of a way to avoid that duplication?
|
|
||
| # Determine what to show | ||
| if not any([count_lines, count_words, count_bytes]): | ||
| count_lines = count_words = count_bytes = True |
There was a problem hiding this comment.
Again, I'd generally avoid putting this all on one line
|
|
||
| print(' '.join(parts), path) | ||
|
|
||
| return (len(lines) if count_lines else 0, |
There was a problem hiding this comment.
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.
| if count_lines: parts.append(str(len(lines))) | ||
| if count_words: parts.append(str(len(words))) | ||
| if count_bytes: parts.append(str(bytes_)) |
There was a problem hiding this comment.
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.
Added error handling for file access issues and improved output formatting.
|
Thanks for the review. I've addressed the remaining comments by:
Please let me know if there is anything else that should be adjusted. |
illicitonion
left a comment
There was a problem hiding this comment.
Sorry for the huge delay in getting back to you on this. Hopefully one last set of comments, which are hopefully useful to think about!
| 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) |
There was a problem hiding this comment.
Personally I would probably to make the if more general here and write this as:
| 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) | |
| if numbering == Numbering.ALL or (numbering == Numbering.NONEMPTY and line.strip()): | |
| line_number = print_numbered_line(line, line_number) | |
| else: | |
| print(line, end='') |
This is because the reason we're picking the two identical behaviours is the same ("We should number this line"), so we ideally only want to write the behaviour once.
And once we've done this, because there's no repetition, I'd probably stop having print_numbered_line return a new number, and instead just:
| 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) | |
| if numbering == Numbering.ALL or (numbering == Numbering.NONEMPTY and line.strip()): | |
| print_numbered_line(line, line_number) | |
| line_number += 1 | |
| else: | |
| print(line, end='') |
| if one_column: | ||
| print(*files, sep='\n') | ||
| else: | ||
| print(*files) |
There was a problem hiding this comment.
This is fine, but an alternative you may want to consider is:
| if one_column: | |
| print(*files, sep='\n') | |
| else: | |
| print(*files) | |
| separator = "\t" | |
| if one_column: | |
| separator = "\n" | |
| print(*files, sep=separator) |
This may be more or less clear, I'm not going to argue strongly one way or the other, but is in keeping with that general "highlight what's the same and what's different" idea.
| return (0, 0, 0) | ||
|
|
||
| except IsADirectoryError: | ||
| print( |
There was a problem hiding this comment.
If this happens, what will the exit code of the process be? What should it be?
(Same question for FileNotFoundError)
| if args.c or show_all: | ||
| parts.append(str(total_bytes)) | ||
|
|
||
| print(' '.join(parts), 'total') |
There was a problem hiding this comment.
This is repetitive with the formatting code, but as it's currently structured it's quite fiddly to deduplicate. Here's a set of steps that may help:
- Move the "if none of the flags are set, set them all to true" code into just after
args = parser.parse_args. Actually updateargs.landargs.wandargs.cas a result. Then you don't need theor show_allany more. - Extract a function
def print_stats(args, lines, words, bytes, filename)which you can call from both places.
Learners, PR Template
Self checklist
Changelist
This PR implements basic versions of the following Unix commands in Python:
These scripts were designed to mimic standard command-line tools and handle typical edge cases like missing files and glob patterns.
Questions