-
-
Notifications
You must be signed in to change notification settings - Fork 42
Manchester | 25-SDC-Nov | Geraldine Edwards | Sprint 3 | Implement Shell Tools #242
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 8 commits
83c381c
3707763
a7bdd7a
4b1a8cd
eace0f9
13fab23
adf7c2b
7b1d67d
fafae17
4902edc
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,4 @@ | ||
| node_modules/ | ||
| *.log | ||
| .DS_Store | ||
| .env |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import { program } from "commander"; | ||
| import { promises as fs } from "node:fs"; | ||
| import process from "node:process"; | ||
|
|
||
| // configure the CLI program with its name, description, arguments, options, and actions (the help instructions) | ||
| program | ||
| .name("cat") | ||
| .description("An alternative to the 'cat' command") | ||
| .argument("<files...>", "The file(s) to process") | ||
| .option("-n, --number", "Number all output lines") | ||
| .option("-b, --number-nonblank", "Number non-blank output lines") | ||
| .action(async (files, options) => { | ||
| try { | ||
| await newCat(files, options); | ||
| } catch (err) { | ||
| console.error(`Error: ${err.message}`); | ||
| } | ||
| }); | ||
|
|
||
| program.parse(process.argv); | ||
|
|
||
| //helper function to format output | ||
| function printLine(line, lineNumber, padWidth) { | ||
| if (lineNumber !== null) { | ||
| console.log(`${lineNumber.toString().padStart(padWidth, ' ')} ${line}`); | ||
| } else { | ||
| console.log(line); | ||
| } | ||
| } | ||
|
|
||
| async function newCat(files, options) { | ||
| let lineNumber = 1; | ||
| const padWidth = 6; | ||
|
|
||
| for (const file of files) { | ||
| // read each file into a single text string | ||
| try { | ||
| const data = await fs.readFile(file, "utf8"); | ||
| // split that string into an array at \n where each element is a line from the file | ||
| // e.g. lines = ["Line 1", "Line 2", "Line 3"] | ||
| const lines = data.split("\n") | ||
|
|
||
| // remove trailing blank line caused by a trailing newline | ||
| if (lines[lines.length - 1] === "") { | ||
| lines.pop(); | ||
| } | ||
|
|
||
| lines.forEach(line => { | ||
| //line trim: truthy = text, falsy = blank | ||
| if (options.numberNonblank && line.trim()) { | ||
| // number non-blank lines only | ||
| printLine(line, lineNumber++, padWidth); | ||
| } else if (options.number){ | ||
| // number all lines | ||
| printLine(line, lineNumber++, padWidth); | ||
| } else { | ||
| // neither flag, print normally | ||
| printLine(line, null, padWidth) | ||
| } | ||
| }); | ||
| } catch (err) { | ||
| console.error(`Error reading file ${file}: ${err.message}`); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { program } from "commander"; | ||
| import { promises as fs } from "node:fs"; | ||
| import process from "node:process"; | ||
|
|
||
| // configure the CLI program with its name, description, arguments, options, and actions (the help instructions) | ||
| program | ||
| .name("ls") | ||
| .description("An alternative to the 'ls' command") | ||
| .argument("[directory]", "The directory to list") | ||
| // Commander stores -1 as a string key that is accessed using options['1'] | ||
| .option("-1", "List all files, one per line") | ||
| .option("-a, --all", "Include hidden files (those starting with .) in the listing") | ||
| .action(async (directory, options) => { | ||
| try { | ||
| // default to current directory if none is specified | ||
| const dir = directory || "."; | ||
|
|
||
| await newLs(dir, options['1'], options.all); | ||
| } catch (err) { | ||
| console.error(`Error: ${err.message}`); | ||
| } | ||
| }); | ||
|
|
||
| program.parse(process.argv); | ||
|
|
||
|
|
||
| // filter files based on visibility (includeHidden = true includes all files) | ||
| function filterFiles(entries, includeHidden) { | ||
| return entries.filter(name => | ||
| includeHidden ? true : !name.startsWith(".") | ||
| ); | ||
| } | ||
|
|
||
| // sort entries: directories first, then files, | ||
| function sortEntries(entries) { | ||
| const dirs = entries.filter(entry => { | ||
| try { | ||
| return fs.statSync(entry).isDirectory(); | ||
| } catch (err) { | ||
| return false; | ||
| } | ||
| }); | ||
|
|
||
| const files = entries.filter(entry => { | ||
| try { | ||
| return fs.statSync(entry).isFile(); | ||
| } catch (err) { | ||
| return false; | ||
| } | ||
| }); | ||
| // localeCompare = take into account rules of system language/region for ordering | ||
| // undefined = uses the system default, numeric = regular number sorting, base = ignore case & accents | ||
| return entries.sort((a, b) => | ||
| a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }) | ||
| ); | ||
| } | ||
|
|
||
|
|
||
| // print entries either one per line (-1 flag) | ||
| function printEntries(entries) { | ||
| entries.forEach(entry => console.log(entry)); | ||
| } | ||
|
|
||
|
|
||
| async function newLs(directory, oneFlag, allFlag) { | ||
| try { | ||
| // check if path exists and determine if file or directory | ||
| const stats = await fs.stat(directory); | ||
|
|
||
| // if a file, just print the name | ||
| if (stats.isFile()) { | ||
| console.log(directory); | ||
| return; | ||
| } | ||
|
|
||
| // reads directory contents | ||
| const entries = await fs.readdir(directory); | ||
|
|
||
| // Filter out hidden files if no -a flag | ||
| const filteredEntries = filterFiles(entries, allFlag); | ||
|
|
||
| // Sort the entries using the sortEntries helper | ||
| const sortedEntries = sortEntries(filteredEntries); | ||
|
|
||
| // print entries for -1 flag (one per line) | ||
| printEntries(sortedEntries); | ||
| } catch (err) { | ||
| console.error(`ls: cannot access '${directory}': ${err.message}`); | ||
| } | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "name": "implement-shell-tools", | ||
| "version": "1.0.0", | ||
| "type": "module", | ||
| "dependencies": { | ||
| "commander": "^14.0.2" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import { program } from "commander"; | ||
| import { promises as fs } from "node:fs"; | ||
| import process from "node:process"; | ||
|
|
||
| // configure the CLI program with its name, description, arguments, options, and actions (the help instructions) | ||
| program | ||
| .name("wc") | ||
| .description("An alternative to the 'wc' command") | ||
| .argument("<files...>", "The file(s) to count lines/words/bytes") | ||
| .option("-l", "--lines", "Print the newline counts") | ||
| .option("-w", "--words", "Print the word counts") | ||
| .option("-c", "--bytes", "Print the byte counts") | ||
| .action(async (files, options) => { | ||
| try { | ||
| // call newWc for all files | ||
| await newWc(files, options) | ||
| } catch (err) { | ||
| console.error(`Error: ${err.message}`); | ||
| } | ||
| }); | ||
|
|
||
| program.parse(process.argv); | ||
|
|
||
| //helper function to format string for output | ||
| function formatCount(count) { | ||
| const paddingStart = 3 | ||
| return count.toString().padStart(paddingStart); | ||
| } | ||
|
|
||
|
|
||
| // helper function to print the wc outputs per case | ||
| function printWcOutput(lineCount, wordCount, byteCount, file, options, noFlags) { | ||
| const parts = []; | ||
|
|
||
| if (noFlags || options.l) parts.push(formatCount(lineCount)); | ||
| if (noFlags || options.w) parts.push(formatCount(wordCount)); | ||
| if (noFlags || options.c) parts.push(formatCount(byteCount)); | ||
|
|
||
| parts.push(file); | ||
| console.log(parts.join(" ")); | ||
| } | ||
|
|
||
| async function newWc(files, options) { | ||
|
|
||
| const noFlags = | ||
| !options.l && | ||
| !options.w && | ||
| !options.c; | ||
|
|
||
| // set the counts variables | ||
| let totalLines = 0; | ||
| let totalWords = 0; | ||
| let totalBytes = 0; | ||
|
|
||
| for (const file of files) { | ||
| try { | ||
| // read each file into a single text string | ||
| const content = await fs.readFile(file, "utf8"); | ||
|
|
||
| // count lines by splitting on '\n' and subtracting 1 because | ||
| // each newline creates an extra array element, so length-1 equals the number of newline characters | ||
| const lineCount = content.split("\n").length - 1; | ||
|
|
||
| // .filter(Boolean) ensures that falsy values like "" (empty string), null, undefined, 0, false are removed | ||
| const wordCount = content.split(/\s+/).filter(Boolean).length; | ||
|
|
||
| // calculates the number of bytes the file uses when encoded as UTF-8. | ||
| // different than just counting chars as some chars (like emojis, accented letters, etc) take more than 1 byte | ||
| const byteCount = Buffer.byteLength(content, "utf8"); | ||
|
|
||
| // update the count | ||
| totalLines += lineCount; | ||
| totalWords += wordCount; | ||
| totalBytes += byteCount; | ||
|
|
||
| printWcOutput(lineCount, wordCount, byteCount, file, options, noFlags); | ||
| } catch (err) { | ||
| console.error(`Error reading file ${file}: ${err.message}`); | ||
| } | ||
| } | ||
| if (files.length > 1) { | ||
| // print the totals as wc does | ||
| printWcOutput(totalLines, totalWords, totalBytes, "total", options, noFlags); | ||
|
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. What should happen if I give only a single option, e.g.
Author
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. Thanks again @LonMcGregor :) I have spotted my error in the program options configuration and changed the remainder of the file as needed to the long flag names. |
||
| } | ||
| } | ||
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.
How does your code differ if the user does not give the
-1flag?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.
Ah thanks for this @LonMcGregor! I hadn't finished formatting the output for not using the -1 flag, oops! Fixed it now :)