TERMINAL: Add grep command (#1381)

This commit is contained in:
muesli4brekkies 2024-06-14 08:00:48 +01:00 committed by GitHub
parent a780880531
commit 4936d14639
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 543 additions and 30 deletions

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,

@ -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);
}
}