如何用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 来源:简书 简书著做权归做者全部,任何形式的转载都请联系做者得到受权并注明出处。
相关文章
相关标签/搜索