如何用node編寫命令行工具,附上一個ginit示例,並推薦好用的命令行工具

原文 手把手教你寫一個 Node.js CLI javascript

強大的 Node.js 除了能寫傳統的 Web 應用,其實還有更普遍的用途。微服務、REST API、各類工具……甚至還能開發物聯網和桌面應用。JavaScript 不愧是宇宙第一語言。php

Node.js 在開發命令行工具方面也是至關方便,經過這篇教程咱們能夠來感覺下。咱們先看看跟命令行有關的幾個第三方包,而後從零開始寫一個真實的命令行工具。css

這個 CLI 的用途就是初始化一個 Git 倉庫。固然,底層就是調用了 git init,可是它的功能不止這麼簡單。它還能從命令行建立一個遠程的 Github 倉庫,容許用戶交互式地建立 .gitignore文件,最後還能完成提交和推代碼。html

 
寫一個 Node CLI 總共分幾步?

爲何要用 Node.js 寫命令行工具

在動手以前,咱們有必要知道爲何選擇 Node.js 開發命令行工具。java

最明顯優點就是——相信你已經猜到了——它是用 JavaScript 寫的。node

另一個緣由是 Node.js 生態系統很是完善,各類用途的 package 應有盡有,其中就有很多是專門爲了開發命令行工具的。git

最後一個緣由是,用npm 管理依賴不用擔憂跨平臺問題,不像 Aptitude、Yum 或者 Homebrew 這些針對特定操做系統的包管理工具,使人頭疼。github

注:這麼說不必定準確,可能命令行只是須要其餘的外部依賴。shell

動手寫一個命令行工具: ginit

 
Ginit, our Node CLI in action

在這篇教程裏咱們來開發一個叫作 ginit 的命令行工具。咱們能夠把它當作高配版的git init。什麼意思呢?咱們都知道,git init 命令會在當前目錄初始化一個 git 倉庫。可是,這僅僅是建立或關聯已有項目到 Git 倉庫的其中一步而已。典型的工做流程是這樣的:npm

  1. 運行git init初始化本地倉庫
  2. 建立遠程倉庫(好比在 Github 或 Bitbucket 上)——這一步一般要脫離命令行,打開瀏覽器來操做
  3. 添加 remote
  4. 建立.gitignore文件
  5. 添加項目文件
  6. commit 本地文件
  7. push 到遠程

可能還有更多步驟,爲了演示咱們只看關鍵部分。你會發現,這些步驟不少都是機械式、重複性的,爲何不用命令行來完成這些工做呢?好比複製粘貼 git 地址這種事情,能忍受手動操做?

ginit 能夠作到:在當前目錄建立 git 倉庫,同時建立遠程倉庫(咱們這裏用 Github 演示),而後提供一個相似操做嚮導的界面來建立 .gitignore 文件,最後提交文件夾內容並推送到遠程倉庫。可能這也節省不了太多時間,可是它確實給建立新項目帶來了些許便利。

好了,咱們開始吧。

項目依賴

有一點是確定的:說到外觀,控制檯不管如何也不會有圖形界面那麼複雜。儘管如此,也並非說控制檯必定是那種原始的純文本醜陋界面。你會驚訝地發現,原來命令行也能夠那麼好看!咱們會用到一些美化命令行界面的庫: chalk 給輸出內容着色, clui 提供一些可視化組件。還有更好玩的, figlet 能夠生成炫酷的 ASCII 字符圖案, clear 用來清除控制檯。

輸入輸出方面,低端的 Readline Node.js 模塊能夠詢問用戶並接受輸入,簡單場景下夠用了。但咱們會用到一個更高端的工具—— Inquirer。除了詢問用戶的功能,它還提供簡單的輸入控件:單選框和複選框,這但是在命令行控制檯啊,有點意外吧。

咱們還用到 minimist 來解析命令行參數。

如下是完整列表:

  • chalk :彩色輸出
  • clear : 清空命令行屏幕
  • clui :繪製命令行中的表格、儀表盤、加載指示器等。
  • figlet :生成字符圖案
  • inquirer :建立交互式的命令行界面
  • minimist :解析參數
  • configstore:輕鬆加載和保存配置

還有這些:

  • @octokit/rest:Node.js 裏的 GitHub REST API 客戶端
  • lodash:JavaScript 工具庫
  • simple-git:在 Node.js 應用程序中運行 Git 命令的工具
  • touch:實現 Unix touch 命令的工具

開始

建立一個項目文件夾。

mkdir ginit
cd ginit 

新建一個package.json文件:

npm init

根據提示一路往下走:

name: (ginit) version: (1.0.0) description: "git init" on steroids entry point: (index.js) test command: git repository: keywords: Git CLI author: [YOUR NAME] license: (ISC) 

安裝依賴:

npm install chalk clear clui figlet inquirer minimist configstore @octokit/rest lodash simple-git touch --save 

最終生成的 package.json 文件大概是這樣的:

{
  "name": "ginit", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ "Git", "CLI" ], "author": "", "license": "ISC", "bin": { "ginit": "./index.js" }, "dependencies": { "@octokit/rest": "^14.0.5", "chalk": "^2.3.0", "clear": "0.0.1", "clui": "^0.3.6", "configstore": "^3.1.1", "figlet": "^1.2.0", "inquirer": "^5.0.1", "lodash": "^4.17.4", "minimist": "^1.2.0", "simple-git": "^1.89.0", "touch": "^3.1.0" } } 

而後在這個文件夾裏新建一個index.js文件,加上這段代碼:

const chalk = require('chalk'); const clear = require('clear'); const figlet = require('figlet'); 

添加一些 Helper 方法

接下來新建一個lib文件夾,用來存放各類 helper 模塊:

  • files.js — 基本的文件管理
  • inquirer.js — 命令行用戶界面
  • github.js — access token 管理
  • repo.js — Git 倉庫管理

先看 lib/files.js,這裏須要完成:

  • 獲取當前路徑(文件夾名做爲默認倉庫名)
  • 檢查路徑是否存在(經過查找名爲.git的目錄,判斷當前目錄是否已是 Git 倉庫)

看上去很簡單直接,但這裏仍是有點坑的。

首先,你可能想用fs模塊的 realpathSync 方法獲取當前路徑:

path.basename(path.dirname(fs.realpathSync(__filename))); 

當咱們在同一路徑下運行應用時(即 node index.js),這沒問題。可是要知道,咱們要把這個控制檯應用作成全局的,就是說咱們想要的是當前工做目錄的名稱,而不是應用的安裝路徑。所以,最好使用 process.cwd:

path.basename(process.cwd()); 

其次,檢查文件或目錄是否存在的推薦方法一直在變。目前的方法是用fs.statfs.statSync。這兩個方法在文件不存在的狀況下會拋異常,因此咱們要用try...catch

最後須要注意的是,當你在寫命令行應用的時候,使用這些方法的同步版本就能夠了。

整理下lib/files.js代碼,一個工具包就出來了:

const fs = require('fs'); const path = require('path'); module.exports = { getCurrentDirectoryBase : () => { return path.basename(process.cwd()); }, directoryExists : (filePath) => { try { return fs.statSync(filePath).isDirectory(); } catch (err) { return false; } } }; 

回到index.js文件,引入這個文件:

const files = require('./lib/files'); 

有了這個,咱們就能夠動手開發應用了。

初始化 Node CLI

如今讓咱們來實現控制檯應用的啓動部分。

爲了展現安裝的這些控制檯輸出強化模塊,咱們先清空屏幕,而後展現一個banner:

clear();
console.log( chalk.yellow( figlet.textSync('Ginit', { horizontalLayout: 'full' }) ) ); 

輸出效果以下圖:

 
The welcome banner on our Node CLI, created using Chalk and Figlet

接着運行簡單的檢查,確保當前目錄不是 Git 倉庫。很容易,咱們只要用剛纔建立的工具方法檢查是否存在 .git 文件夾就好了:

if (files.directoryExists('.git')) { console.log(chalk.red('Already a git repository!')); process.exit(); } 

提示:咱們用了 chalk 模塊 來展現紅色的消息。

提示用戶輸入

接下來咱們要作的就是寫個函數,提示用戶輸入 Github 登陸憑證。這個能夠用 Inquirer 來實現。這個模塊包含一些支持各類提示類型的方法,語法上跟 HTML 表單控件相似。爲了收集用戶的 Github 用戶名和密碼,咱們分別用了 inputpassword 類型。

首先新建 lib/inquirer.js 文件,加入如下代碼:

const inquirer = require('inquirer'); const files = require('./files'); module.exports = { askGithubCredentials: () => { const questions = [ { name: 'username', type: 'input', message: 'Enter your GitHub username or e-mail address:', validate: function( value ) { if (value.length) { return true; } else { return 'Please enter your username or e-mail address.'; } } }, { name: 'password', type: 'password', message: 'Enter your password:', validate: function(value) { if (value.length) { return true; } else { return 'Please enter your password.'; } } } ]; return inquirer.prompt(questions); }, } 

如你所見,inquirer.prompt() 向用戶詢問一系列問題,並以數組的形式做爲參數傳入。數組的每一個元素都是一個對象,分別定義了nametypemessage屬性。

用戶提供的輸入信息返回一個 promise 給調用函數。若是成功,咱們會獲得一個對象,包含usernamepassword屬性。

能夠在index.js裏測試下:

const inquirer = require('./lib/inquirer'); const run = async () => { const credentials = await inquirer.askGithubCredentials(); console.log(credentials); } run(); 

運行 node index.js

 
Getting user input with Inquirer

處理 GitHub 身份驗證

下一步是建立一個函數,用來獲取 Github API 的OAuth token。咱們其實是用用戶名和密碼換取 token的。

固然了,咱們不能讓用戶每次使用這個工具的時候都須要輸入身份憑證,而是把 OAuth token 存起來給後續請求使用。這個是就要用到 configstore 這個包了。

保存配置信息

保存配置信息表面上看起來很是簡單直接:無需第三方庫,直接存取 JSON 文件就行了。可是,configstore 這個包還有幾個關鍵的優點:

  1. 它會根據你的操做系統和當前用戶來決定最佳的文件存儲位置。
  2. 不須要直接讀寫文件,只要修改 configstore 對象,後面的事都幫你搞定了。

用法也很簡單,建立一個實例,傳入應用標識符就好了。例如:

const Configstore = require('configstore'); const conf = new Configstore('ginit'); 

若是 configstore 文件不存在,它會返回一個空對象並在後臺建立該文件。若是 configstore 文件已經存在,內容會被解析成 JSON,應用程序就可使用它了。 你能夠把 conf 當成簡單的對象,根據須要獲取或設置屬性。剛纔已經說了,你不用擔憂保存的問題,它已經幫你作好了。

提示:macOS/Linux 系統該文件位於 /Users/[YOUR-USERNME]/.config/configstore/ginit.json

與 GitHub API 通訊

讓咱們寫一個庫,處理 GitHub token。新建文件 lib/github.js 並添加如下代碼:

const octokit = require('@octokit/rest')(); const Configstore = require('configstore'); const pkg = require('../package.json'); const _ = require('lodash'); const CLI = require('clui'); const Spinner = CLI.Spinner; const chalk = require('chalk'); const inquirer = require('./inquirer'); const conf = new Configstore(pkg.name); 

再添加一個函數,檢查訪問 token 是否已經存在。咱們還添加了一個函數,以便其餘庫能夠訪問到 octokit(GitHub) 相關函數:

...
module.exports = {

  getInstance: () => { return octokit; }, getStoredGithubToken : () => { return conf.get('github.token'); }, setGithubCredentials : async () => { ... }, registerNewToken : async () => { ... } } 

若是 conf 對象存在而且有 github.token 屬性,就表示 token 已經存在。在這裏咱們把 token 值返回給調用的函數。咱們稍後會講到它。

若是 token 沒找到,咱們須要獲取它。固然了,獲取 OAuth token 牽涉到網絡請求,對用戶來講有短暫的等待過程。借這個機會咱們能夠看看 clui 這個包,它給控制檯應用提供了強化功能,轉菊花就是其中一個。

建立一個菊花很簡單:

const status = new Spinner('Authenticating you, please wait...'); status.start(); 

任務完成後就能夠停掉它,它就從屏幕上消失了:

status.stop(); 

提示:你也能夠用update方法動態更新文字內容。當你須要展現進度時這會很是有用,好比顯示完成的百分比。

完成 GitHub 認證的代碼在這:

...
  setGithubCredentials : async () => { const credentials = await inquirer.askGithubCredentials(); octokit.authenticate( _.extend( { type: 'basic', }, credentials ) ); }, registerNewToken : async () => { const status = new Spinner('Authenticating you, please wait...'); status.start(); try { const response = await octokit.authorization.create({ scopes: ['user', 'public_repo', 'repo', 'repo:status'], note: 'ginits, the command-line tool for initalizing Git repos' }); const token = response.data.token; if(token) { conf.set('github.token', token); return token; } else { throw new Error("Missing Token","GitHub token was not found in the response"); } } catch (err) { throw err; } finally { status.stop(); } }, 

咱們一步一步來看:

  1. 用以前定義的 setGithubCredentials 方法提示用戶輸入憑證
  2. 試圖獲取 OAuth token以前採用 basic authentication
  3. 嘗試註冊新的 token
  4. 若是成功獲取了 token,保存到 configstore
  5. 返回 token

你建立的任何 token,不管是經過人工仍是 API,均可以在 這裏看到。在開發過程當中,你可能須要刪除 ginit 的 access token ——能夠經過上面的 note 參數辨認—— 以便從新生成。

提示:若是你的 Github 帳戶啓用了雙重認證,這個過程會稍微複雜點。你須要請求驗證碼(好比經過手機短信),而後經過 X-GitHub-OTP 請求頭提供該驗證碼。更多信息請參閱 Github 開發文檔

更新下index.js文件裏的run()函數,看看效果:

const run = async () => { let token = github.getStoredGithubToken(); if(!token) { await github.setGithubCredentials(); token = await github.registerNewToken(); } console.log(token); } 

請注意,若是某個地方出錯的話,你會獲得一個 Promise 錯誤,好比輸入的密碼不對。稍後咱們會講處處理這些錯誤的方式。

建立倉庫

一旦得到了 OAuth token,咱們就能夠用它來建立遠程 Github 倉庫了。

一樣,咱們能夠用 Inquirer 給用戶提問。咱們須要倉庫名稱、可選的描述信息以及倉庫是公開仍是私有。

咱們用 minimist 從可選的命令行參數中提取名稱和描述的默認值。例如:

ginit my-repo "just a test repository" 

這樣就設置了默認名稱爲my-repo ,默認描述爲just a test repository

下面這行代碼把參數放在一個數組裏:

const argv = require('minimist')(process.argv.slice(2)); // { _: [ 'my-repo', 'just a test repository' ] } 

提示:這裏只展現了 minimist 功能的一點皮毛而已。你還能夠用它來解析標誌位參數、開關和鍵值對。更多功能請查看它的文檔。

接下來咱們加上解析命令行參數的代碼,並向用戶提出一系列問題。首先更新lib/inquirer.js文件,在askGithubCredentials函數後面加上如下代碼:

...
  askRepoDetails: () => {
    const argv = require('minimist')(process.argv.slice(2)); const questions = [ { type: 'input', name: 'name', message: 'Enter a name for the repository:', default: argv._[0] || files.getCurrentDirectoryBase(), validate: function( value ) { if (value.length) { return true; } else { return 'Please enter a name for the repository.'; } } }, { type: 'input', name: 'description', default: argv._[1] || null, message: 'Optionally enter a description of the repository:' }, { type: 'list', name: 'visibility', message: 'Public or private:', choices: [ 'public', 'private' ], default: 'public' } ]; return inquirer.prompt(questions); }, 

接着建立lib/repo.js 文件,加上這些代碼:

const _ = require('lodash'); const fs = require('fs'); const git = require('simple-git')(); const CLI = require('clui') const Spinner = CLI.Spinner; const inquirer = require('./inquirer'); const gh = require('./github'); module.exports = { createRemoteRepo: async () => { const github = gh.getInstance(); const answers = await inquirer.askRepoDetails(); const data = { name : answers.name, description : answers.description, private : (answers.visibility === 'private') }; const status = new Spinner('Creating remote repository...'); status.start(); try { const response = await github.repos.create(data); return response.data.ssh_url; } catch(err) { throw err; } finally { status.stop(); } }, } 

根據獲取的信息,咱們就能夠利用 Github 包 建立倉庫了,它會返回新建立的倉庫 URL。而後咱們就能夠把這個地址設置爲本地倉庫的 remote。不過仍是先新建一個.gitignore 文件吧。

建立 .gitignore 文件

下一步咱們將要建立一個簡單的「嚮導」命令行,用來生成 .gitignore 文件。若是用戶在已有項目路徑裏運行咱們的應用程序,咱們給用戶列出當前工做目錄的文件和目錄, 以讓他們選擇忽略哪些。

Inquirer 提供的 checkbox 輸入類型就是用來作這個的。

 
Inquirer’s checkboxes in action

首先咱們須要作的就是掃描當前目錄,忽略.git文件夾和任何現有的 .gitignore 文件。咱們用 lodash 的 without 方法來作:

const filelist = _.without(fs.readdirSync('.'), '.git', '.gitignore'); 

若是沒有符合條件的結果,就不必繼續執行了,直接 touch 當前的.gitignore 文件並退出函數。

if (filelist.length) { ... } else { touch('.gitignore'); } 

最後,咱們用 Inquirer’s 的 checkbox 列出全部文件。在 lib/inquirer.js 加上以下代碼:

...
  askIgnoreFiles: (filelist) => { const questions = [ { type: 'checkbox', name: 'ignore', message: 'Select the files and/or folders you wish to ignore:', choices: filelist, default: ['node_modules', 'bower_components'] } ]; return inquirer.prompt(questions); }, .. 

請注意,咱們也能夠提供默認忽略列表。在這裏咱們預先選擇了 node_modulesbower_components 目錄,若是存在的話。

有了 Inquirer 的代碼,如今咱們能夠寫 createGitignore() 函數了。在 lib/repo.js 文件裏插入這些代碼:

...
  createGitignore: async () => { const filelist = _.without(fs.readdirSync('.'), '.git', '.gitignore'); if (filelist.length) { const answers = await inquirer.askIgnoreFiles(filelist); if (answers.ignore.length) { fs.writeFileSync( '.gitignore', answers.ignore.join( '\n' ) ); } else { touch( '.gitignore' ); } } else { touch('.gitignore'); } }, ... 

一旦用戶確認,咱們把選中的文件列表用換行符拼接起來,寫入 .gitignore 文件。 有了.gitignore 文件,能夠初始化 Git 倉庫了。

應用程序中的 Git 操做

操做 Git 的方法有不少,最簡單的可能就是使用 simple-git 了。它提供一系列的鏈式方法運行 Git 命令。

咱們用它來自動化的重複性任務有這些:

  1. 運行 git init
  2. 添加 .gitignore 文件
  3. 添加工做目錄的其他內容
  4. 執行初次 commit
  5. 添加新建立的遠程倉庫
  6. push 工做目錄到遠端

lib/repo.js 中插入如下代碼:

...
  setupRepo: async (url) => { const status = new Spinner('Initializing local repository and pushing to remote...'); status.start(); try { await git .init() .add('.gitignore') .add('./*') .commit('Initial commit') .addRemote('origin', url) .push('origin', 'master'); return true; } catch(err) { throw err; } finally { status.stop(); } }, ... 

所有串起來

首先在 lib/github.js中寫幾個 helper 函數。一個用來方便地存取 token,一個用來創建 oauth認證:

...
  githubAuth : (token) => { octokit.authenticate({ type : 'oauth', token : token }); }, getStoredGithubToken : () => { return conf.get('github.token'); }, ... 

接着在 index.js裏寫個函數用來處理獲取 token 的邏輯。在run()函數前加入這些代碼:

const getGithubToken = async () => { //從 config store 獲取 token let token = github.getStoredGithubToken(); if(token) { return token; } // 沒找到 token ,使用憑證訪問 GitHub 帳號 await github.setGithubCredentials(); // 註冊新 token token = await github.registerNewToken(); return token; } 

最後,更新run() 函數,加上應用程序主要邏輯處理代碼。

const run = async () => { try { // 獲取並設置認證 Token const token = await getGithubToken(); github.githubAuth(token); // 建立遠程倉庫 const url = await repo.createRemoteRepo(); // 建立 .gitignore 文件 await repo.createGitignore(); // 創建本地倉庫並推送到遠端 const done = await repo.setupRepo(url); if(done) { console.log(chalk.green('All done!')); } } catch(err) { if (err) { switch (err.code) { case 401: console.log(chalk.red('Couldn\'t log you in. Please provide correct credentials/token.')); break; case 422: console.log(chalk.red('There already exists a remote repository with the same name')); break; default: console.log(err); } } } } 

如你所見,在順序調用其餘函數(createRemoteRepo(), createGitignore(), setupRepo())以前,咱們要確保用戶是經過認證的。代碼還處理了異常,並給予了用戶適當的反饋。

讓 ginit 命令全局可用

剩下的一件事是讓咱們的命令行在全局可用。爲此,咱們須要在index.js文件頂部加上一行叫 shebang 的代碼:

#!/usr/bin/env node 

接着在package.json 文件中新增一個 bin 屬性。它用來綁定命令名稱(ginit)和對應被執行的文件(路徑相對於 package.json)。

"bin": { "ginit": "./index.js" } 

而後,在全局安裝這個模塊,這樣一個可用的 shell 命令就生成了。

npm install -g

提示:Windows下也是有效的, 由於 npm 會幫你的腳本安裝一個 cmd 外殼程序

更進一步

咱們已經作出了一個漂亮卻很簡單的命令行應用程序用來初始化 Git 倉庫。可是你還能夠作不少事來進一步增強它。

若是你是 Bitbucket 用戶,你能夠適配該程序去使用 Bitbucket API 來建立倉庫。有個 [Node.js API (https://www.npmjs.com/package/bitbucket-api) 能夠幫你起步。你可能但願增長几個命令行選項,或者讓用戶選擇使用 Github 仍是 Bitbucket(用 Inquirer 再適合不過了),或者直接把 Github 相關的代碼替換成 Bitbucket 對應的代碼。

你還能夠指定.gitgnore 文件默認列表,這方面 preferences 包比較合適,或者能夠提供一些模板—— 多是讓用戶選擇項目類型。還能夠把它集成到 .gitignore.io

除此以外,你還能夠添加額外的驗證、提供跳過某些步驟的功能等等。發揮你的想象力,若是還有其餘想法,歡迎留言評論!

做者:空引 連接:https://www.jianshu.com/p/1c5d086c68fa 來源:簡書 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。
相關文章
相關標籤/搜索