diff --git a/.gitignore b/.gitignore index 3c3629e64..274d04915 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.venv \ No newline at end of file diff --git a/implement-shell-tools/.gitignore b/implement-shell-tools/.gitignore new file mode 100644 index 000000000..e6e0086b4 --- /dev/null +++ b/implement-shell-tools/.gitignore @@ -0,0 +1,7 @@ +# Ignore node_modules everywhere +**/node_modules/ + +#ignore logs, environment files. +*.log +.env +dist/ diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js new file mode 100644 index 000000000..624b885ac --- /dev/null +++ b/implement-shell-tools/cat/cat.js @@ -0,0 +1,75 @@ +import { program } from "commander"; +import { promises as fs } from "node:fs"; +import process from "node:process"; + +program + .name("my-cat") + .description("Reads and prints one or more files, optionally numbering lines continuously") + .argument("", "One or more file paths") + .option("-n, --number", "Number all output lines", false) + .option("-b, --number-nonblank", "Number non-empty output lines", false); + +program.parse(); + +const filePaths = program.args; +const options = program.opts(); + +const numberNonBlank = options.numberNonblank; +const numberAll = options.number && !numberNonBlank; + +(async () => { + let lineNumber = 1; + + // calculate total lines if using -n + let totalLines = 0; + if (numberAll) { + for (const path of filePaths) { + try { + const content = await fs.readFile(path, "utf-8"); + const lines = content.split("\n"); + + totalLines += content.endsWith("\n") ? lines.length - 1 : lines.length; + } catch (err) { + console.error(`Error reading file "${path}": ${err.message}`); + process.exit(1); + } + } + } + + for (const path of filePaths) { + try { + const content = await fs.readFile(path, "utf-8"); + const lines = content.split("\n"); + + if (numberNonBlank) { + const maxDigits = String(lines.filter(line => line.trim() !== '').length).length; + + for (const line of lines) { + if (line.trim() === '') { + process.stdout.write("\n"); + } else { + const numStr = String(lineNumber++).padStart(maxDigits, " "); + process.stdout.write(`${numStr}\t${line}\n`); + } + } + } else if (numberAll) { + const maxDigits = totalLines > 0 ? String(totalLines).length : 1; + + + for (const line of lines) { + const numStr = String(lineNumber++).padStart(maxDigits, " "); + process.stdout.write(`${numStr}\t${line}\n`); + } + } else { + // no numbering, just print file content as is + process.stdout.write(content); + if (!content.endsWith("\n")) { + process.stdout.write("\n"); // ensure newline after file if missing + } + } + } catch (err) { + console.error(`Error reading file "${path}": ${err.message}`); + process.exit(1); + } + } +})(); diff --git a/implement-shell-tools/cat/package-lock.json b/implement-shell-tools/cat/package-lock.json new file mode 100644 index 000000000..7c3bcb9aa --- /dev/null +++ b/implement-shell-tools/cat/package-lock.json @@ -0,0 +1,25 @@ +{ + "name": "cat", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cat", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "commander": "^14.0.0" + } + }, + "node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + } + } +} diff --git a/implement-shell-tools/cat/package.json b/implement-shell-tools/cat/package.json new file mode 100644 index 000000000..4ef36c57d --- /dev/null +++ b/implement-shell-tools/cat/package.json @@ -0,0 +1,16 @@ +{ + "name": "cat", + "version": "1.0.0", + "description": "You should already be familiar with the `cat` command line tool.", + "main": "cat.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "commander": "^14.0.0" + } +} diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js new file mode 100644 index 000000000..8925dd7d6 --- /dev/null +++ b/implement-shell-tools/ls/ls.js @@ -0,0 +1,69 @@ +import { program } from "commander"; +import { promises as fs } from "node:fs"; +import path from "node:path"; + +program + .name("my-ls") + .description("List files in a directory (simplified ls implementation)") + .argument("[paths...]", "One or more file or directory paths") + .option("-l, --longList", "Long listing format", false) + .option("-a, --all", "Include hidden files", false); + +program.parse(); + +// CHANGED: apply default value for paths here if none are provided +let filePaths = program.args.length > 0 ? program.args : ['.']; +const options = program.opts(); + +const showLong = options.longList; +const showAll = options.all; + +// Helper function to convert mode to rwxrwxrwx string +function formatPermissions(mode, isDir) { + const typeChar = isDir ? 'd' : '-'; + const perms = [0, 1, 2].map(i => { + const shift = 6 - i * 3; + const r = (mode >> shift) & 4 ? 'r' : '-'; + const w = (mode >> shift) & 2 ? 'w' : '-'; + const x = (mode >> shift) & 1 ? 'x' : '-'; + return r + w + x; + }).join(''); + return typeChar + perms; +} + +(async () => { + for (const inputPath of filePaths) { + try { + const stat = await fs.stat(inputPath); + + if (stat.isFile()) { + if (showLong) { + const perms = formatPermissions(stat.mode, false); + console.log(`${perms} ${stat.size.toString().padStart(6)} ${inputPath}`); + } else { + console.log(inputPath); + } + } else if (stat.isDirectory()) { + const entries = await fs.readdir(inputPath, { withFileTypes: true }); + + const filtered = showAll + ? entries + : entries.filter((entry) => !entry.name.startsWith(".")); + + for (const entry of filtered) { + const fullPath = path.join(inputPath, entry.name); + const entryStat = await fs.stat(fullPath); + if (showLong) { + const perms = formatPermissions(entryStat.mode, entryStat.isDirectory()); + console.log(`${perms} ${entryStat.size.toString().padStart(6)} ${entry.name}`); + } else { + console.log(entry.name); + } + } + } + } catch (err) { + console.error(`Error reading "${inputPath}": ${err.message}`); + process.exit(1); + } + } +})(); diff --git a/implement-shell-tools/ls/package-lock.json b/implement-shell-tools/ls/package-lock.json new file mode 100644 index 000000000..bff254485 --- /dev/null +++ b/implement-shell-tools/ls/package-lock.json @@ -0,0 +1,21 @@ +{ + "name": "ls", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "commander": "^14.0.0" + } + }, + "node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + } + } +} diff --git a/implement-shell-tools/ls/package.json b/implement-shell-tools/ls/package.json new file mode 100644 index 000000000..1ca3878d3 --- /dev/null +++ b/implement-shell-tools/ls/package.json @@ -0,0 +1,17 @@ +{ + "dependencies": { + "commander": "^14.0.0" + }, + "name": "ls", + "version": "1.0.0", + "description": "You should already be familiar with the `ls` command line tool.", + "main": "ls.js", + "type": "module", + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/implement-shell-tools/wc/package-lock.json b/implement-shell-tools/wc/package-lock.json new file mode 100644 index 000000000..b3a3f4f8d --- /dev/null +++ b/implement-shell-tools/wc/package-lock.json @@ -0,0 +1,21 @@ +{ + "name": "wc", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "commander": "^14.0.0" + } + }, + "node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + } + } +} diff --git a/implement-shell-tools/wc/package.json b/implement-shell-tools/wc/package.json new file mode 100644 index 000000000..68279600c --- /dev/null +++ b/implement-shell-tools/wc/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "dependencies": { + "commander": "^14.0.0" + } +} diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js new file mode 100644 index 000000000..4d5588abe --- /dev/null +++ b/implement-shell-tools/wc/wc.js @@ -0,0 +1,74 @@ +import { program } from "commander"; +import { promises as fs } from "node:fs"; +import { stat } from "node:fs/promises"; + +program + .name("my-wc") + .description("Simplified implementation of wc") + .argument("[paths...]", "One or more file or directory paths") + .option("-l, --line", "Count lines") + .option("-w, --word", "Count words") + .option("-c, --character", "Count characters"); + +program.parse(); + +const filePaths = program.args.length > 0 ? program.args : ['.']; +const options = program.opts(); + +const countContent = (content) => { + const lines = content.split('\n').length; + const words = content.trim().split(/\s+/).filter(Boolean).length; + const characters = content.length; + return { lines, words, characters }; +}; + +const total = { + lines: 0, + words: 0, + characters: 0 +}; + +//Output formatting function +function formatOutput(counts, label, options) { + const { lines, words, characters } = counts; + const showAll = !options.line && !options.word && !options.character; + + const parts = []; + + if (options.line || showAll) parts.push(lines.toString().padStart(8)); + if (options.word || showAll) parts.push(words.toString().padStart(8)); + if (options.character || showAll) parts.push(characters.toString().padStart(8)); + + parts.push(label); + return parts.join(" "); +} + +(async () => { + let fileCount = 0; + + for (const inputPath of filePaths) { + try { + const stats = await stat(inputPath); + if (stats.isDirectory()) { + console.log(`${inputPath} is a directory. Skipping.`); + continue; + } + + const content = await fs.readFile(inputPath, "utf-8"); + const counts = countContent(content); + + total.lines += counts.lines; + total.words += counts.words; + total.characters += counts.characters; + fileCount++; + + console.log(formatOutput(counts, inputPath, options)); + } catch (err) { + console.error(`Error reading "${inputPath}": ${err.message}`); + } + } + + if (fileCount > 1) { + console.log(formatOutput(total, "total", options)); + } +})(); diff --git a/shell-pipelines/ls-grep/script-01.sh b/shell-pipelines/ls-grep/script-01.sh index 8c7d968a2..87c191290 100755 --- a/shell-pipelines/ls-grep/script-01.sh +++ b/shell-pipelines/ls-grep/script-01.sh @@ -4,3 +4,5 @@ set -euo pipefail # TODO: Write a command to output the names of the files in the sample-files directory whose name contains at least one upper case letter. # Your output should contain 11 files. + +ls sample-files | grep '[A-Z]' diff --git a/shell-pipelines/ls-grep/script-02.sh b/shell-pipelines/ls-grep/script-02.sh index 16f5f71d9..bf4e06cdd 100755 --- a/shell-pipelines/ls-grep/script-02.sh +++ b/shell-pipelines/ls-grep/script-02.sh @@ -4,3 +4,6 @@ set -euo pipefail # TODO: Write a command to output the names of the files in the sample-files directory whose name starts with an upper case letter. # Your output should contain 10 files. + +ls sample-files | grep '^[A-Z]' + diff --git a/shell-pipelines/ls-grep/script-03.sh b/shell-pipelines/ls-grep/script-03.sh index a302ab036..efcd84613 100755 --- a/shell-pipelines/ls-grep/script-03.sh +++ b/shell-pipelines/ls-grep/script-03.sh @@ -4,3 +4,5 @@ set -euo pipefail # TODO: Write a command to output the names of the files in the sample-files directory whose name starts with an upper case letter and doesn't contain any other upper case letters. # Your output should contain 7 files. + +ls sample-files | grep -E '^[A-Z][^A-Z]*$' diff --git a/shell-pipelines/ls-grep/script-04.sh b/shell-pipelines/ls-grep/script-04.sh index c000b7e3b..2da3786f2 100755 --- a/shell-pipelines/ls-grep/script-04.sh +++ b/shell-pipelines/ls-grep/script-04.sh @@ -4,3 +4,5 @@ set -euo pipefail # TODO: Write a command to count the number of files in the sample-files directory whose name starts with an upper case letter and doesn't contain any other upper case letters. # Your output should be the number 7. + +ls sample-files | grep -E '^[A-Z][^A-Z]*$' | wc -l \ No newline at end of file diff --git a/shell-pipelines/sort-uniq-head-tail/script-01.sh b/shell-pipelines/sort-uniq-head-tail/script-01.sh index 171e1f989..038386659 100755 --- a/shell-pipelines/sort-uniq-head-tail/script-01.sh +++ b/shell-pipelines/sort-uniq-head-tail/script-01.sh @@ -5,3 +5,5 @@ set -euo pipefail # The input for this script is the scores-table.txt file. # TODO: Write a command to output scores-table.txt, with lines sorted by the person's name. # The first line of your output should be "Ahmed London 1 10 4" (with no quotes). And the third line should be "Chandra Birmingham 12 6". + +sort scores-table.txt diff --git a/shell-pipelines/sort-uniq-head-tail/script-02.sh b/shell-pipelines/sort-uniq-head-tail/script-02.sh index 29c3c2524..3853cc614 100755 --- a/shell-pipelines/sort-uniq-head-tail/script-02.sh +++ b/shell-pipelines/sort-uniq-head-tail/script-02.sh @@ -5,3 +5,6 @@ set -euo pipefail # The input for this script is the scores-table.txt file. # TODO: Write a command to output scores-table.txt, with lines sorted by the person's first score, descending. # The first line of your output should be "Basia London 22 9 6" (with no quotes). + + +sort -k3,3 -nr scores-table.txt diff --git a/shell-pipelines/sort-uniq-head-tail/script-03.sh b/shell-pipelines/sort-uniq-head-tail/script-03.sh index bcbaf3420..3fb062dc1 100755 --- a/shell-pipelines/sort-uniq-head-tail/script-03.sh +++ b/shell-pipelines/sort-uniq-head-tail/script-03.sh @@ -8,3 +8,5 @@ set -euo pipefail # Basia London 22 9 6 # Piotr Glasgow 15 2 25 11 8 # Chandra Birmingham 12 6 + +sort -k3,3 -nr scores-table.txt | head -n 3 \ No newline at end of file diff --git a/shell-pipelines/sort-uniq-head-tail/script-04.sh b/shell-pipelines/sort-uniq-head-tail/script-04.sh index 65a5cfba8..6809dfe7d 100755 --- a/shell-pipelines/sort-uniq-head-tail/script-04.sh +++ b/shell-pipelines/sort-uniq-head-tail/script-04.sh @@ -5,3 +5,5 @@ set -euo pipefail # The input for this script is the scores-table.txt file. # TODO: Write a command to output scores-table.txt, with shows the line for the player whose first score was the second highest. # Your output should be: "Piotr Glasgow 15 2 25 11 8" (without quotes). + +sort -k3,3 -nr scores-table.txt | head -n 2 | tail -n 1 diff --git a/shell-pipelines/sort-uniq-head-tail/script-05.sh b/shell-pipelines/sort-uniq-head-tail/script-05.sh index a93cd9f9d..7bba7d19a 100755 --- a/shell-pipelines/sort-uniq-head-tail/script-05.sh +++ b/shell-pipelines/sort-uniq-head-tail/script-05.sh @@ -6,3 +6,5 @@ set -euo pipefail # TODO: Write a command to show a list of all events that have happened, without duplication. # The order they're displayed doesn't matter, but we never want to see the same event listed twice. # Your output should contain 6 lines. + +sort events.txt | uniq diff --git a/shell-pipelines/sort-uniq-head-tail/script-06.sh b/shell-pipelines/sort-uniq-head-tail/script-06.sh index 715c7ae5c..4d8a50c5c 100755 --- a/shell-pipelines/sort-uniq-head-tail/script-06.sh +++ b/shell-pipelines/sort-uniq-head-tail/script-06.sh @@ -5,3 +5,6 @@ set -euo pipefail # The input for this script is the events.txt file. # TODO: Write a command to show how many times anyone has entered and exited. # It should be clear from your script's output that there have been 5 Entry events and 4 Exit events. + + +awk '{print $3}' events.txt | sort | uniq -c diff --git a/shell-pipelines/sort-uniq-head-tail/script-07.sh b/shell-pipelines/sort-uniq-head-tail/script-07.sh index 7fd07e1ff..23cc6c898 100755 --- a/shell-pipelines/sort-uniq-head-tail/script-07.sh +++ b/shell-pipelines/sort-uniq-head-tail/script-07.sh @@ -6,3 +6,7 @@ set -euo pipefail # TODO: Write a command to show how many times anyone has entered and exited. # It should be clear from your script's output that there have been 5 Entry events and 4 Exit events. # The word "Event" should not appear in your script's output. + + +awk '$3 == "Entry" || $3 == "Exit" {print $3}' events-with-timestamps.txt | sort | uniq -c + diff --git a/shell-pipelines/tr/script-01.sh b/shell-pipelines/tr/script-01.sh index 8bb0211e9..b638ad8f6 100755 --- a/shell-pipelines/tr/script-01.sh +++ b/shell-pipelines/tr/script-01.sh @@ -6,3 +6,5 @@ set -euo pipefail # The author got feedback that they're using too many exclamation marks (!). # # TODO: Write a command to output the contents of text.txt with every exclamation mark (!) replaced with a full-stop (.). + +tr '!' '.' < text.txt diff --git a/shell-pipelines/tr/script-02.sh b/shell-pipelines/tr/script-02.sh index cf3a503a2..126177b90 100755 --- a/shell-pipelines/tr/script-02.sh +++ b/shell-pipelines/tr/script-02.sh @@ -7,3 +7,5 @@ set -euo pipefail # so every Y should be a Z, and every Z should be a Y! # # TODO: Write a command to output the contents of text.txt with every Y and Z swapped (both upper and lower case). + +tr 'yYzZ' 'zZyY' < text.txt