diff --git a/src/api/commands/cd/cd-and-pwd.test.ts b/src/api/commands/cd/cd-and-pwd.test.ts index 4751b28..97501dd 100644 --- a/src/api/commands/cd/cd-and-pwd.test.ts +++ b/src/api/commands/cd/cd-and-pwd.test.ts @@ -40,7 +40,8 @@ test("cd affects working directory of exec", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: sh -c "echo $PWD" + ", "stdout": "/src ", } diff --git a/src/api/commands/which/which.help.md b/src/api/commands/which/which.help.md index f9e8965..7abfbdf 100644 --- a/src/api/commands/which/which.help.md +++ b/src/api/commands/which/which.help.md @@ -20,7 +20,9 @@ declare function which( options?: { searchPaths?: Array; suffixes?: Array; - trace?: (...args: Array) => void; + logging?: { + trace?: (...args: Array) => void; + }; } ): Path | null; ``` diff --git a/src/api/commands/which/which.inc.d.ts b/src/api/commands/which/which.inc.d.ts index 688511a..84ecf0c 100644 --- a/src/api/commands/which/which.inc.d.ts +++ b/src/api/commands/which/which.inc.d.ts @@ -12,8 +12,36 @@ declare function which( binaryName: string, options?: { + /** + * A list of folders where programs may be found. Defaults to + * `env.PATH?.split(Path.OS_ENV_VAR_SEPARATOR) || []`. + */ searchPaths?: Array; + + /** + * A list of filename extension suffixes to include in the search, ie + * `[".exe"]`. Defaults to {@link Path.OS_PROGRAM_EXTENSIONS}. + */ suffixes?: Array; - trace?: (...args: Array) => void; + + /** Options which control logging. */ + logging?: { + /** + * If provided, this logging function will be called multiple times as + * `which` runs, to help you understand what's going on and/or troubleshoot + * things. In most cases, it makes sense to use a function from `console` + * here, like so: + * + * ```js + * which("bash", { + * logging: { trace: console.log } + * }); + * ``` + * + * Defaults to the current value of {@link logger.trace}. `logger.trace` + * defaults to a no-op function. + */ + trace?: (...args: Array) => void; + }; } ): Path | null; diff --git a/src/api/commands/which/which.test.ts b/src/api/commands/which/which.test.ts index 7cfc593..78ea461 100644 --- a/src/api/commands/which/which.test.ts +++ b/src/api/commands/which/which.test.ts @@ -31,10 +31,14 @@ test("which", async () => { which("sample", { suffixes: [".one", ".two"], - trace: console.error, + logging: { + trace: console.error, + }, }), which("program2", { - trace: console.error, + logging: { + trace: console.error, + }, }), ]); `; diff --git a/src/api/commands/which/which.ts b/src/api/commands/which/which.ts index bd373e9..dc3e3fb 100644 --- a/src/api/commands/which/which.ts +++ b/src/api/commands/which/which.ts @@ -5,6 +5,7 @@ import { setHelpText } from "../../help"; import whichHelpText from "./which.help.md"; import { assert } from "../../assert"; import { types } from "../../types"; +import { is } from "../../is"; import { quote } from "../../strings"; import { logger } from "../../logger"; @@ -12,7 +13,9 @@ function optionDefaults() { return { searchPaths: env.PATH?.split(Path.OS_ENV_VAR_SEPARATOR) || [], suffixes: Array.from(Path.OS_PROGRAM_EXTENSIONS), - trace: logger.trace, + logging: { + trace: logger.trace, + }, }; } @@ -21,7 +24,9 @@ export function which( options?: { searchPaths?: Array; suffixes?: Array; - trace?: (...args: Array) => void; + logging?: { + trace?: (...args: Array) => void; + }; } ): Path | null { assert.type(binaryName, types.string, "'binaryName' must be a string"); @@ -46,17 +51,25 @@ export function which( "when present, 'options.suffixes' must be an Array of strings" ); assert.type( - options.trace, - types.or(types.undefined, types.Function), - "when present, 'options.trace' must be a Function" + options.logging, + types.or(types.undefined, types.object), + "when present, 'options.logging' must be an object" ); + + if (is(options.logging, types.object)) { + assert.type( + options.logging.trace, + types.or(types.undefined, types.Function), + "when present, 'options.logging.trace' must be a Function" + ); + } } const defaults = optionDefaults(); const { searchPaths = defaults.searchPaths, suffixes = defaults.suffixes, - trace = defaults.trace, + logging: { trace = defaults.logging.trace } = defaults.logging, } = options ?? defaults; for (const lookupPath of searchPaths) { diff --git a/src/api/env/env.test.ts b/src/api/env/env.test.ts index 6294ed1..28a14f9 100644 --- a/src/api/env/env.test.ts +++ b/src/api/env/env.test.ts @@ -43,7 +43,8 @@ test("setting env affects child processes", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: -e "env.BLAH_BLAH" + ", "stdout": "yes ", } diff --git a/src/api/exec/ChildProcess.help.md b/src/api/exec/ChildProcess.help.md index 932a0c2..eb5eac6 100644 --- a/src/api/exec/ChildProcess.help.md +++ b/src/api/exec/ChildProcess.help.md @@ -7,7 +7,7 @@ Generally, you should not need to use the `ChildProcess` class directly, and sho ```ts // Defined in yavascript/src/api/exec/ChildProcess.ts declare class ChildProcess { - new( + constructor( args: string | Path | Array, options?: { cwd?: string | Path; @@ -17,7 +17,9 @@ declare class ChildProcess { out?: FILE; err?: FILE; }; - trace?: (...args: Array) => void; + logging?: { + trace?: (...args: Array) => void; + }; } ): ChildProcess; @@ -30,7 +32,6 @@ declare class ChildProcess { out: FILE; err: FILE; }; - trace?: (...args: Array) => void; pid: number | null; start(): number; diff --git a/src/api/exec/ChildProcess.inc.d.ts b/src/api/exec/ChildProcess.inc.d.ts index 76ccc21..86d45ce 100644 --- a/src/api/exec/ChildProcess.inc.d.ts +++ b/src/api/exec/ChildProcess.inc.d.ts @@ -26,12 +26,6 @@ declare interface ChildProcess { err: FILE; }; - /** - * Optional trace function which, if present, will be called at various times - * to provide information about the lifecycle of the process. - */ - trace?: (...args: Array) => void; - pid: number | null; /** Spawns the process and returns its pid (process id). */ @@ -68,11 +62,17 @@ declare type ChildProcessOptions = { err?: FILE; }; - /** - * Optional trace function which, if present, will be called at various times - * to provide information about the lifecycle of the process. - */ - trace?: (...args: Array) => void; + /** Options which control logging */ + logging?: { + /** + * Optional trace function which, if present, will be called at various + * times to provide information about the lifecycle of the process. + * + * Defaults to the current value of {@link logger.trace}. `logger.trace` + * defaults to a function which writes to stderr. + */ + trace?: (...args: Array) => void; + }; }; declare interface ChildProcessConstructor { diff --git a/src/api/exec/ChildProcess.ts b/src/api/exec/ChildProcess.ts index 75ab054..098c8fd 100644 --- a/src/api/exec/ChildProcess.ts +++ b/src/api/exec/ChildProcess.ts @@ -20,7 +20,9 @@ export type ChildProcessOptions = { out?: FILE; err?: FILE; }; - trace?: (...args: Array) => void; + logging?: { + trace?: (...args: Array) => void; + }; }; export class ChildProcess { @@ -32,7 +34,10 @@ export class ChildProcess { out: FILE; err: FILE; }; - trace: (...args: Array) => void; + + private _logging: { + trace: (...args: Array) => void; + }; pid: number | null = null; @@ -95,18 +100,20 @@ export class ChildProcess { "when present, 'stdio.err' option must be a FILE object" ); - this.trace = options.trace ?? logger.trace; + this._logging = { + trace: options.logging?.trace ?? logger.trace, + }; assert.type( - this.trace, + this._logging.trace, types.Function, - "when present, 'options.trace' must be a function" + "when present, 'options.logging.trace' must be a function" ); } /** returns pid */ start(): number { - this.trace.call(null, "ChildProcess.start:", this.args); + this._logging.trace.call(null, "ChildProcess.start:", this.args); this.pid = os.exec(this.args, { block: false, @@ -134,11 +141,22 @@ export class ChildProcess { if (ret == pid) { if (os.WIFEXITED(status)) { const ret = { status: os.WEXITSTATUS(status), signal: undefined }; - this.trace.call(null, "ChildProcess result:", this.args, "->", ret); + this._logging.trace.call( + null, + "ChildProcess result:", + this.args, + "->", + ret + ); return ret; } else if (os.WIFSIGNALED(status)) { const ret = { status: undefined, signal: os.WTERMSIG(status) }; - this.trace.call(null, "ChildProcess result:", this.args, "->"); + this._logging.trace.call( + null, + "ChildProcess result:", + this.args, + "->" + ); return ret; } } diff --git a/src/api/exec/exec.help.md b/src/api/exec/exec.help.md index 8b14640..ac5e92b 100644 --- a/src/api/exec/exec.help.md +++ b/src/api/exec/exec.help.md @@ -27,14 +27,13 @@ The intent is that it behaves similarly to what you would expect from a UNIX she `exec` also supports a second argument, an options object which supports the following keys (all are optional): -| Property | Purpose | -| ------------------------------ | ----------------------------------------------------- | -| cwd (string) | current working directory for the child process | -| env (object) | environment variables for the process | -| failOnNonZeroStatus (boolean) | whether to throw error on nonzero exit status | -| captureOutput (boolean/string) | controls how stdout/stderr is directed | -| trace (function) | used to log detailed info about the process execution | -| info (function) | used to log info about the process execution | +| Property | Purpose | +| ------------------------------ | ----------------------------------------------- | +| cwd (string) | current working directory for the child process | +| env (object) | environment variables for the process | +| failOnNonZeroStatus (boolean) | whether to throw error on nonzero exit status | +| captureOutput (boolean/string) | controls how stdout/stderr is directed | +| logging (object) | controls how/whether info messages are logged | The return value of `exec` varies depending on the options passed: @@ -55,11 +54,13 @@ declare function exec( /** Defaults to `env` */ env?: { [key: string | number]: string | number | boolean }; - /** Defaults to `undefined` */ - trace?: (...args: Array) => void; + logging?: { + /** Defaults to `logger.trace`. */ + trace?: (...args: Array) => void; - /** Defaults to {@link logger.info}. */ - info?: (...args: Array) => void; + /** Defaults to `logger.info`. */ + info?: (...args: Array) => void; + } /** Defaults to true */ failOnNonZeroStatus?: boolean; diff --git a/src/api/exec/exec.inc.d.ts b/src/api/exec/exec.inc.d.ts index e7d5948..dd9a728 100644 --- a/src/api/exec/exec.inc.d.ts +++ b/src/api/exec/exec.inc.d.ts @@ -5,24 +5,34 @@ declare type BaseExecOptions = { /** Sets environment variables within the process. */ env?: { [key: string | number]: string | number | boolean }; - /** - * If provided, this function will be called multiple times as `exec` - * runs, to help you understand what's going on and/or troubleshoot things. - * In most cases, it makes sense to use a logging function here, like so: - * - * ```js - * exec(["echo", "hi"], { trace: console.log }); - * ``` - */ - trace?: (...args: Array) => void; - - /** - * An optional, user-provided logging function to be used for informational - * messages. - * - * Defaults to {@link logger.info}. - */ - info?: (...args: Array) => void; + /** Options which control logging. */ + logging?: { + /** + * If provided, this logging function will be called multiple times as + * `exec` runs, to help you understand what's going on and/or troubleshoot + * things. In most cases, it makes sense to use a function from `console` + * here, like so: + * + * ```js + * exec(["echo", "hi"], { + * logging: { trace: console.log }, + * }); + * ``` + * + * Defaults to the current value of {@link logger.trace}. `logger.trace` + * defaults to a no-op function. + */ + trace?: (...args: Array) => void; + + /** + * An optional, user-provided logging function to be used for informational + * messages. Less verbose than `logging.trace`. + * + * Defaults to the current value of {@link logger.info}. `logger.info` + * defaults to a function which logs to stderr. + */ + info?: (...args: Array) => void; + }; /** * Whether an Error should be thrown when the process exits with a nonzero diff --git a/src/api/exec/exec.test.ts b/src/api/exec/exec.test.ts index fb37e0b..54c06f3 100644 --- a/src/api/exec/exec.test.ts +++ b/src/api/exec/exec.test.ts @@ -12,7 +12,8 @@ test("exec true - string", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: true + ", "stdout": "", } `); @@ -24,7 +25,8 @@ test("exec true - array", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: true + ", "stdout": "", } `); @@ -36,7 +38,8 @@ test("exec true - Path", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: /usr/bin/true + ", "stdout": "", } `); @@ -48,7 +51,8 @@ test("exec false - string", async () => { { "code": 1, "error": false, - "stderr": "Error: Command failed: "false" (status = 1, signal = undefined) + "stderr": "exec: false + Error: Command failed: "false" (status = 1, signal = undefined) at somewhere { status: 1 @@ -66,7 +70,8 @@ test("exec false - array", async () => { { "code": 1, "error": false, - "stderr": "Error: Command failed: ["false"] (status = 1, signal = undefined) + "stderr": "exec: false + Error: Command failed: ["false"] (status = 1, signal = undefined) at somewhere { status: 1 @@ -84,7 +89,8 @@ test("exec false - Path", async () => { { "code": 1, "error": false, - "stderr": "Error: Command failed: "/usr/bin/false" (status = 1, signal = undefined) + "stderr": "exec: /usr/bin/false + Error: Command failed: "/usr/bin/false" (status = 1, signal = undefined) at somewhere { status: 1 @@ -106,7 +112,8 @@ test("exec - child process receives args", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: -e scriptArgs bla blah -- haha + ", "stdout": "[ Frozen "" @@ -132,7 +139,8 @@ test("exec with env vars", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: -e env + ", "stdout": "{ HI_MOM: "yup" } @@ -149,7 +157,8 @@ test("exec with cwd", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: -e "pwd()" + ", "stdout": "Path { /tmp } ", } @@ -162,7 +171,8 @@ test("exec with env", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: env + ", "stdout": "HI=yeah ", } @@ -177,7 +187,8 @@ test("exec with captureOutput true", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: echo "hi there" + ", "stdout": "{ stdout: "hi there\\n" stderr: "" @@ -197,7 +208,8 @@ test("exec with captureOutput 'utf8'", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: echo "hi there" + ", "stdout": "{ stdout: "hi there\\n" stderr: "" @@ -217,7 +229,8 @@ test("exec with captureOutput 'arraybuffer'", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: echo "hi there" + ", "stdout": "{ stdout: ArrayBuffer { │0x00000000│ 68 69 20 74 68 65 72 65 0A @@ -239,7 +252,9 @@ test("exec with failOnNonZeroStatus false - running 'false'", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: false + exec -> {"status":1} + ", "stdout": "{ status: 1 signal: undefined @@ -257,7 +272,8 @@ test("exec with failOnNonZeroStatus false - running 'true'", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: true + ", "stdout": "{ status: 0 signal: undefined @@ -273,7 +289,8 @@ test("$ echo hi - string", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: echo hi + ", "stdout": "{ stdout: "hi\\n" stderr: "" @@ -291,7 +308,8 @@ test("$ echo hi - array", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: echo hi + ", "stdout": "{ stdout: "hi\\n" stderr: "" @@ -309,7 +327,8 @@ test("$ echo hi - array with Path", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: echo hi + ", "stdout": "{ stdout: "hi\\n" stderr: "" @@ -327,7 +346,8 @@ test("$ echo hi 2 - array with number", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: echo hi 2 + ", "stdout": "{ stdout: "hi 2\\n" stderr: "" @@ -345,7 +365,8 @@ test("$ false", async () => { { "code": 1, "error": false, - "stderr": "Error: Command failed: "false" (status = 1, signal = undefined) + "stderr": "exec: false + Error: Command failed: "false" (status = 1, signal = undefined) at somewhere { status: 1 @@ -365,7 +386,8 @@ test("exec parses arg string properly", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: bash -c "echo hi" + ", "stdout": "hi ", } @@ -378,7 +400,8 @@ test("exec's string parsing does not interpolate env vars", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: echo "$HI" + ", "stdout": "$HI ", } @@ -391,7 +414,8 @@ test("exec's string parsing does not parse globs", async () => { { "code": 0, "error": false, - "stderr": "", + "stderr": "exec: echo "**/*" + ", "stdout": "**/* ", } @@ -406,28 +430,8 @@ test("logging", async () => { { "code": 0, "error": false, - "stderr": "ChildProcess.start: [ - "echo" - "hi" - ] - ChildProcess result: [ - "echo" - "hi" - ] -> { - status: 0 - signal: undefined - } - ChildProcess.start: [ - "echo" - " hi" - ] - ChildProcess result: [ - "echo" - " hi" - ] -> { - status: 0 - signal: undefined - } + "stderr": "exec: echo hi + exec: echo " hi" ", "stdout": "hi hi diff --git a/src/api/exec/exec.ts b/src/api/exec/exec.ts index 841d38d..a2b4c89 100644 --- a/src/api/exec/exec.ts +++ b/src/api/exec/exec.ts @@ -10,6 +10,7 @@ import execHelpText from "./exec.help.md"; import dollarHelpText from "./_dollar.help.md"; import { ChildProcess } from "./ChildProcess"; import { types } from "../types"; +import { quote } from "../strings"; const exec = ( args: Array | string | Path, @@ -18,8 +19,10 @@ const exec = ( env?: { [key: string | number]: string | number | boolean }; failOnNonZeroStatus?: boolean; captureOutput?: boolean | "utf8" | "arraybuffer"; - trace?: (...args: Array) => void; - info?: (...args: Array) => void; + logging?: { + trace?: (...args: Array) => void; + info?: (...args: Array) => void; + }; } = {} ): any => { // 'args' type gets checked in ChildProcess constructor @@ -34,8 +37,7 @@ const exec = ( captureOutput = false, cwd, env, - trace = logger.trace, - info = logger.info, + logging: { trace = logger.trace, info = logger.info } = {}, } = options; assert.type( @@ -56,16 +58,16 @@ const exec = ( assert.type( trace, types.Function, - "when present, 'options.trace' must be a function" + "when present, 'options.logging.trace' must be a function" ); assert.type( info, types.Function, - "when present, 'options.info' must be a function" + "when present, 'options.logging.info' must be a function" ); - const child = new ChildProcess(args, { cwd, env, trace }); + const child = new ChildProcess(args, { cwd, env, logging: { trace } }); let tmpOut: FILE | null = null; let tmpErr: FILE | null = null; @@ -78,10 +80,14 @@ const exec = ( let result: ReturnType | null = null; try { - info(`exec: ${child.args.join(" ")}`); + info( + `exec: ${child.args + .map((arg) => (/[^A-Za-z\-0-9_=\/]/.test(arg) ? quote(arg) : arg)) + .join(" ")}` + ); child.start(); result = child.waitUntilComplete(); - if (result.status !== 0) { + if (result.status !== 0 && !failOnNonZeroStatus) { info(` exec -> ${JSON.stringify(result)}`); } diff --git a/src/api/filesystem/copy.help.md b/src/api/filesystem/copy.help.md index 74a2697..d8a9bda 100644 --- a/src/api/filesystem/copy.help.md +++ b/src/api/filesystem/copy.help.md @@ -18,25 +18,34 @@ declare function copy( */ whenTargetExists?: "overwrite" | "skip" | "error"; - /** - * If provided, this function will be called multiple times as `copy` - * traverses the filesystem, to help you understand what's going on and/or - * troubleshoot things. In most cases, it makes sense to use a logging - * function here, like so: - * - * ```js - * copy("./source", "./destination", { trace: console.log }); - * ``` - */ - trace?: (...args: Array) => void; + /** Options which control logging. */ + logging?: { + /** + * If provided, this function will be called multiple times as `copy` + * traverses the filesystem, to help you understand what's going on and/or + * troubleshoot things. In most cases, it makes sense to use a logging + * function here, like so: + * + * ```js + * copy("./source", "./destination", { + * logging: { trace: console.log }, + * }); + * ``` + * + * Defaults to the current value of {@link logger.trace}. `logger.trace` + * defaults to a no-op function. + */ + trace?: (...args: Array) => void; - /** - * An optional, user-provided logging function to be used for informational - * messages. - * - * Defaults to {@link logger.info}. - */ - info?: (...args: Array) => void; + /** + * An optional, user-provided logging function to be used for informational + * messages. + * + * Defaults to the current value of {@link logger.info}. `logger.info` + * defaults to a function which writes to stderr. + */ + info?: (...args: Array) => void; + } }; ): void; ```` diff --git a/src/api/filesystem/copy.ts b/src/api/filesystem/copy.ts index eed7546..6f736ac 100644 --- a/src/api/filesystem/copy.ts +++ b/src/api/filesystem/copy.ts @@ -19,7 +19,9 @@ function formatPath(path: Path | string): string { path = new Path(path); } if (path.startsWith(pwd())) { - return new Path(path).replace(pwd(), "").toString(); + return new Path(path).replace(pwd(), []).toString(); + } else if (path.startsWith(".")) { + return new Path(path).replace(".", []).toString(); } else { return path.toString(); } @@ -101,8 +103,10 @@ function copyRaw( export type CopyOptions = { whenTargetExists?: "overwrite" | "skip" | "error"; - trace?: (...args: Array) => void; - info?: (...args: Array) => void; + logging?: { + trace?: (...args: Array) => void; + info?: (...args: Array) => void; + }; }; export function copy( @@ -130,8 +134,7 @@ export function copy( const { whenTargetExists = "error", - trace = logger.trace, - info = logger.info, + logging: { trace = logger.trace, info = logger.info } = {}, } = options; assert.type( @@ -147,13 +150,13 @@ export function copy( assert.type( trace, types.or(types.undefined, types.anyFunction), - "when present, 'trace' option must be a function." + "when present, 'logging.trace' option must be a function." ); assert.type( info, types.or(types.undefined, types.anyFunction), - "when present, 'info' option must be a function." + "when present, 'logging.info' option must be a function." ); if (is(to, types.Path)) { diff --git a/src/api/filesystem/filesystem.inc.d.ts b/src/api/filesystem/filesystem.inc.d.ts index 82c391a..6196373 100644 --- a/src/api/filesystem/filesystem.inc.d.ts +++ b/src/api/filesystem/filesystem.inc.d.ts @@ -108,25 +108,34 @@ declare type CopyOptions = { */ whenTargetExists?: "overwrite" | "skip" | "error"; - /** - * If provided, this function will be called multiple times as `copy` - * traverses the filesystem, to help you understand what's going on and/or - * troubleshoot things. In most cases, it makes sense to use a logging - * function here, like so: - * - * ```js - * copy("./source", "./destination", { trace: console.log }); - * ``` - */ - trace?: (...args: Array) => void; - - /** - * An optional, user-provided logging function to be used for informational - * messages. - * - * Defaults to {@link logger.info}. - */ - info?: (...args: Array) => void; + /** Options which control logging. */ + logging?: { + /** + * If provided, this function will be called multiple times as `copy` + * traverses the filesystem, to help you understand what's going on and/or + * troubleshoot things. In most cases, it makes sense to use a logging + * function here, like so: + * + * ```js + * copy("./source", "./destination", { + * logging: { trace: console.log }, + * }); + * ``` + * + * Defaults to the current value of {@link logger.trace}. `logger.trace` + * defaults to a no-op function. + */ + trace?: (...args: Array) => void; + + /** + * An optional, user-provided logging function to be used for informational + * messages. + * + * Defaults to the current value of {@link logger.info}. `logger.info` + * defaults to a function which writes to stderr. + */ + info?: (...args: Array) => void; + }; }; /** diff --git a/src/api/filesystem/filesystem.test.ts b/src/api/filesystem/filesystem.test.ts index 4f0fc8e..b03ebc1 100644 --- a/src/api/filesystem/filesystem.test.ts +++ b/src/api/filesystem/filesystem.test.ts @@ -500,12 +500,15 @@ test("copy", async () => { const result = await evaluate( `copy(${JSON.stringify(source)}, ${JSON.stringify(target)})` ); - expect(cleanResult(result)).toEqual({ - code: 0, - error: false, - stderr: "", - stdout: "", - }); + expect(cleanResult(result)).toMatchInlineSnapshot(` + { + "code": 0, + "error": false, + "stderr": "copy: src/test_fixtures/copy/blah/blah2/hi.txt -> src/test_fixtures/copy/blah_copy/blah2/hi.txt + ", + "stdout": "", + } + `); const blah2 = path.join(target, "blah2"); const blah3 = path.join(target, "blah3"); diff --git a/src/api/glob/glob.help.md b/src/api/glob/glob.help.md index e9b2fdc..1ef98ad 100644 --- a/src/api/glob/glob.help.md +++ b/src/api/glob/glob.help.md @@ -37,25 +37,34 @@ declare function glob( */ followSymlinks?: boolean; - /** - * If provided, this function will be called multiple times as `glob` - * traverses the filesystem, to help you understand what's going on and/or - * troubleshoot things. In most cases, it makes sense to use a logging - * function here, like so: - * - * ```js - * glob(["./*.js"], { trace: console.log }); - * ``` - */ - trace?: (...args: Array) => void; + /** Options which control logging. */ + logging?: { + /** + * If provided, this function will be called multiple times as `glob` + * traverses the filesystem, to help you understand what's going on and/or + * troubleshoot things. In most cases, it makes sense to use a logging + * function here, like so: + * + * ```js + * glob(["./*.js"], { + * logging: { trace: console.log } + * }); + * ``` + * + * Defaults to the current value of {@link logger.trace}. `logger.trace` + * defaults to a no-op function. + */ + trace?: (...args: Array) => void; - /** - * An optional, user-provided logging function to be used for informational - * messages. - * - * Defaults to {@link logger.info}. - */ - info?: (...args: Array) => void; + /** + * An optional, user-provided logging function to be used for informational + * messages. Less verbose than `logging.trace`. + * + * Defaults to the current value of {@link logger.info}. `logger.info` + * defaults to a function which writes to stderr. + */ + info?: (...args: Array) => void; + }; /** * Directory to interpret glob patterns relative to. Defaults to `pwd()`. diff --git a/src/api/glob/glob.inc.d.ts b/src/api/glob/glob.inc.d.ts index 16355b0..0803bf5 100644 --- a/src/api/glob/glob.inc.d.ts +++ b/src/api/glob/glob.inc.d.ts @@ -10,25 +10,34 @@ declare type GlobOptions = { */ followSymlinks?: boolean; - /** - * If provided, this function will be called multiple times as `glob` - * traverses the filesystem, to help you understand what's going on and/or - * troubleshoot things. In most cases, it makes sense to use a logging - * function here, like so: - * - * ```js - * glob(["./*.js"], { trace: console.log }); - * ``` - */ - trace?: (...args: Array) => void; + /** Options which control logging. */ + logging?: { + /** + * If provided, this function will be called multiple times as `glob` + * traverses the filesystem, to help you understand what's going on and/or + * troubleshoot things. In most cases, it makes sense to use a logging + * function here, like so: + * + * ```js + * glob(["./*.js"], { + * logging: { trace: console.log } + * }); + * ``` + * + * Defaults to the current value of {@link logger.trace}. `logger.trace` + * defaults to a no-op function. + */ + trace?: (...args: Array) => void; - /** - * An optional, user-provided logging function to be used for informational - * messages. - * - * Defaults to {@link logger.info}. - */ - info?: (...args: Array) => void; + /** + * An optional, user-provided logging function to be used for informational + * messages. Less verbose than `logging.trace`. + * + * Defaults to the current value of {@link logger.info}. `logger.info` + * defaults to a function which writes to stderr. + */ + info?: (...args: Array) => void; + }; /** * Directory to interpret glob patterns relative to. Defaults to `pwd()`. diff --git a/src/api/glob/glob.test.ts b/src/api/glob/glob.test.ts index 4ef3bac..298928e 100644 --- a/src/api/glob/glob.test.ts +++ b/src/api/glob/glob.test.ts @@ -40,7 +40,6 @@ function testGlob( expect(result).toMatchObject({ code: 0, error: false, - stderr: "", }); const cleaned = cleanResult(result); @@ -203,11 +202,17 @@ test("error reading dead link does not stop search", async () => { const cleaned = cleanResult(result); - expect(cleaned).toMatchObject({ - code: 0, - error: false, - stderr: `glob encountered error: No such file or directory (errno = 2, path = /src/test_fixtures/symlinks/dead-link, linkpath = ./nowhere-real)\n`, - }); + expect(cleaned).toMatchInlineSnapshot(` + { + "code": 0, + "error": false, + "stderr": "glob: expanding ["**/*"] + glob encountered error: No such file or directory (errno = 2, path = /src/test_fixtures/symlinks/dead-link, linkpath = ./nowhere-real) + ", + "stdout": "["/src/test_fixtures/symlinks/link-to-file","/src/test_fixtures/symlinks/link-to-folder","/src/test_fixtures/symlinks/some-folder","/src/test_fixtures/symlinks/some-file"] + ", + } + `); compareResult(cleaned, expected); }); @@ -249,7 +254,9 @@ testGlob( test("using trace", async () => { const result = await evaluate( `JSON.stringify(glob(["**/*.txt", "!**/potato/**"], { - trace: console.error, + logging: { + trace: console.error, + }, dir: ${JSON.stringify(globDir)} }))` ); @@ -291,6 +298,7 @@ test("using trace", async () => { found 3 children of /src/test_fixtures/glob/cabana found 4 children of /src/test_fixtures/glob/hi found 8 children of /src/test_fixtures/glob + glob: expanding ["**/*.txt","!**/potato/**"] match info: {"didMatch":false,"pattern":"**/*.txt","negated":false,"fullName":"/src/test_fixtures/glob/cabana"} match info: {"didMatch":false,"pattern":"**/*.txt","negated":false,"fullName":"/src/test_fixtures/glob/cabana/.gitkeep"} match info: {"didMatch":false,"pattern":"**/*.txt","negated":false,"fullName":"/src/test_fixtures/glob/hi"} diff --git a/src/api/glob/glob.ts b/src/api/glob/glob.ts index 3bd11e3..aa38a30 100644 --- a/src/api/glob/glob.ts +++ b/src/api/glob/glob.ts @@ -36,8 +36,10 @@ function compile(pattern: string, startingDir: string) { export type GlobOptions = { dir?: string | Path; followSymlinks?: boolean; - trace?: (...args: Array) => void; - info?: (...args: Array) => void; + logging?: { + trace?: (...args: Array) => void; + info?: (...args: Array) => void; + }; }; const HAS_GLOB_METACHARS_RE = /[*{}]|\+\(|^!/; @@ -65,14 +67,25 @@ export function glob( ); assert.type( - options.trace, - types.or(types.undefined, types.anyFunction), - "when present, 'trace' option must be a function" + options.logging, + types.or(types.undefined, types.object), + "when present, 'logging' option must be an object" ); let dir = options.dir ?? null; - const trace = options.trace ?? logger.trace; - const info = options.info ?? logger.info; + const trace = options.logging?.trace ?? logger.trace; + const info = options.logging?.info ?? logger.info; + + assert.type( + trace, + types.or(types.undefined, types.anyFunction), + "when present, 'logging.trace' option must be a function" + ); + assert.type( + info, + types.or(types.undefined, types.anyFunction), + "when present, 'logging.info' option must be a function" + ); const patternsArray = Array.isArray(patterns) ? patterns : [patterns]; info(`glob: expanding ${JSON.stringify(patternsArray)}`); diff --git a/src/globals.test.ts b/src/globals.test.ts index 3bedd08..c2eed79 100644 --- a/src/globals.test.ts +++ b/src/globals.test.ts @@ -184,7 +184,7 @@ test("globals", async () => { [x] JSX: object [x] CSV: object [x] YAML: object - [ ] traceAll: function + [ ] logger: object [x] parseScriptArgs: function [ ] startRepl: function [ ] InteractivePrompt: function diff --git a/yavascript.d.ts b/yavascript.d.ts index 80f2eed..bbeea6e 100644 --- a/yavascript.d.ts +++ b/yavascript.d.ts @@ -277,25 +277,34 @@ declare type CopyOptions = { */ whenTargetExists?: "overwrite" | "skip" | "error"; - /** - * If provided, this function will be called multiple times as `copy` - * traverses the filesystem, to help you understand what's going on and/or - * troubleshoot things. In most cases, it makes sense to use a logging - * function here, like so: - * - * ```js - * copy("./source", "./destination", { trace: console.log }); - * ``` - */ - trace?: (...args: Array) => void; + /** Options which control logging. */ + logging?: { + /** + * If provided, this function will be called multiple times as `copy` + * traverses the filesystem, to help you understand what's going on and/or + * troubleshoot things. In most cases, it makes sense to use a logging + * function here, like so: + * + * ```js + * copy("./source", "./destination", { + * logging: { trace: console.log }, + * }); + * ``` + * + * Defaults to the current value of {@link logger.trace}. `logger.trace` + * defaults to a no-op function. + */ + trace?: (...args: Array) => void; - /** - * An optional, user-provided logging function to be used for informational - * messages. - * - * Defaults to {@link logger.info}. - */ - info?: (...args: Array) => void; + /** + * An optional, user-provided logging function to be used for informational + * messages. + * + * Defaults to the current value of {@link logger.info}. `logger.info` + * defaults to a function which writes to stderr. + */ + info?: (...args: Array) => void; + }; }; /** @@ -818,9 +827,37 @@ declare function touch(path: string | Path): void; declare function which( binaryName: string, options?: { + /** + * A list of folders where programs may be found. Defaults to + * `env.PATH?.split(Path.OS_ENV_VAR_SEPARATOR) || []`. + */ searchPaths?: Array; + + /** + * A list of filename extension suffixes to include in the search, ie + * `[".exe"]`. Defaults to {@link Path.OS_PROGRAM_EXTENSIONS}. + */ suffixes?: Array; - trace?: (...args: Array) => void; + + /** Options which control logging. */ + logging?: { + /** + * If provided, this logging function will be called multiple times as + * `which` runs, to help you understand what's going on and/or troubleshoot + * things. In most cases, it makes sense to use a function from `console` + * here, like so: + * + * ```js + * which("bash", { + * logging: { trace: console.log } + * }); + * ``` + * + * Defaults to the current value of {@link logger.trace}. `logger.trace` + * defaults to a no-op function. + */ + trace?: (...args: Array) => void; + }; } ): Path | null; @@ -831,24 +868,34 @@ declare type BaseExecOptions = { /** Sets environment variables within the process. */ env?: { [key: string | number]: string | number | boolean }; - /** - * If provided, this function will be called multiple times as `exec` - * runs, to help you understand what's going on and/or troubleshoot things. - * In most cases, it makes sense to use a logging function here, like so: - * - * ```js - * exec(["echo", "hi"], { trace: console.log }); - * ``` - */ - trace?: (...args: Array) => void; + /** Options which control logging. */ + logging?: { + /** + * If provided, this logging function will be called multiple times as + * `exec` runs, to help you understand what's going on and/or troubleshoot + * things. In most cases, it makes sense to use a function from `console` + * here, like so: + * + * ```js + * exec(["echo", "hi"], { + * logging: { trace: console.log }, + * }); + * ``` + * + * Defaults to the current value of {@link logger.trace}. `logger.trace` + * defaults to a no-op function. + */ + trace?: (...args: Array) => void; - /** - * An optional, user-provided logging function to be used for informational - * messages. - * - * Defaults to {@link logger.info}. - */ - info?: (...args: Array) => void; + /** + * An optional, user-provided logging function to be used for informational + * messages. Less verbose than `logging.trace`. + * + * Defaults to the current value of {@link logger.info}. `logger.info` + * defaults to a function which logs to stderr. + */ + info?: (...args: Array) => void; + }; /** * Whether an Error should be thrown when the process exits with a nonzero @@ -1047,12 +1094,6 @@ declare interface ChildProcess { err: FILE; }; - /** - * Optional trace function which, if present, will be called at various times - * to provide information about the lifecycle of the process. - */ - trace?: (...args: Array) => void; - pid: number | null; /** Spawns the process and returns its pid (process id). */ @@ -1089,11 +1130,17 @@ declare type ChildProcessOptions = { err?: FILE; }; - /** - * Optional trace function which, if present, will be called at various times - * to provide information about the lifecycle of the process. - */ - trace?: (...args: Array) => void; + /** Options which control logging */ + logging?: { + /** + * Optional trace function which, if present, will be called at various + * times to provide information about the lifecycle of the process. + * + * Defaults to the current value of {@link logger.trace}. `logger.trace` + * defaults to a function which writes to stderr. + */ + trace?: (...args: Array) => void; + }; }; declare interface ChildProcessConstructor { @@ -1125,25 +1172,34 @@ declare type GlobOptions = { */ followSymlinks?: boolean; - /** - * If provided, this function will be called multiple times as `glob` - * traverses the filesystem, to help you understand what's going on and/or - * troubleshoot things. In most cases, it makes sense to use a logging - * function here, like so: - * - * ```js - * glob(["./*.js"], { trace: console.log }); - * ``` - */ - trace?: (...args: Array) => void; + /** Options which control logging. */ + logging?: { + /** + * If provided, this function will be called multiple times as `glob` + * traverses the filesystem, to help you understand what's going on and/or + * troubleshoot things. In most cases, it makes sense to use a logging + * function here, like so: + * + * ```js + * glob(["./*.js"], { + * logging: { trace: console.log } + * }); + * ``` + * + * Defaults to the current value of {@link logger.trace}. `logger.trace` + * defaults to a no-op function. + */ + trace?: (...args: Array) => void; - /** - * An optional, user-provided logging function to be used for informational - * messages. - * - * Defaults to {@link logger.info}. - */ - info?: (...args: Array) => void; + /** + * An optional, user-provided logging function to be used for informational + * messages. Less verbose than `logging.trace`. + * + * Defaults to the current value of {@link logger.info}. `logger.info` + * defaults to a function which writes to stderr. + */ + info?: (...args: Array) => void; + }; /** * Directory to interpret glob patterns relative to. Defaults to `pwd()`.