mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-12-18 12:15:44 +01:00
TERMINAL: Add grep command (#1381)
This commit is contained in:
parent
a780880531
commit
4936d14639
79
package-lock.json
generated
79
package-lock.json
generated
@ -5946,13 +5946,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.4",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
@ -5960,7 +5961,7 @@
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.1",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
@ -5974,6 +5975,7 @@
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@ -5983,6 +5985,7 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
@ -5991,7 +5994,8 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bonjour-service": {
|
||||
"version": "1.1.1",
|
||||
@ -6029,12 +6033,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -6599,6 +6604,7 @@
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -6610,10 +6616,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -8233,17 +8240,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.1",
|
||||
"body-parser": "1.20.2",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.5.0",
|
||||
"cookie": "0.6.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@ -8485,10 +8493,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@ -8635,9 +8644,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.3",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
|
||||
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -8645,6 +8654,7 @@
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
@ -9811,6 +9821,7 @@
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
@ -10306,6 +10317,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
@ -12503,14 +12515,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/katex": {
|
||||
"version": "0.16.9",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz",
|
||||
"integrity": "sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==",
|
||||
"version": "0.16.10",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
|
||||
"integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://opencollective.com/katex",
|
||||
"https://github.com/sponsors/katex"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^8.3.0"
|
||||
},
|
||||
@ -13127,6 +13140,7 @@
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -14926,6 +14940,7 @@
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
},
|
||||
@ -14998,10 +15013,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
@ -15017,6 +15033,7 @@
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@ -16788,6 +16805,7 @@
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
@ -16959,6 +16977,7 @@
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
|
@ -15,6 +15,8 @@ export const TerminalHelpText: string[] = [
|
||||
" download [script/text file] Downloads scripts or text files to your computer",
|
||||
" expr [math expression] Evaluate a mathematical expression",
|
||||
" free Check the machine's memory (RAM) usage",
|
||||
" grep [opts]... pattern [file]... Search for PATTERN (string/regular expression) in each FILE and print results to terminal",
|
||||
" [-O] [target file]",
|
||||
" grow Spoof money in a servers bank account, increasing the amount available.",
|
||||
" hack Hack the current machine",
|
||||
" help [command] Display this help text, or the help text for a command",
|
||||
@ -229,6 +231,47 @@ export const HelpTexts: Record<string, string[]> = {
|
||||
"how much of it is being used.",
|
||||
" ",
|
||||
],
|
||||
grep: [
|
||||
"Usage: grep [OPTION]... PATTERN [FILE]... [-O] [OUTFILE] [-B/A/C] [NUM]",
|
||||
" ",
|
||||
"Search for PATTERN in each FILE and print results to terminal.",
|
||||
"Example: grep -n -h 'hello world' file1.js file2.txt -O output.txt -C 10 -V",
|
||||
" ",
|
||||
"OPTIONS: ",
|
||||
" --help output this usage message and exit",
|
||||
" ",
|
||||
"Search control:",
|
||||
" -* --search-all search for PATTERN in each FILE on server. Ignores any FILE argument(s) passed",
|
||||
" -p --pipe-terminal search for PATTERN in terminal output. Ignores any FILE argument(s) passed",
|
||||
" ",
|
||||
"Pattern selection and interpretation:",
|
||||
" -R, --regexp PATTERN is basic regular expression. PATTERN is a string by default",
|
||||
" ",
|
||||
"Output control:",
|
||||
" -m --max-count NUM stop after NUM selected lines",
|
||||
" -H --with-filename print filename with output lines. Default when multiple FILE arguments passed",
|
||||
" -h --no-filename suppress printing file name with output lines. Default when one FILE argument passed. Overrides -H",
|
||||
" -n --line-number print line number with output lines",
|
||||
" -q --quiet --silent suppress printing to terminal",
|
||||
" -O --output OUTFILE pipe output to text file. The following argument must be a valid .txt or .json filename. Does NOT overwrite by default",
|
||||
" -f --allow-overwrite combine with [-O/--output] to allow overwriting provided output file",
|
||||
" ",
|
||||
"Context control:",
|
||||
" -B --before-context NUM print NUM lines of leading context",
|
||||
" -A --after-context NUM print NUM lines of trailing context",
|
||||
" -C --context NUM print NUM lines of output context",
|
||||
" ",
|
||||
"Miscellaneous:",
|
||||
" -V --verbose print PATTERN, count of matches and FILE(s) searched after regular output",
|
||||
" -v --invert-match select non-matching lines",
|
||||
" ",
|
||||
"Regular OPTIONs may be combined into one. Context, max-count and output OPTIONs must be separated. Example: grep test -VnH* -O output.txt -C 5",
|
||||
"By default PATTERN is interpreted as a simple string.",
|
||||
"At least one FILE argument must be passed, or pass -*/--search-all to search all files.",
|
||||
"The argument immediately following -m, -O and -B/A/C will be interpreted as the parameter for that OPTION.",
|
||||
'If encountering difficulties with argument parsing, consider explicitly passing a string as PATTERN. Example: grep -G "(complex|regexp|\\w+)" script.js',
|
||||
" ",
|
||||
],
|
||||
grow: [
|
||||
"Usage: grow",
|
||||
" ",
|
||||
|
@ -46,6 +46,7 @@ import { cp } from "./commands/cp";
|
||||
import { download } from "./commands/download";
|
||||
import { expr } from "./commands/expr";
|
||||
import { free } from "./commands/free";
|
||||
import { grep } from "./commands/grep";
|
||||
import { grow } from "./commands/grow";
|
||||
import { hack } from "./commands/hack";
|
||||
import { help } from "./commands/help";
|
||||
@ -760,6 +761,7 @@ export class Terminal {
|
||||
download: download,
|
||||
expr: expr,
|
||||
free: free,
|
||||
grep: grep,
|
||||
grow: grow,
|
||||
hack: hack,
|
||||
help: help,
|
||||
|
449
src/Terminal/commands/grep.ts
Normal file
449
src/Terminal/commands/grep.ts
Normal file
@ -0,0 +1,449 @@
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
import { hasTextExtension } from "../../Paths/TextFilePath";
|
||||
import { ContentFile, ContentFilePath, allContentFiles } from "../../Paths/ContentFile";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { help } from "../commands/help";
|
||||
import { Output } from "../OutputTypes";
|
||||
|
||||
type LineParser = (options: Options, filename: string, line: string, i: number) => ParsedLine;
|
||||
|
||||
const RED: string = "\x1b[31m";
|
||||
const DEFAULT: string = "\x1b[0m";
|
||||
const GREEN: string = "\x1b[32m";
|
||||
const MAGENTA: string = "\x1b[35m";
|
||||
const CYAN: string = "\x1b[36m";
|
||||
const WHITE: string = "\x1b[37m";
|
||||
|
||||
// Options and ValidArgs key names must correlate
|
||||
class ArgStrings {
|
||||
short: readonly string[];
|
||||
long: readonly string[];
|
||||
|
||||
constructor(validArgs: ArgStrings) {
|
||||
this.long = validArgs.long;
|
||||
this.short = validArgs.long;
|
||||
}
|
||||
}
|
||||
|
||||
interface Options {
|
||||
isRegExpr: boolean;
|
||||
|
||||
isLineNum: boolean;
|
||||
isNamed: boolean;
|
||||
isNotNamed: boolean;
|
||||
isInvertMatch: boolean;
|
||||
isMaxMatches: boolean;
|
||||
|
||||
isQuiet: boolean;
|
||||
isVerbose: boolean;
|
||||
|
||||
isToFile: boolean;
|
||||
isOverWrite: boolean;
|
||||
|
||||
isPreContext: boolean;
|
||||
isContext: boolean;
|
||||
isPostContext: boolean;
|
||||
|
||||
isHelp: boolean;
|
||||
|
||||
isSearchAll: boolean;
|
||||
isPipeIn: boolean;
|
||||
|
||||
// exceptions: these options are not explicitly checked against passed arguments
|
||||
isMultiFile: boolean;
|
||||
hasContextFlag: boolean;
|
||||
}
|
||||
interface ValidArgs {
|
||||
isRegExpr: ArgStrings;
|
||||
|
||||
isLineNum: ArgStrings;
|
||||
isNamed: ArgStrings;
|
||||
isNotNamed: ArgStrings;
|
||||
isInvertMatch: ArgStrings;
|
||||
isMaxMatches: ArgStrings;
|
||||
|
||||
isQuiet: ArgStrings;
|
||||
isVerbose: ArgStrings;
|
||||
|
||||
isToFile: ArgStrings;
|
||||
isOverWrite: ArgStrings;
|
||||
|
||||
isPreContext: ArgStrings;
|
||||
isContext: ArgStrings;
|
||||
isPostContext: ArgStrings;
|
||||
|
||||
isHelp: ArgStrings;
|
||||
|
||||
isSearchAll: ArgStrings;
|
||||
isPipeIn: ArgStrings;
|
||||
}
|
||||
const VALID_ARGS: ValidArgs = {
|
||||
isRegExpr: { short: ["-R"], long: ["--regexp"] },
|
||||
|
||||
isLineNum: { short: ["-n"], long: ["--line-number"] },
|
||||
isNamed: { short: ["-H"], long: ["--with-filename"] },
|
||||
isNotNamed: { short: ["-h"], long: ["--no-filename"] },
|
||||
isInvertMatch: { short: ["-v"], long: ["--invert-match"] },
|
||||
isMaxMatches: { short: ["-m"], long: ["--max-count"] },
|
||||
|
||||
isQuiet: { short: ["-q"], long: ["--silent", "--quiet"] },
|
||||
isVerbose: { short: ["-V"], long: ["--verbose"] },
|
||||
|
||||
isToFile: { short: ["-O"], long: ["--output"] },
|
||||
isOverWrite: { short: ["-f"], long: ["--allow-overwrite"] },
|
||||
|
||||
isPreContext: { short: ["-B"], long: ["--before-context"] },
|
||||
isContext: { short: ["-C"], long: ["--context"] },
|
||||
isPostContext: { short: ["-A"], long: ["--after-context"] },
|
||||
|
||||
isSearchAll: { short: ["-*"], long: ["--search-all"] },
|
||||
isPipeIn: { short: ["-p"], long: ["--pipe-terminal"] },
|
||||
|
||||
isHelp: { short: [], long: ["--help"] },
|
||||
} as const;
|
||||
//
|
||||
|
||||
interface Errors {
|
||||
noArgs: string;
|
||||
noSearchArg: string;
|
||||
badSearchFile: (str: string[]) => string;
|
||||
badParameter: (opt: string, arg: string) => string;
|
||||
badOutFile: (str: string) => string;
|
||||
outFileExists: (str: string) => string;
|
||||
truncated: () => string;
|
||||
}
|
||||
|
||||
const ERR: Errors = {
|
||||
noArgs: "grep argument error. Usage: grep [OPTION]... PATTERN [FILE]... [-O] [OUTPUT FILE] [-B/A/C] [NUM]",
|
||||
noSearchArg:
|
||||
"grep argument error: At least one FILE argument must be passed, or pass -*/--search-all to search all files on server",
|
||||
badSearchFile: (files: string[]) =>
|
||||
`grep argument error: Invalid filename(s): ${files.join(
|
||||
", ",
|
||||
)}. OPTIONS with additional parameters (-O, -m, -B/A/C) must be separated from other options`,
|
||||
badParameter: (option: string, arg: string) =>
|
||||
`grep argument error: Incorrect ${option} argument "${arg}". Must be a number.`,
|
||||
outFileExists: (path: string) =>
|
||||
`grep file output failed: Invalid output file "${path}". Output file must not already exist. Pass -f/--allow-overwrite to overwrite.`,
|
||||
badOutFile: (path: string) =>
|
||||
`grep file output failed: Invalid output file "${path}". Output file must be a text file.`,
|
||||
truncated: () => `\n${RED}Terminal output truncated to ${Settings.MaxTerminalCapacity} lines (Max terminal capacity)`,
|
||||
} as const;
|
||||
|
||||
class Args {
|
||||
args: string[];
|
||||
|
||||
constructor(args: (string | number | boolean)[]) {
|
||||
this.args = args.map(String);
|
||||
}
|
||||
|
||||
initOptions: Options = {
|
||||
isRegExpr: false,
|
||||
|
||||
isLineNum: false,
|
||||
isNamed: false,
|
||||
isNotNamed: false,
|
||||
isInvertMatch: false,
|
||||
isMaxMatches: false,
|
||||
|
||||
isQuiet: false,
|
||||
isVerbose: false,
|
||||
|
||||
isToFile: false,
|
||||
isOverWrite: false,
|
||||
|
||||
isPreContext: false,
|
||||
isContext: false,
|
||||
isPostContext: false,
|
||||
|
||||
isSearchAll: false,
|
||||
isPipeIn: false,
|
||||
|
||||
isHelp: false,
|
||||
|
||||
isMultiFile: false,
|
||||
hasContextFlag: false,
|
||||
};
|
||||
|
||||
mapArgToOpts(fullArg: string, options: Options): [Options, boolean] {
|
||||
let isOption = false;
|
||||
for (const key of Object.keys(VALID_ARGS)) {
|
||||
const stripDash = (arg: string) => arg.replace("-", "");
|
||||
if (!fullArg.startsWith("-")) break;
|
||||
// check long args
|
||||
const theseArgs = VALID_ARGS[key as keyof ValidArgs];
|
||||
const allArgs = [...theseArgs.long, ...theseArgs.short];
|
||||
if (allArgs.includes(fullArg)) {
|
||||
options[key as keyof Options] = true;
|
||||
isOption = true;
|
||||
}
|
||||
// check multiflag args
|
||||
const multiFlag = stripDash(fullArg);
|
||||
const shortArgs = theseArgs.short.map(stripDash);
|
||||
if (multiFlag.length > 1 && shortArgs.some((arg) => [...multiFlag].includes(arg))) {
|
||||
options[key as keyof Options] = true;
|
||||
isOption = true;
|
||||
}
|
||||
}
|
||||
return [options, isOption];
|
||||
}
|
||||
|
||||
splitOptsAndArgs(): [Options, string[], string, string, string] {
|
||||
let outFile, limit, context;
|
||||
|
||||
[outFile, this.args] = this.spliceOptParam(VALID_ARGS.isToFile);
|
||||
[limit, this.args] = this.spliceOptParam(VALID_ARGS.isMaxMatches);
|
||||
[context, this.args] = this.spliceOptParam(VALID_ARGS.isPreContext);
|
||||
if (!context) [context, this.args] = this.spliceOptParam(VALID_ARGS.isContext);
|
||||
if (!context) [context, this.args] = this.spliceOptParam(VALID_ARGS.isPostContext);
|
||||
|
||||
const [options, otherArgs] = this.args.reduce(
|
||||
([options, otherArgs]: [Options, string[]], fullArg: string): [Options, string[]] => {
|
||||
let isOption = false;
|
||||
[options, isOption] = this.mapArgToOpts(fullArg, options);
|
||||
return isOption ? [options, otherArgs] : [options, [...otherArgs, fullArg]];
|
||||
},
|
||||
[this.initOptions, []],
|
||||
);
|
||||
const outFileStr = outFile ?? "";
|
||||
const limitNum = limit ?? "";
|
||||
const contextNum = context ?? "";
|
||||
|
||||
return [options, otherArgs, outFileStr, contextNum, limitNum];
|
||||
}
|
||||
|
||||
spliceOptParam(validArgs: ArgStrings): [string, string[]] | [undefined, string[]] {
|
||||
const argIndex = [...validArgs.long, ...validArgs.short].reduce((ret: number, arg: string) => {
|
||||
const argIndex = this.args.indexOf(arg);
|
||||
return argIndex > -1 ? argIndex : ret;
|
||||
}, NaN);
|
||||
|
||||
if (isNaN(argIndex)) return [undefined, this.args];
|
||||
|
||||
const nextArg = this.args.splice(argIndex + 1, 1)[0];
|
||||
|
||||
return [nextArg, this.args];
|
||||
}
|
||||
}
|
||||
|
||||
interface LineStrings {
|
||||
rawLine: string;
|
||||
prettyLine: string;
|
||||
}
|
||||
|
||||
interface ParsedLine {
|
||||
isPrint: boolean;
|
||||
isMatched: boolean;
|
||||
lines: LineStrings;
|
||||
filename: string;
|
||||
isFileSep: boolean;
|
||||
}
|
||||
|
||||
class Results {
|
||||
lines: ParsedLine[];
|
||||
areEdited: boolean;
|
||||
numMatches: number;
|
||||
options: Options;
|
||||
matchCounter: number;
|
||||
matchLimit: number;
|
||||
|
||||
constructor(results: ParsedLine[], options: Options, matchLimit: number) {
|
||||
this.lines = results;
|
||||
this.options = options;
|
||||
this.areEdited = results.some((line) => line.isMatched);
|
||||
this.numMatches = results.reduce((acc, result) => acc + Number(result.isMatched), 0);
|
||||
this.matchLimit = matchLimit;
|
||||
this.matchCounter = 0;
|
||||
}
|
||||
|
||||
addContext(context: number): Results {
|
||||
const nContext = isNaN(Number(context)) ? 0 : Number(context);
|
||||
for (const [editLineIndex, line] of this.lines.entries()) {
|
||||
if (!line.isMatched) continue;
|
||||
for (let contextLineIndex = 0; contextLineIndex <= nContext; contextLineIndex++) {
|
||||
let contextLine;
|
||||
if (this.options.isPreContext) {
|
||||
contextLine = this.lines[editLineIndex - contextLineIndex];
|
||||
} else if (this.options.isPostContext) {
|
||||
contextLine = this.lines[editLineIndex + contextLineIndex];
|
||||
} else if (this.options.isContext) {
|
||||
contextLine = this.lines[editLineIndex - Math.floor(nContext / 2) + contextLineIndex];
|
||||
} else {
|
||||
contextLine = line;
|
||||
}
|
||||
|
||||
if (contextLine && !line.isFileSep && line.filename === contextLine.filename) contextLine.isPrint = true;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
splitAndFilter(): [string[], string[]] {
|
||||
const rawResult = [];
|
||||
const prettyResult = [];
|
||||
for (const lineInfo of this.lines) {
|
||||
if (lineInfo.isPrint === this.options.isInvertMatch) continue;
|
||||
rawResult.push(lineInfo.lines.rawLine);
|
||||
prettyResult.push(lineInfo.lines.prettyLine);
|
||||
}
|
||||
return [rawResult, prettyResult];
|
||||
}
|
||||
|
||||
capMatches(limit: number): Results {
|
||||
if (!this.options.isMaxMatches) return this;
|
||||
for (const line of this.lines) {
|
||||
if (line.isMatched) this.matchCounter += 1;
|
||||
if (this.matchCounter > limit) line.isMatched = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
getVerboseInfo(files: ContentFile[], pattern: string | RegExp, options: Options): string {
|
||||
if (!options.isVerbose) return "";
|
||||
const suffix = (pre: string, num: number) => pre + (num === 1 ? "" : "s");
|
||||
const totalLines = this.lines.length;
|
||||
const matchCount = Math.abs((options.isInvertMatch ? totalLines : 0) - this.numMatches);
|
||||
const inputStr = options.isPipeIn
|
||||
? "piped from terminal "
|
||||
: `in ${files.length} ${suffix("file", files.length)}:\n`;
|
||||
const filesStr = files
|
||||
.map((file, i) => `${i % 2 ? WHITE : ""}${file.filename}(${file.content.split("\n").length}loc)${DEFAULT}`)
|
||||
.join(", ");
|
||||
|
||||
return [
|
||||
`\n${(options.isMaxMatches ? this.matchLimit : matchCount) + (options.isInvertMatch ? " INVERTED" : "")} `,
|
||||
suffix("line", matchCount) + " matched ",
|
||||
`against PATTERN "${pattern.toString()}" `,
|
||||
`in ${totalLines} ${suffix("line", totalLines)}, `,
|
||||
inputStr,
|
||||
`${filesStr}`,
|
||||
].join("");
|
||||
}
|
||||
}
|
||||
|
||||
function getServerFiles(server: BaseServer): [ContentFile[], string[]] {
|
||||
const files = [];
|
||||
for (const tuple of allContentFiles(server)) {
|
||||
files.push(tuple[1]);
|
||||
}
|
||||
return [files, []];
|
||||
}
|
||||
|
||||
function getArgFiles(args: string[]): [ContentFile[], string[]] {
|
||||
const notFiles = [];
|
||||
const files = [];
|
||||
|
||||
for (const arg of args) {
|
||||
const file = hasTextExtension(arg) ? Terminal.getTextFile(arg) : Terminal.getScript(arg);
|
||||
if (!file) {
|
||||
notFiles.push(arg);
|
||||
} else {
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
return [files, notFiles];
|
||||
}
|
||||
|
||||
function parseLine(pattern: string | RegExp, options: Options, filename: string, line: string, i: number): ParsedLine {
|
||||
const editedLine = line.replaceAll(pattern, `${RED}$&${DEFAULT}`);
|
||||
|
||||
const name = options.isMultiFile || (options.isNamed && !options.isNotNamed) ? `${filename}` : "";
|
||||
const lineNo = options.isLineNum ? `${i + 1}` : "";
|
||||
|
||||
const [colName, rawName] = name ? [`${MAGENTA}${name}${CYAN}:${DEFAULT}`, `${name}:`] : ["", ""];
|
||||
const [colLineNo, rawLineNo] = lineNo ? [`${GREEN}${lineNo}${CYAN}:${DEFAULT}`, `${lineNo}:`] : ["", ""];
|
||||
const lines: LineStrings = { rawLine: rawName + rawLineNo + line, prettyLine: colName + colLineNo + editedLine };
|
||||
|
||||
const isMatched = line !== editedLine;
|
||||
return { lines, filename, isMatched, isPrint: false, isFileSep: false };
|
||||
}
|
||||
|
||||
function parseFile(lineParser: LineParser, options: Options, file: ContentFile, i: number): ParsedLine[] {
|
||||
const parseLineFn = lineParser.bind(null, options, file.filename);
|
||||
const editedContent: ParsedLine[] = file.content.split("\n").map(parseLineFn);
|
||||
|
||||
const hasMatch = editedContent.some((line) => line.isMatched);
|
||||
|
||||
const isPrintFileSep = options.hasContextFlag && hasMatch && i !== 0;
|
||||
|
||||
const fileSeparator: ParsedLine = {
|
||||
lines: { prettyLine: `${CYAN}--${DEFAULT}`, rawLine: "--" },
|
||||
isPrint: true,
|
||||
isMatched: false,
|
||||
isFileSep: true,
|
||||
filename: "",
|
||||
};
|
||||
return isPrintFileSep ? [fileSeparator, ...editedContent] : editedContent;
|
||||
}
|
||||
|
||||
function writeToTerminal(
|
||||
prettyResult: string[],
|
||||
options: Options,
|
||||
results: Results,
|
||||
files: ContentFile[],
|
||||
pattern: string | RegExp,
|
||||
): void {
|
||||
const printResult = prettyResult.slice(prettyResult.length - Settings.MaxTerminalCapacity); // limit printing to terminal
|
||||
const isTruncated = prettyResult.length !== printResult.length;
|
||||
const verboseInfo = results.getVerboseInfo(files, pattern, options);
|
||||
const truncateInfo = isTruncated ? ERR.truncated() : "";
|
||||
|
||||
if (results.areEdited) Terminal.print(printResult.join("\n") + truncateInfo);
|
||||
if (options.isVerbose) Terminal.print(verboseInfo);
|
||||
}
|
||||
|
||||
function checkOutFile(outFileStr: string, options: Options, server: BaseServer): ContentFilePath | void {
|
||||
if (!options.isToFile) return;
|
||||
const outFilePath = Terminal.getFilepath(outFileStr);
|
||||
if (!outFilePath || !hasTextExtension(outFilePath)) {
|
||||
return Terminal.error(ERR.badOutFile(outFileStr));
|
||||
}
|
||||
if (!options.isOverWrite && server.textFiles.has(outFilePath)) return Terminal.error(ERR.outFileExists(outFileStr));
|
||||
return outFilePath;
|
||||
}
|
||||
|
||||
function grabTerminal(): string[] {
|
||||
return Terminal.outputHistory.map((line) => (line as Output).text ?? "");
|
||||
}
|
||||
|
||||
export function grep(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
if (!args.length) return Terminal.error(ERR.noArgs);
|
||||
|
||||
const [options, otherArgs, outFile, context, limit] = new Args(args).splitOptsAndArgs();
|
||||
const [files, notFiles] = options.isSearchAll ? getServerFiles(server) : getArgFiles(otherArgs.slice(1));
|
||||
const outFilePath = checkOutFile(outFile, options, server);
|
||||
|
||||
options.isMultiFile = files.length > 1;
|
||||
options.hasContextFlag = options.isContext || options.isPreContext || options.isPostContext;
|
||||
|
||||
// error checking
|
||||
if (options.isToFile && !outFilePath) return; // associated errors are printed in checkOutFile
|
||||
if (options.isHelp) return help(["grep"]);
|
||||
if (notFiles.length) return Terminal.error(ERR.badSearchFile(notFiles));
|
||||
if (!options.isPipeIn && !options.isSearchAll && !files.length) return Terminal.error(ERR.noSearchArg);
|
||||
if (options.hasContextFlag && (context === "" || isNaN(Number(context))))
|
||||
return Terminal.error(ERR.badParameter("context", context));
|
||||
if (options.isMaxMatches && (limit === "" || isNaN(Number(limit))))
|
||||
return Terminal.error(ERR.badParameter("limit", limit));
|
||||
|
||||
const nContext = Number(context);
|
||||
const nLimit = Number(limit);
|
||||
|
||||
try {
|
||||
const pattern = options.isRegExpr ? new RegExp(otherArgs[0], "g") : otherArgs[0];
|
||||
const lineParser = parseLine.bind(null, pattern);
|
||||
const termParser = lineParser.bind(null, options, "Terminal");
|
||||
const fileParser = parseFile.bind(null, lineParser, options);
|
||||
const contentToMatch = options.isPipeIn ? grabTerminal().map(termParser) : files.flatMap(fileParser);
|
||||
const results = new Results(contentToMatch, options, nLimit);
|
||||
const [rawResult, prettyResult] = results.capMatches(nLimit).addContext(nContext).splitAndFilter();
|
||||
|
||||
if (options.isPipeIn) files.length = 0;
|
||||
if (!options.isQuiet) writeToTerminal(prettyResult, options, results, files, pattern);
|
||||
if (options.isToFile && outFilePath) server.writeToContentFile(outFilePath, rawResult.join("\n"));
|
||||
} catch (e) {
|
||||
Terminal.error("grep processing error: " + e);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user