google/zx 使ってみる & TypeScriptで動くようにする
シェルスクリプトをJSで簡単に書けるツール
google/zx: A tool for writing better scripts
インスコ
❯ npm i -g zx
...
さらに俺は asdf を使っているのでasdf reshim nodejs
する。
さてhelpを見るか
❯ zx --help
internal/process/esm_loader.js:74
internalBinding('errors').triggerUncaughtException(
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/korosuke613/ghq/github.com/korosuke613/playground/zx/--help' imported from /Users/korosuke613/.asdf/installs/nodejs/14.16.1/.npm/lib/node_modules/zx/zx.mjs
at finalizeResolution (internal/modules/esm/resolve.js:276:11)
at moduleResolve (internal/modules/esm/resolve.js:699:10)
at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:810:11)
at Loader.resolve (internal/modules/esm/loader.js:86:40)
at Loader.getModuleJob (internal/modules/esm/loader.js:230:28)
at Loader.import (internal/modules/esm/loader.js:165:28)
at importModuleDynamically (internal/modules/esm/translators.js:114:35)
at exports.importModuleDynamicallyCallback (internal/process/esm_loader.js:30:14)
at file:///Users/korosuke613/.asdf/installs/nodejs/14.16.1/.npm/lib/node_modules/zx/zx.mjs:57:5
at ModuleJob.run (internal/modules/esm/module_job.js:152:23) {
code: 'ERR_MODULE_NOT_FOUND'
}
...
--help
っていうファイルを実行しようとしてる???
-h
やhelp
も試してみたが同じような結果となった。
help無さそう...
❯ zx --version
zx version 1.2.2
さすがにバージョンは出せるのね
このリポジトリで作業する
とりあえずHello Worldする。top-level awaitをするために拡張子は.mjs
がおすすめとのこと
#!usr/bin/env zx
const msg = "Hello World"
await $`echo ${msg}`
実行。zx
コマンドにファイルパスを渡す。
❯ zx helloWorld.mjs
$ echo 'Hello World'
Hello World
おお!とりあえず実行できた。
zx helloWorld.mjs
で実行するのでshebangはいらないのでは?とのアドバイスをもらった。
const msg = "Hello World"
await $`echo ${msg}`
上をzx helloWorld.mjs
したら普通に実行できた。
gitのworking treeがクリーンかどうか確認するコードを書いた。git status
はdirtyでもexitcodeが0になるので文字数でdirtyかどうか判断する。--porcelain
はマシンリーダブルな形式にしてくれるオプション。
const status = await $`git status --porcelain`
if(status.length > 0){
throw new Error("git working tree is dirty.")
}
実行
❯ zx checkDirtyWorkingTreeForGit.mjs
$ git status --porcelain
A zx/checkDirtyWorkingTreeForGit.mjs
M zx/helloWorld.mjs
うまくいかんぞ。git status
の結果って標準エラー出力に出るんだっけ。どうやって取得するんだ
$`command`の出力であるProcessOutput
classはstderr
を持ってるらしい。
出力。
const status = await $`git status --porcelain`
console.log(`stdout: ${status.stdout}`)
console.log(`stderr: ${status.stderr}`)
console.log(`exitCode: ${status.exitCode}`)
console.log(`status.length: ${status.length}`)
console.log(`status: ${status}`)
console.log(status)
実行。
❯ zx checkDirtyWorkingTreeForGit.mjs
$ git status --porcelain
AM zx/checkDirtyWorkingTreeForGit.mjs
M zx/helloWorld.mjs
stdout: AM zx/checkDirtyWorkingTreeForGit.mjs
M zx/helloWorld.mjs
stderr:
exitCode: 0
status.length: undefined
status: AM zx/checkDirtyWorkingTreeForGit.mjs
M zx/helloWorld.mjs
ProcessOutput {}
statusの結果はstdout
に入ってた。そうか。そもそもstatus.length
はundefined
になるのか。
それはいいとして、console.log(status)
の出力はProcessOutput{}
になるのか...
console.log(`status: ${status}`)の出力はstatus.stdout
になるらしい。ここら辺よくわからん
とりあえず俺がやりたかったことは以下でできた。
const status = await $`git status --porcelain`
if(status.stdout.length > 0){
throw new Error("git working tree is dirty.")
}
↓
❯ zx checkDirtyWorkingTreeForGit.mjs
$ git status --porcelain
AM zx/checkDirtyWorkingTreeForGit.mjs
M zx/helloWorld.mjs
file:///Users/korosuke613/ghq/github.com/korosuke613/playground/zx/checkDirtyWorkingTreeForGit.mjs:11
throw new Error("git working tree is dirty.")
^
Error: git working tree is dirty.
at file:///Users/korosuke613/ghq/github.com/korosuke613/playground/zx/checkDirtyWorkingTreeForGit.mjs:11:9
at processTicksAndRejections (internal/process/task_queues.js:93:5)
まあこの内容なら普通にシェルでいいんだけど
Importing from other scripts
It's possible to use and others with explicit import.$
#!/usr/bin/env node import {$} from 'zx' await $`date`
なんかインポートして普通にnodeから呼び出せそう。
これならデバッグもできそうだしTypeScriptで実行できそう
checkDirtyWorkingTreeForGit.ts
を作って実行した。
import {$} from 'zx'
(async ()=>{
const status = await $`git status --porcelain`
if(status.stdout.length > 0){
throw new Error("git working tree is dirty.")
}
})()
❯ npx ts-node checkDirtyWorkingTreeForGit.ts
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/node_modules/zx/index.mjs
at Module.load (internal/modules/cjs/loader.js:926:11)
at Function.Module._load (internal/modules/cjs/loader.js:769:14)
at Module.require (internal/modules/cjs/loader.js:952:19)
at require (internal/modules/cjs/helpers.js:88:18)
at Object.<anonymous> (/Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/checkDirtyWorkingTreeForGit.ts:1:1)
at Module._compile (internal/modules/cjs/loader.js:1063:30)
at Module.m._compile (/Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/node_modules/ts-node/src/index.ts:1056:23)
at Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
at Object.require.extensions.<computed> [as .ts] (/Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/node_modules/ts-node/src/index.ts:1059:12)
at Module.load (internal/modules/cjs/loader.js:928:32)
ERR_REQUIRE_ESM
。あーーーー
色々がんばった結果、nodeの--loader
でts-node/esm
を指定することにより実行できた。(参考)
"type": "module"
が必要。
{
"type": "module",
"dependencies": {
"zx": "^1.2.2"
},
"devDependencies": {
"ts-node": "^9.1.1"
}
}
"module": "ESNext"
はtop-level awaitするため
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
import {$} from 'zx'
const status = await $`git status --porcelain`
if(status.stdout.length > 0){
throw new Error("git working tree is dirty.")
}
❯ node --loader ts-node/esm checkDirtyWorkingTreeForGit.ts
(node:64769) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
$ git status --porcelain
AM zx/use-typescript/checkDirtyWorkingTreeForGit.mjs
AM zx/use-typescript/checkDirtyWorkingTreeForGit.ts
AM zx/use-typescript/main.ts
?? zx/use-typescript/node_modules/
?? zx/use-typescript/package-lock.json
?? zx/use-typescript/package.json
?? zx/use-typescript/tsconfig.json
Error: git working tree is dirty.
at file:///Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/checkDirtyWorkingTreeForGit.ts:6:9
at processTicksAndRejections (internal/process/task_queues.js:93:5)
ちなみに、ts-node
をインストールしてないと下みたいに怒られる。
❯ node --loader ts-node/esm checkDirtyWorkingTreeForGit.ts
(node:67480) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
internal/process/esm_loader.js:74
internalBinding('errors').triggerUncaughtException(
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'ts-node' imported from /Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/
at packageResolve (internal/modules/esm/resolve.js:655:9)
at moduleResolve (internal/modules/esm/resolve.js:696:18)
at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:810:11)
at Loader.resolve (internal/modules/esm/loader.js:86:40)
at Loader.getModuleJob (internal/modules/esm/loader.js:230:28)
at Loader.import (internal/modules/esm/loader.js:165:28)
at internal/process/esm_loader.js:57:31
at initializeLoader (internal/process/esm_loader.js:62:5)
at Object.loadESM (internal/process/esm_loader.js:67:11)
at runMainESM (internal/modules/run_main.js:43:31) {
code: 'ERR_MODULE_NOT_FOUND'
}
tsになったおかげで補完や型チェックが効いて書きやすくなった。index.d.ts
用意してるのになんでこう面倒なのか
ts-nodeじゃなくてもtscでコンパイルして使えた。
やはり"type": "module"
は必要。
❯ npx tsc
❯ node checkDirtyWorkingTreeForGit.js
$ git status --porcelain
AM zx/use-typescript/checkDirtyWorkingTreeForGit.mjs
AM zx/use-typescript/checkDirtyWorkingTreeForGit.ts
AM zx/use-typescript/main.ts
?? zx/use-typescript/checkDirtyWorkingTreeForGit.js
?? zx/use-typescript/main.js
?? zx/use-typescript/node_modules/
?? zx/use-typescript/package-lock.json
?? zx/use-typescript/package.json
?? zx/use-typescript/tsconfig.json
file:///Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/checkDirtyWorkingTreeForGit.js:4
throw new Error("git working tree is dirty.");
^
Error: git working tree is dirty.
at file:///Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/checkDirtyWorkingTreeForGit.js:4:11
at processTicksAndRejections (internal/process/task_queues.js:93:5)
nodeじゃなくてzx
コマンドでも動作した。
scripts
でnode --loader ts-node/esm
をすることで、IntelliJのデバッグもなんなくできた。
{
"type": "module",
"scripts": {
"checkDirtyWorkingTreeForGit": "node --loader ts-node/esm checkDirtyWorkingTreeForGit.ts"
},
"dependencies": {
"zx": "^1.2.2"
},
"devDependencies": {
"ts-node": "^9.1.1"
}
}
開発時はTSで動かし、運用時は動かしたい場所に置くときはコンパイルしたJSを置くようにしてzx
で実行すれば良さそう。
TSで動いたことで自動補完も型チェックも働き、インポートして使うことでリッチにデバッグもできるようになった。
今後シェルスクリプトを作るときにまた試してみたい。まだまだ知らん機能がたくさんある。
最終的なファイルたちがこちら
go製のojichatとnpmにあるcowsayを悪魔合体した。
import {$} from "zx"
// @ts-ignore cowsayは型が提供されてなかった...
import cowsay from "cowsay";
const getOjichatMessage = async () => {
try {
await $`ojichat -v`
return $`ojichat`
} catch (p) {
return $`docker run --rm -i greymd/ojichat:latest`
}
}
$.verbose = false // `set -x`を無効
const ojichatMessage = await getOjichatMessage()
console.log(
cowsay.say({
text: ojichatMessage.stdout
})
)
実行
❯ node --loader ts-node/esm ojichatToCowsay.ts
(node:24785) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
_________________________________________________________________________________________________________________
/ マユちゃん、ヤッホー😊何してるのかい😜⁉️🤔俺は、近所に新しくできたバー🍷に行ってきたよ。味はまぁまぁだったカナ(^з<) \
\ /
-----------------------------------------------------------------------------------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
なにやってんだろ俺
なんと zx のコントリビュータっぽい人からTwitterで連絡が来た。
TypeScript 対応したっぽい。これは試さねば!
現在の zx のバージョン
❯ zx -v
zx version 1.2.2
一方最新バージョンは 1.10.1。
結構更新されてるっぽい。
とりあえずコマンドとしての zx を更新する。
❯ npm upgrade -g zx
added 3 packages, removed 1 package, changed 9 packages, and audited 13 packages in 1s
2 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
❯ zx -v
zx version 1.10.1
更新完了
awsSts.ts
で試してみる。
v1.2.2で実行した結果
❯ zx awsSts.ts
internal/process/esm_loader.js:74
internalBinding('errors').triggerUncaughtException(
^
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/awsSts.ts
at Loader.defaultGetFormat [as _getFormat] (internal/modules/esm/get_format.js:71:15)
at Loader.getFormat (internal/modules/esm/loader.js:102:42)
at Loader.getModuleJob (internal/modules/esm/loader.js:231:31)
at async Loader.import (internal/modules/esm/loader.js:165:17)
at async file:///Users/korosuke613/.asdf/installs/nodejs/14.16.1/.npm/lib/node_modules/zx/zx.mjs:57:5 {
code: 'ERR_UNKNOWN_FILE_EXTENSION'
}
v1.10.1で実行した結果
❯ zx awsSts.ts
$ tsc --target esnext --module esnext --moduleResolution node /Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/awsSts.ts
/bin/bash: tsc: command not found
Error: /bin/bash: tsc: command not found
at importPath (file:///Users/korosuke613/.asdf/installs/nodejs/14.16.1/.npm/lib/node_modules/zx/zx.mjs:120:12)
おお!tscを求められた。コンパイルしようとしている。
tsc をグローバルインストールする。
❯ npm i -g typescript
added 1 package, and audited 2 packages in 1s
found 0 vulnerabilities
再度zx awsSts.ts
する。(結果を一部黒塗りしてる。)
❯ zx awsSts.ts
$ tsc --target esnext --module esnext --moduleResolution node /Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/awsSts.ts
$ aws sts get-caller-identity
{
"UserId": "xxxxxx:yyyy@example.com",
"Account": "zzzzzzzzzzzz",
"Arn": "arn:aws:sts::zzzzzzzzzz"
}
Account ID: zzzzzzzzzzz
実行できた!!!やったぜ
tsc コマンドを呼び出してコンパイルしてるんだな。
実装を見つけた。
if (ext === '.ts') {
let {dir, name} = parse(filepath)
let outFile = join(dir, name + '.mjs')
await $`tsc --target esnext --module esnext --moduleResolution node ${filepath}`
await fs.rename(join(dir, name + '.js'), outFile)
let wait = importPath(outFile, filepath)
await fs.rm(outFile)
return wait
}
拡張子がtsかどうか判断しtsc
コマンドでコンパイルして生成されたjs
を実行してるようだ
デバッグしたいため、zx awsSts.ts
をnpm scriptsに追加してIntelliJで実行した。
{
"type": "module",
"scripts": {
"checkDirtyWorkingTreeForGit": "node --loader ts-node/esm checkDirtyWorkingTreeForGit.ts",
- "ojichat": "node --loader ts-node/esm ojichatToCowsay.ts"
+ "ojichat": "node --loader ts-node/esm ojichatToCowsay.ts",
+ "awsSts": "zx awsSts.ts"
},
"dependencies": {
"cowsay": "^1.4.0",
- "zx": "^1.2.2"
+ "zx": "^1.10.1"
},
"devDependencies": {
"ts-node": "^9.1.1"
}
}
結論から言うとブレイクポイントを無視して終了してしまった。コンパイルしてから実行している以上しょうがないか...
以前可能だったnode --loader ts-node/esm awsSts.ts
ができるかどうか確認する。
こちらならデバッグ可能だったはず。
❯ node --loader ts-node/esm awsSts.ts
(node:14261) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
ReferenceError: $ is not defined
at file:///Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/awsSts.ts:9:30
at ModuleJob.run (internal/modules/esm/module_job.js:152:23)
at Loader.import (internal/modules/esm/loader.js:166:24)
at Object.loadESM (internal/process/esm_loader.js:68:5)
!?
ReferenceError: $ is not defined
$が定義されてないことになってる。
試しにv1.2.2に戻して実行し直したが、素直に実行できた。
さっきのコミットのindex.d.ts
を見てみると、export const $: $
が削除されていた。つまり型定義的にexportしていない。
無理矢理 node_modules にある index.d.ts でexport const $: $
したら普通に動いた。
なぜ行を削除したのか気になる。
話は変わるけど$
をimportしなかったらどうなるのか試してみた。
❯ zx awsSts.ts
$ tsc --target esnext --module esnext --moduleResolution node /Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/awsSts.ts
awsSts.ts(7,24): error TS1375: 'await' expressions are only allowed at the top level of a file when that file is a module, but this file has no imports or exports. Consider adding an empty 'export {}' to make this file a module.
awsSts.ts(7,30): error TS2581: Cannot find name '$'. Do you need to install type definitions for jQuery? Try `npm i --save-dev @types/jquery`.
Error:
at importPath (file:///Users/korosuke613/.asdf/installs/nodejs/14.16.1/.npm/lib/node_modules/zx/zx.mjs:120:12)
importしないと使えない...?
examples/typescript.tsも実行してみた。
#!/usr/bin/env zx
// Copyright 2021 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.
/// <reference types=".." />
void async function () {
await $`pwd`
}()
やっぱ同じエラーでるな。俺の環境だけ?とにかくtsの場合コンパイルする関係からかimport { $ } from "zx";
は必要そう。
先程のexport const $ = $
をつけた状態でzx awsSts.ts
してみたらエラーになった。
❯ zx awsSts.ts
$ tsc --target esnext --module esnext --moduleResolution node /Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/awsSts.ts
node_modules/zx/index.d.ts(31,18): error TS1254: A 'const' initializer in an ambient context must be a string or numeric literal or literal enum reference.
Error:
at importPath (file:///Users/korosuke613/.asdf/installs/nodejs/14.16.1/.npm/lib/node_modules/zx/zx.mjs:120:12)
だから消したのかー
これなんとかならんかな
ts-ignore
でなんとかなった
// @ts-ignore
export const $ = $
issue書くか
issueとプルリク作ったけどimport "zx"
で解決する内容だったでござる
そして 1.11.0 がたった今リリースされた。
README.mdが更新されてimport "zx"
の件が書かれてる。くぅ〜
色々あってimport {$} from "zx"
には対応しようぜって話になった。
ただ、antonmedv さんは問題を再現できないらしい。うーん