hell腳本是一個命令語言,面向的是操作系統執行。如果寫過shell腳本的話,應該體會過編寫過程的痛苦。因為shell并不是一個編程語言,并不支持常見的數組,JSON等數據結構,也不支持面向對象編程的開發方法,因此對開發人員很不友好。
目前針對這種情況,大家一般會用shell調用node執行JS腳本,真正的處理邏輯放在JS腳本中處理。現在谷歌推出了 ZX NPM包,它能夠用JS編寫shell腳本。
那如何使用呢?
npm install -g zx
安裝完后,在終端中輸入 zx 命令檢查安裝是否成功。
新建zx腳本文件:test.mjs
#!/usr/bin/env zx
const branch=await $`git branch --show-current`
console.log(`Current branch: ${branch}`)
第一行是指定腳本的執行器。
$ 是內置的函數,能夠執行命令并配合 await 返回執行結果。其他的寫法都和JS毫無差別。
zx ./test.mjs
或者:
chmod +x ./test.mjs
./test.mjs
控制臺就會輸出當前的分支。
上面只是小試牛刀,zx 的強大遠不止如此。由于 zx 在內部實現了 Bash 的解釋器,所以可以執行全部的shell命令。另外 zx 還內置很多nodejs模塊,比如 fs, os,fetch等。所以可以直接在腳本中使用這些模塊。
另外作為TS編寫的庫,全部的JS語法都能夠支持。包括但不限于 數組,Promise,class等。
下面再舉一個例子:
let resp=await fetch('http://wttr.in')
if (resp.ok) {
console.log(await resp.text())
}
let hosts=[...]
await Promise.all(hosts.map(host=>
$`rsync -azP ./src ${host}:/var/www`
))
try {
await $`exit 1`
} catch (p) {
console.log(`Exit code: ${p.exitCode}`)
console.log(`Error: ${p.stderr}`)
}
總結一下,zx 的最大優點是結合了Bash和JavaScript,解決了shell腳本復雜邏輯編程的問題。同時也讓對shell不熟悉的開發者也能用JS完成shell腳本的開發,而且更加靈活高效。
如果你還有更多問題,可以參考NPM倉庫 zx 包的介紹,或者訪問其github地址。
歡迎幫忙點贊,評論,轉發~
過很多 bash 腳本的人都知道,bash 的坑不是一般的多。 其實 bash 本身并不是一個很嚴謹的語言,但是很多時候也不得不用。以下總結了一些鵝廠程序員在編寫可靠 bash 腳本的一些小 tips。
在寫腳本時,在一開始(Shebang 之后)就加上這一句,或者它的縮略版:
set -xeuo pipefail
這能避免很多問題,更重要的是能讓很多隱藏的問題暴露出來。
下面說明每個參數的作用,以及一些例外的處理方式 :
-x : 在執行每一個命令之前把經過變量展開之后的命令打印出來。
這個對于 debug 腳本、輸出 Log 時非常有用。 正式運行的腳本也可以不加。
-e : 遇到一個命令失敗(返回碼非零)時,立即退出。
bash 跟其它的腳本語言最大的不同點之一,應該就是遇到異常時繼續運行下一條命令。 這在很多時候會遇到意想不到的問題。加上 -e ,會讓 bash 在遇到一個命令失敗時,立即退出。
如果有時確實需要忽略個別命令的返回碼,可以用 || true 。如:
some_cmd || true # 即使some_cmd失敗了,仍然會繼續運行
some_cmd || RET=$? # 或者可以這樣來收集some_cmd的返回碼,供后面的邏輯判斷使用
但是在管道串起多條命令的情況下,只有最后一條命令失敗時才會退出。如果想讓管道中任意一條命令失敗就退出,就要用后面提到的-o pipefail 了。
加-e 有時候可能會不太方便,動不動就退出。但覺得還是應該堅持所謂的fail-fast 原則,也就是有異常時停止正常運行,而不是繼續嘗試運行可能存在缺陷的過程。如果有命令可以明確忽略異常,那可以用上面提到的 || true 等方式明確地忽略之。
-u :試圖使用未定義的變量,就立即退出。
如果在 bash 里使用一個未定義的變量,默認是會展開成一個空串。有時這種行為會導致問題,比如:
rm -rf $MYDIR/data
如果 MYDIR 變量因為某種原因沒有賦值,這條命令就會變成 rm -rf /data 。 這就比較搞笑了。。 使用 -u 可以避免這種情況。
但有時候在已經設置了-u 后,某些地方還是希望能把未定義變量展開為空串,可以這樣寫:
${SOME_VAR:-}
# bash變量展開語法,可以參考:
https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
-o pipefail : 只要管道中的一個子命令失敗,整個管道命令就失敗。
pipefail 與-e 結合使用的話,就可以做到管道中的一個子命令失敗,就退出腳本。
在一些場景中,我們通常不希望一個腳本有多個實例在同時運行。比如用 crontab 周期性運行腳本時,有時不希望上一個輪次還沒運行完,下一個輪次就開始運行了。 這時可以用 flock 命令來解決。 flock 通過文件鎖的方式來保證獨占運行,并且還有一個好處是進程退出時,文件鎖也會自動釋放,不需要額外處理。
用法 1: 假設你的入口腳本是 myscript.sh,可以新建一個腳本,通過 flock 來運行它:
# flock --wait 超時時間 -e 鎖文件 -c "要執行的命令"
# 例如:
flock --wait 5 -e "lock_myscript" -c "bash myscript.sh"
用法 2: 也可以在原有腳本里使用 flock。 可以把文件打開為一個文件描述符,然后使用 flock 對它上鎖(flock 可以接受文件描述符參數)。
exec 123<>lock_myscript # 把lock_myscript打開為文件描述符123
flock --wait 5 123 || { echo 'cannot get lock, exit'; exit 1; }
我們的腳本通常會啟動好多子腳本和子進程,當父腳本意外退出時,子進程其實并不會退出,而是繼續運行著。 如果腳本是周期性運行的,有可能發生一些意想不到的問題。
在 stackoverflow 上找到的一個方法,原理就是利用 trap 命令在腳本退出時 kill 掉它整個進程組。 把下面的代碼加在腳本開頭區,實測管用:
trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
不過如果父進程是用 SIGKILL (kill -9) 殺掉的,就不行了。因為 SIGKILL 時,進程是沒有機會運行任何代碼的。
有時候需要對命令設置一個超時時間。這時可以使用 timeout 命令,用法很簡單:
timeout 600s some_command arg1 arg2
命令在超時時間內運行結束時,返回碼為 0,否則會返回一個非零返回碼。
timeout 在超時時默認會發送 TERM 信號,也可以用 -s 參數讓它發送其它信號。
有時候我們會用到把好多條命令用管道串在一起的情況。如 cmd1 | cmd2 | cmd3 | ...這樣會讓問題變得難以排查,因為中間數據我們都看不到。
如果改成這樣的格式:
cmd1 > out1.dat
cat out1 | cmd2 > out2.dat
cat out2 | cmd3 > out3.dat
性能又不太好,因為這樣 cmd1, cmd2, cmd3 是串行運行的,這時可以用 tee 命令:
cmd1 | tee out1.dat | cmd2 | tee out2.dat | cmd3 > out3.dat
言
在這篇文章中,我們將學習谷歌的 zx 庫提供了什么,以及我們如何使用它來用 Node.js 編寫 shell 腳本。然后,我們將學習如何通過構建一個命令行工具來使用 zx 的功能,幫助我們為新的 Node.js 項目引導配置。
編寫 Shell 腳本的問題
創建一個由 Bash 或者 zsh 執行的 shell 腳本,是自動化重復任務的好方法。Node.js 似乎是編寫 shell 腳本的理想選擇,因為它為我們提供了許多核心模塊,并允許我們導入任何我們選擇的庫。它還允許我們訪問 JavaScript 提供的語言特性和內置函數。
如果你嘗試編寫運行在 Node.js 中的 shell 腳本,你會發現這沒有你想象中的那么順利。你需要為子進程編寫特殊的處理程序,注意轉義命令行參數,然后最終與 stdout(標準輸出)和 stderr(標準錯誤)打交道。這不是特別直觀,而且會使 shell 腳本變得相當笨拙。
Bash shell 腳本語言是編寫 shell 腳本的普遍選擇。不需要編寫代碼來處理子進程,而且它有內置的語言特性來處理 stdout 和 stderr。但是用 Bash 編寫 shell 腳本也不是那么容易。語法可能相當混亂,使得它實現邏輯,或者處理諸如提示用戶輸入的事情非常困難。
谷歌的 zx 庫有助于讓使用 Node.js 編寫的 shell 腳本變得高效和舒適。
前置條件
往下閱讀之前,有幾個前置條件需要遵循:
本文中的所有代碼都可以從 GitHub https://link.segmentfault.com/?enc=ysCUhsc%2BhqUmtqCo55t8jw%3D%3D.aWhjUaPje6eTlkcFFdhW%2FeIVYyAz5G%2FoPbGuXjsxlpcJphMKguwz3NoHWQ9o2vDb47Nfnm9kpIP6Ol5r6Euc8A%3D%3D 上獲得。
zx 如何運作
Google 的 zx 提供了創建子進程的函數,以及處理這些進程的 stdout 和 stderr 的函數。我們將使用的主要函數是$函數。下面是它的一個實際例子:
import { $ } from "zx";
await $`ls`;
下面是執行上述代碼的輸出:
$ ls
bootstrap-tool
hello-world
node_modules
package.json
README.md
typescript
上面的例子中的 JavaScript 語法可能看起來有點古怪。它使用了一種叫做帶標簽的模板字符串 https://link.segmentfault.com/?enc=VUkodq5er%2Fynbhfl3MUQZA%3D%3D.%2Flx6oaDCVK4XuYyYLqvDi2QMWjCwW1jKBvNQgfaGG0AVwpl7I2CYD4sJYHuonDSA6jj1qSSypc0aGVO%2BYuBMUiibG6pBkVwusg%2Bai1hbMXetqlwMTWVUEAbtabMCbXIs 的語言特性。它在功能上與編寫 await $("ls")相同。
谷歌的 zx 提供了其他幾個實用功能,使編寫 shell 腳本更容易。比如:
除了 zx 提供的實用功能外,它還為我們提供了幾個流行的庫,比如:
chalk。https://link.segmentfault.com/?enc=%2FL15Y8OQNrp05Scx6N4iaQ%3D%3D.r8AZkfcE1Ye%2BlNUznFA9RNJMhfyM0lttTiD0TmqjJQwoi7zjQjs5YiLjI%2FWDEooY 這個庫允許我們為腳本的輸出添加顏色。
minimist。https://link.segmentfault.com/?enc=5tMQ5d6qJ4STEHZeuiN0MA%3D%3D.x9GPOpXVZp5TGKknZCkwA10QP%2Ftw%2BW7fwnpYGg%2BnlJPJLK3RboT3jqy5WuPLFwPZ 一個解析命令行參數的庫。然后它們在 argv 對象下被暴露出來。
fetch。https://link.segmentfault.com/?enc=71LC44tCu%2FZoLOY2B7MfuA%3D%3D.GO%2FMi37T0KTmj8UXau9xdFUBiIk1I%2F8M%2FHk2hZYrhRb%2Fd3Vl0bzlr05hanNxzuVb Fetch API 的 Node.js 實現。我們可以用它來進行 HTTP 請求。
fs-extra。https://link.segmentfault.com/?enc=cPZzXniTRFJdbq87FdwLWw%3D%3D.IsmhSUEjIcZAqw8FDidbUnNhhIVPT1gsFSo%2BchiXCAs9AaxUVT%2FEgbIvswUDVmK2 一個暴露 Node.js 核心 fs 模塊的庫,以及一些額外的方法,使其更容易與文件系統一起工作。
現在我們知道了 zx 給了我們什么,讓我們用它創建第一個 shell 腳本。
zx 如何使用
首先,我們先創建一個新項目:
mkdir zx-shell-scripts
cd zx-shell-scripts
npm init --yes
然后安裝 zx 庫:
npm install --save-dev zx
注意:zx 的文檔建議用 npm 全局安裝該庫。通過將其安裝為我們項目的本地依賴,我們可以確保 zx 總是被安裝,并控制 shell 腳本使用的版本。
頂級 await
為了在 Node.js 中使用頂級 await,也就是 await 位于 async 函數的外部,我們需要在 ES 模塊的模式下編寫代碼,該模式支持頂級 await。
我們可以通過在 package.json 中添加 "type": "module" 怎么怎么來表明項目中的所有模塊都是 ES 模塊。或者我們可以將單個腳本的文件擴展名設置為 .mjs 。在本文的例子中,我們將使用 .mjs 文件擴展名。
運行命令并捕獲輸出
創建一個新腳本,將其命名為 hello-world.mjs 。我們將添加一個 Shebang 行,它告訴操作系統(OS)的內核要用 node 程序運行該腳本:
#! /usr/bin/env node
然后,我們添加一些代碼,使用 zx 來運行命令。
在下面的代碼中,我們運行命令執行 ls 程序。ls 程序將列出當前工作目錄(腳本所在的目錄)中的文件。我們將從命令的進程中捕獲標準輸出,將其存儲在一個變量中,然后打印到終端:
// hello-world.mjs
import { $ } from "zx";
const output = (await $`ls`).stdout;
console.log(output);
注意:zx 文檔建議把/usr/bin/env zx 放在我們腳本的 shebang 行中,但我們用/usr/bin/env node 代替。這是因為我們已經安裝 zx,并作為項目的本地依賴。然后我們明確地從 zx 包中導入我們想要使用的函數和對象。這有助于明確我們腳本中使用的依賴來自哪里。
我們使用 chmod 來讓腳本可執行:
chmod u+x hello-world.mjs
運行項目:
./hello-world.mjs
可以看到如下輸出:
$ ls
hello-world.mjs
node_modules
package.json
package-lock.json
README.md
hello-world.mjs
node_modules
package.json
package-lock.json
README.md
你會注意到:
zx 默認以 verbose 模式運行。它將輸出你傳遞給$函數的命令,同時也輸出該命令的標準輸出。我們可以通過在運行 ls 命令前加入以下一行代碼來改變這種行為:
$.verbose = false;
大多數命令行程序,如 ls,會在其輸出的結尾處輸出一個新行字符,以使輸出在終端中更易讀。這對可讀性有好處,但由于我們要將輸出存儲在一個變量中,我們不希望有這個額外的新行。我們可以用 JavaScript String#trim()函數把它去掉:
- const output = (await $`ls`).stdout;
+ const output = (await $`ls`).stdout.trim();
再次運行腳本,結果看起來好很多:
hello-world.mjs
node_modules
package.json
package-lock.json
引入 TypeScript
如果我們想在 TypeScript 中編寫使用 zx 的 shell 腳本,有幾個微小的區別我們需要加以說明。
注意:TypeScript 編譯器提供了大量的配置選項,允許我們調整它如何編譯我們的 TypeScript 代碼。考慮到這一點,下面的 TypeScript 配置和代碼是為了在大多數 TypeScript 版本下工作。
首先,安裝需要運行 TypeScript 代碼的依賴:
npm install --save-dev typescript ts-node
ts-node 包提供了一個 TypeScript 執行引擎,讓我們能夠轉譯和運行 TypeScript 代碼。
需要創建 tsconfig.json 文件包含下面的配置:
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs"
}
}
創建新的腳本,并命名為 hello-world-typescript.ts。首先,添加 Shebang 行,告訴 OS 內核使用 ts-node 程序來運行我們的腳本:
#! ./node_modules/.bin/ts-node
為了在我們的 TypeScript 代碼中使用 await 關鍵字,我們需要把它包裝在一個立即調用函數表達式(IIFE)中,正如 zx 文檔所建議的那樣:
// hello-world-typescript.ts
import { $ } from "zx";
void (async function () {
await $`ls`;
})();
然后需要讓腳本可執行:
chmod u+x hello-world-typescript.ts
運行腳本:
./hello-world-typescript.ts
可以看到下面的輸出:
$ ls
hello-world-typescript.ts
node_modules
package.json
package-lock.json
README.md
tsconfig.json
在 TypeScript 中用 zx 編寫腳本與使用 JavaScript 相似,但需要對我們的代碼進行一些額外的配置和包裝。
構建項目啟動工具
現在我們已經學會了用谷歌的 zx 編寫 shell 腳本的基本知識,我們要用它來構建一個工具。這個工具將自動創建一個通常很耗時的過程:為一個新的 Node.js 項目的配置提供引導。
我們將創建一個交互式 shell 腳本,提示用戶輸入。它還將使用 zx 內置的 chalk 庫,以不同的顏色高亮輸出,并提供一個友好的用戶體驗。我們的 shell 腳本還將安裝新項目所需的 npm 包,所以它已經準備好讓我們立即開始開發。
準備開始
首先創建一個名為 bootstrap-tool.mjs 的新文件,并添加 shebang 行。我們還將從 zx 包中導入我們要使用的函數和模塊,以及 Node.js 核心 path 模塊:
#! /usr/bin/env node
// bootstrap-tool.mjs
import { $, argv, cd, chalk, fs, question } from "zx";
import path from "path";
與我們之前創建的腳本一樣,我們要使我們的新腳本可執行:
chmod u+x bootstrap-tool.mjs
我們還將定義一個輔助函數,用紅色文本輸出一個錯誤信息,并以錯誤退出代碼 1 退出 Node.js 進程:
function exitWithError(errorMessage) {
console.error(chalk.red(errorMessage));
process.exit(1);
}
當我們需要處理一個錯誤時,我們將通過我們的 shell 腳本在各個地方使用這個輔助函數。
檢查依賴
我們要創建的工具需要使用三個不同程序來運行命令:git、node 和 npx。我們可以使用 which 庫來幫助我們檢查這些程序是否已經安裝并可以使用。
首先,我們需要安裝 which:
npm install --save-dev which
然后引入它:
import which from "which";
然后創建一個使用它的 checkRequiredProgramsExist 函數:
async function checkRequiredProgramsExist(programs) {
try {
for (let program of programs) {
await which(program);
}
} catch (error) {
exitWithError(`Error: Required command ${error.message}`);
}
}
上面的函數接受一個程序名稱的數組。它循環遍歷數組,對每個程序調用 which 函數。如果 which 找到了程序的路徑,它將返回該程序。否則,如果該程序找不到,它將拋出一個錯誤。如果有任何程序找不到,我們就調用 exitWithError 輔助函數來顯示一個錯誤信息并停止運行腳本。
我們現在可以添加一個對 checkRequiredProgramsExist 的調用,以檢查我們的工具所依賴的程序是否可用:
await checkRequiredProgramsExist(["git", "node", "npx"]);
添加目標目錄選項
由于我們正在構建的工具將幫助我們啟動新的 Node.js 項目,因此我們希望在項目的目錄中運行我們添加的任何命令。我們現在要給腳本添加一個 --directory 命令行參數。
zx 內置了 minimist 包,它能夠解析傳遞給腳本的任何命令行參數。這些被解析的命令行參數被 zx 包作為 argv 提供:
讓我們為名為 directory 的命令行參數添加一個檢查:
let targetDirectory = argv.directory;
if (!targetDirectory) {
exitWithError("Error: You must specify the --directory argument");
}
如果 directory 參數被傳遞給了我們的腳本,我們要檢查它是否是已經存在的目錄的路徑。我們將使用 fs-extra 提供的 fs.pathExists 方法:
targetDirectory = path.resolve(targetDirectory);
if (!(await fs.pathExists(targetDirectory))) {
exitWithError(`Error: Target directory '${targetDirectory}' does not exist`);
}
如果目標路徑存在,我們將使用 zx 提供的 cd 函數來切換當前的工作目錄:?
cd(targetDirectory);
如果我們現在在沒有--directory 參數的情況下運行腳本,我們應該會收到一個錯誤:
$ ./bootstrap-tool.mjs
Error: You must specify the --directory argument
檢查全局 Git 設置
稍后,我們將在項目目錄下初始化一個新的 Git 倉庫,但首先我們要檢查 Git 是否有它需要的配置。我們要確保提交會被 GitHub 等代碼托管服務正確歸類。
為了做到這一點,這里創建一個 getGlobalGitSettingValue 函數。它將運行 git config 命令來檢索 Git 配置設置的值:
async function getGlobalGitSettingValue(settingName) {
$.verbose = false;
let settingValue = "";
try {
settingValue = (
await $`git config --global --get ${settingName}`
).stdout.trim();
} catch (error) {
// Ignore process output
}
$.verbose = true;
return settingValue;
}
你會注意到,我們正在關閉 zx 默認設置的 verbose 模式。這意味著,當我們運行 git config 命令時,該命令和它發送到標準輸出的任何內容都不會被顯示。我們在函數的結尾處將 verbose 模式重新打開,這樣我們就不會影響到我們稍后在腳本中添加的任何其他命令。
現在我們添加 checkGlobalGitSettings 函數,該函數接收 Git 設置名稱組成的數組。它將循環遍歷每個設置名稱,并將其傳遞給 getGlobalGitSettingValue 函數以檢索其值。如果設置沒有值,將顯示警告信息:
async function checkGlobalGitSettings(settingsToCheck) {
for (let settingName of settingsToCheck) {
const settingValue = await getGlobalGitSettingValue(settingName);
if (!settingValue) {
console.warn(
chalk.yellow(`Warning: Global git setting '${settingName}' is not set.`)
);
}
}
}
讓我們給 checkGlobalGitSettings 添加一個調用,檢查 user.name 和 user.email 的 Git 設置是否已經被設置:
await checkGlobalGitSettings(["user.name", "user.email"]);
初始化 Git 倉庫
我們可以通過添加以下命令在項目目錄下初始化一個新的 Git 倉庫:
await $`git init`;
生成 package.json
每個 Node.js 項目都需要 package.json 文件。這是我們為項目定義元數據的地方,指定項目所依賴的包,以及添加實用的腳本。
在我們為項目生成 package.json 文件之前,我們要創建幾個輔助函數。第一個是 readPackageJson 函數,它將從項目目錄中讀取 package.json 文件:
async function readPackageJson(directory) {
const packageJsonFilepath = `${directory}/package.json`;
return await fs.readJSON(packageJsonFilepath);
}
然后我們將創建一個 writePackageJson 函數,我們可以用它來向項目的 package.json 文件寫入更改:
async function writePackageJson(directory, contents) {
const packageJsonFilepath = `${directory}/package.json`;
await fs.writeJSON(packageJsonFilepath, contents, { spaces: 2 });
}
我們在上面的函數中使用的 fs.readJSON 和 fs.writeJSON 方法是由 fs-extra 庫提供的。
在定義了 package.json 輔助函數后,我們可以開始考慮 package.json 文件的內容。
Node.js 支持兩種模塊類型:
Node.js 生態系統正在逐步采用 ES 模塊,這在客戶端 JavaScript 中是很常見的。當事情處于過渡階段時,我們需要決定我們的 Node.js 項目默認使用 CJS 模塊還是 ESM 模塊。讓我們創建一個 promptForModuleSystem 函數,詢問這個新項目應該使用哪種模塊類型:
async function promptForModuleSystem(moduleSystems) {
const moduleSystem = await question(
`Which Node.js module system do you want to use? (${moduleSystems.join(
" or "
)}) `,
{
choices: moduleSystems,
}
);
return moduleSystem;
}
上面函數使用的 question 函數由 zx 提供。
現在我們將創建一個 getNodeModuleSystem 函數,以調用 promptForModuleSystem 函數。它將檢查所輸入的值是否有效。如果不是,它將再次詢問:
async function getNodeModuleSystem() {
const moduleSystems = ["module", "commonjs"];
const selectedModuleSystem = await promptForModuleSystem(moduleSystems);
const isValidModuleSystem = moduleSystems.includes(selectedModuleSystem);
if (!isValidModuleSystem) {
console.error(
chalk.red(
`Error: Module system must be either '${moduleSystems.join(
"' or '"
)}'\n`
)
);
return await getNodeModuleSystem();
}
return selectedModuleSystem;
}
現在我們可以通過運行 npm init 命令生成我們項目的 package.json 文件:
await $`npm init --yes`;
然后我們將使用 readPackageJson 輔助函數來讀取新創建的 package.json 文件。我們將詢問項目應該使用哪個模塊系統,并將其設置為 packageJson 對象中的 type 屬性值,然后將其寫回到項目的 package.json 文件中:
const packageJson = await readPackageJson(targetDirectory);
const selectedModuleSystem = await getNodeModuleSystem();
packageJson.type = selectedModuleSystem;
await writePackageJson(targetDirectory, packageJson);
提示:當你用--yes 標志運行 npm init 時,要想在 package.json 中獲得合理的默認值,請確保你設置了 npminit-*的配置設置 https://link.segmentfault.com/?enc=X0aVKO8sVtj0bgZhFmgjVw%3D%3D.hxYbJxX5X2odlcPpk5MdIVLkT7ESAsFMhFnFSRBftI2c2BjNTUCtV1gwdXnpxWGI 。
安裝所需項目依賴
為了使運行我們的啟動工具后能夠輕松地開始項目開發,我們將創建一個 promptForPackages 函數,詢問要安裝哪些 npm 包:
async function promptForPackages() {
let packagesToInstall = await question(
"Which npm packages do you want to install for this project? "
);
packagesToInstall = packagesToInstall
.trim()
.split(" ")
.filter((pkg) => pkg);
return packagesToInstall;
}
為了防止我們在輸入包名時出現錯別字,我們將創建一個 identifyInvalidNpmPackages 函數。這個函數將接受一個 npm 包名數組,然后運行 npm view 命令來檢查它們是否存在:
async function identifyInvalidNpmPackages(packages) {
$.verbose = false;
let invalidPackages = [];
for (const pkg of packages) {
try {
await $`npm view ${pkg}`;
} catch (error) {
invalidPackages.push(pkg);
}
}
$.verbose = true;
return invalidPackages;
}
讓我們創建一個 getPackagesToInstall 函數,使用我們剛剛創建的兩個函數:
async function getPackagesToInstall() {
const packagesToInstall = await promptForPackages();
const invalidPackages = await identifyInvalidNpmPackages(packagesToInstall);
const allPackagesExist = invalidPackages.length === 0;
if (!allPackagesExist) {
console.error(
chalk.red(
`Error: The following packages do not exist on npm: ${invalidPackages.join(
", "
)}\n`
)
);
return await getPackagesToInstall();
}
return packagesToInstall;
}
如果有軟件包名稱不正確,上面的函數將顯示一個錯誤,然后再次詢問要安裝的軟件包。
一旦我們得到需要安裝的有效包列表,就可以使用 npm install 命令來安裝它們:
const packagesToInstall = await getPackagesToInstall();
const havePackagesToInstall = packagesToInstall.length > 0;
if (havePackagesToInstall) {
await $`npm install ${packagesToInstall}`;
}
為工具生成配置
創建項目配置是我們用項目啟動工具自動完成的最佳事項。首先,讓我們添加一個命令來生成一個.gitignore 文件,這樣我們就不會意外地提交我們不希望在 Git 倉庫中出現的文件:
await $`npx gitignore node`;
上面的命令使用 gitignore https://link.segmentfault.com/?enc=V%2FIoxipE2WxmNECDRNKMqg%3D%3D.7Y4d34n%2BoUVlJkYEiJt3NLa9RHn2pYtKq%2BHpO67HIQjfjKAHIxcMynZzTudktHaV 包,從 GitHub 的 gitignore https://link.segmentfault.com/?enc=Szh56TKqT6gYmrJidM7ZXg%3D%3D.BK7Vf5F73GaplJKeN48OsobM6U4EPriatZOr3dBhsSVZIuIUiktyqfS1jhLLtQ7o 模板中拉取 Node.js 的.gitignore 文件。
為了生成我們的 EditorConfig https://link.segmentfault.com/?enc=dGD5%2BEyCTqLfqDSif7%2FHgQ%3D%3D.xgNWBjR6m%2FIUFlG1UmM3CYX2R%2BLXFN531lZhlvCRRWs%3D、Prettier https://link.segmentfault.com/?enc=n7clHFqLMZZala0zu1JjdQ%3D%3D.jivAg4cL8HkSAhuurMUHrlfhGGILOpveTQR0rjCRXyg%3D 和 ESLint https://link.segmentfault.com/?enc=J4jXEAmmBQ83%2FWSrOtTsAQ%3D%3D.b%2FKC0kT%2FZWZQ9KBItXDMQHWJBzc5aW9hUZsxWHBsMY4%3D 配置文件,我們將使用一個叫做 Mrm 的命令行工具。
全局安裝我們需要的 mrm 依賴項:
npm install --global mrm mrm-task-editorconfig mrm-task-prettier mrm-task-eslint
然后添加 mrm 命令行生成配置文件:
await $`npx mrm editorconfig`;
await $`npx mrm prettier`;
await $`npx mrm eslint`;
Mrm 負責生成配置文件,以及安裝所需的 npm 包。它還提供了大量的配置選項,允許我們調整生成的配置文件以符合我們的個人偏好。
生成 README
我們可以使用我們的 readPackageJson 輔助函數,從項目的 package.json 文件中讀取項目名稱。然后我們可以生成一個基本的 Markdown 格式的 README,并將其寫入 README.md 文件中:
const { name: projectName } = await readPackageJson(targetDirectory);
const readmeContents = `# ${projectName}
...
`;
await fs.writeFile(`${targetDirectory}/README.md`, readmeContents);
在上面的函數中,我們正在使用 fs-extra 暴露的 fs.writeFile 的 promise 變量。
提交項目骨架
最后,是時候提交我們用 git 創建的項目骨架了:
await $`git add .`;
await $`git commit -m "Add project skeleton"`;
然后我們將顯示一條消息,確認我們的新項目已經成功啟動:
console.log(
chalk.green(
`\n?? The project ${projectName} has been successfully bootstrapped!\n`
)
);
console.log(chalk.green(`Add a git remote and push your changes.`));
啟動新項目
mkdir new-project
./bootstrap-tool.mjs --directory new-project
并觀看我們所做的一切。
總結
在這篇文章中,我們已經學會了如何在 Node.js 中借助 Google 的 zx 庫來創建強大的 shell 腳本。我們使用了它提供的實用功能和庫來創建一個靈活的命令行工具。
到目前為止,我們所構建的工具只是一個開始。這里有一些功能點子,你可能想嘗試自己添加:
自動創建目標目錄。如果目標目錄還不存在,則提示用戶并詢問他們是否想要為他們創建該目錄。
開源衛生。問問用戶他們是否在創建一個將是開源的項目。如果是的話,運行命令來生成許可證和貢獻者文件。
自動創建 GitHub 上的倉庫。添加使用 GitHub CLI 的命令,在 GitHub 上創建一個遠程倉庫。一旦用 Git 提交了初始骨架,新項目就可以被推送到這個倉庫。
本文中的所有代碼都可以在 GitHub 上找到。
本文譯自:https://link.segmentfault.com/?enc=5Z%2ByLQNgua%2FY5gnguxXllg%3D%3D.SeNINYETng%2BH6uZupDjhsYNRqM5GWB5jXVDlvMdMqsO9u3CmWfwe8DwT48iAvK%2Fsm8pflQ8jbxHc1sB281%2B6pA%3D%3D
作者:Simon Plenderleith
以上就是本文的所有內容。如果對你有所幫助,歡迎點贊、收藏、轉發~
*請認真填寫需求信息,我們會在24小時內與您取得聯系。