如何開發(fā)一個(gè)屬于自己的命令行工具

平常經(jīng)常使用一些npm cli工具期升,你是否好奇這些工具又是怎么開發(fā)出來(lái)的呢?接下來(lái)這篇文章亲茅,就會(huì)介紹如何利用Node.js開發(fā)一個(gè)屬于你自己的命令行工具略步。

創(chuàng)建基礎(chǔ)的文件目錄

首先,我們需要先創(chuàng)建一個(gè)基本的項(xiàng)目結(jié)構(gòu):

mkdir git-repo-cli
cd git-repo-cli
npm init #初始化項(xiàng)目

接著我們創(chuàng)建所需的文件:

touch index.js
mkdir lib
cd lib
touch files.js
touch github_credentials.js
touch inquirer.js
touch create_a_repo.js

接著我們先來(lái)寫一個(gè)簡(jiǎn)單的入口程序,在index.js文件中代碼如下:

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

commander
    .command('init')
    .description('Hello world')
    .action(() => {
        clear();
        console.log(chalk.magenta(figlet.textSync('Git Repo Cli', {
            hosrizontalLayout: 'full'
        })));
    });

commander.parse(process.argv);

if (!commander.args.length) {
    commander.help();
}

上面的代碼中引用了chalk,clear,figletcommander這幾個(gè)npm庫(kù),,其中chalk負(fù)責(zé)命令行不同顏色文本的顯示,便于用戶使用放妈。clear庫(kù)負(fù)責(zé)清空命令行界面,figlet可以在命令行中以ASCII ART形式顯示文本荐操。最后commander庫(kù)就是用來(lái)實(shí)現(xiàn)命令行接口最主要的庫(kù)芜抒。

寫完代碼后,我們可以在命令行中輸入如下代碼啟動(dòng):

node index.js init

功能模塊

文件模塊

把基本的架子搭起來(lái)后托启,我們就可以開始寫功能模塊的代碼了宅倒。

lib/files.js文件中,主要要實(shí)現(xiàn)以下兩個(gè)功能::

  • 獲取當(dāng)前文件目錄名
  • 檢查該路徑是否已經(jīng)是個(gè)git倉(cāng)庫(kù)
const fs = require('fs');
const path = require('path');

module.exports = {
    getCurrentDirectoryBase: () => path.basename(process.cwd()),

    directoryExists: (filePath) => {
        try {
            return fs.statSync(filePath).isDirectory();
        } catch (err) {
            return false;
        }
    },

    isGitRepository: () => {
        if (files.directoryExists('.git')) {
            console.log(chalk.red("Sorry! Can't create a new git repo because this directory has been existed"))
            process.exit();
        }
    }
};

詢問(wèn)模塊

在用戶在執(zhí)行命令行工具的時(shí)候屯耸,需要收集一些變量信息拐迁,因此蹭劈,可以我們需要利用inquirer這個(gè)npm庫(kù)來(lái)實(shí)現(xiàn)“詢問(wèn)模塊”。

const inquirer = require('inquirer');

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 Github username:'
                    }
                }
            },
            {
                name: "password",
                type: 'password',
                message: 'Enter your password:',
                validate: function(value) {
                    if (value.length) {
                        return true;
                    } else {
                        return 'Please enter your Github username:'
                    }
                }
                
            }
        ];
        return inquirer.prompt(questions);
    }
}

github認(rèn)證

為了實(shí)現(xiàn)和github的接口通信线召,需要獲得token認(rèn)證信息铺韧。因此,在lib/github_credentials.js文件中缓淹,我們獲得token,并借助configstore庫(kù)寫入package.json文件中哈打。

const Configstore = require('configstore');
const pkg = require('../package.json')
const octokit = require('@octokit/rest')();
const _ = require('lodash');

const inquirer = require("./inquirer");

const conf = new Configstore(pkg.name);

module.exports = {
    getInstance: () => {
        return octokit;
    },
    
    githubAuth: (token) => {
        octokit.authenticate({
            type: 'oauth',
            token: token
        });
    },
    
    getStoredGithubToken: () => {
        return conf.get('github_credentials.token');
    },
    
    setGithubCrendeitals: async () => {
        const credentials = await inquirer.askGithubCredentials();
        octokit.authenticate(
            _.extend({
                type: 'basic'
            }, credentials)
        )
    },
    
    registerNewToken: async () => {
        // 該方法可能會(huì)被棄用,可以手動(dòng)在github設(shè)置頁(yè)面設(shè)置新的token
        try {
            const response = await octokit.oauthAuthorizations.createAuthorization({
                scope: ['user', 'public_repo', 'repo', 'repo:status'],
                note: 'git-repo-cli: register new token'
            });
            const token = response.data.token;
            if (token) {
                conf.set('github_credentials.token', token);
                return token;
            } else {
                throw new Error('Missing Token', 'Can not retrive token')
            }
        } catch(error) {
            throw error;
        }
    }
}

其中@octokit/rest是node端與github通信主要的庫(kù)讯壶。

接下來(lái)就可以寫我們的接口了:

// index.js
const github = require('./lib/gitub_credentials');

commander.
    command('check-token')
    .description('Check user Github credentials')
    .action(async () => {
        let token = github.getStoredGithubToken();
        if (!token) {
            await github.setGithubCredentials();
            token = await github.registryNewToken();
        }
        console.log(token);
    });

最后料仗,在命令行中輸入如下命令:

node index.js check-token

它會(huì)先會(huì)在configstore的默認(rèn)文件夾下~/.config尋找token, 如果沒(méi)有發(fā)現(xiàn)的話,就會(huì)提示用戶輸入用戶名和密碼后新建一場(chǎng)新的token伏蚊。

有了token后罢维,就可以執(zhí)行g(shù)ithub的很多的操作,我們以新建倉(cāng)庫(kù)為例:

首先丙挽,先在inquirer.js中新建askRepositoryDetails用來(lái)獲取相關(guān)的repo信息:

    askRepositoryDetails: () => {
        const args = require('minimist')(process.argv.slice(2));
        const questions = [
            {
                type: 'input',
                name: 'name',
                message: 'Please enter a name for your repository:',
                default: args._[1] || files.getCurrentDirectoryBase(),
                validate: function(value) {
                    if (value.length) {
                        return true;
                    } else {
                        return 'Please enter a unique name for the repository.'
                    }
                }
            },
            {
                type: 'input',
                name: 'description',
                default: args._[2] || null,
                message: 'Now enter description:'
            },
            {
                type: 'input',
                name: 'visiblity',
                message: 'Please choose repo type',
                choices: ['public', 'private'],
                default: 'public'
            }
        ];

        return inquirer.prompt(questions);
    },

    askIgnoreFiles: (filelist) => {
        const questions = [{
            type: 'checkbox',
            choices: filelist,
            message: 'Please choose ignore files'
        }];
        return inquirer.prompt(questions);
    }

接著,實(shí)現(xiàn)對(duì)應(yīng)的新建倉(cāng)庫(kù)匀借、新建.gitignore文件等操作:

// create_a_repo.js
const _ = require('lodash');
const fs = require('fs');
const git = require('simple-git')();

const inquirer = require('./inquirer');
const gh = require('./github_credentials');

module.exports = {
    createRemoteRepository: async () => {
        const github = gh.getInstance(); // 獲取octokit實(shí)例
        const answers = await inquirer.askRepositoryDetails();
        const data = {
            name: answers.name,
            descriptions: answers.description,
            private: (answers.visibility === 'private')
        };

        try {
            // 利用octokit 來(lái)新建倉(cāng)庫(kù)
            const response = await github.repos.createForAuthenticatedUser(data);
            return response.data.ssh_url;
        } catch (error) {
            throw error;
        }
    },

    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('.gitnore');
            }
        } else {
            touch('.gitignore');
        }
    },

    setupRepo: async (url) => {
        try {
            await git.
                init()
                .add('.gitignore')
                .add('./*')
                .commit('Initial commit')
                .addRemote('origin', url)
                .push('origin', 'master')
            return true;
        } catch (err) {
            throw err;
        }
    }
}

最后颜阐,在index.js文件中新建一個(gè)create-repo的命令,執(zhí)行整個(gè)流程吓肋。

// index.js
commander
    .command('create-repo')
    .description('create a new repo')
    .action(async () => {
        try {
            const token = await github.getStoredGithubToken();
            github.githubAuth(token);
            const url = await repo.createRemoteRepository();
            await repo.createGitIgnore();

            const complete = await repo.setupRepository(url);

            if (complete) {
                console.log(chalk.green('All done!'));
            }
        } catch (error) {
            if (error) {
                switch (error.status) {
                    case 401:
                        console.log('xxx');
                        break;
                }
            }
        }
    })

寫完代碼后凳怨,在命令行中執(zhí)行如下命令即可:

node index.js create-repo

總結(jié)

總的來(lái)說(shuō),利用node.js來(lái)實(shí)現(xiàn)命令行工具還是比較簡(jiǎn)單的是鬼。目前有很多比較成熟的工具庫(kù)肤舞,基本上常用的功能都能夠?qū)崿F(xiàn)。如果有需要自己造輪子均蜜,大家可以參考本文的實(shí)現(xiàn)思路李剖。

參考資料

代碼參考https://github.com/Tereflech17/musette

https://github.com/tj/commander.js/blob/HEAD/Readme_zh-CN.md

https://www.lynda.com/Node-js-tutorials/Building-Your-First-CLI-App-Node

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市囤耳,隨后出現(xiàn)的幾起案子篙顺,更是在濱河造成了極大的恐慌,老刑警劉巖充择,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件德玫,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡椎麦,警方通過(guò)查閱死者的電腦和手機(jī)宰僧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)观挎,“玉大人琴儿,你說(shuō)我怎么就攤上這事段化。” “怎么了凤类?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵穗泵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我谜疤,道長(zhǎng)佃延,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任夷磕,我火速辦了婚禮履肃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘坐桩。我一直安慰自己尺棋,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布绵跷。 她就那樣靜靜地躺著膘螟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪碾局。 梳的紋絲不亂的頭發(fā)上荆残,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音净当,去河邊找鬼内斯。 笑死,一個(gè)胖子當(dāng)著我的面吹牛像啼,可吹牛的內(nèi)容都是我干的俘闯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼忽冻,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼真朗!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起僧诚,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蜜猾,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后振诬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蹭睡,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年赶么,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肩豁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖清钥,靈堂內(nèi)的尸體忽然破棺而出琼锋,到底是詐尸還是另有隱情,我是刑警寧澤祟昭,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布缕坎,位于F島的核電站,受9級(jí)特大地震影響篡悟,放射性物質(zhì)發(fā)生泄漏谜叹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一搬葬、第九天 我趴在偏房一處隱蔽的房頂上張望荷腊。 院中可真熱鬧,春花似錦急凰、人聲如沸女仰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)疾忍。三九已至,卻和暖如春床三,著一層夾襖步出監(jiān)牢的瞬間锭碳,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工勿璃, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人推汽。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓补疑,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親歹撒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子莲组,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354