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
49 changes: 49 additions & 0 deletions implement-shell-tools/cat/cat.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {program} from 'commander';
import {promises as fs} from 'node:fs';
import process from 'node:process';

program
.name('cat')
.description('Concatenates and prints the contents of files.')
.argument('<paths...>', 'The paths to the files to concatenate')
.option('-n, --number', 'Number all output lines')
.option('-b, --number-nonblank', 'Number nonempty output lines');

program.parse();

const argv = program.args;
const options = program.opts();

if (argv.length < 1) {
console.error(
`Expected at least 1 argument (a path) to be passed but got ${argv.length}.`,
);
process.exit(1);
}

let showLineNumbers = options.number || false;
let showNonEmptyLineNumbers = options.numberNonblank || false;

if (showLineNumbers && showNonEmptyLineNumbers) {
showLineNumbers = false;
}

let lineNumber = 1;
const spacer = ' ';

for (const path of argv) {
const content = await fs.readFile(path, 'utf-8');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What happens if file is absent?

const lines = content.split('\n');
while (lines.length > 0 && lines[lines.length - 1] === '') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

while pops all trailing blank lines, not just one. A file intentionally ending with multiple blank lines will have them silently removed.

lines.pop();
}
for (const line of lines) {
if (showLineNumbers) {
process.stdout.write(`${spacer} ${lineNumber++} ${line}\n`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

real cat places tab between numbers and line, and also aligns line number to the length of the number.

} else if (showNonEmptyLineNumbers && line.trim() !== '') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Real cat -b considers only truly empty lines (zero characters) as blank, not whitespace-only lines.

process.stdout.write(`${spacer} ${lineNumber++} ${line}\n`);
} else {
process.stdout.write(`${line}\n`);
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing newline at end of file.

25 changes: 25 additions & 0 deletions implement-shell-tools/cat/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions implement-shell-tools/cat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "cat",
"version": "1.0.0",
"description": "You should already be familiar with the `cat` command line tool.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"commander": "^14.0.3"
}
}
35 changes: 35 additions & 0 deletions implement-shell-tools/ls/ls.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { program } from 'commander';
import process from 'node:process';
import { promises as fs } from 'node:fs';

program
.name('ls')
.description('Lists the contents of a directory.')
.argument('[path]', 'The path to the directory to list, defaults to the current directory')
.option('-a, --all', 'Do not ignore entries starting with .')
.option('-1', 'List one file per line');

program.parse();

const argv = program.args;
const path = argv[0] || '.';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Real ls can take multiple paths


let showAll = program.opts().all || false;
let onePerLine = program.opts()['1'] || false;

try {
const files = await fs.readdir(path);
files.sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sort is case-insensitive (sensitivity: 'base'). Real ls is case-sensitive by default (uppercase before lowercase).

for (const file of files) {
if (showAll || !file.startsWith('.')) {
if (onePerLine) {
process.stdout.write(`${file}\n`);
} else {
process.stdout.write(`${file} `);
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Default output (no -1 flag) has no trailing newline. The loop writes file1 file2 file3 with a trailing space but never writes \n at the end.

} catch (err) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good case of error handling, we need similar in other utils.

process.stderr.write(`cannot access '${path}': No such file or directory\n`);
process.exit(1);
}
25 changes: 25 additions & 0 deletions implement-shell-tools/ls/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions implement-shell-tools/ls/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "ls",
"version": "1.0.0",
"description": "You should already be familiar with the `ls` command line tool.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"commander": "^14.0.3"
}
}
26 changes: 26 additions & 0 deletions implement-shell-tools/wc/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions implement-shell-tools/wc/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "wc",
"version": "1.0.0",
"description": "You should already be familiar with the `wc` command line tool.",
"main": "wc.mjs",
"type": "module",
"dependencies": {
"commander": "^14.0.3"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
34 changes: 34 additions & 0 deletions implement-shell-tools/wc/wc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { program } from 'commander';
import process from 'node:process';
import { promises as fs } from 'node:fs';

program
.name('wc')
.description('Counts the number of lines, words, and characters in a file.')
.argument('<path>', 'The path to the file to analyze')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Real wc accepts multiple files and prints a total row.

.option('-l, --lines', 'Only count lines')
.option('-w, --words', 'Only count words')
.option('-c, --characters', 'Only count characters');

program.parse();

const argv = program.args;

if (argv.length != 1) {
console.error(
`Expected exactly 1 argument (a path) to be passed but got ${argv.length}.`);
process.exit(1);
}
const path = argv[0];
const options = program.opts();

let showLines = options.lines || (!options.words && !options.characters);
let showWords = options.words || (!options.lines && !options.characters);
let showCharacters = options.characters || (!options.lines && !options.words);

const content = await fs.readFile(path, 'utf-8');

const lineCount = content.split('\n').filter(Boolean).length;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

File with a blank line in the middle will have it excluded from the count.

const wordCount = content.split(' ').filter(Boolean).length;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Only splits on spaces — tabs and newlines between words are not treated as separators.

const characterCount = content.length;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

content.length counts UTF-16 code units. Real wc -c counts bytes.

console.log(` ${showLines ? lineCount : ''} ${showWords ? wordCount : ''} ${showCharacters ? characterCount : ''} ${path}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What happens if some flags are missing? Does it affect the format?

Loading