node實(shí)現(xiàn)shell命令管理工具及commander.js學(xué)習(xí)
背景、
github 地址: https://github.com/lulu-up/record-shell
你有沒有經(jīng)歷過忘記某個shell命令怎么拼寫? 或是懶得打一長串命令的經(jīng)歷? 比如我的mac筆記本的tachbar偶爾會'卡死', 這時我就要輸入 killall ControlStrip 命令重啟tachbar, 你也看到了這個命令真心懶得打。
還有新建react項(xiàng)目我每次都要輸入npx create-react-app 項(xiàng)目名 --template typescript, 在公司的日常開發(fā)中我習(xí)慣每次寫新需求都單獨(dú)clone項(xiàng)目并創(chuàng)建新的分支進(jìn)行開發(fā), 此時就需要去gitlab上復(fù)制項(xiàng)目地址然后在本地git clone xxxxxxxxxx 新的項(xiàng)目名, 理論上這些操作真的很重復(fù)。
首先本次要帶你用node一起動手做一款記錄shell命令的小插件, 當(dāng)然網(wǎng)上類似插件也是有的, 但我這次做了一個最簡單粗暴的版本, 自己用著也爽的版本, 并且也想趁機(jī)溫習(xí)一遍命令行相關(guān)知識。
一、用法演示
先一起看看這個'庫'是否真的方便:
1: 安裝
npm install record-shell -g
安裝完畢你的全局會多出 rs命令:

2: 添加
rs add
起名隨意, 甚至全用漢語更舒服, 這里先演示輸入簡單命令:

3: 查看 + 使用'
rs ls

命令是可選擇的, 這里我先多加幾個湊所的命令用來演示:

可以按上下鍵移動選擇, 回車即可執(zhí)行命令:

當(dāng)然也可以查看命令詳情, 只需-a參數(shù):
rs ls -a

4: 移除
rs rm


5: add有變量的命令
我們的命令當(dāng)然不會都是寫'死'的模式啦, 比如命令 echo 內(nèi)容 > a.txt, 這里的意思是我要把內(nèi)容寫入目標(biāo)文件:

6: 使用變量
使用命令時會引導(dǎo)我們填入變量, 所以定義時寫漢語就行:



二、初始化自己的node項(xiàng)目
接下來一起從零開始做出這個庫, 考慮到一些新手同學(xué)可能沒做過這種全局的node包, 我這里就講的詳細(xì)一些。
初始化項(xiàng)目沒啥好說的, 隨便起名:
npm init
改造package.json文件:
"bin": {
"rs": "./bin/www"
},這里在 bin內(nèi)指明, 當(dāng)運(yùn)行 rs 命令的時候, 訪問"./bin/www"。

#! /usr/bin/env node
require('../src/index.js')#!這個符號通常在Unix系統(tǒng)的基本中第一行開頭中出現(xiàn),用于指明這個腳本文件的解釋程序。/usr/bin/env因?yàn)榭赡艽蠹視?code>node安裝到不同的目錄下, 這里直接告訴系統(tǒng)可以在PATH目錄中查找, 這樣就兼容了不同的node安裝路徑。node這個自不必說, 就是去查找咱們的node命令。
三、初始化命令 + 全局安裝
這里講一下如何將我們的命令掛在到全局, 使你可以在任何地方都能使用全局的rs命令:
// cd 我們的項(xiàng)目 npm install . -g
這里比較好理解吧, 相當(dāng)于直接把項(xiàng)目安裝在了全局, 我們平時install xxx -g 是去遠(yuǎn)端拉取, 這個命令是拉當(dāng)前目錄。
此時那你向index.js文件內(nèi)寫入console.log('全局執(zhí)行'), 再全局執(zhí)行 rs 并看到如下效果就是成功了:

四、commander.js (node命令行解決方案)
先安裝再聊:
npm install commander
commander的可以幫我們非常規(guī)范的處理用戶的命令, 比如用戶在命令行輸入rs ls -a, 原生node的情況下我可以先將輸入的args進(jìn)行拆解, 拆解出 ls 與 -a, 然后再寫一堆if判斷如果是ls并且后面有-a則如何去做, 但顯然這樣寫不規(guī)范, 代碼也難以維護(hù), commander就是來幫我們規(guī)范這些寫法的:
將下面的代碼放進(jìn) index.js文件中:
const fs = require("fs");
const path = require("path");
const program = require('commander');
const packagePath = path.join(__dirname, "../package.json")
const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
program.version(packageData.version)
program
.command('ls [-type]')
.description('description')
.action((value) => {
console.log('你輸入的是:', value)
})
program.parse(process.argv)在命令行輸入:
rs ls 123456

逐句解釋一下代碼:
const program = require('commander')這里很明顯引入了commander。program.version(packageData.version)此處是定義了當(dāng)前庫的版本, 當(dāng)你輸入rs -V時會展示program.version方法獲取到的值, 此處直接使用了package.json里面的version字段。program.command('ls')定義了名為ls的參數(shù), 當(dāng)我們輸入rs ls時才會觸發(fā)我們后面的處理方法, 我之所以寫成program.command('ls [-type]')是因?yàn)榧由?code>[-type]后commander才會認(rèn)為ls命令后面可以跟其他參數(shù), 當(dāng)然你叫[xxxxx]也可以, 讓使用者能看懂即可。
.description('description')顧名思義這里是簡介描述, 當(dāng)我們輸入rs -h的時候會出現(xiàn):

.action方法就是commander檢測到當(dāng)前命令觸發(fā)時的處理函數(shù), 第一個參數(shù)是用戶傳入的參數(shù), 第二個參數(shù)是Command對象, 后續(xù)我們會在這里彈出選擇列表。process.argv這里要先知道process是node中的全局變量, 其中argv是啟動命令行時的所有參數(shù)。program.parse(process.argv)看完上面這里就好理解了, 將命令行參數(shù)傳遞給commander開始執(zhí)行。
番外
如果你配置program.option('ls', 'ls的介紹'), 則當(dāng)用戶輸入rs -h時會出現(xiàn), 但我感覺加了有點(diǎn)亂, 咱們的插件追求簡單所以就沒加。
五、inquirer.js(node命令行交互插件)
npm install inquirer
inquirer可以幫我們生成各種命令行問答功能, 就像vue-cli差不多的效果, 大家可以輸入下面代碼試一試'單選模式':
program
.command('ls [-type]')
.description('description')
.action(async (value) => {
const answer = await inquirer.prompt([{
name: "key",
type: "rawlist",
message: "message1",
choices: [
{
name: 'name1',
value: 'value1'
},
{
name: 'name2',
value: 'value2'
}
]
}])
console.log(answer)
})

逐句解釋一下代碼:
- 首先這里是一個
async與awite的模式。 inquirer.prompt參數(shù)是一個數(shù)組, 因?yàn)樗梢赃B續(xù)操作, 比如進(jìn)行兩次單選列表操作。name就是最終的key, 比如name為xxxx用戶選擇了1, 則最終返回結(jié)果就是{xxxx:1}。type指定交互類型rawlist單選列表、input輸入、checkbox多選列表等。message就是提示語, 我們讓用戶選擇之前總要告訴他這里在做啥吧。choices選項(xiàng)的數(shù)組,name選項(xiàng)名,value選項(xiàng)值。
六、添加命令: add
正式開始做第一個命令, 我新建了一個名為env的文件夾, 里面創(chuàng)建record-list.json文件用了存儲用戶的命令:

add命令無非就是往record-list.json文件里面增加內(nèi)容:
program
.command('add')
.description('添加命令')
.action(async () => {
const answer = await inquirer.prompt([{
name: "name",
type: "input",
message: "命令名稱:",
validate: ((name) => {
if (name !== '') return true
})
}, {
name: "command",
type: "input",
message: "命令語句, 可采用[var]的形式傳入變量:",
validate: ((command) => {
if (command !== '') return true
})
}])
let shellList = getShellList();
shellList = shellList.filter((item) => item.name !== answer.name);
shellList.push({
"name": answer.name,
"command": answer.command
})
fs.writeFileSync(dataPath, JSON.stringify(shellList));
})逐句解釋一下代碼:
- 首先我們使用
commander定義了add命令; - 當(dāng)觸發(fā)
add命令時我們使用inquirer定義了兩個輸入框, 第一個輸入命令名稱, 第二個輸入命令語句。 validate定義了對入?yún)⒌男r?yàn), 注意: 用戶不輸入值不是undefined而是空字符串, 所以使用了!== '', 如果校驗(yàn)不通過無法繼續(xù)操作。- 用戶填寫完畢就向
record-list.json添加數(shù)據(jù), 同時如果是重名的命令就進(jìn)行替換。
名稱可能會重復(fù), 但是不要緊, 因?yàn)樗氖褂脠鼍皼Q定了它不需要做過多的限制。
七、移除命令: rm
這里的原理就是拉取record-list.json數(shù)據(jù)進(jìn)行刪減, 然后更新record-list.json:
program
.command('rm')
.description('移除命令')
.action(async () => {
let shellList = getShellList();
const choices = shellList.map((item) => ({
key: item.name,
name: item.name,
value: item.name,
}));
const answer = await inquirer.prompt([{
name: "names",
type: "checkbox",
message: `請'選擇'要刪除的記錄`,
choices,
validate: ((_choices) => {
if (_choices.length) return true
})
}])
shellList = shellList.filter((item) => {
return !answer.names.includes(item.name)
})
fs.writeFileSync(dataPath, JSON.stringify(shellList));
})逐句解釋一下代碼:
choices是定義了一組可選項(xiàng)。- 使用
checkbox多選模式, 讓用戶可以一次刪除多個命令。 validate校驗(yàn)了什么都不刪的情況, 因?yàn)榭赡苁褂脩敉它c(diǎn)擊選取(空格鍵)。- 使用
filter過濾掉名稱相同的命令。 - 最后更新
record-list.json文件。
八、查看+使用: ls
這里內(nèi)容稍微多一點(diǎn), 畢竟一個命令負(fù)責(zé)兩個能力, 這里的核心原理是拉取record-list.json文件的內(nèi)容展示成單選列表, 然后根據(jù)用戶選取的值進(jìn)行命令的執(zhí)行, 最后返回執(zhí)行結(jié)果;
1: 查看ls, 支持傳參 -a
program
.command('ls')
.alias('l')
.description('命令列表')
.option('-a detailed')
.action(async (_, options) => {
const shellList = getShellList();
const choices = shellList.map(item => ({
key: item.name,
name: `${item.name}${options.detailed ? ': ' + item.command : ''}`,
value: item.command
}));
if (choices.length === 0) {
console.log(`
您當(dāng)前沒有錄入命令, 可使用'rs add' 進(jìn)行添加
`)
return
}
const answer = await inquirer.prompt([{
name: "key",
type: "rawlist",
message: "選擇要執(zhí)行的命令",
choices
}])
})逐句解釋一下代碼:
option('-a detailed')定義了可以接收-a參數(shù), 比如ls -a, 并且如果用戶傳了-a則會得到返回值{detailed: true}。- 如果有
-a則將命令本身放在name屬性里展示出來。 choices是轉(zhuǎn)換了record-list.json文件里的數(shù)據(jù)的列表數(shù)據(jù)。- 如果
record-list.json數(shù)據(jù)是空的, 則提示用戶去使用rs add進(jìn)行添加。 - 使用
inquirer生成單選列表。
2: 判斷命令語句中是否有變量
由于允許用戶輸入的命令內(nèi)帶變量, 比如前面演示過的 echo [內(nèi)容] > [文件名], 那我就要判斷當(dāng)前用戶選中的命令內(nèi)是否有變量:
const optionsReg = /\[.*?\]/g;
function getShellOptions(command) {
const arr = command.match(optionsReg) || [];
if (arr.length) {
return arr.map((message) => ({
name: message,
type: "input",
message,
}));
} else {
return []
}
}逐句解釋一下代碼:
optionsReg正則匹配出所有 '[這種寫法]'的變量。- 如果匹配到了變量則返回一個數(shù)組, 這個數(shù)組的長度是變量的個數(shù), 因?yàn)槊總€變量都要有一次輸入的機(jī)會。
- 沒有對重復(fù)的
name進(jìn)行特殊處理, 并且name會變成返回值的key, 所以不可以重名, 重名的話回會導(dǎo)致只處理第一個變量。
3: 無變量 -> 執(zhí)行
這里有一個新的概念:
const child_process = require('child_process'); child_process可以生成node的'子進(jìn)程', child_process.exec方法是啟動了一個系統(tǒng)shell來解析參數(shù),因此可以是非常復(fù)雜的命令,包括管道和重定向。
child_process.exec(command, function (error, stdout) {
console.log(`${stdout}`)
if (error !== null) {
console.log('error: ' + error);
}
});逐句解釋一下代碼:
command是要執(zhí)行的命令。stdout執(zhí)行命令的輸出, 比如ls就是輸出當(dāng)前目錄中的文件信息。error這里也很重要, 如果報錯了要讓用戶知道報錯信息, 所以也console了。
4: 有變量 -> 執(zhí)行
核心原理是解析'變量'后對命令語句進(jìn)行替換, 然后正常執(zhí)行就ok:
function answerOptions2Command(command, answerMap) {
for (let key in answerMap) {
command = command.replace(`[${key}]`, answerMap[key])
}
return command;
}
function handleExec(command) {
child_process.exec(command, function (error, stdout) {
console.log(`${stdout}`)
if (error !== null) {
console.log('error: ' + error);
}
});
}
if (shellOptions.length) {
const answerMap = await inquirer.prompt(shellOptions)
const command = answerOptions2Command(answer.key, answerMap)
handleExec(command)
} else {
handleExec(answer.key)
}逐句解釋一下代碼:
inquirer執(zhí)行完會返回一個字典, 比如{[文本]:"xxxxx", [文件名]:"a.txt"}, 因?yàn)槲覀冊O(shè)置了name與message使用同樣的名稱。answerOptions2Command循環(huán)執(zhí)行replace進(jìn)行變量的替換。handleExec負(fù)責(zé)執(zhí)行語句。
九、讓文字變色 (chalk)
功能都完成了, 但是我們的提示文字還是'黑白的', 我們當(dāng)然希望命令行中多姿多彩一些, 在node中使用:
var red = "\033[31m red \033[0m";
console.log('你好紅色:', red)
\033是c語言中的轉(zhuǎn)義字符這里就不擴(kuò)了, 反正看到他就是要對屏幕進(jìn)行操作了, 但是我們可以看出上面的寫法很不友好, 肯定要封裝一下下, chalk.js就是個不錯的已有輪子, 我們下進(jìn)行安裝:
npm install chalk
使用:
const chalk = require('chalk')
chalk.red('你好: 紅色')你高興太早了, 現(xiàn)在是有問題的 !!

其他教程里都沒說怎么解決, 其實(shí)那你只要把chalk的版本降低到4就ok了!
以上就是node實(shí)現(xiàn)shell命令管理工具及commander.js學(xué)習(xí)的詳細(xì)內(nèi)容,更多關(guān)于node shell命令管理的資料請關(guān)注腳本之家其它相關(guān)文章!

