Skip to content

London | 26-SDC-Mar | Mariia Serhiienko | Sprint 3 | Implement shell tools#499

Open
Konvaly wants to merge 5 commits intoCodeYourFuture:mainfrom
Konvaly:implement-shell-tools
Open

London | 26-SDC-Mar | Mariia Serhiienko | Sprint 3 | Implement shell tools#499
Konvaly wants to merge 5 commits intoCodeYourFuture:mainfrom
Konvaly:implement-shell-tools

Conversation

@Konvaly
Copy link
Copy Markdown

@Konvaly Konvaly commented Apr 17, 2026

Learners, PR Template

Self checklist

  • I have titled my PR with Region | Cohort | FirstName LastName | Sprint | Assignment Title
  • My changes meet the requirements of the task
  • I have tested my changes
  • My changes follow the style guide

Changelist

Implemented three shell tools in NodeJS:

  • cat - prints file contents, supports -n (number all lines) and -b (number non-blank lines only)
  • ls - lists directory contents, supports -1 (one per line) and -a (show hidden files)
  • wc - counts lines, words and bytes in files, supports -l, -w, -c flags and multiple files with a total row

Each tool matches the output of the real Unix commands.

@Konvaly Konvaly added the Needs Review Trainee to add when requesting review. PRs without this label will not be reviewed. label Apr 17, 2026
@illicitonion illicitonion added Review in progress This review is currently being reviewed. This label will be replaced by "Reviewed" soon. and removed Needs Review Trainee to add when requesting review. PRs without this label will not be reviewed. labels Apr 21, 2026
Copy link
Copy Markdown
Member

@illicitonion illicitonion left a comment

Choose a reason for hiding this comment

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

Generally looking good, but some bits of duplication to think about :)

Comment on lines +6 to +11
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");
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.

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.

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.

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) {
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.

These are really clear variable names, which makes this if statement read very clearly - well done!

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.

Thank you!

Comment thread implement-shell-tools/cat/cat.js Outdated
Comment on lines +28 to +33
let isLastLine;
if (i === lines.length - 1) {
isLastLine = true;
} else {
isLastLine = false;
}
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 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;

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.

Ah yes, that makes total sense! I've simplified it. Much cleaner, thanks for the tip!

Comment thread implement-shell-tools/cat/cat.js Outdated
break;
}

if (showAllLineNumbers) {
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.

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?

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, 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.

Comment thread implement-shell-tools/ls/ls.js Outdated

const paths = args.filter((arg) => !arg.startsWith("-"));

const targetPath = paths.length > 0 ? paths[0] : process.cwd();
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.

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?

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.

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.

Comment thread implement-shell-tools/ls/ls.js Outdated
Comment on lines +23 to +25
const cleanA = a.replace(/^\.+/, "");
const cleanB = b.replace(/^\.+/, "");
return cleanA.localeCompare(cleanB);
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's this "cleaning" doing? What would break if you removed it?

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.

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.

Comment thread implement-shell-tools/wc/wc.js Outdated
Comment on lines +59 to +67
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");
}
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 is very repetitive - can you think how to make this less repetitive?

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.

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.

Comment thread implement-shell-tools/wc/wc.js Outdated
Comment on lines +61 to +63
} else if (showLines) {
process.stdout.write(formatLine([lines], filePath) + "\n");
} else if (showWords) {
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 happens with the real wc if I pass multiple flags (e.g. wc -wc /some/file)? What does your implementation do?

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.

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 illicitonion added Reviewed Volunteer to add when completing a review with trainee action still to take. and removed Review in progress This review is currently being reviewed. This label will be replaced by "Reviewed" soon. labels Apr 21, 2026
@Konvaly Konvaly added Needs Review Trainee to add when requesting review. PRs without this label will not be reviewed. and removed Reviewed Volunteer to add when completing a review with trainee action still to take. labels Apr 27, 2026
Copy link
Copy Markdown
Member

@illicitonion illicitonion left a comment

Choose a reason for hiding this comment

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

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)}'`);
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.

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(
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.

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?

Comment on lines +6 to +29
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);
}
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 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.

Comment on lines +62 to +64
if (noSpecificFlag || showLines) counts.push(lines);
if (noSpecificFlag || showWords) counts.push(words);
if (noSpecificFlag || showBytes) counts.push(bytes);
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.

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?

@illicitonion illicitonion added Reviewed Volunteer to add when completing a review with trainee action still to take. and removed Needs Review Trainee to add when requesting review. PRs without this label will not be reviewed. labels Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Reviewed Volunteer to add when completing a review with trainee action still to take.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants