Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 {
Comment on lines +14 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In the printFile function, the logic that formats and prints numbered lines is repeated for both the -b (number non-empty) and -n (number all) options. In both branches you:

  • Convert globalLineCounter to a string
  • Pad it to width 6
  • Add a tab
  • Append the line content
  • Then increment globalLineCounter.

The only real difference between the two branches is when numbering is applied (all lines vs non-empty). When similar formatting logic appears in multiple branches like this, it can make later changes a bit harder: if you decide to adjust the formatting (for example, change the padding or separator), you’d need to remember to update it in every place. How might you extract the shared “print numbered line and increment the counter” behavior into a single helper, and then call it from both the -b and -n paths so that the format is defined in just one place?

To "like" or "dislike" this comment, please follow this link

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

} 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 = [];

Comment on lines +47 to +48

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

filePatterns is later used directly as the list of files to read, without any pattern/glob expansion. For a reader, the name filePatterns usually implies something like wildcard patterns (*.txt) that will be expanded or matched, not plain file paths that are used as-is. If someone later tries to add real pattern support, or just reads the code quickly, this name could make them overestimate what this array contains. How might you rename this so that it more clearly communicates that these are the raw file arguments/paths you’re going to open?

To "like" or "dislike" this comment, please follow this link

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;
}

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);
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) {
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');

Comment on lines +26 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The directories array actually holds all non-option arguments, and you then treat each element as either a directory or a file (you call fs.statSync(arg) and may print the name directly if it’s a file). Naming this array directories suggests it only contains directories, which can mislead a reader into thinking there are no files in that list. That can make the later stats.isDirectory() branch a bit surprising to follow. Could a more neutral name that reflects "paths" or "args" make the intent clearer and reduce that confusion?

To "like" or "dislike" this comment, please follow this link

// If no directory is specified, list the current directory
if(directories.length === 0) {
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) {
//default is to print all counts
parts.push(counts.lines, counts.words, counts.bytes);
}
Comment on lines +12 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In printCounts, you’re building up the parts array in two ways: first by checking each option individually (-l, -w, -c), and then again pushing all three counts when no options are provided. This leads to repeating access to counts.lines, counts.words, and counts.bytes in two separate blocks. While this works correctly, it does mean that if you ever wanted to change the order or add another dimension, you’d have to keep both sections in sync. Could you think of a way to describe the available counters and their corresponding options once (for example in a small structure or loop), and then derive both the “only selected fields” and “all fields when none selected” behaviors from that single description?

To "like" or "dislike" this comment, please follow this link

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