London | 26-SDC-Mar | Mariia Serhiienko | Sprint 3 | Implement shell tools#499
London | 26-SDC-Mar | Mariia Serhiienko | Sprint 3 | Implement shell tools#499Konvaly wants to merge 5 commits intoCodeYourFuture:mainfrom
Conversation
illicitonion
left a comment
There was a problem hiding this comment.
Generally looking good, but some bits of duplication to think about :)
| if (args.length === 0) { | ||
| console.error("Usage: node cat.js [-n] [-b] <file...>"); | ||
| process.exit(1); | ||
| } | ||
| const showAllLineNumbers = args.includes("-n"); | ||
| const showNonBlankNumbers = args.includes("-b"); |
There was a problem hiding this comment.
Imagine if I called this programme with a different flag, e.g. node cat.js -x /some/file. I probably passed that -x flag expecting it would do something. But your programme just ignores it.
What do you think we should do if we run into a flag our programme doesn't understand?
This applies to all three tools.
There was a problem hiding this comment.
Thanks for pointing this out! You're right, silently ignoring unknown flags is confusing for the user. I've added a check in all three tools that shows an error message and exits if the user passes a flag we don't recognise. For example, node cat.js -x file.txt will now print cat: invalid option -- 'x' instead of just running as if nothing happened.
| for (const filePath of filePaths) { | ||
| const content = await fs.readFile(filePath, "utf-8"); | ||
|
|
||
| if (!showAllLineNumbers && !showNonBlankNumbers) { |
There was a problem hiding this comment.
These are really clear variable names, which makes this if statement read very clearly - well done!
| let isLastLine; | ||
| if (i === lines.length - 1) { | ||
| isLastLine = true; | ||
| } else { | ||
| isLastLine = false; | ||
| } |
There was a problem hiding this comment.
This could be a bit more concise - generally every time you have code of the form:
if (condition) {
true
} else {
false
}you can probably use the fact that condition is a boolean, to just use that expression. e.g.
const isLastLine = i === lines.length - 1;There was a problem hiding this comment.
Ah yes, that makes total sense! I've simplified it. Much cleaner, thanks for the tip!
| break; | ||
| } | ||
|
|
||
| if (showAllLineNumbers) { |
There was a problem hiding this comment.
The code in these two branches is quite repetitive - both do padding, hard-code what number we're padding, etc.
Can you think how to write this in such a way that we don't need to repeat e.g. the padding twice?
There was a problem hiding this comment.
You're right, I've refactored it. Now I first figure out whether the line needs a number using const needsLineNumber = showAllLineNumbers || (showNonBlankNumbers && !isBlankLine), and then do the padding only once inside a single if block. This way if I ever need to change the formatting, I only change it in one place.
|
|
||
| const paths = args.filter((arg) => !arg.startsWith("-")); | ||
|
|
||
| const targetPath = paths.length > 0 ? paths[0] : process.cwd(); |
There was a problem hiding this comment.
If I call this programme with more than one argument (e.g. ls /tmp /dev), it will ignore the second one and just list the first. As a user, what would you expect to happen here?
There was a problem hiding this comment.
I'd definitely expect it to show both directories. I've fixed it , now it loops through all the paths, not just the first one. It also shows directory names as headers when there's more than one, like the real ls does.
| const cleanA = a.replace(/^\.+/, ""); | ||
| const cleanB = b.replace(/^\.+/, ""); | ||
| return cleanA.localeCompare(cleanB); |
There was a problem hiding this comment.
What's this "cleaning" doing? What would break if you removed it?
There was a problem hiding this comment.
This removes leading dots from filenames before comparing them in the sort. Without it .hidden.txt would appear before 1.txt because . comes before numbers in ASCII.
| if (noSpecificFlag) { | ||
| process.stdout.write(formatLine([lines, words, bytes], filePath) + "\n"); | ||
| } else if (showLines) { | ||
| process.stdout.write(formatLine([lines], filePath) + "\n"); | ||
| } else if (showWords) { | ||
| process.stdout.write(formatLine([words], filePath) + "\n"); | ||
| } else if (showBytes) { | ||
| process.stdout.write(formatLine([bytes], filePath) + "\n"); | ||
| } |
There was a problem hiding this comment.
This is very repetitive - can you think how to make this less repetitive?
There was a problem hiding this comment.
Agreed. I pulled the logic into a getCounts() function that builds the right array depending on which flags are set. So now instead of four near-identical branches, it's just two lines.
| } else if (showLines) { | ||
| process.stdout.write(formatLine([lines], filePath) + "\n"); | ||
| } else if (showWords) { |
There was a problem hiding this comment.
What happens with the real wc if I pass multiple flags (e.g. wc -wc /some/file)? What does your implementation do?
There was a problem hiding this comment.
Real wc -wc shows both words and bytes. Mine didn't because the else if meant only one could win. Fixed it, combined flags like -wc and -lw work now.
illicitonion
left a comment
There was a problem hiding this comment.
This is looking great, well done!
A couple of small things to pick up, and then a few things to think about that don't block marking this as complete :)
| ); | ||
|
|
||
| if (unknownFlags.length > 0) { | ||
| console.error(`cat: invalid option -- '${unknownFlags[0].slice(1)}'`); |
There was a problem hiding this comment.
Optional: It's often useful to tell people about all of the problems you find, if it's easy to do so, so they can fix all of them - the experience of running:
% cat -f -x /tmp/t
cat: invalid option -- f
% cat -x /tmp/t
cat: invalid option: -- x
% cat /tmp/t
It worked!is quite frustrating compared to:
% cat -f -x /tmp/t
cat: invalid options: f, x
% cat /tmp/t
It worked!| try { | ||
| entries = await fs.readdir(targetPath); | ||
| } catch (error) { | ||
| console.error( |
There was a problem hiding this comment.
Currently if there's an error (e.g. if you specify a path that doesn't exist), the exit code of this program will be 0. Is that what we want?
| const expandedArgs = []; | ||
| for (const arg of args) { | ||
| if (arg.startsWith("-") && arg.length > 2) { | ||
| for (const char of arg.slice(1)) { | ||
| expandedArgs.push(`-${char}`); | ||
| } | ||
| } else { | ||
| expandedArgs.push(arg); | ||
| } | ||
| } | ||
|
|
||
| const showLines = expandedArgs.includes("-l"); | ||
| const showWords = expandedArgs.includes("-w"); | ||
| const showBytes = expandedArgs.includes("-c"); | ||
|
|
||
| const supportedFlags = ["-l", "-w", "-c"]; | ||
| const unknownFlags = expandedArgs.filter( | ||
| (arg) => arg.startsWith("-") && !supportedFlags.includes(arg), | ||
| ); | ||
|
|
||
| if (unknownFlags.length > 0) { | ||
| console.error(`wc: invalid option -- '${unknownFlags[0].slice(1)}'`); | ||
| process.exit(1); | ||
| } |
There was a problem hiding this comment.
This is good code, and works well considering different edge cases (well done!), but is a lot of code you've had to write to handle this :) you don't need to for this exercise, but you may want to look at a library like https://github.com/tj/commander.js which can do a lot of this for you.
| if (noSpecificFlag || showLines) counts.push(lines); | ||
| if (noSpecificFlag || showWords) counts.push(words); | ||
| if (noSpecificFlag || showBytes) counts.push(bytes); |
There was a problem hiding this comment.
These work, but quite tightly couple the CLI flags to the behaviour here. We don't need to use only what flags we happened to expose to users when writing our logic - we can transform things ourselves. Can you think how you could set up showLines, showWords and showBytes when parsing the flags so that these checks would just read:
if (showLines) counts.push(lines);
if (showWords) counts.push(words);
if (showBytes) counts.push(bytes);but still work correctly when not flags were passed?
Learners, PR Template
Self checklist
Changelist
Implemented three shell tools in NodeJS:
Each tool matches the output of the real Unix commands.