Commit 8eb0493
Changed files (14)
src/context.ts
@@ -1,33 +0,0 @@
-// Copyright 2022 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import { AsyncLocalStorage } from 'node:async_hooks'
-
-let root: any
-
-const storage = new AsyncLocalStorage<any>()
-
-export function getCtx() {
- return storage.getStore()
-}
-export function setRootCtx(ctx: any) {
- storage.enterWith(ctx)
- root = ctx
-}
-export function getRootCtx() {
- return root
-}
-export function runInCtx(ctx: any, cb: any) {
- return storage.run(ctx, cb)
-}
src/core.ts
@@ -12,91 +12,189 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { ChalkInstance } from 'chalk'
import {
ChildProcessByStdio,
SpawnOptionsWithStdioTuple,
StdioPipe,
} from 'child_process'
+import { AsyncLocalStorage } from 'node:async_hooks'
import { Readable, Writable } from 'node:stream'
import { inspect, promisify } from 'node:util'
import { spawn } from 'node:child_process'
-
+import assert from 'node:assert'
+import { ChalkInstance } from 'chalk'
import { chalk, which } from './goods.js'
-import { runInCtx, getCtx, setRootCtx } from './context.js'
-import { printStd, printCmd } from './print.js'
-import { quote, substitute } from './guards.js'
-
-import psTreeModule from 'ps-tree'
-
-const psTree = promisify(psTreeModule)
-
-export function $(pieces: TemplateStringsArray, ...args: any[]) {
- let resolve, reject
- let promise = new ProcessPromise((...args) => ([resolve, reject] = args))
-
- let cmd = pieces[0],
- i = 0
- let quote = getCtx().quote
- while (i < args.length) {
- let s
- if (Array.isArray(args[i])) {
- s = args[i].map((x: any) => quote(substitute(x))).join(' ')
- } else {
- s = quote(substitute(args[i]))
- }
- cmd += s + pieces[++i]
- }
-
- promise.ctx = {
- ...getCtx(),
- cmd,
- __from: new Error().stack!.split(/^\s*at\s/m)[2].trim(),
- resolve,
- reject,
- }
-
- setImmediate(() => promise._run()) // Make sure all subprocesses are started, if not explicitly by await or then().
-
- return promise
-}
-
-setRootCtx($)
+import { printCmd } from './print.js'
+import { noop, quote, substitute, psTree, exitCodeInfo } from './util.js'
-$.cwd = process.cwd()
-$.env = process.env
-$.quote = quote
-$.spawn = spawn
-$.verbose = true
-$.maxBuffer = 200 * 1024 * 1024 /* 200 MiB*/
-$.prefix = '' // Bash not found, no prefix.
-try {
- $.shell = which.sync('bash')
- $.prefix = 'set -euo pipefail;'
-} catch (e) {}
+type Shell = (pieces: TemplateStringsArray, ...args: any[]) => ProcessPromise
type Options = {
- nothrow: boolean
verbose: boolean
- cmd: string
cwd: string
env: NodeJS.ProcessEnv
+ shell: string | boolean
prefix: string
- shell: string
- maxBuffer: number
- __from: string
- resolve: any
- reject: any
+ quote: typeof quote
+ spawn: typeof spawn
}
+const storage = new AsyncLocalStorage<Options>()
+
+export const $ = new Proxy<Shell & Options>(
+ function (pieces, ...args) {
+ let from = new Error().stack!.split(/^\s*at\s/m)[2].trim()
+ let resolve: Resolve, reject: Resolve
+ let promise = new ProcessPromise((...args) => ([resolve, reject] = args))
+ let cmd = pieces[0],
+ i = 0
+ while (i < args.length) {
+ let s
+ if (Array.isArray(args[i])) {
+ s = args[i].map((x: any) => $.quote(substitute(x))).join(' ')
+ } else {
+ s = $.quote(substitute(args[i]))
+ }
+ cmd += s + pieces[++i]
+ }
+ promise._bind(cmd, $.cwd, from, resolve!, reject!)
+ // Make sure all subprocesses are started, if not explicitly by await or then().
+ setImmediate(() => promise._run())
+ return promise
+ } as Shell & Options,
+ {
+ set(_, key, value) {
+ let context = storage.getStore()
+ assert(context)
+ Reflect.set(context, key, value)
+ return true
+ },
+ get(_, key) {
+ let context = storage.getStore()
+ assert(context)
+ return Reflect.get(context, key)
+ },
+ }
+)
+
+void (function init() {
+ storage.enterWith({
+ verbose: true,
+ cwd: process.cwd(),
+ env: process.env,
+ shell: true,
+ prefix: '',
+ quote,
+ spawn,
+ })
+ try {
+ $.shell = which.sync('bash')
+ $.prefix = 'set -euo pipefail;'
+ } catch (err) {
+ // ¯\_(ツ)_/¯
+ }
+})()
+
+type Resolve = (out: ProcessOutput) => void
+
export class ProcessPromise extends Promise<ProcessOutput> {
child?: ChildProcessByStdio<Writable, Readable, Readable>
- _resolved = false
+ private _command = ''
+ private _cwd = ''
+ private _from = ''
+ private _resolve: Resolve = noop
+ private _reject: Resolve = noop
+ _nothrow = false
+ _quiet = false
+ private _resolved = false
_inheritStdin = true
_piped = false
- _prerun: any = undefined
- _postrun: any = undefined
- ctx?: Options
+ _prerun = noop
+ _postrun = noop
+
+ _bind(
+ cmd: string,
+ cwd: string,
+ from: string,
+ resolve: Resolve,
+ reject: Resolve
+ ) {
+ this._command = cmd
+ this._cwd = cwd
+ this._from = from
+ this._resolve = resolve
+ this._reject = reject
+ }
+
+ _run() {
+ if (this.child) return // The _run() called from two places: then() and setTimeout().
+ this._prerun() // In case $1.pipe($2), the $2 returned, and on $2._run() invoke $1._run().
+
+ let context = storage.getStore()
+ assert(context)
+ storage.run(context, () => {
+ if ($.verbose && !this._quiet) {
+ printCmd(this._command)
+ }
+
+ let options: SpawnOptionsWithStdioTuple<any, StdioPipe, StdioPipe> = {
+ cwd: this._cwd,
+ shell: typeof $.shell === 'string' ? $.shell : true,
+ stdio: [this._inheritStdin ? 'inherit' : 'pipe', 'pipe', 'pipe'],
+ windowsHide: true,
+ env: $.env,
+ }
+ let child: ChildProcessByStdio<Writable, Readable, Readable> = spawn(
+ $.prefix + this._command,
+ options
+ )
+
+ child.on('close', (code, signal) => {
+ let message = `exit code: ${code}`
+ if (code != 0 || signal != null) {
+ message = `${stderr || '\n'} at ${this._from}`
+ message += `\n exit code: ${code}${
+ exitCodeInfo(code) ? ' (' + exitCodeInfo(code) + ')' : ''
+ }`
+ if (signal != null) {
+ message += `\n signal: ${signal}`
+ }
+ }
+ let output = new ProcessOutput({
+ code,
+ signal,
+ stdout,
+ stderr,
+ combined,
+ message,
+ })
+ if (code === 0 || this._nothrow) {
+ this._resolve(output)
+ } else {
+ this._reject(output)
+ }
+ this._resolved = true
+ })
+
+ let stdout = '',
+ stderr = '',
+ combined = ''
+ let onStdout = (data: any) => {
+ if ($.verbose && !this._quiet) process.stdout.write(data)
+ stdout += data
+ combined += data
+ }
+ let onStderr = (data: any) => {
+ if ($.verbose && !this._quiet) process.stderr.write(data)
+ stderr += data
+ combined += data
+ }
+ if (!this._piped) child.stdout.on('data', onStdout) // If process is piped, don't collect or print output.
+ child.stderr.on('data', onStderr) // Stderr should be printed regardless of piping.
+ this.child = child
+ this._postrun() // In case $1.pipe($2), after both subprocesses are running, we can pipe $1.stdout to $2.stdin.
+ })
+ }
get stdin() {
this._inheritStdin = false
@@ -171,91 +269,14 @@ export class ProcessPromise extends Promise<ProcessOutput> {
process.kill(this.child.pid, signal)
} catch (e) {}
}
-
- _run() {
- if (this.child) return // The _run() called from two places: then() and setTimeout().
- if (this._prerun) this._prerun() // In case $1.pipe($2), the $2 returned, and on $2._run() invoke $1._run().
-
- runInCtx(this.ctx, () => {
- const {
- nothrow,
- cmd,
- cwd,
- env,
- prefix,
- shell,
- maxBuffer,
- __from,
- resolve,
- reject,
- } = this.ctx!
-
- printCmd(cmd)
-
- let options: SpawnOptionsWithStdioTuple<any, StdioPipe, StdioPipe> = {
- cwd,
- shell: typeof shell === 'string' ? shell : true,
- stdio: [this._inheritStdin ? 'inherit' : 'pipe', 'pipe', 'pipe'],
- windowsHide: true,
- // TODO: Surprise: maxBuffer have no effect for spawn.
- // maxBuffer,
- env,
- }
- let child: ChildProcessByStdio<Writable, Readable, Readable> = spawn(
- prefix + cmd,
- options
- )
-
- child.on('close', (code, signal) => {
- let message = `exit code: ${code}`
- if (code != 0 || signal != null) {
- message = `${stderr || '\n'} at ${__from}`
- message += `\n exit code: ${code}${
- exitCodeInfo(code) ? ' (' + exitCodeInfo(code) + ')' : ''
- }`
- if (signal != null) {
- message += `\n signal: ${signal}`
- }
- }
- let output = new ProcessOutput({
- code,
- signal,
- stdout,
- stderr,
- combined,
- message,
- })
- ;(code === 0 || nothrow ? resolve : reject)(output)
- this._resolved = true
- })
-
- let stdout = '',
- stderr = '',
- combined = ''
- let onStdout = (data: any) => {
- printStd(data)
- stdout += data
- combined += data
- }
- let onStderr = (data: any) => {
- printStd(null, data)
- stderr += data
- combined += data
- }
- if (!this._piped) child.stdout.on('data', onStdout) // If process is piped, don't collect or print output.
- child.stderr.on('data', onStderr) // Stderr should be printed regardless of piping.
- this.child = child
- if (this._postrun) this._postrun() // In case $1.pipe($2), after both subprocesses are running, we can pipe $1.stdout to $2.stdin.
- })
- }
}
export class ProcessOutput extends Error {
- #code: number | null = null
- #signal: NodeJS.Signals | null = null
- #stdout = ''
- #stderr = ''
- #combined = ''
+ readonly #code: number | null
+ readonly #signal: NodeJS.Signals | null
+ readonly #stdout: string
+ readonly #stderr: string
+ readonly #combined: string
constructor({
code,
@@ -316,39 +337,18 @@ export class ProcessOutput extends Error {
}
}
-function exitCodeInfo(exitCode: number | null): string | undefined {
- return {
- 2: 'Misuse of shell builtins',
- 126: 'Invoked command cannot execute',
- 127: 'Command not found',
- 128: 'Invalid exit argument',
- 129: 'Hangup',
- 130: 'Interrupt',
- 131: 'Quit and dump core',
- 132: 'Illegal instruction',
- 133: 'Trace/breakpoint trap',
- 134: 'Process aborted',
- 135: 'Bus error: "access to undefined portion of memory object"',
- 136: 'Floating point exception: "erroneous arithmetic operation"',
- 137: 'Kill (terminate immediately)',
- 138: 'User-defined 1',
- 139: 'Segmentation violation',
- 140: 'User-defined 2',
- 141: 'Write to pipe with no one reading',
- 142: 'Signal raised by alarm',
- 143: 'Termination (request to terminate)',
- 145: 'Child process terminated, stopped (or continued*)',
- 146: 'Continue if stopped',
- 147: 'Stop executing temporarily',
- 148: 'Terminal stop signal',
- 149: 'Background process attempting to read from tty ("in")',
- 150: 'Background process attempting to write to tty ("out")',
- 151: 'Urgent data available on socket',
- 152: 'CPU time limit exceeded',
- 153: 'File size limit exceeded',
- 154: 'Signal raised by timer counting virtual time: "virtual timer expired"',
- 155: 'Profiling timer expired',
- 157: 'Pollable event',
- 159: 'Bad syscall',
- }[exitCode || -1]
+export function nothrow(promise: ProcessPromise) {
+ promise._nothrow = true
+ return promise
+}
+
+export function quiet(promise: ProcessPromise) {
+ promise._quiet = true
+ return promise
+}
+
+export function within(callback: () => void) {
+ let context = storage.getStore()
+ assert(context)
+ storage.run({ ...context }, callback)
}
src/experimental.ts
@@ -15,9 +15,6 @@
import { ProcessOutput, $ } from './core.js'
import { sleep } from './goods.js'
import { isString } from './util.js'
-import { getCtx, runInCtx } from './context.js'
-
-export { getCtx, runInCtx }
// Retries a command a few times. Will return after the first
// successful attempt, or will throw after specifies attempts count.
src/globals.ts
@@ -17,5 +17,6 @@ declare global {
var quiet: typeof _.quiet
var sleep: typeof _.sleep
var which: typeof _.which
+ var within: typeof _.within
var YAML: typeof _.YAML
}
src/goods.ts
@@ -16,8 +16,7 @@ import * as globbyModule from 'globby'
import minimist from 'minimist'
import { setTimeout as sleep } from 'node:timers/promises'
import nodeFetch, { RequestInfo, RequestInit } from 'node-fetch'
-import { getCtx, getRootCtx } from './context.js'
-import { colorize } from './print.js'
+import { colorize } from './util.js'
export { default as chalk } from 'chalk'
export { default as fs } from 'fs-extra'
@@ -40,7 +39,7 @@ globbyModule)
export const glob = globby
export async function fetch(url: RequestInfo, init?: RequestInit) {
- if (getCtx().verbose) {
+ if ($.verbose) {
if (typeof init !== 'undefined') {
console.log('$', colorize(`fetch ${url}`), init)
} else {
@@ -50,8 +49,7 @@ export async function fetch(url: RequestInfo, init?: RequestInit) {
return nodeFetch(url, init)
}
-export function cd(path: string) {
- if (getCtx().verbose) console.log('$', colorize(`cd ${path}`))
- process.chdir(path)
- getRootCtx().cwd = getCtx().cwd = process.cwd()
+export function cd(dir: string) {
+ if ($.verbose) console.log('$', colorize(`cd ${dir}`))
+ $.cwd = path.resolve($.cwd, dir)
}
src/guards.ts
@@ -1,42 +0,0 @@
-// Copyright 2022 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import { getCtx } from './context.js'
-import { ProcessPromise } from './core.js'
-
-export function quote(arg: string) {
- if (/^[a-z0-9/_.-]+$/i.test(arg) || arg === '') {
- return arg
- }
- return (
- `$'` +
- arg
- .replace(/\\/g, '\\\\')
- .replace(/'/g, "\\'")
- .replace(/\f/g, '\\f')
- .replace(/\n/g, '\\n')
- .replace(/\r/g, '\\r')
- .replace(/\t/g, '\\t')
- .replace(/\v/g, '\\v')
- .replace(/\0/g, '\\0') +
- `'`
- )
-}
-
-export function substitute(arg: ProcessPromise | any) {
- if (arg?.stdout) {
- return arg.stdout.replace(/\n$/, '')
- }
- return `${arg}`
-}
src/hooks.ts
@@ -1,25 +0,0 @@
-// Copyright 2022 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import { ProcessPromise } from './core.js'
-
-export function nothrow(promise: ProcessPromise) {
- promise.ctx!.nothrow = true
- return promise
-}
-
-export function quiet(promise: ProcessPromise) {
- promise.ctx!.verbose = false
- return promise
-}
src/index.ts
@@ -26,9 +26,15 @@ import {
YAML,
os,
} from './goods.js'
-import { nothrow, quiet } from './hooks.js'
import { question } from './question.js'
-import { $, ProcessPromise, ProcessOutput } from './core.js'
+import {
+ $,
+ ProcessPromise,
+ ProcessOutput,
+ nothrow,
+ quiet,
+ within,
+} from './core.js'
export {
$,
@@ -48,5 +54,6 @@ export {
quiet,
sleep,
which,
+ within,
YAML,
}
src/print.ts
@@ -12,11 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { getCtx } from './context.js'
-import { chalk } from './goods.js'
+import { colorize } from './util.js'
export function printCmd(cmd: string) {
- if (!getCtx()?.verbose) return
if (/\n/.test(cmd)) {
console.log(
cmd
@@ -28,15 +26,3 @@ export function printCmd(cmd: string) {
console.log('$', colorize(cmd))
}
}
-
-export function printStd(data: any, err?: any) {
- if (!getCtx()?.verbose) return
- if (data) process.stdout.write(data)
- if (err) process.stderr.write(err)
-}
-
-export function colorize(cmd: string) {
- return cmd.replace(/^[\w_.-]+(\s|$)/, (substr) => {
- return chalk.greenBright(substr)
- })
-}
src/util.ts
@@ -12,6 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import chalk from 'chalk'
+import { promisify } from 'node:util'
+import psTreeModule from 'ps-tree'
+import { ProcessPromise } from './core.js'
+
+export const psTree = promisify(psTreeModule)
+
+export function noop() {}
+
export function randomId() {
return Math.random().toString(36).slice(2)
}
@@ -19,3 +28,72 @@ export function randomId() {
export function isString(obj: any) {
return typeof obj === 'string'
}
+
+export function quote(arg: string) {
+ if (/^[a-z0-9/_.-]+$/i.test(arg) || arg === '') {
+ return arg
+ }
+ return (
+ `$'` +
+ arg
+ .replace(/\\/g, '\\\\')
+ .replace(/'/g, "\\'")
+ .replace(/\f/g, '\\f')
+ .replace(/\n/g, '\\n')
+ .replace(/\r/g, '\\r')
+ .replace(/\t/g, '\\t')
+ .replace(/\v/g, '\\v')
+ .replace(/\0/g, '\\0') +
+ `'`
+ )
+}
+
+export function substitute(arg: ProcessPromise | any) {
+ if (arg?.stdout) {
+ return arg.stdout.replace(/\n$/, '')
+ }
+ return `${arg}`
+}
+
+export function colorize(cmd: string) {
+ return cmd.replace(/^[\w_.-]+(\s|$)/, (substr) => {
+ return chalk.greenBright(substr)
+ })
+}
+
+export function exitCodeInfo(exitCode: number | null): string | undefined {
+ return {
+ 2: 'Misuse of shell builtins',
+ 126: 'Invoked command cannot execute',
+ 127: 'Command not found',
+ 128: 'Invalid exit argument',
+ 129: 'Hangup',
+ 130: 'Interrupt',
+ 131: 'Quit and dump core',
+ 132: 'Illegal instruction',
+ 133: 'Trace/breakpoint trap',
+ 134: 'Process aborted',
+ 135: 'Bus error: "access to undefined portion of memory object"',
+ 136: 'Floating point exception: "erroneous arithmetic operation"',
+ 137: 'Kill (terminate immediately)',
+ 138: 'User-defined 1',
+ 139: 'Segmentation violation',
+ 140: 'User-defined 2',
+ 141: 'Write to pipe with no one reading',
+ 142: 'Signal raised by alarm',
+ 143: 'Termination (request to terminate)',
+ 145: 'Child process terminated, stopped (or continued*)',
+ 146: 'Continue if stopped',
+ 147: 'Stop executing temporarily',
+ 148: 'Terminal stop signal',
+ 149: 'Background process attempting to read from tty ("in")',
+ 150: 'Background process attempting to write to tty ("out")',
+ 151: 'Urgent data available on socket',
+ 152: 'CPU time limit exceeded',
+ 153: 'File size limit exceeded',
+ 154: 'Signal raised by timer counting virtual time: "virtual timer expired"',
+ 155: 'Profiling timer expired',
+ 157: 'Pollable event',
+ 159: 'Bad syscall',
+ }[exitCode || -1]
+}
test/fixtures/markdown.md
@@ -25,6 +25,7 @@ VAR=$(echo hello)
echo "$VAR"
```
+ // ignore
console.log('world')
Other code blocks are ignored:
test/experimental.test.js
@@ -15,7 +15,6 @@
import { test } from 'uvu'
import * as assert from 'uvu/assert'
import '../build/globals.js'
-
import {
echo,
retry,
test/index.test.js
@@ -15,12 +15,12 @@
import { test } from 'uvu'
import * as assert from 'uvu/assert'
import { inspect } from 'node:util'
-import chalk from 'chalk'
import { Writable } from 'node:stream'
import { Socket } from 'node:net'
import '../build/globals.js'
import { ProcessPromise } from '../build/index.js'
-import {getCtx, runInCtx} from '../build/experimental.js'
+
+$.verbose = false
test('only stdout is used during command substitution', async () => {
let hello = await $`echo Error >&2; echo Hello`
@@ -45,7 +45,6 @@ test('arguments are quoted', async () => {
})
test('undefined and empty string correctly quoted', async () => {
- $.verbose = true
assert.is((await $`echo -n ${undefined}`).toString(), 'undefined')
assert.is((await $`echo -n ${''}`).toString(), '')
})
@@ -66,7 +65,6 @@ test('pipefail is on', async () => {
try {
p = await $`cat /dev/not_found | sort`
} catch (e) {
- console.log('Caught an exception -> ok')
p = e
}
assert.is.not(p.exitCode, 0)
@@ -214,8 +212,6 @@ test('globby available', async () => {
assert.is(typeof globby.isDynamicPattern, 'function')
assert.is(typeof globby.isGitIgnored, 'function')
assert.is(typeof globby.isGitIgnoredSync, 'function')
- console.log(chalk.greenBright('globby available'))
-
assert.equal(await globby('*.md'), ['README.md'])
})
@@ -258,18 +254,16 @@ test('cd() works with relative paths', async () => {
fs.mkdirpSync('/tmp/zx-cd-test/one/two')
cd('/tmp/zx-cd-test/one/two')
let p1 = $`pwd`
- assert.ok($.cwd.endsWith('/two'))
- assert.ok(process.cwd().endsWith('/two'))
+ assert.match($.cwd, '/two')
+ assert.equal(process.cwd(), cwd)
cd('..')
let p2 = $`pwd`
- assert.ok($.cwd.endsWith('/one'))
- assert.ok(process.cwd().endsWith('/one'))
+ assert.match($.cwd, '/one')
cd('..')
let p3 = $`pwd`
- assert.ok(process.cwd().endsWith('/zx-cd-test'))
- assert.ok($.cwd.endsWith('/tmp/zx-cd-test'))
+ assert.match($.cwd, '/tmp/zx-cd-test')
let results = (await Promise.all([p1, p2, p3])).map((p) =>
path.basename(p.stdout.trim())
@@ -285,27 +279,24 @@ test('cd() works with relative paths', async () => {
}
})
-test('cd() does not affect parallel contexts', async () => {
+test('cd() does affect parallel contexts', async () => {
let cwd = process.cwd()
let resolve, reject
- let promise = new ProcessPromise((...args) => ([resolve, reject] = args))
+ let promise = new Promise((...args) => ([resolve, reject] = args))
try {
fs.mkdirpSync('/tmp/zx-cd-parallel')
- runInCtx({ ...getCtx() }, async () => {
+ within(async () => {
assert.equal($.cwd, cwd)
await sleep(10)
cd('/tmp/zx-cd-parallel')
- assert.ok(getCtx().cwd.endsWith('/zx-cd-parallel'))
assert.ok($.cwd.endsWith('/zx-cd-parallel'))
})
- runInCtx({ ...getCtx() }, async () => {
+ within(async () => {
assert.equal($.cwd, cwd)
- assert.equal(getCtx().cwd, cwd)
await sleep(20)
- assert.equal(getCtx().cwd, cwd)
- assert.ok($.cwd.endsWith('/zx-cd-parallel'))
+ assert.ok(!$.cwd.endsWith('/zx-cd-parallel'))
resolve()
})
@@ -346,4 +337,49 @@ test('which available', async () => {
assert.is(which.sync('npm'), await which('npm'))
})
+test('within() works', async () => {
+ let resolve, reject
+ let promise = new Promise((...args) => ([resolve, reject] = args))
+
+ function yes() {
+ assert.equal($.verbose, true)
+ resolve()
+ }
+
+ $.verbose = false
+ assert.equal($.verbose, false)
+
+ within(() => {
+ $.verbose = true
+ })
+ assert.equal($.verbose, false)
+
+ within(async () => {
+ $.verbose = true
+ setTimeout(yes, 10)
+ })
+ assert.equal($.verbose, false)
+
+ await promise
+})
+
+test('within() restores previous cwd', async () => {
+ let resolve, reject
+ let promise = new Promise((...args) => ([resolve, reject] = args))
+
+ let pwd = await $`pwd`
+
+ within(async () => {
+ $.verbose = false
+ cd('/tmp')
+ setTimeout(async () => {
+ assert.match((await $`pwd`).stdout, '/tmp')
+ resolve()
+ }, 1000)
+ })
+
+ assert.equal((await $`pwd`).stdout, pwd.stdout)
+ await promise
+})
+
test.run()
README.md
@@ -225,6 +225,7 @@ if ((await nothrow($`[[ -d path ]]`)).exitCode == 0) {
...
}
```
+
### `quiet()`
Changes behavior of `$` to disable verbose output.
@@ -240,6 +241,34 @@ await quiet($`grep something from-file`)
// Command and output will not be displayed.
```
+### `within()`
+
+Create a new async context.
+
+```ts
+function within(callback): void
+```
+
+Usage:
+
+```js
+$.verbose = true
+await $`pwd` // => /home/path
+
+within(async () => {
+ $.verbose = false
+ cd('/tmp')
+
+ setTimeout(async () => {
+ await $`pwd` // => /tmp
+ assert($.verbose == false)
+ }, 1000)
+})
+
+await $`pwd` // => /home/path
+assert($.verbose == true)
+```
+
## Packages
Following packages are available without importing inside scripts.
@@ -438,28 +467,6 @@ import {withTimeout} from 'zx/experimental'
await withTimeout(100, 'SIGTERM')`sleep 9999`
```
-### `getCtx()` and `runInCtx()`
-
-[async_hooks](https://nodejs.org/api/async_hooks.html) methods to manipulate bound context.
-This object is used by zx inners, so it has a significant impact on the call mechanics. Please use this carefully and wisely.
-
-```js
-import {getCtx, runInCtx} from 'zx/experimental'
-
-runInCtx({ ...getCtx() }, async () => {
- await sleep(10)
- cd('/foo')
- // $.cwd refers to /foo
- // getCtx().cwd === $.cwd
-})
-
-runInCtx({ ...getCtx() }, async () => {
- await sleep(20)
- // $.cwd refers to /foo
- // but getCtx().cwd !== $.cwd
-})
-```
-
## FAQ
### Passing env variables
@@ -592,31 +599,6 @@ jobs:
EOF
```
-### Customizing
-
-You can build your very own custom zx version that best suits your needs.
-```ts
-// index.js
-import {$} from 'zx'
-
-$.quote = () => {}
-$.extraFn = async () => {
- await $`ping example.com`
- await $`npm whoami`
-}
-
-// cli.js
-#!/usr/bin/env node
-
-import './index.js'
-import 'zx/cli'
-
-// script.mjs
-await $.extraFn()
-
-// zx-custom script.mjs
-```
-
## License
[Apache-2.0](LICENSE)