Skip to content

London | 26-SDC-Mar | Ebrahim Beiati-Asl | Sprint 3 | implements-shell-tool#376

Open
ebrahimbeiati wants to merge 4 commits intoCodeYourFuture:mainfrom
ebrahimbeiati:implement-shell
Open

London | 26-SDC-Mar | Ebrahim Beiati-Asl | Sprint 3 | implements-shell-tool#376
ebrahimbeiati wants to merge 4 commits intoCodeYourFuture:mainfrom
ebrahimbeiati:implement-shell

Conversation

@ebrahimbeiati
Copy link

@ebrahimbeiati ebrahimbeiati commented Mar 14, 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

Exercise for implement-shell-tools

@github-actions

This comment has been minimized.

@ebrahimbeiati ebrahimbeiati added the Needs Review Trainee to add when requesting review. PRs without this label will not be reviewed. label Mar 14, 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 Mar 16, 2026
Copy link
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 generally looks good, but I left a few comments to consider. Well done!

Comment on lines +50 to +61
if(arg === '-n') {
options.numberAll = true;
} else if(arg === '-b') {
options.numberNonEmpty = true;
} else {
filePatterns.push(arg);
}
});
// -b takes precedence over -n
if(options.numberNonEmpty) {
options.numberAll = false;
}
Copy link
Member

Choose a reason for hiding this comment

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

Rather than having two different options which are mutually exclusive, I would probably model this as one option with three possible values: options.numberMode = "off" | "all" | "non-empty". That way you don't need to re-set numberAll if actually numberNonEmpty - you just have one variable you set correctly up-front and which you can check.

process.exit(1);
}

const files = filePatterns;
Copy link
Member

Choose a reason for hiding this comment

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

Why did you call this filePatterns just to rename it to files later? Why not just name it files or fileNames from the start?

Comment on lines +63 to +71
if(filePatterns.length === 0) {
console.log("cat: missing file operand");
process.exit(1);
}

const files = filePatterns;

files.forEach((file) => {
const resolvedPath = path.resolve(process.cwd(), file);
Copy link
Member

Choose a reason for hiding this comment

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

What advantage does path.resolve(process.cwd(), file) have over just using file directly?

const names = filtered.map(entry => entry.name);
console.log(names.join(' '));
}
} catch (error) {
Copy link
Member

Choose a reason for hiding this comment

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

This indentation isn't correct - please format all of your code before submitting for review. I'd recommend setting up your IDE to auto-format your code on save to avoid needing to remember to do this.

if(options.word) parts.push(counts.words);
if(options.byte) parts.push(counts.bytes);
//if no specific count options are provided, print all counts
if(!options.line && !options.word && !options.byte) {
Copy link
Member

Choose a reason for hiding this comment

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

It may be simpler to do this check when setting up options, rather than when consuming it - i.e. in your options parsing to see "if no options were set, set all to true" - that way the rest of your code doesn't need to think about this special case, they can just handle wc foo and wc -l -w -c foo exactly the same.

(This about this as an abstraction boundary - the point of parsing command line flags into an options object is so that the rest of your code doesn't need to worry about the specifics of flags, this is just one more example of a place you can avoid your code needing to know the specifics of flags)

const directories = args.filter(arg => arg !== '-a' && arg !== '-1');

// If no directory is specified, list the current directory
if(directories.length === 0) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you think how to fold the "no directories" case into the main loop so you don't have to have two separate calls to listDirectory in this function?

Comment on lines +13 to +34
lines.forEach((line) => {
if(options.numberNonEmpty) {
//-b option: number non-empty lines
if(line.trim()) {
process.stdout.write(
`${String(globalLineCounter).padStart(6)}\t${line}\n`
);
globalLineCounter++;
} else {
process.stdout.write('\n');
}
} else if(options.numberAll) {
//-n option: number all lines
process.stdout.write(
`${String(globalLineCounter).padStart(6)}\t${line}\n`
);
globalLineCounter++;
} else {
//default: just print the line
process.stdout.write(line + '\n');
}
});
Copy link
Member

Choose a reason for hiding this comment

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

The three branches here look quite similar and repetitive. In general, if you have multiple similar branches, it's more clear to extract the differences into variables, and then run the same code, i.e. so you'd only have one call to process.stdout.write which looks more like process.stdout.write(`${prefix}${line}\n`) where prefix may be set differently based on options (including potentially an empty string).

This way it's easier for someone reading the code to see what's the same / different in each case, and also avoids the hazard that someone updates one of the branches but forgets to update the other ones.

@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 Mar 16, 2026
@ebrahimbeiati ebrahimbeiati 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 Mar 16, 2026
Copy link
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.

Looking really good, great improvements! A few more small things to think about :)

let prefix = "";

if (options.numberMode === "non-empty") {
//-b option: number non-empty lines
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 feels redundant - reading the if condition tells me the same information as the comment does

Comment on lines +14 to +22
if (options.numberMode === "non-empty") {
//-b option: number non-empty lines
if (line.trim() !== "") {
prefix = `${String(globalLineCounter).padStart(6)}\t`;
globalLineCounter++;
}
} else if (options.numberMode === "all") {
prefix = `${String(globalLineCounter).padStart(6)}\t`;
globalLineCounter++;
Copy link
Member

Choose a reason for hiding this comment

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

There's still some repetition here in setting up the prefix. How about something like:

if (options.numberMode === "all" || (options.numberMode === "non-empty" && line.trim() !== "") {

filtered.forEach((entry) => console.log(entry.name));
} else {
const names = filtered.map((entry) => entry.name);
console.log(names.join(" "));
Copy link
Member

Choose a reason for hiding this comment

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

In cat you used process.stdout.write and here you're using console.log - what's the difference? When would you choose to use one or the other?

Copy link
Author

Choose a reason for hiding this comment

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

We use process.stdout.write when we need full control over spacing and newlines, byte-exact output matters, and to avoid automatic newlines, like in cat.
We use console.log when we just want simple, line‑by‑line output with automatic newlines, like in ls.

console.log(names.join(" "));
}
} catch (error) {
console.error(`Error reading directory ${dirPath}: ${error.message}`);
Copy link
Member

Choose a reason for hiding this comment

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

What exit code should our process terminate with if it hit an error? What does your code do?

if (directories.length === 0) {
directories = [process.cwd()];
}
//If a directory is specified, list that directory
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 seems a bit confusing? I'd remove it

Comment on lines +42 to +43
if (directories.length > 1 && index < directories.length - 1)
console.log("");
Copy link
Member

Choose a reason for hiding this comment

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

I'd recommend always using {}s around if conditions, or perhaps sometimes allowing single-line statements (like your above if (directories.length > 1) console.log(`${arg}:`);) - see https://www.blackduck.com/blog/understanding-apple-goto-fail-vulnerability-2.html for an example of why


printCounts(file, counts, options);
} catch (error) {
console.error(`Error reading file ${file}: ${error.message}`);
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 exit codes

@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 Mar 16, 2026
@ebrahimbeiati ebrahimbeiati 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 Mar 16, 2026
Copy link
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.

Looks good! Just a few last things :)

process.stdout.write(`${prefix}${line}\n`);
});
} catch (error) {
console.error(`cat: ${filePath}: ${error.message}`);
Copy link
Member

Choose a reason for hiding this comment

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

This will still exit 0 if e.g. a file didn't exist.

const stats = fs.statSync(arg);

if (stats.isDirectory()) {
//Print header if multiple directories are listed
Copy link
Member

Choose a reason for hiding this comment

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

Is this comment useful compared to just reading the code?

//remove options from args
let directories = args.filter((arg) => arg !== "-a" && arg !== "-1");

// Default to current directory if no directories are specified
Copy link
Member

Choose a reason for hiding this comment

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

Is this comment useful compared to just reading the code?

}
}

function main() {
Copy link
Member

Choose a reason for hiding this comment

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

This tool behaves slightly differently than built-in cat for the examples given in https://github.com/CodeYourFuture/Module-Tools/blob/main/implement-shell-tools/cat/README.md

Copy link
Member

Choose a reason for hiding this comment

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

Your wc does too for the same reason.

Copy link
Author

Choose a reason for hiding this comment

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

Thank you so much for that. I'll write an integration test to find the issue.

@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 Mar 16, 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