Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
76 changes: 76 additions & 0 deletions implement-shell-tools/cat/cat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');

//shared line counter across all files(matches cat -n)
let globalLineCounter = 1;

function printFile(filePath, options) {
try {
const content = fs.readFileSync(filePath, 'utf-8');
const lines = content.split('\n');

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
Copy Markdown
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.


} catch (error) {
console.error(`Error reading file ${filePath}: ${error.message}`);
}
}

function main() {
const args = process.argv.slice(2);
const options = {
numberNonEmpty: false,
numberAll: false,
};
const filePatterns = [];

args.forEach((arg) => {
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
Copy Markdown
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.


if(filePatterns.length === 0) {
console.log("cat: missing file operand");
process.exit(1);
}

const files = filePatterns;
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.

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


files.forEach((file) => {
const resolvedPath = path.resolve(process.cwd(), file);
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 advantage does path.resolve(process.cwd(), file) have over just using file directly?

printFile(resolvedPath, options);
});
}

main();
52 changes: 52 additions & 0 deletions implement-shell-tools/ls/ls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');

function listDirectory(dirPath, showAll, onePerLine) {
try {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
const filtered = entries.filter((entry) => showAll || !entry.name.startsWith('.'));

if(onePerLine) {
filtered.forEach(entry => console.log(entry.name));
} else {
const names = filtered.map(entry => entry.name);
console.log(names.join(' '));
}
} catch (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.

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.

console.error(`Error reading directory ${dirPath}: ${error.message}`);
}
}
function main() {
const args = process.argv.slice(2);
// Check for options
const showAll = args.includes('-a');
const onePerLine = args.includes('-1');
//remove options from args
const directories = args.filter(arg => arg !== '-a' && arg !== '-1');

// If no directory is specified, list the current directory
if(directories.length === 0) {
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.

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?

listDirectory(process.cwd(), showAll, onePerLine);
return;
}
//If a directory is specified, list that directory
directories.forEach((arg, index) => {
try {
const stats = fs.statSync(arg);
if(stats.isDirectory()) {
//Print header if multiple directories are listed
if(directories.length > 1) console.log(`${arg}:`);

listDirectory(arg, showAll, onePerLine);
//add a blank line between directory listings if there are multiple directories
if(directories.length > 1 && index < directories.length - 1) console.log('');
} else{
console.log(arg);// single file
}
} catch (error) {
console.error(`Error accessing ${arg}: ${error.message}`);
}
});
}
main();
71 changes: 71 additions & 0 deletions implement-shell-tools/wc/wc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env node
const fs = require('node:fs');
// Function to count lines, words, and bytes in a file
function countFileContent(content) {
const lines = content.split('\n').length; // Count lines by splitting on newline characters
const words = content.trim().split(/\s+/).filter(Boolean).length; // Split by whitespace and filter out empty strings
const bytes = Buffer.byteLength(content, 'utf8');
return { lines, words, bytes };
}

//print counts in the format of wc according to options
function printCounts(filePath, counts, options) {
const parts = [];
if(options.line) parts.push(counts.lines);
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
Copy Markdown
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)

//default is to print all counts
parts.push(counts.lines, counts.words, counts.bytes);
}
console.log(parts.join('\t'),filePath);
}

function main() {
const args = process.argv.slice(2);
const options = {
line: false,
word: false,
byte: false,
};

//Separate options from file paths
const files = [];
args.forEach((arg) => {
if(arg === '-l') options.line = true;
else if(arg === '-w') options.word = true;
else if(arg === '-c') options.byte = true;
else files.push(arg);
});

if(files.length === 0) {
console.error('No files specified');
process.exit(1);
}

let totalCounts = { lines: 0, words: 0, bytes: 0 };
const multipleFiles = files.length > 1;

files.forEach(file => {
try {
const content = fs.readFileSync(file, 'utf-8');
const counts = countFileContent(content);

// Sum counts for total if multiple files
totalCounts.lines += counts.lines;
totalCounts.words += counts.words;
totalCounts.bytes += counts.bytes;

printCounts(file, counts, options);
} catch (error) {
console.error(`Error reading file ${file}: ${error.message}`);
}
});

// If multiple files, print total counts
if(multipleFiles) {
printCounts('total', totalCounts, options);
}
}
main();
Loading