Commit 2c8711c
Changed files (11)
src/cli.ts
@@ -25,7 +25,8 @@ import { randomId } from './util.js'
import './globals.js'
await (async function main() {
- $.verbose = !argv.quiet
+ $.verbose = +(argv.verbose || argv.quiet ? 0 : $.verbose)
+
if (typeof argv.shell === 'string') {
$.shell = argv.shell
}
@@ -220,7 +221,8 @@ function printUsage() {
zx [options] <script>
Options:
- --quiet : don't echo commands
+ --verbose : verbosity level 0|1|2
+ --quiet : don't echo commands, same as --verbose=0
--shell=<path> : custom shell binary
--prefix=<command> : prefix all commands
--experimental : enable new api proposals
src/context.ts
@@ -15,13 +15,17 @@
import { AsyncLocalStorage } from 'node:async_hooks'
export type Options = {
- verbose: boolean
+ verbose: boolean | number
cwd: string
env: NodeJS.ProcessEnv
prefix: string
shell: string
maxBuffer: number
quote: (v: string) => string
+ logOutput?: 'stdout' | 'stderr'
+ logFormat?: (...msg: any[]) => string | string[]
+ logPrint?: (data: any, err?: any) => void
+ logIgnore?: string | string[]
}
export type Context = Options & {
@@ -46,6 +50,4 @@ export function setRootCtx(ctx: Options) {
export function getRootCtx() {
return root
}
-export function runInCtx(ctx: Options, cb: any) {
- return storage.run(ctx, cb)
-}
+export const runInCtx = storage.run.bind(storage)
src/core.ts
@@ -24,7 +24,7 @@ import { spawn } from 'node:child_process'
import { chalk, which } from './goods.js'
import { runInCtx, getCtx, setRootCtx, Context } from './context.js'
-import { printStd, printCmd } from './print.js'
+import { printCmd, log } from './print.js'
import { quote, substitute } from './guards.js'
import psTreeModule from 'ps-tree'
@@ -61,13 +61,11 @@ export function $(pieces: TemplateStringsArray, ...args: any[]) {
return promise
}
-setRootCtx($)
-
$.cwd = process.cwd()
$.env = process.env
$.quote = quote
$.spawn = spawn
-$.verbose = true
+$.verbose = 2
$.maxBuffer = 200 * 1024 * 1024 /* 200 MiB*/
$.prefix = '' // Bash not found, no prefix.
try {
@@ -75,6 +73,8 @@ try {
$.prefix = 'set -euo pipefail;'
} catch (e) {}
+setRootCtx($)
+
export class ProcessPromise extends Promise<ProcessOutput> {
child?: ChildProcessByStdio<Writable, Readable, Readable>
_resolved = false
@@ -219,12 +219,12 @@ export class ProcessPromise extends Promise<ProcessOutput> {
stderr = '',
combined = ''
let onStdout = (data: any) => {
- printStd(data)
+ log({ scope: 'cmd', output: 'stdout', raw: true, verbose: 2 }, data)
stdout += data
combined += data
}
let onStderr = (data: any) => {
- printStd(null, data)
+ log({ scope: 'cmd', output: 'stderr', raw: true, verbose: 2 }, data)
stderr += data
combined += data
}
src/experimental.ts
@@ -16,6 +16,9 @@ import { ProcessOutput, $ } from './core.js'
import { sleep } from './goods.js'
import { isString } from './util.js'
import { getCtx, runInCtx } from './context.js'
+import { log } from './print.js'
+
+export { log }
// Retries a command a few times. Will return after the first
// successful attempt, or will throw after specifies attempts count.
@@ -78,13 +81,8 @@ export function startSpinner(title = '') {
)(setInterval(spin, 100))
}
-export function ctx(
- cb: Parameters<typeof runInCtx>[1]
-): ReturnType<typeof runInCtx> {
+export function ctx<R extends any>(cb: (_$: typeof $) => R): R {
const _$ = Object.assign($.bind(null), getCtx())
- function _cb() {
- return cb(_$)
- }
- return runInCtx(_$, _cb)
+ return runInCtx(_$, cb, _$)
}
src/print.ts
@@ -12,25 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { getCtx } from './context.js'
+import { getCtx, getRootCtx } from './context.js'
import { chalk } from './goods.js'
-
-export function printCmd(cmd: string) {
- if (!getCtx()?.verbose) return
- if (/\n/.test(cmd)) {
- console.log(
- cmd
- .split('\n')
- .map((line, i) => (i === 0 ? '$' : '>') + ' ' + colorize(line))
- .join('\n')
- )
- } else {
- console.log('$', colorize(cmd))
- }
-}
+import { default as ignore } from 'ignore'
+import { asArray } from './util.js'
export function printStd(data: any, err?: any) {
- if (!getCtx()?.verbose) return
if (data) process.stdout.write(data)
if (err) process.stderr.write(err)
}
@@ -40,3 +27,46 @@ export function colorize(cmd: string) {
return chalk.greenBright(substr)
})
}
+
+export function log(
+ opts: {
+ scope: string
+ verbose?: 0 | 1 | 2
+ output?: 'stdout' | 'stderr'
+ raw?: boolean
+ },
+ ...msg: any[]
+) {
+ let { scope, verbose = 1, output, raw } = opts
+ let ctx = getCtx()
+ let {
+ logOutput = output,
+ logFormat = () => msg,
+ logPrint = printStd,
+ logIgnore = [],
+ } = ctx
+ let level = Math.min(+getRootCtx().verbose, +ctx.verbose)
+ if (verbose > level) return
+
+ const ig = ignore().add(logIgnore)
+
+ if (!ig.ignores(scope)) {
+ msg = raw ? msg[0] : asArray(logFormat(msg)).join(' ') + '\n'
+ // @ts-ignore
+ logPrint(...(logOutput === 'stdout' ? [msg] : [null, msg]))
+ }
+}
+
+export function printCmd(cmd: string) {
+ if (/\n/.test(cmd)) {
+ log(
+ { scope: 'cmd' },
+ cmd
+ .split('\n')
+ .map((line, i) => (i === 0 ? '$' : '>') + ' ' + colorize(line))
+ .join('\n')
+ )
+ } else {
+ log({ scope: 'cmd' }, '$', colorize(cmd))
+ }
+}
src/util.ts
@@ -19,3 +19,6 @@ export function randomId() {
export function isString(obj: any) {
return typeof obj === 'string'
}
+
+export const asArray = <T>(value: T[] | T | undefined): T[] =>
+ value ? (Array.isArray(value) ? value : [value]) : []
test/cli.test.js
@@ -16,7 +16,7 @@ import { test } from 'uvu'
import * as assert from 'uvu/assert'
import '../build/globals.js'
-$.verbose = false
+$.verbose = 0
test('supports `-v` flag / prints version', async () => {
assert.match((await $`node build/cli.js -v`).toString(), /\d+.\d+.\d+/)
test/experimental.test.js
@@ -22,6 +22,7 @@ import {
startSpinner,
withTimeout,
ctx,
+ log,
} from '../build/experimental.js'
import chalk from 'chalk'
@@ -110,4 +111,8 @@ test('ctx() provides isolates running scopes', async () => {
$.verbose = false
})
+test('log() API is available', () => {
+ assert.ok(typeof log === 'function')
+})
+
test.run()
test/log.test.js
@@ -0,0 +1,50 @@
+// 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 { test } from 'uvu'
+import * as assert from 'uvu/assert'
+import { log } from '../build/print.js'
+
+test('logger works', async () => {
+ $.verbose = 1
+ let stdout = ''
+ let stderr = ''
+ $.logFormat = (msg) => msg.map((m) => m.toUpperCase())
+ $.logPrint = (data, err) => {
+ if (data) stdout += data
+ if (err) stderr += err
+ }
+ $.logIgnore = ['b*', 'fetch']
+ log({ scope: 'foo' }, 'foo-test')
+ log({ scope: 'bar' }, 'bar-test')
+ log({ scope: 'baz' }, 'baz-test')
+ log({ scope: 'fetch' }, 'example.com')
+
+ assert.ok(stderr.includes('FOO-TEST'))
+ assert.ok(!stderr.includes('BAR-TEST'))
+ assert.ok(!stderr.includes('BAZ-TEST'))
+ assert.ok(!stderr.includes('EXAMPLE.COM'))
+
+ $.logOutput = 'stdout'
+ log({ scope: 'foo' }, 'foo')
+ assert.ok(stdout.includes('FOO'))
+
+ delete $.logPrint
+ delete $.logFormat
+ delete $.logIgnore
+ delete $.logOutput
+ $.verbose = 0
+})
+
+test.run()
package.json
@@ -49,6 +49,7 @@
"chalk": "^5.0.1",
"fs-extra": "^10.1.0",
"globby": "^13.1.1",
+ "ignore": "^5.2.0",
"minimist": "^1.2.6",
"node-fetch": "^3.2.4",
"ps-tree": "^1.2.0",
README.md
@@ -359,6 +359,49 @@ outputs.
Or use a CLI argument `--quiet` to set `$.verbose = false`.
+### `$.logOutput`
+
+Specifies zx debug channel: `stdout/stderr`. Defaults to `stderr`.
+
+### `$.logFormat`
+
+Specifies zx log output formatter. Defaults to `identity`.
+
+```js
+// Nice place to add masker, if you pass creds to zx methods
+$.logFormat = (msg) => msg.map(m => m.toUpperCase())
+```
+
+### `$.logIgnore`
+
+Specifies log events to filter out. Defaults to `''`, so everything is being logged.
+
+```js
+$.logIgnore = ['cd', 'fetch']
+cd('/tmp/foo')
+$.fetch('https://example.com')
+// `$ cd /tmp/foo` is omitted
+// `$ fetch https://example.com` is not printed too
+
+$.logIgnore = 'cmd'
+$`echo 'test'`
+// prints `test` w/o `$ echo 'test'`
+```
+
+### `$.logPrint`
+
+Specifies event logging stream. Defaults to `process.stdout/process.stderr`.
+
+```js
+let stdout = ''
+let stderr = ''
+
+$.logPrint = (data, err) => {
+ if (data) stdout += data
+ if (err) stderr += err
+}
+```
+
### `$.env`
Specifies env map. Defaults to `process.env`.
@@ -461,6 +504,18 @@ ctx(async ($) => {
})
```
+### `log()`
+
+The logger. Accepts config via `$.log*` options.
+
+```js
+import {log} from 'zx/experimental'
+
+log({scope: 'foo', verbose: 1, output: 'stderr'}, 'some', 'data')
+// some data
+```
+
+
## FAQ
### Passing env variables