-
-
Notifications
You must be signed in to change notification settings - Fork 27
London | 25-SDC-July | Pezhman Azizi | Sprint 3 | Implement-shell-tools-with-NodeJS #169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1a250cb
59bc083
4e01f5a
d5eafa1
fff1928
f495a40
cf8040c
bf61510
bc06d2e
6ac53b9
4d73eab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
#!/usr/bin/env node | ||
// cat.js — ESM: supports -n and -b (-b overrides -n) | ||
|
||
import fs from "node:fs"; | ||
import readline from "node:readline"; | ||
|
||
const args = process.argv.slice(2); | ||
let numberAll = false; | ||
let numberNonblank = false; | ||
const files = []; | ||
|
||
// parse flags + files (flags can appear anywhere) | ||
for (const a of args) { | ||
if (a === "-n") numberAll = true; | ||
else if (a === "-b") numberNonblank = true; | ||
else files.push(a); | ||
} | ||
// precedence: -b wins over -n | ||
if (numberNonblank) numberAll = false; | ||
|
||
if (files.length === 0) { | ||
console.error("Usage: node cat.js [-n|-b] <file...>"); | ||
process.exit(1); | ||
} | ||
|
||
let hadError = false; | ||
let lineNo = 1; | ||
|
||
for (const file of files) { | ||
try { | ||
const stat = await fs.promises.stat(file); | ||
if (stat.isDirectory()) { | ||
console.error(`cat: ${file}: Is a directory`); | ||
hadError = true; | ||
continue; | ||
} | ||
if (!numberAll && !numberNonblank) { | ||
await pipeFile(file); | ||
} else { | ||
await numberFile(file, { nonblank: numberNonblank }); | ||
} | ||
} catch (err) { | ||
if (err?.code === "ENOENT" || err?.code === "ENOTDIR") { | ||
console.error(`cat: ${file}: No such file or directory`); | ||
} else { | ||
console.error(`cat: ${file}: ${err?.message || "Error"}`); | ||
} | ||
hadError = true; | ||
} | ||
} | ||
|
||
if (hadError) process.exitCode = 1; | ||
|
||
function pipeFile(file) { | ||
return new Promise((resolve, reject) => { | ||
const rs = fs.createReadStream(file); | ||
rs.on("error", reject); | ||
rs.on("end", resolve); | ||
rs.pipe(process.stdout, { end: false }); // keep stdout open between files | ||
}); | ||
} | ||
|
||
function numberFile(file, { nonblank }) { | ||
return new Promise((resolve, reject) => { | ||
const rs = fs.createReadStream(file); | ||
const rl = readline.createInterface({ input: rs, crlfDelay: Infinity }); | ||
|
||
rl.on("line", (line) => { | ||
if (nonblank) { | ||
if (line.length === 0) { | ||
process.stdout.write("\n"); // blank line, no number | ||
} else { | ||
process.stdout.write(formatNum(lineNo++) + line + "\n"); | ||
} | ||
} else { | ||
process.stdout.write(formatNum(lineNo++) + line + "\n"); | ||
} | ||
}); | ||
rl.on("close", resolve); | ||
rl.on("error", reject); | ||
rs.on("error", reject); | ||
}); | ||
} | ||
|
||
function formatNum(n) { | ||
// GNU cat style: width 6, right-aligned, then a tab | ||
return String(n).padStart(6, " ") + "\t"; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
#!/usr/bin/env node | ||
|
||
|
||
import fs from "node:fs"; | ||
import { pathToFileURL } from "node:url"; | ||
|
||
const raw = process.argv.slice(2); | ||
|
||
let includeAll = false; // -a | ||
let onePerLine = false; // -1 (format already one-per-line) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As it defaults to one per line, this argument is not doing anything. What change would you need to make to the program to ensure that the default behaviour is to output all on the same line? |
||
const targets = []; | ||
|
||
// Parse flags (supports combined like -a1 / -1a). Treat lone "-" as a path. | ||
for (const arg of raw) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there an advantage of writing argument parsing yourself rather than using an API or library? |
||
if (arg === "-" || !arg.startsWith("-")) { | ||
targets.push(arg); | ||
continue; | ||
} | ||
if (arg === "-1") { | ||
onePerLine = true; | ||
continue; | ||
} | ||
for (const ch of arg.slice(1)) { | ||
if (ch === "a") includeAll = true; | ||
else if (ch === "1") onePerLine = true; | ||
else { | ||
// ignore unknown short flags | ||
} | ||
} | ||
} | ||
|
||
const paths = targets.length ? targets : ["."]; | ||
let hadError = false; | ||
|
||
for (let i = 0; i < paths.length; i++) { | ||
const p = paths[i]; | ||
try { | ||
const st = await fs.promises.lstat(p); | ||
|
||
if (st.isDirectory()) { | ||
if (paths.length > 1) console.log(`${p}:`); | ||
|
||
const entries = await fs.promises.readdir(p, { withFileTypes: true }); | ||
let names = entries.map(d => d.name); | ||
|
||
if (!includeAll) { | ||
names = names.filter(n => !n.startsWith(".")); | ||
} else { | ||
// mimic `ls -a` by including "." and ".." | ||
names = ["." , "..", ...names]; | ||
} | ||
|
||
names.sort((a, b) => a.localeCompare(b)); | ||
for (const name of names) { | ||
// one-per-line output; -1 flag just affirms it | ||
console.log(name); | ||
} | ||
|
||
if (paths.length > 1 && i !== paths.length - 1) console.log(""); | ||
} else { | ||
// file or symlink => print the argument as given | ||
console.log(p); | ||
} | ||
} catch (err) { | ||
if (err?.code === "ENOENT") { | ||
console.error(`ls: cannot access '${p}': No such file or directory`); | ||
} else if (err?.code === "EACCES") { | ||
console.error(`ls: cannot open directory '${p}': Permission denied`); | ||
} else { | ||
console.error(`ls: ${p}: ${err?.message || "Error"}`); | ||
} | ||
hadError = true; | ||
} | ||
} | ||
|
||
if (hadError) process.exitCode = 1; | ||
|
||
// run only when executed directly | ||
const isDirect = import.meta.url === pathToFileURL(process.argv[1]).href; | ||
if (!isDirect) { | ||
// allow importing in tests without auto-executing | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"type": "module", | ||
"name": "implement-shell-tools-with-node", | ||
"version": "1.0.0", | ||
"description": "Your task is to re-implement shell tools you have used.", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
}, | ||
"author": "pezhman azizi", | ||
"license": "ISC" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
#!/usr/bin/env node | ||
// wc.js — ESM: supports flags -l (lines), -w (words), -c (bytes) | ||
// Behaviors covered by the README: | ||
// - wc sample-files/* | ||
// - wc -l sample-files/3.txt | ||
// - wc -w sample-files/3.txt | ||
// - wc -c sample-files/3.txt | ||
// - wc -l sample-files/* | ||
// Also works with multiple flags (e.g., -lw), like GNU wc. | ||
|
||
import fs from "node:fs"; | ||
import { pathToFileURL } from "node:url"; | ||
|
||
async function main() { | ||
const argv = process.argv.slice(2); | ||
|
||
let showLines = false; | ||
let showWords = false; | ||
let showBytes = false; | ||
const files = []; | ||
|
||
// Parse flags + files. Support combined short flags like -lw, -cl, etc. | ||
for (const arg of argv) { | ||
if (arg.startsWith("-") && arg !== "-") { | ||
for (const ch of arg.slice(1)) { | ||
if (ch === "l") showLines = true; | ||
else if (ch === "w") showWords = true; | ||
else if (ch === "c") showBytes = true; | ||
else { | ||
// ignore unknown short options for this assignment | ||
} | ||
} | ||
} else { | ||
files.push(arg); | ||
} | ||
} | ||
|
||
if (files.length === 0) { | ||
console.error("Usage: node wc.js [-l|-w|-c] <file...>"); | ||
process.exit(1); | ||
} | ||
|
||
// No flags → show all three like wc | ||
const showAll = !showLines && !showWords && !showBytes; | ||
|
||
let hadError = false; | ||
let total = { lines: 0, words: 0, bytes: 0 }; | ||
const results = []; | ||
|
||
for (const file of files) { | ||
try { | ||
const st = await fs.promises.lstat(file); | ||
if (st.isDirectory()) { | ||
console.error(`wc: ${file}: Is a directory`); | ||
hadError = true; | ||
continue; | ||
} | ||
const counts = await countFile(file); | ||
results.push({ file, ...counts }); | ||
total.lines += counts.lines; | ||
total.words += counts.words; | ||
total.bytes += counts.bytes; | ||
} catch (err) { | ||
if (err?.code === "ENOENT") { | ||
console.error(`wc: ${file}: No such file or directory`); | ||
} else if (err?.code === "EACCES") { | ||
console.error(`wc: ${file}: Permission denied`); | ||
} else { | ||
console.error(`wc: ${file}: ${err?.message || "Error"}`); | ||
} | ||
hadError = true; | ||
} | ||
} | ||
|
||
for (const r of results) { | ||
console.log(formatRow(r, { showAll, showLines, showWords, showBytes })); | ||
} | ||
if (results.length > 1) { | ||
console.log(formatRow({ file: "total", ...total }, { showAll, showLines, showWords, showBytes })); | ||
} | ||
|
||
if (hadError) process.exitCode = 1; | ||
} | ||
|
||
async function countFile(file) { | ||
const buf = await fs.promises.readFile(file); // Buffer | ||
const bytes = buf.length; | ||
|
||
// Lines: count '\n' bytes | ||
let lines = 0; | ||
for (let i = 0; i < buf.length; i++) if (buf[i] === 0x0a) lines++; | ||
|
||
// Words: sequences of non-whitespace | ||
const text = buf.toString("utf8"); | ||
const words = (text.match(/\S+/g) || []).length; | ||
|
||
return { lines, words, bytes }; | ||
} | ||
|
||
function formatRow({ lines, words, bytes, file }, opts) { | ||
const cols = []; | ||
if (opts.showAll || opts.showLines) cols.push(pad(lines)); | ||
if (opts.showAll || opts.showWords) cols.push(pad(words)); | ||
if (opts.showAll || opts.showBytes) cols.push(pad(bytes)); | ||
return `${cols.join(" ")} ${file}`; | ||
} | ||
|
||
function pad(n) { | ||
// Right-align like `wc` | ||
return String(n).padStart(7, " "); | ||
} | ||
|
||
// run only when executed directly | ||
const isDirect = import.meta.url === pathToFileURL(process.argv[1]).href; | ||
if (isDirect) await main(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why you would want two separate functions for printing out content normally and numbered? Could these be combined into one function?