diff --git a/src/Terminal/HelpText.ts b/src/Terminal/HelpText.ts index d77d413f3..22eed0e28 100644 --- a/src/Terminal/HelpText.ts +++ b/src/Terminal/HelpText.ts @@ -3,49 +3,70 @@ import { IMap } from "../types"; export const TerminalHelpText: string[] = [ "Type 'help name' to learn more about the command ", - " ", - ' alias [-g] [name="value"] Create or display Terminal aliases', - " analyze Get information about the current machine ", - " backdoor Install a backdoor on the current machine ", - " buy [-l/-a/program] Purchase a program through the Dark Web", - " cat [file] Display a .msg, .lit, or .txt file", - " cd [dir] Change to a new directory", - " check [script] [args...] Print a script's logs to Terminal", - " clear Clear all text on the terminal ", - " cls See 'clear' command ", - " connect [hostname] Connects to a remote server", - " cp [src] [dst] Copy a file", - " 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", - " 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", - " home Connect to home computer", - " hostname Displays the hostname of the machine", - " kill [script/pid] [args...] Stops the specified script on the current server ", - " killall Stops all running scripts on the current machine", - " ls [dir] [| grep pattern] Displays all files on the machine", - " lscpu Displays the number of CPU cores on the machine", - " mem [script] [-t n] Displays the amount of RAM required to run the script", - " mv [src] [dest] Move/rename a text or script file", - " nano [file ...] Text editor - Open up and edit one or more scripts or text files", - " ps Display all scripts that are currently running", - " rm [file] Delete a file from the server", - " run [name] [-t n] [--tail] [args...] Execute a program or script", - " scan Prints all immediately-available network connections", - " scan-analyze [d] [-a] Prints info for all servers up to d nodes away", - " scp [file ...] [server] Copies a file to a destination server", - " sudov Shows whether you have root access on this computer", - " tail [script] [args...] Displays dynamic logs for the specified script", - " top Displays all running scripts and their RAM usage", - " unalias [alias name] Deletes the specified alias", - " vim [file ...] Text editor - Open up and edit one or more scripts or text files in vim mode", - " weaken Reduce the security of the current machine", - " wget [url] [target file] Retrieves code/text from a web server", - " ", + "", + 'alias [-g] [name="value"] Create or display Terminal aliases', + "analyze Get information about the current machine ", + "backdoor Install a backdoor on the current machine ", + "buy [-l/program] Purchase a program through the Dark Web", + "cat [file] Display a .msg, .lit, or .txt file", + "cd [dir] Change to a new directory", + "check [script] [args...] Print a script's logs to Terminal", + "clear Clear all text on the terminal ", + "cls See 'clear' command ", + "connect [hostname] Connects to a remote server", + "cp [src] [dst] Copy a file", + "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", + "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", + "home Connect to home computer", + "hostname Displays the hostname of the machine", + "kill [script/pid] [args...] Stops the specified script on the current server ", + "killall Stops all running scripts on the current machine", + "ls [dir] [| grep pattern] Displays all files on the machine", + "lscpu Displays the number of CPU cores on the machine", + "mem [script] [-t n] Displays the amount of RAM required to run the script", + "mv [src] [dest] Move/rename a text or script file", + "nano [file ...] | [glob] Text editor - Open up and edit one or more scripts or text files", + "ps Display all scripts that are currently running", + "rm [file] Delete a file from the server", + "run [name] [-t n] [--tail] [args...] Execute a program or script", + "scan Prints all immediately-available network connections", + "scan-analyze [d] [-a] Prints info for all servers up to d nodes away", + "scp [file ...] [server] Copies a file to a destination server", + "sudov Shows whether you have root access on this computer", + "tail [script] [args...] Displays dynamic logs for the specified script", + "top Displays all running scripts and their RAM usage", + "unalias [alias name] Deletes the specified alias", + "vim [file ...] | [glob] Text editor - Open up and edit one or more scripts or text files in vim mode", + "weaken Reduce the security of the current machine", + "wget [url] [target file] Retrieves code/text from a web server", ]; +const TemplatedHelpTexts: IMap<(command: string) => string[]> = { + scriptEditor: (command) => { + return [ + `${command} [file ...] | [glob]`, + ` `, + `Opens up the specified file(s) in the Script Editor. Only scripts (.js, .ns, .script) or text files (.txt) `, + `can be edited using the Script Editor. If a file does not exist a new one will be created`, + ` `, + `If provided a glob as the only argument, ${command} can spider directories and open all matching `, + `files at once. ${command} cannot create files using globs, so your scripts must already exist.`, + ` `, + `Examples:`, + ` `, + `${command} test.js`, + `${command} test.js test2.js`, + ` `, + `${command} test.*`, + `${command} /my-dir/*.js`, + ] + } +} + export const HelpTexts: IMap = { alias: [ 'Usage: alias [-g] [name="value"] ', @@ -322,16 +343,8 @@ export const HelpTexts: IMap = { " mv myScript.js myOldScript.js", " ", ], - nano: [ - "Usage: nano [file ...]", - " ", - "Opens up the specified file(s) in the Text Editor. Only scripts (.script) or text files (.txt) can be ", - "edited using the Text Editor. If the file does not already exist, then a new, empty one ", - "will be created", - " ", - ], + nano: TemplatedHelpTexts.scriptEditor('nano'), ps: ["Usage: ps", " ", "Prints all scripts that are running on the current server", " "], - rm: [ "Usage: rm [file]", " ", @@ -433,14 +446,7 @@ export const HelpTexts: IMap = { "It is not necessary to differentiate between global and non-global aliases when using 'unalias'", " ", ], - vim: [ - "Usage: vim [file ...]", - " ", - "Opens up the specified file(s) in the Text Editor in vim mode. Only scripts (.script) or text files (.txt) can be ", - "edited using the Text Editor. If the file does not already exist, then a new, empty one ", - "will be created", - " ", - ], + vim: TemplatedHelpTexts.scriptEditor('vim'), weaken: [ "Usage: weaken", " ", diff --git a/src/Terminal/commands/common/editor.ts b/src/Terminal/commands/common/editor.ts index bf5c23960..8d6b6c7c5 100644 --- a/src/Terminal/commands/common/editor.ts +++ b/src/Terminal/commands/common/editor.ts @@ -1,9 +1,12 @@ import { ITerminal } from "../../ITerminal"; +import { removeLeadingSlash, removeTrailingSlash } from '../../DirectoryHelpers' import { IRouter, ScriptEditorRouteOptions } from "../../../ui/Router"; import { IPlayer } from "../../../PersonObjects/IPlayer"; import { BaseServer } from "../../../Server/BaseServer"; import { isScriptFilename } from "../../../Script/isScriptFilename"; import { CursorPositions } from "../../../ScriptEditor/CursorPositions"; +import { Script } from "../../../Script/Script"; +import { isEmpty } from "lodash"; interface EditorParameters { terminal: ITerminal; @@ -22,6 +25,74 @@ export async function main(ns) { }`; +interface ISimpleScriptGlob { + glob: string; + preGlob: string; + postGlob: string; + globError: string; + globMatches: string[]; + globAgainst: Script[]; +} + +function containsSimpleGlob(filename: string): boolean { + return filename.includes("*"); +} + +function detectSimpleScriptGlob( + args: EditorParameters["args"], + player: IPlayer, + terminal: ITerminal, +): ISimpleScriptGlob | null { + if (args.length == 1 && containsSimpleGlob(`${args[0]}`)) { + const filename = `${args[0]}`; + const scripts = player.getCurrentServer().scripts; + const parsedGlob = parseSimpleScriptGlob(filename, scripts, terminal); + return parsedGlob; + } + return null; +} + +function parseSimpleScriptGlob(globString: string, globDatabase: Script[], terminal: ITerminal): ISimpleScriptGlob { + const parsedGlob: ISimpleScriptGlob = { + glob: globString, + preGlob: "", + postGlob: "", + globError: "", + globMatches: [], + globAgainst: globDatabase, + }; + + // Ensure deep globs are minified to simple globs, which act as deep globs in this impl + globString = globString.replace("**", "*"); + + // Ensure only a single glob is present + if (globString.split("").filter((c) => c == "*").length !== 1) { + parsedGlob.globError = "Only a single glob is supported per command.\nexample: `nano my-dir/*.js`"; + return parsedGlob; + } + + // Split arg around glob, normalize preGlob path + [parsedGlob.preGlob, parsedGlob.postGlob] = globString.split("*"); + parsedGlob.preGlob = removeLeadingSlash(parsedGlob.preGlob); + + // Add CWD to preGlob path + const cwd = removeTrailingSlash(terminal.cwd()) + parsedGlob.preGlob = `${cwd}/${parsedGlob.preGlob}` + + // For every script on the current server, filter matched scripts per glob values & persist + globDatabase.forEach((script) => { + const filename = script.filename.startsWith('/') ? script.filename : `/${script.filename}` + if (filename.startsWith(parsedGlob.preGlob) && filename.endsWith(parsedGlob.postGlob)) { + parsedGlob.globMatches.push(filename); + } + }); + + // Rebuild glob for potential error reporting + parsedGlob.glob = `${parsedGlob.preGlob}*${parsedGlob.postGlob}` + + return parsedGlob; +} + export function commonEditor( command: string, { terminal, router, player, args }: EditorParameters, @@ -32,8 +103,15 @@ export function commonEditor( return; } + let filesToLoadOrCreate = args; try { - const files = args.map((arg) => { + const globSearch = detectSimpleScriptGlob(args, player, terminal); + if (globSearch) { + if (isEmpty(globSearch.globError) === false) throw new Error(globSearch.globError); + filesToLoadOrCreate = globSearch.globMatches; + } + + const files = filesToLoadOrCreate.map((arg) => { const filename = `${arg}`; if (isScriptFilename(filename)) { @@ -55,12 +133,18 @@ export function commonEditor( if (filename.endsWith(".txt")) { const filepath = terminal.getFilepath(filename); const txt = terminal.getTextFile(player, filename); - return [filepath, txt == null ? "" : txt.text]; + return [filepath, txt === null ? "" : txt.text]; } - throw new Error(`Invalid file. Only scripts (.script, .ns, .js), or text files (.txt) can be edited with ${command}`); + throw new Error( + `Invalid file. Only scripts (.script, .ns, .js), or text files (.txt) can be edited with ${command}`, + ); }); + if (globSearch && files.length === 0) { + throw new Error(`Could not find any valid files to open with ${command} using glob: \`${globSearch.glob}\``) + } + router.toScriptEditor(Object.fromEntries(files), scriptEditorRouteOptions); } catch (e) { terminal.error(`${e}`);