Build a Command Line Interface(CLI) with Node.js

Posted MARVEL大叔笔记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Build a Command Line Interface(CLI) with Node.js相关的知识,希望对你有一定的参考价值。

前言

在创建新项目的时候,总是会觉得很麻烦,因此在看到这篇文章之后,就也动手做了一个。 已发布至npmgithub

首先回顾一下,手动操作的话流程如下:

  1. 通过 git init命令在当前目录初始化本地git仓库

  2. 创建remote仓库

  3. 将本地仓库关联到remote仓库

  4. 创建 .gitignore文件

  5. 提交本地文件并推送到remote仓库

接下来将会按照这个流程来一步步实现CLI。

依赖项

会用到以下这些依赖性:

  • axios: Promise based HTTP client(http客户端)

  • chalk: colorizes the output(让命令行输出有颜色)

  • clui: draws command-line tables, gauges and spinners(在命令行画菊花 :D)

  • configstore: easily loads and saves config without you having to think about where and how. (读取和写入配置文件的客户端)

  • figlet: creates ASCII art from text(以艺术字的形式展示文字)

  • inquirer: creates interactive command-line user interface(创建交互式命令行界面)

  • lodash

  • minimist: parses argument options(转换命令行参数)

  • request: Http request client

  • simple-git: a tool for running Git commands in a Node.js application(在nodejs中快速执行git命令)

初始化项目

先创建一个名为 Yginit的目录,在该目录下执行 npm init,按照提示一步一步生成 package.json文件,然后安装相关依赖性。

 
   
   
 
  1. mkdir Yginit

  2. cd Yginit

  3. npm install --save axios chalk clui configstore figlet inquirer lodash minimist request simple-git

添加一些帮助类

在根目录添加 lib文件夹,并添加以下四个帮助类:

  • files.js: 文件相关的管理方法

  • inquirer.js: 命令行交互方法

  • git.js: git操作相关方法

  • repo.js: repo操作相关方法

files.js

接下来,让我们从 lib\files.js开始, 这个帮助类需要提供两个方法:

  • 获取当前文件夹的名称(用于repository的默认名称)

  • 判断目录是否存在(通过检查当前文件夹下是否存在 .git文件夹来判断当前文件夹是否已经是一个Git Repository了)

首先,你可能会尝试使用 fs module的 realPathSync方法来获取当前目录:

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

当我们在和这个应用相同文件夹时,使用这个方法是没问题的,(e.g: node index),但是,要牢记我们这个应用是个全局应用,这意味着我们需要获取的是执行当前命令所在的文件夹,而不是这个应用所在的文件夹。因此,更好的选择是使用 process.cwd()

其次,使用 fs.statSync来判断文件是否存在时,如果文件不存在,会抛出异常,因此在这里要使用 try...catch...来包裹。

最后,在一个命令行应用中,选择 同步版本的命令是个更好的选择(比如用 fs.statSync) 最终, lib\files.js的内容如下:

 
   
   
 
  1. const fs   = require('fs');

  2. const path = require('path');

  3. module.exports = {

  4.  getCurrentDirectoryBase () {

  5.    return path.basename(process.cwd());

  6.  },

  7.  directoryExists (filePath) {

  8.    try {

  9.      return fs.statSync(filePath).isDirectory();

  10.    } catch (err) {

  11.      return false;

  12.    }

  13.  }

  14. };

回到 index.js,引入这个文件:

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

接下来,我们就要开始开发CLI Application的具体内容了。

初始化CLi

为了让用户更好的交互,首先让我们清理下命令行的输出,并显示一个Banner。

 
   
   
 
  1. // 清除屏幕

  2. clear();

  3. // 打印Banner

  4. console.log(

  5.  chalk.yellow(

  6.    figlet.textSync('Welcome To Use YgInit', {

  7.      // font: 'Ghost',

  8.      horizontalLayout: 'full',

  9.      verticalLayout: 'default'

  10.    })

  11.    )

  12.  );

备注:这里用到3个第三方库, clear顾名思义用来清理console的output, chalk用来在console输出有颜色的文字, figlet用来生成艺术字。

接下来,使用上面的 files.js中提供的方法来判断是否包含 .git文件夹,如果包含,则结束。

 
   
   
 
  1. // 判断是否存在.git

  2. if (files.directoryExists('.git')) {

  3.  console.log(chalk.red('Already a git repository!'));

  4.  process.exit();

  5. } else {

  6.  console.log(chalk.blue('let\'s begin!'));

  7. }

和用户交互

接下来就要采用 QA的方式来和用户进行交互,收集所需的信息了。 创建 lib\inquirer.js,这个文件用来处理和用户的交互 inquirer.prompt会问用户一系列问题,拿到的结果就是用户输入的信息。

 
   
   
 
  1. // 仓库信息交互

  2.  askRepoDetails (groupList) {

  3.    const argv = require('minimist')(process.argv.slice(2), {

  4.      string: ['name', 'desc'],

  5.      alias: {

  6.        name: 'n',

  7.        desc: 'd'

  8.      }

  9.    });

  10.    const questions = [

  11.      {

  12.        name: 'group',

  13.        type: 'list',

  14.        message: 'Choise Group:',

  15.        pageSize: 20,

  16.        choices: groupList.map(item => {

  17.          return {

  18.            name: item.path + '(' + item.description + ')',

  19.            value: item.id

  20.          }

  21.        }),

  22.        validate (val) {

  23.          if (val) {

  24.            return true;

  25.          } else {

  26.            return 'Please Choice a Group';

  27.          }

  28.        }

  29.      },

  30.      {

  31.        type: 'input',

  32.        name: 'reponame',

  33.        message: 'Enter a name for the repository:',

  34.        default: argv.name || files.getCurrentDirectoryBase(),

  35.        validate (val) {

  36.          if (val) {

  37.            return true;

  38.          } else {

  39.            return 'Please Enter Repo Name';

  40.          }

  41.        }

  42.      },

  43.      {

  44.        type: 'input',

  45.        name: 'repodesc',

  46.        message: 'Optionally  enter desc for the repository:',

  47.        default: argv.desc || null

  48.      },

  49.      {

  50.        type: 'list',

  51.        name: 'repotype',

  52.        message: 'Public or private',

  53.        choices: ['public', 'private'],

  54.        default: 'private'

  55.      }

  56.    ];

  57.    return inquirer.prompt(questions);

  58.  }

关于 inquirer的详细用法可以看官方文档

一般问题有以下属性:

  • name: 必填项,返回答案的key值

  • type: 这里只用到了input、list(单选)、checkbox(多选)、password

  • message: 问题

  • choice: 如果是list、checkbox等的话的候选项

  • default: 默认值

  • validate: 验证

上面还用到了 minimist这个lib,主要是用来转换参数,还可以给参数起别名。

处理和Git的交互

和gitlab的交互都在这个文件夹中,主要有通过用户名密码获取 private_token,获取群组信息。

 
   
   
 
  1. module.exports = {

  2.  registerNewToken (userInfo, url) {

  3.    // 调用api,获取private_token,并存入configstore中

  4.  },

  5.  getStoredToken () {

  6.    // 从configstore中获取private_token

  7.  },

  8.  registerNewGitUrl (url) {

  9.    // 将git_url存入configstore中

  10.  },

  11.  getStoredGitUrl () {

  12.    // 从configstore中获取git_url

  13.  },

  14.  getGroup (url) {

  15.    // 获取当前用户有权限看到的群组列表

  16.  }

  17. };

config store

通过以下方法获取configstore的实例:

 
   
   
 
  1. const ConfigStore = require('configstore');

  2. const pkg         = require('../package.json');

  3. const conf        = new ConfigStore(pkg.name);

然后很简单的使用 getset就可以从configstore中读取或者写入配置项。这样下次如果判断之前有保存,就可以重新直接读取了。

创建远程仓库

在拿到gitlab的private_token之后,就可以创建remote仓库了。 首先让用户选择项目所属群组,然后输入项目的基本信息,再调用api即可创建远程仓库

 
   
   
 
  1. module.exports = {

  2.  // create remote repo

  3.  createRemoteRepo (url) {

  4.    return new Promise(async (resolve, reject) => {

  5.      const baseUrl = `http://${url}/api/v3`;

  6.      // 获取群组列表

  7.      const groupList = await gitlab.getGroup(baseUrl);

  8.      // 获取选择的群组

  9.      const answers   = await inquirer.askRepoDetails(groupList);

  10.      const postData = {

  11.        name: answers.reponame,

  12.        path: answers.reponame,

  13.        namespace_id: answers.group,

  14.        description: answers.repodesc,

  15.        public: answers.repotype === 'public'

  16.      }

  17.      const status = new Spinner('Creating remote repository...');

  18.      status.start();

  19.      // 创建remote repository

  20.      axios.post(`${baseUrl}/projects/?private_token=${conf.get(pkg.name)}`, postData)

  21.      .then(res => {

  22.        status.stop();

  23.        console.log(`repo created...url:${res.data.web_url}`);

  24.        resolve(res.data.http_url_to_repo);

  25.      })

  26.      .catch(err => {

  27.        console.log(err.toString());

  28.        status.stop();

  29.        reject();

  30.      });

  31.    });

  32.  }

  33. }

创建.gitignore文件

根据用户选择的项目类型(e.g:node、visual stuiod、maven、java),从github/gitignore下载对应的 .gitignore文件,保存在当前文件夹。 目前还很简单,也只支持4中项目类型。

 
   
   
 
  1. // 创建.gitignore文件

  2.  async createGitIgnore () {

  3.    // 获取项目类型:比如node,java,maven,visual studio等

  4.    const ignoreInfo = await inquirer.askIgnoreFiles();

  5.    const protype = ignoreInfo.protype;

  6.    // 根据项目类型获取.gitignore文件

  7.    let downloadUrl = '';

  8.    switch (protype) {

  9.      case 'visual studio':

  10.      downloadUrl = 'https://raw.githubusercontent.com/github/gitignore/master/VisualStudio.gitignore';

  11.      break;

  12.      case 'node':

  13.      downloadUrl = 'https://raw.githubusercontent.com/github/gitignore/master/Node.gitignore';

  14.      break;

  15.      case 'maven':

  16.      downloadUrl = 'https://raw.githubusercontent.com/github/gitignore/master/Maven.gitignore';

  17.      break;

  18.      case 'java':

  19.      downloadUrl = 'https://raw.githubusercontent.com/github/gitignore/master/Java.gitignore';

  20.      break;

  21.    }

  22.    // 下载并生成.gitignore文件

  23.    return new Promise((resolve, reject) => {

  24.      const ws = fs.createWriteStream('.gitignore');

  25.      request(downloadUrl).pipe(ws);

  26.      ws.on('finish', () => {

  27.        resolve();

  28.      });

  29.      ws.on('error', () => {

  30.        reject();

  31.      })

  32.    });

  33.  }

设置本地Git

接下来使用 simple-git来完成以下几步操作:

  1. 运行 git init

  2. 添加 .gitignore

  3. 添加其他所有文件

  4. 运行 commit

  5. 将新创建的git增加到remote repository

  6. git push

 
   
   
 
  1. async setupRepo (url) {

  2.    const status = new Spinner('Initializing local repository and pushing to remote...');

  3.    status.start();

  4.    try {

  5.      // 利用simple-git链式编程依次执行

  6.      // git init

  7.      // git add .gitignore

  8.      // git add ./*

  9.      // git commit -m "Initial commit"

  10.      // git remote add remote-url

  11.      // git push origin master

  12.      await git

  13.            .init()

  14.            .add('.gitignore')

  15.            .add('./*')

  16.            .commit('Initial commit')

  17.            .addRemote('origin', url)

  18.            .push('origin', 'master');

  19.      return true;

  20.    } catch (err) {

  21.      console.log(err.toString());

  22.      status.stop();

  23.      return false;

  24.    } finally {

  25.      status.stop();

  26.    }

  27.  }

最终效果

将上面这些操作合在一起之后, index.js的伪代码如下:

 
   
   
 
  1. // 1.0 清除屏幕

  2. // 2.0 打印Banner

  3. // 3.0 判断当前文件夹是否有.git文件夹,如果已存在则提示当前已经存在一个git repository了

  4. // 4.0 创建仓库

  5. // 4.2 交互式方式获取用户密码,登陆gitlab换取private_token

  6. // 4.3 创建remote仓库

  7. // 4.4 创建.gitignore文件

  8. // 4.5 初始化仓库

  9. // 5.0 完成

让命令可以在全局执行

上面的开发完成之后,还要最后两步,才能让命令在全局运行。

  1. index.js的最上面,加入 #!/usr/bin/env node,这个称之为Shebang


  2. package.json中加入以下节点:


 
   
   
 
  1. "bin": {

  2.    "yginit": "./index.js"

  3.  }

发布到npm

使用 npm login登录之后,再使用 npm publish即可发布了。

至此,一个CLI就开发完成了。


以上是关于Build a Command Line Interface(CLI) with Node.js的主要内容,如果未能解决你的问题,请参考以下文章

libtoolize: command not found

caffe command line

DSO missing from command line

Chapter 4. Using the Gradle Command-Line

python os.system command_line

Mac OS 在线安装Command line tools