Commit e8ac8fc
Changed files (5)
test
src/cli.ts
@@ -20,6 +20,7 @@ import { tmpdir } from 'node:os'
import { basename, dirname, extname, join, resolve } from 'node:path'
import url from 'node:url'
import { $, argv, fetch, ProcessOutput, chalk } from './index.js'
+import { startRepl } from './repl.js'
import { randomId } from './util.js'
import './globals.js'
@@ -35,16 +36,32 @@ await (async function main() {
Object.assign(global, await import('./experimental.js'))
}
try {
- if (['--version', '-v', '-V'].includes(process.argv[2])) {
- console.log(createRequire(import.meta.url)('../package.json').version)
+ if (
+ process.argv.length == 3 &&
+ ['--version', '-v', '-V'].includes(process.argv[2])
+ ) {
+ console.log(getVersion())
+ return (process.exitCode = 0)
+ }
+ if (
+ process.argv.length == 3 &&
+ ['--help', '-h'].includes(process.argv[2])
+ ) {
+ printUsage()
+ return (process.exitCode = 0)
+ }
+ if (
+ process.argv.length == 3 &&
+ ['--interactive', '-i'].includes(process.argv[2])
+ ) {
+ startRepl()
return (process.exitCode = 0)
}
let firstArg = process.argv.slice(2).find((a) => !a.startsWith('--'))
if (typeof firstArg === 'undefined' || firstArg === '-') {
let ok = await scriptFromStdin()
if (!ok) {
- printUsage()
- return (process.exitCode = 2)
+ startRepl()
}
} else if (
firstArg.startsWith('http://') ||
@@ -211,18 +228,25 @@ function transformMarkdown(buf: Buffer) {
return output.join('\n')
}
+function getVersion(): string {
+ return createRequire(import.meta.url)('../package.json').version
+}
+
function printUsage() {
console.log(`
- ${chalk.bgGreenBright.black(' ZX ')}
+ ${chalk.bold('zx ' + getVersion())}
+ A tool for writing better scripts
- Usage:
+ ${chalk.bold('Usage')}
zx [options] <script>
- Options:
- --quiet : don't echo commands
- --shell=<path> : custom shell binary
- --prefix=<command> : prefix all commands
- --experimental : enable new api proposals
- --version, -v : print current zx version
+ ${chalk.bold('Options')}
+ --quiet don't echo commands
+ --shell=<path> custom shell binary
+ --prefix=<command> prefix all commands
+ --interactive, -i start repl
+ --experimental enable new api proposals
+ --version, -v print current zx version
+ --help, -h print help
`)
}
src/core.ts
@@ -37,6 +37,35 @@ type Options = {
const storage = new AsyncLocalStorage<Options>()
+function init() {
+ storage.enterWith({
+ verbose: true,
+ cwd: process.cwd(),
+ env: process.env,
+ shell: true,
+ prefix: '',
+ quote,
+ spawn,
+ })
+ if (process.env.ZX_VERBOSE) $.verbose = process.env.ZX_VERBOSE == 'true'
+ try {
+ $.shell = which.sync('bash')
+ $.prefix = 'set -euo pipefail;'
+ } catch (err) {
+ // ¯\_(ツ)_/¯
+ }
+}
+
+function getStore() {
+ let context = storage.getStore()
+ if (!context) {
+ init()
+ context = storage.getStore()
+ assert(context)
+ }
+ return context
+}
+
export const $ = new Proxy<Shell & Options>(
function (pieces, ...args) {
let from = new Error().stack!.split(/^\s*at\s/m)[2].trim()
@@ -60,37 +89,15 @@ export const $ = new Proxy<Shell & Options>(
} as Shell & Options,
{
set(_, key, value) {
- let context = storage.getStore()
- assert(context)
- Reflect.set(context, key, value)
+ Reflect.set(getStore(), key, value)
return true
},
get(_, key) {
- let context = storage.getStore()
- assert(context)
- return Reflect.get(context, key)
+ return Reflect.get(getStore(), 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> {
@@ -330,7 +337,5 @@ export function quiet(promise: ProcessPromise) {
}
export function within<R>(callback: (...args: any) => R): R {
- let context = storage.getStore()
- assert(context)
- return storage.run({ ...context }, callback)
+ return storage.run({ ...getStore() }, callback)
}
src/globals.ts
@@ -1,3 +1,17 @@
+// 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 * as _ from './index.js'
Object.assign(global, _)
src/repl.ts
@@ -0,0 +1,36 @@
+// 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 repl from 'node:repl'
+import path from 'node:path'
+import os from 'node:os'
+import { inspect } from 'node:util'
+import chalk from 'chalk'
+import { ProcessOutput } from './core.js'
+
+export function startRepl() {
+ process.env.ZX_VERBOSE = 'false'
+ const r = repl.start({
+ prompt: chalk.greenBright.bold('❯ '),
+ useGlobal: true,
+ preview: false,
+ writer(output: any) {
+ if (output instanceof ProcessOutput) {
+ return output.toString().replace(/\n$/, '')
+ }
+ return inspect(output, { colors: true })
+ },
+ })
+ r.setupHistory(path.join(os.homedir(), '.zx_repl_history'), () => {})
+}
test/cli.test.js
@@ -18,17 +18,34 @@ import '../build/globals.js'
$.verbose = false
-test('supports `-v` flag / prints version', async () => {
+test('prints version', async () => {
assert.match((await $`node build/cli.js -v`).toString(), /\d+.\d+.\d+/)
})
test('prints help', async () => {
- let p = nothrow($`node build/cli.js`)
+ let p = $`node build/cli.js -h`
p.stdin.end()
let help = await p
assert.match(help.stdout, 'zx')
})
+test('starts repl', async () => {
+ let p = $`node build/cli.js`
+ p.stdin.end()
+ let out = await p
+ assert.match(out.stdout, '❯')
+})
+
+test('starts repl with -i', async () => {
+ let p = $`node build/cli.js -i`
+ p.stdin.write('await $`echo f"o"o`\n')
+ p.stdin.write('"b"+"ar"\n')
+ p.stdin.end()
+ let out = await p
+ assert.match(out.stdout, 'foo')
+ assert.match(out.stdout, 'bar')
+})
+
test('supports `--experimental` flag', async () => {
await $`echo 'echo("test")' | node build/cli.js --experimental`
})