node實(shí)現(xiàn)shell命令管理工具及commander.js學(xué)習(xí)
背景、
github 地址: https://github.com/lulu-up/record-shell
你有沒有經(jīng)歷過忘記某個(gè)shell
命令怎么拼寫? 或是懶得打一長(zhǎng)串命令的經(jīng)歷? 比如我的mac
筆記本的tachbar
偶爾會(huì)'卡死', 這時(shí)我就要輸入 killall ControlStrip
命令重啟tachbar
, 你也看到了這個(gè)命令真心懶得打。
還有新建react
項(xiàng)目我每次都要輸入npx create-react-app 項(xiàng)目名 --template typescript
, 在公司的日常開發(fā)中我習(xí)慣每次寫新需求都單獨(dú)clone
項(xiàng)目并創(chuàng)建新的分支進(jìn)行開發(fā), 此時(shí)就需要去gitlab
上復(fù)制項(xiàng)目地址然后在本地git clone xxxxxxxxxx 新的項(xiàng)目名
, 理論上這些操作真的很重復(fù)。
首先本次要帶你用node
一起動(dòng)手做一款記錄shell
命令的小插件, 當(dāng)然網(wǎng)上類似插件也是有的, 但我這次做了一個(gè)最簡(jiǎn)單粗暴的版本, 自己用著也爽的版本, 并且也想趁機(jī)溫習(xí)一遍命令行相關(guān)知識(shí)。
一、用法演示
先一起看看這個(gè)'庫'是否真的方便:
1: 安裝
npm install record-shell -g
安裝完畢你的全局會(huì)多出 rs
命令:
2: 添加
rs add
起名隨意, 甚至全用漢語更舒服, 這里先演示輸入簡(jiǎn)單命令:
3: 查看 + 使用'
rs ls
命令是可選擇的, 這里我先多加幾個(gè)湊所的命令用來演示:
可以按上下鍵移動(dòng)選擇, 回車即可執(zhí)行命令:
當(dāng)然也可以查看命令詳情, 只需-a
參數(shù):
rs ls -a
4: 移除
rs rm
5: add有變量的命令
我們的命令當(dāng)然不會(huì)都是寫'死'的模式啦, 比如命令 echo 內(nèi)容 > a.txt
, 這里的意思是我要把內(nèi)容寫入目標(biāo)文件:
6: 使用變量
使用命令時(shí)會(huì)引導(dǎo)我們填入變量, 所以定義時(shí)寫漢語就行:
二、初始化自己的node項(xiàng)目
接下來一起從零開始做出這個(gè)庫, 考慮到一些新手同學(xué)可能沒做過這種全局的node
包, 我這里就講的詳細(xì)一些。
初始化項(xiàng)目沒啥好說的, 隨便起名:
npm init
改造package.json
文件:
"bin": { "rs": "./bin/www" },
這里在 bin
內(nèi)指明, 當(dāng)運(yùn)行 rs
命令的時(shí)候, 訪問"./bin/www"
。
#! /usr/bin/env node require('../src/index.js')
#!
這個(gè)符號(hào)通常在Unix系統(tǒng)的基本中第一行開頭中出現(xiàn),用于指明這個(gè)腳本文件的解釋程序。/usr/bin/env
因?yàn)榭赡艽蠹視?huì)把node
安裝到不同的目錄下, 這里直接告訴系統(tǒng)可以在PATH目錄中查找, 這樣就兼容了不同的node
安裝路徑。node
這個(gè)自不必說, 就是去查找咱們的node
命令。
三、初始化命令 + 全局安裝
這里講一下如何將我們的命令掛在到全局, 使你可以在任何地方都能使用全局的rs
命令:
// cd 我們的項(xiàng)目 npm install . -g
這里比較好理解吧, 相當(dāng)于直接把項(xiàng)目安裝在了全局, 我們平時(shí)install xxx -g
是去遠(yuǎn)端拉取, 這個(gè)命令是拉當(dāng)前目錄。
此時(shí)那你向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
時(shí)會(huì)展示program.version
方法獲取到的值, 此處直接使用了package.json
里面的version
字段。program.command('ls')
定義了名為ls
的參數(shù), 當(dāng)我們輸入rs ls
時(shí)才會(huì)觸發(fā)我們后面的處理方法, 我之所以寫成program.command('ls [-type]')
是因?yàn)榧由?code>[-type]后commander
才會(huì)認(rèn)為ls
命令后面可以跟其他參數(shù), 當(dāng)然你叫[xxxxx]
也可以, 讓使用者能看懂即可。
.description('description')
顧名思義這里是簡(jiǎn)介描述, 當(dāng)我們輸入rs -h
的時(shí)候會(huì)出現(xiàn):
.action
方法就是commander
檢測(cè)到當(dāng)前命令觸發(fā)時(shí)的處理函數(shù), 第一個(gè)參數(shù)是用戶傳入的參數(shù), 第二個(gè)參數(shù)是Command
對(duì)象, 后續(xù)我們會(huì)在這里彈出選擇列表。process.argv
這里要先知道process
是node
中的全局變量, 其中argv
是啟動(dòng)命令行時(shí)的所有參數(shù)。program.parse(process.argv)
看完上面這里就好理解了, 將命令行參數(shù)傳遞給commander
開始執(zhí)行。
番外
如果你配置program.option('ls', 'ls的介紹')
, 則當(dāng)用戶輸入rs -h
時(shí)會(huì)出現(xiàn), 但我感覺加了有點(diǎn)亂, 咱們的插件追求簡(jiǎ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) })
逐句解釋一下代碼:
- 首先這里是一個(gè)
async
與awite
的模式。 inquirer.prompt
參數(shù)是一個(gè)數(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
正式開始做第一個(gè)命令, 我新建了一個(gè)名為env
的文件夾, 里面創(chuàng)建record-list.json
文件用了存儲(chǔ)用戶的命令:
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
命令時(shí)我們使用inquirer
定義了兩個(gè)輸入框, 第一個(gè)輸入命令名稱, 第二個(gè)輸入命令語句。 validate
定義了對(duì)入?yún)⒌男r?yàn), 注意: 用戶不輸入值不是undefined
而是空字符串
, 所以使用了!== ''
, 如果校驗(yàn)不通過無法繼續(xù)操作。- 用戶填寫完畢就向
record-list.json
添加數(shù)據(jù), 同時(shí)如果是重名的命令就進(jìn)行替換。
名稱可能會(huì)重復(fù), 但是不要緊, 因?yàn)樗氖褂脠?chǎng)景決定了它不需要做過多的限制。
七、移除命令: 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: `請(qǐng)'選擇'要?jiǎng)h除的記錄`, 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
多選模式, 讓用戶可以一次刪除多個(gè)命令。 validate
校驗(yàn)了什么都不刪的情況, 因?yàn)榭赡苁褂脩敉它c(diǎn)擊選取(空格鍵)。- 使用
filter
過濾掉名稱相同的命令。 - 最后更新
record-list.json
文件。
八、查看+使用: ls
這里內(nèi)容稍微多一點(diǎn), 畢竟一個(gè)命令負(fù)責(zé)兩個(gè)能力, 這里的核心原理是拉取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
則會(huì)得到返回值{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
正則匹配出所有 '[這種寫法]'的變量。- 如果匹配到了變量則返回一個(gè)數(shù)組, 這個(gè)數(shù)組的長(zhǎng)度是變量的個(gè)數(shù), 因?yàn)槊總€(gè)變量都要有一次輸入的機(jī)會(huì)。
- 沒有對(duì)重復(fù)的
name
進(jìn)行特殊處理, 并且name
會(huì)變成返回值的key
, 所以不可以重名, 重名的話回會(huì)導(dǎo)致只處理第一個(gè)變量。
3: 無變量 -> 執(zhí)行
這里有一個(gè)新的概念:
const child_process = require('child_process');
child_process
可以生成node
的'子進(jìn)程', child_process.exec
方法是啟動(dòng)了一個(gè)系統(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
這里也很重要, 如果報(bào)錯(cuò)了要讓用戶知道報(bào)錯(cuò)信息, 所以也console
了。
4: 有變量 -> 執(zhí)行
核心原理是解析'變量'后對(duì)命令語句進(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í)行完會(huì)返回一個(gè)字典, 比如{[文本]:"xxxxx", [文件名]:"a.txt"}
, 因?yàn)槲覀冊(cè)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ò)了, 反正看到他就是要對(duì)屏幕進(jìn)行操作了, 但是我們可以看出上面的寫法很不友好, 肯定要封裝一下下, chalk.js
就是個(gè)不錯(cuò)的已有輪子, 我們下進(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命令管理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
用node.js寫一個(gè)jenkins發(fā)版腳本
這篇文章主要介紹了用node.js寫一個(gè)jenkins發(fā)版腳本,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05Node如何后臺(tái)數(shù)據(jù)庫使用增刪改查功能
這篇文章主要介紹了Node如何后臺(tái)數(shù)據(jù)庫使用增刪改查功能,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11