Merge pull request #2527 from smolgumball/add-simple-globs-nano-vim

feat: add simple glob support to open multiple files with `nano` & `vim` commands
This commit is contained in:
hydroflame 2022-01-15 17:38:39 -05:00 committed by GitHub
commit c791e8264a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 151 additions and 61 deletions

@ -7,7 +7,7 @@ export const TerminalHelpText: string[] = [
'alias [-g] [name="value"] Create or display Terminal aliases', 'alias [-g] [name="value"] Create or display Terminal aliases',
"analyze Get information about the current machine ", "analyze Get information about the current machine ",
"backdoor Install a backdoor on the current machine ", "backdoor Install a backdoor on the current machine ",
" buy [-l/-a/program] Purchase a program through the Dark Web", "buy [-l/program] Purchase a program through the Dark Web",
"cat [file] Display a .msg, .lit, or .txt file", "cat [file] Display a .msg, .lit, or .txt file",
"cd [dir] Change to a new directory", "cd [dir] Change to a new directory",
"check [script] [args...] Print a script's logs to Terminal", "check [script] [args...] Print a script's logs to Terminal",
@ -29,7 +29,7 @@ export const TerminalHelpText: string[] = [
"lscpu Displays the number of CPU cores 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", "mem [script] [-t n] Displays the amount of RAM required to run the script",
"mv [src] [dest] Move/rename a text or script file", "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", "nano [file ...] | [glob] Text editor - Open up and edit one or more scripts or text files",
"ps Display all scripts that are currently running", "ps Display all scripts that are currently running",
"rm [file] Delete a file from the server", "rm [file] Delete a file from the server",
"run [name] [-t n] [--tail] [args...] Execute a program or script", "run [name] [-t n] [--tail] [args...] Execute a program or script",
@ -40,12 +40,33 @@ export const TerminalHelpText: string[] = [
"tail [script] [args...] Displays dynamic logs for the specified script", "tail [script] [args...] Displays dynamic logs for the specified script",
"top Displays all running scripts and their RAM usage", "top Displays all running scripts and their RAM usage",
"unalias [alias name] Deletes the specified alias", "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", "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", "weaken Reduce the security of the current machine",
"wget [url] [target file] Retrieves code/text from a web server", "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<string[]> = { export const HelpTexts: IMap<string[]> = {
alias: [ alias: [
'Usage: alias [-g] [name="value"] ', 'Usage: alias [-g] [name="value"] ',
@ -322,16 +343,8 @@ export const HelpTexts: IMap<string[]> = {
" mv myScript.js myOldScript.js", " mv myScript.js myOldScript.js",
" ", " ",
], ],
nano: [ nano: TemplatedHelpTexts.scriptEditor('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",
" ",
],
ps: ["Usage: ps", " ", "Prints all scripts that are running on the current server", " "], ps: ["Usage: ps", " ", "Prints all scripts that are running on the current server", " "],
rm: [ rm: [
"Usage: rm [file]", "Usage: rm [file]",
" ", " ",
@ -433,14 +446,7 @@ export const HelpTexts: IMap<string[]> = {
"It is not necessary to differentiate between global and non-global aliases when using 'unalias'", "It is not necessary to differentiate between global and non-global aliases when using 'unalias'",
" ", " ",
], ],
vim: [ vim: TemplatedHelpTexts.scriptEditor('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",
" ",
],
weaken: [ weaken: [
"Usage: weaken", "Usage: weaken",
" ", " ",

@ -1,9 +1,12 @@
import { ITerminal } from "../../ITerminal"; import { ITerminal } from "../../ITerminal";
import { removeLeadingSlash, removeTrailingSlash } from '../../DirectoryHelpers'
import { IRouter, ScriptEditorRouteOptions } from "../../../ui/Router"; import { IRouter, ScriptEditorRouteOptions } from "../../../ui/Router";
import { IPlayer } from "../../../PersonObjects/IPlayer"; import { IPlayer } from "../../../PersonObjects/IPlayer";
import { BaseServer } from "../../../Server/BaseServer"; import { BaseServer } from "../../../Server/BaseServer";
import { isScriptFilename } from "../../../Script/isScriptFilename"; import { isScriptFilename } from "../../../Script/isScriptFilename";
import { CursorPositions } from "../../../ScriptEditor/CursorPositions"; import { CursorPositions } from "../../../ScriptEditor/CursorPositions";
import { Script } from "../../../Script/Script";
import { isEmpty } from "lodash";
interface EditorParameters { interface EditorParameters {
terminal: ITerminal; 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( export function commonEditor(
command: string, command: string,
{ terminal, router, player, args }: EditorParameters, { terminal, router, player, args }: EditorParameters,
@ -32,8 +103,15 @@ export function commonEditor(
return; return;
} }
let filesToLoadOrCreate = args;
try { 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}`; const filename = `${arg}`;
if (isScriptFilename(filename)) { if (isScriptFilename(filename)) {
@ -55,12 +133,18 @@ export function commonEditor(
if (filename.endsWith(".txt")) { if (filename.endsWith(".txt")) {
const filepath = terminal.getFilepath(filename); const filepath = terminal.getFilepath(filename);
const txt = terminal.getTextFile(player, 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); router.toScriptEditor(Object.fromEntries(files), scriptEditorRouteOptions);
} catch (e) { } catch (e) {
terminal.error(`${e}`); terminal.error(`${e}`);