什么是CLI工具?
CLI工具允许你从你的终端或命令行提示符中直接运行某些任务或操作。它们可以使用不同的编程语言来构建,而创建CLI工具的一种方法是使用Node.js。
在这篇文章中,你将学习如何使用Node.js创建一个CLI工具,对其进行测试,然后在npm上发布。
我们将创建一个名为todos-cli 的工具,允许用户查看他们的待办事项列表,向其中添加项目,然后将这些项目勾选掉。
你可以在这个GitHub存储库中找到本教程的全部代码。
设置项目
首先,创建一个存放CLI工具的目录。
mkdir todos-cli
cd todos-cli
接下来,我们将初始化我们的Node.js项目。
npm init
你将被要求输入几条信息。如果你愿意,你可以通过向命令传递-y 参数来跳过这一点。
npm init -y
这将把这些信息设置为默认值。一旦完成这些,我们需要安装一些软件包,这些软件包将有助于创建我们的CLI工具。这些包是
- commander。这个包使创建CLI工具更容易。它提供的功能将使我们能够设置命令、选项等。
- chalk。这个包可以让我们在控制台中打印彩色信息。它将帮助我们使我们的CLI工具看起来更漂亮。
- conf:这个包允许我们在用户的机器上保存持久的信息。我们将用它来保存用户的待办事项清单。
要安装这些包,请运行。
npm i commander chalk conf
一旦这些软件包安装完毕,我们就可以开始开发CLI工具了。
创建一个CLI工具
在项目的根目录下创建一个文件index.js 。这将是CLI工具的主条目,将初始化它的命令。
**注意:**如果你使用Windows进行开发,确保行结束符被设置为LF ,而不是CRLF ,否则工具将无法工作。大多数编码编辑器有选项可以设置。另外,在Windows上测试可能无法工作,所以我建议使用其他操作系统或使用Windows Subsystemfor Linux (WSL)。然而,你发布的工具最终将在所有操作系统上工作,没有任何问题。
为了确保你的CLI工具能正常工作,在index.js 的开头添加以下代码。
#! /usr/bin/env node
接下来,为了创建一个具有基本配置和功能的CLI,我们可以使用commander 。首先,让我们从commander 要求program 。
const { program } = require('commander')
为了声明一个命令,我们将使用以下函数。
program.command:接收一个定义命令格式的字符串program.description: 为用户描述该命令。当用户用选项执行我们的工具时,这很有帮助。--helpprogram.option:这个命令可以采取的选项,如果有的话program.action:这个命令所执行的动作,这将是一个函数。
我们将使用这些函数来声明我们的命令。但是,我们这个CLI工具的命令是什么呢?
我们需要以下命令。
todos list:这将列出用户待办事项列表中的任务todos add:这将在用户的待办事项列表中添加一个新任务。todos mark-done: 这将在列表中把特定的任务或所有任务标记为done
设置列表命令
列表命令将只显示用户之前添加的任务列表。它将不接受任何选项。用户应该能够通过在终端运行以下命令来运行它。
todos list
在index.js ,在我们之前添加的代码下面添加以下内容。
program
.command('list')
.description('List all the TODO tasks')
.action(list)
正如你所看到的,我们正在使用command 函数来声明我们CLI工具中的命令。你传递的参数是一个字符串,显示了预期的命令格式。我们还在使用description 函数来向用户描述这个命令,当他们用--help 选项运行我们的工具时。最后,我们要把这个动作分配给一个叫做list 的函数,我们很快就会创建这个函数。
将我们的命令放在不同的文件中有助于使我们的代码可读性和易于维护。
现在,创建文件commands/list.js 。这个文件将保存用户在其终端运行to-dos list 时运行的函数。这个函数将只是从配置中检索任务列表并显示它们。
为了存储和检索任务,我们将使用conf 包。它有以下功能。
set:这将在一个特定的键下设置我们需要的信息get:这将获得我们之前在特定键下设置的信息。
因此,让我们先在commands/list.js 中要求并实例化conf。
const conf = new (require('conf'))()
接下来,我们需要实现list函数,我们将导出该函数,以便在index.js 中使用。
function list () {
}
list 函数不接受任何参数,因为 list 命令没有任何选项或参数。
在列表函数中,我们将只是检索键todo-list 下的数据,这将是一个数组,并显示每个TODO任务。todo-list 将是一个格式如下的对象数组。
{
text, //string, the text of the todo task
done, //boolean, whether the todo task is marked done or not
}
现在我们知道了我们的数据结构将是怎样的,让我们回到list 函数。我们需要做的第一件事是检索待办事项列表中的任务。
const todoList = conf.get('todo-list')
接下来,如果用户的待办事项列表中有任务,我们将对其进行循环,并将已完成的任务显示为绿色,将仍未完成的任务显示为黄色。我们还将在开始时用蓝色告知用户每种颜色的含义。
如果用户的待办事项列表中没有任何任务,我们将向他们显示一条红色的信息,表示他们的待办事项列表中没有任何任务。
正如我们之前提到的,我们将使用chalk 来给终端中的用户的信息着色。因此,让我们在commands/list.js 的开头,在conf 之后要求它。
const conf = new (require('conf'))()
const chalk = require('chalk')
//rest of our code
然后,在list 函数中,让我们添加我们提到的if条件。
const todoList = conf.get('todo-list')
if (todoList && todoList.length) {
//user has tasks in todoList
} else {
//user does not have tasks in todoList
}
让我们先在else 部分工作。我们需要显示一条信息,告诉用户他们的待办事项列表中没有任何任务。我们需要使用chalk ,将其显示为红色。
else {
//user does not have tasks in todoList
console.log(
chalk.red.bold('You don\'t have any tasks yet.')
)
}
正如你从代码中看到的,我们可以用chalk 使用chalk.COLOR 输出不同颜色的消息,你可以通过使用chalk.COLOR.bold 使它变成粗体。COLOR 可以是红色、蓝色、黄色、绿色等等。
list 函数的部分用例已经完成。接下来的部分将是在用户确实有任务时显示任务。首先,我们将向用户显示一条信息,详细说明任务的颜色含义。
if (todoList && todoList.length) {
console.log(
chalk.blue.bold('Tasks in green are done. Tasks in yellow are still not done.')
)
}
在这里,我们是以蓝色粗体显示。
接下来,我们将在todoList ,对于每个任务,检查它是否已经完成,如果是,则显示为绿色。如果没有,则显示为黄色。
todoList.forEach((task, index) => {
if (task.done) {
console.log(
chalk.greenBright(`${index}. ${task.text}`)
)
} else {
console.log(
chalk.yellowBright(`${index}. ${task.text}`)
)
}
})
我们的list 函数已经完成了!最后,为了能够在index.js 中使用它,让我们导出该函数。
module.exports = list
command/list.js 的完整代码应该是。
const conf = new (require('conf'))()
const chalk = require('chalk')
function list () {
const todoList = conf.get('todo-list')
if (todoList && todoList.length) {
console.log(
chalk.blue.bold('Tasks in green are done. Tasks in yellow are still not done.')
)
todoList.forEach((task, index) => {
if (task.done) {
console.log(
chalk.greenBright(`${index}. ${task.text}`)
)
} else {
console.log(
chalk.yellowBright(`${index}. ${task.text}`)
)
}
})
} else {
console.log(
chalk.red.bold('You don\'t have any tasks yet.')
)
}
}
module.exports = list
让我们回到index.js 。我们只需要要求list 。
const list = require('./commands/list')
然后,在文件的末尾添加以下内容。
program.parse()
这是对commander 的需要。一旦我们完成了对命令的声明,我们就会解析用户的输入,这样commander 就可以弄清用户正在运行的命令并执行它。
测试CLI工具
我们的CLI现在已经准备好进行测试了。测试的第一步是在package.json 中添加以下键。
"bin": {
"todos": "index.js"
}
todos ,当从我们的todos-cli 命令中运行命令时,将在终端中使用。你可以把它改成你想要的任何东西。我们把它指向index.js ,因为这是我们的主要入口点。
这个步骤不仅对测试工具很重要,而且对以后的发布也很重要。所以,请确保添加它。
接下来,我们要运行以下程序,在我们的机器上全局安装我们的包。
npm i -g
一旦完成,我们现在就可以从终端运行我们的工具了让我们通过运行来测试它。
todos --help
你会看到关于我们的CLI的信息,你可以看到list 是在命令下。
现在,让我们试着运行我们的list命令。
todos list
它将只显示我们还没有任何任务。现在让我们实现一个新的命令,添加任务。
添加命令
add 命令将接受一个参数,这将是任务的文本。下面是该命令的一个例子。
todos add "Make Dinner"
"做晚餐 "是一个参数,将是任务的文本。我们使用引号是因为它有一个空格。你也可以尝试用\ 来逃避空格。如果文本中没有空格,那么就不需要加引号。
要添加一个新的命令,在index.js ,在list 命令的声明下,在program.parse() 之前,添加以下内容。
program
.command('add <task>')
.description('Add a new TODO task')
.action(add)
正如你所看到的,我们向command 函数传递了add <task> ,其中<task> 是用户传递的参数。在commander ,当一个参数是必须的,我们使用<ARG_NAME> ,而如果是可选的,我们使用[ARG_NAME] 。另外,你给参数起的名字就是在action 中传递给函数的参数的名字。
现在,我们需要实现add 函数。像我们对list 所做的那样,让我们创建文件commands/add.js ,内容如下。
const conf = new (require('conf'))()
const chalk = require('chalk')
function add (task) {
}
module.exports = add
注意,我们将task 传给了add 函数,这将是用户传递的参数。
add 函数将获取task 并使用conf 将其存储在todos-list 数组中。然后,我们将使用chalk 向用户显示一个绿色的成功信息。
我们将首先从conf 检索todo-list ,然后将新的任务推送到数组中,然后使用conf.set 来设置todo-list 的新值。
下面是add 函数的全部代码。
function add (task) {
//get the current todo-list
let todosList = conf.get('todo-list')
if (!todosList) {
//default value for todos-list
todosList = []
}
//push the new task to the todos-list
todosList.push({
text: task,
done: false
})
//set todos-list in conf
conf.set('todo-list', todosList)
//display message to user
console.log(
chalk.green.bold('Task has been added successfully!')
)
}
这很简单!在创建了列表命令后,事情越来越清晰,越来越容易理解。
现在,我们回到index.js ,要求我们刚刚创建的add 函数。
const add = require('./commands/add')
让我们来测试一下。在你的终端,运行。
todos add "Make Dinner"
我们将得到这样的信息:"任务已成功添加!"绿色的。为了检查任务是否真的被添加,在你的终端中运行。
todos list
你就可以看到你刚刚添加的任务。试着添加几个任务,看看列表的增长情况。
我们要添加的最后一条命令是mark-done ,它将把一个任务标记为done 。
mark-done 命令
mark-done 命令,默认情况下,会将所有任务标记为已完成。然而,如果我们给它传递--tasks 选项,并在后面跟上我们想标记为已完成的任务的至少一个索引,它将只标记这些任务为已完成。
这里有一个例子。
todos mark-done --tasks 1 2
为了简单起见,我们只使用任务的索引来标记它们完成。在现实生活中,你可能会给任务分配ID,这将是唯一和随机的。
让我们在add 命令下面声明我们的新命令。
program
.command('mark-done')
.description('Mark commands done')
.option('-t, --tasks <tasks...>', 'The tasks to mark done. If not specified, all tasks will be marked done.')
.action(markDone)
这个命令与之前的命令的主要区别是使用了option 函数。第一个参数是选项的格式。-t, --tasks 表示用户可以使用-t 或--tasks 来传递这个选项。<tasks...> 表示它可以是一个以上的任务,但由于我们使用的是<> ,这意味着它应该至少包括一个。第二个参数是该选项的描述。这在用户运行todos mark-done --help 时是很有用的。
接下来,我们将创建markDone 函数。就像我们之前做的那样,让我们创建文件commands/markDone.js ,内容如下。
const conf = new (require('conf'))()
const chalk = require('chalk')
function markDone({tasks}) {
}
module.exports = markDone
正如你所看到的,markDone 接受一个对象,其中包括一个tasks 属性。如果用户向命令传递了-t 或--tasks 选项,tasks 将是用户传递的数值的一个数组。否则,它将是未定义的。
我们在markDone 函数中需要做的是,从conf 检索todo-list 数组。如果todo-list 不是空的,就在它上面循环。如果tasks 是一个至少有一个项目的数组,只将用户输入的索引的任务标记为完成。如果tasks 是未定义的,那么将所有任务都标记为已完成。
这将是markDone 函数。
function markDone({tasks}) {
let todosList = conf.get('todo-list')
if (todosList) {
//loop over the todo list tasks
todosList = todosList.map((task, index) => {
//check if the user specified the tasks to mark done
if (tasks) {
//check if this task is one of the tasks the user specified
if (tasks.indexOf(index.toString()) !== -1) {
//mark only specified tasks by user as done
task.done = true
}
} else {
//if the user didn't specify tasks, mark all as done
task.done = true
}
return task
});
//set the new todo-list
conf.set('todo-list', todosList)
}
//show the user a message
console.log(
chalk.green.bold('Tasks have been marked as done successfully')
)
}
我们在todosList (如果它不是空的)内循环,map 。然后,我们检查tasks 是否被定义(这意味着用户已经通过特定的任务来标记为完成)。
如果定义了tasks ,通过检查索引是否在tasks 数组中,检查迭代中的当前任务项是否是用户指定的任务之一。注意,我们使用的是index.toString() ,因为tasks 数组将保存用户输入的字符串指标。如果索引是tasks 数组的一部分,则标记为完成,否则没有任何变化。
然而,如果没有定义tasks ,那么,正如我们之前提到的,我们将把所有项目都标记为完成。一旦循环完成,我们有了更新的列表,我们就会用conf.set ,将todo-list 设置为新的数组。最后,我们向用户显示一个成功信息。
最后,让我们回到index.js ,要求我们的markDone 函数。
const markDone = require('./commands/markDone')
现在,我们可以测试它了。首先尝试通过运行来标记所有完成的任务。
todos mark-done
如果一切正常,你可以运行todos list ,看到所有项目现在都是绿色的。
接下来,试着再添加一些任务,然后用它们的索引来标记这些任务的完成,这是一个将单个任务标记为完成的例子。
todos mark-done -t 1
或者标记多个任务。
todos mark-done -t 1 3 6
你可以尝试任何组合,然后使用todos list 命令检查哪些被标记为完成,哪些没有。
我们的CLI工具已经完成了!todos-cli 现在允许用户添加任务,查看任务,并将其标记为已完成。
下一步,也是最后一步,是发布你的CLI工具。
发布CLI工具
用Node.js构建的CLI工具是作为一个包在NPM上发布的。因此,如果你还没有一个NPM账户,你需要创建一个NPM账户。
在你创建了你的NPM账户后,在你的项目目录下的终端,运行以下程序。
npm login
你会被要求输入你的用户名、密码和电子邮件。如果全部正确,你将被登录。
接下来,运行下面的命令。
npm publish
这个命令将在npm上公开发布你的CLI工具。如果存在另一个同名的包,你可能会得到一个错误。如果是这样的话,你将需要改变软件包的名称。package.json
"name": "PACKAGE_NAME",
请记住,PACKAGE_NAME 与我们用于 CLI 命令的名称不同。PACKAGE_NAME 用于在你的机器上安装工具,但你在bin 中指定作为密钥的名称是用于从终端访问该工具的名称。
如果npm上没有其他软件包有相同的名字,你的软件包将是公开的,可供使用的要安装它,请运行。
npm i -g <PACKAGE_NAME>
<PACKAGE_NAME> 是你为该包挑选的名字。注意,如果你在开发过程中已经在包中运行了npm i -g ,那么在使用工具目录下的npm remove -g 安装你发布的包之前,最好先把它删除。
更新CLI工具包
如果后来你需要更新你的CLI工具包,你可以用下面的命令来做。
npm version <UPDATE_TYPE>
<UPDATE_TYPE> 可以是以下的一种。
patch:一个小改动。这将只增加版本的最后一个数字。这通常是用来修复错误或做一些小的修改,不应该影响最终用户对你的工具或包的使用语法。minor:一个小的改动。这将使版本的第二个数字增加。这通常用于你的软件包或工具的小改动,也许是增加功能,但保持旧的功能不动。major:一个主要的变化。这将增加版本的第一个数字。这通常用于你的软件包或工具的重大变化,可能会影响最终用户对它的使用。
你可以到这里来阅读更多关于版本控制的信息。
总结
恭喜你,你学会了如何使用Node.js创建一个CLI工具。可能性是无限的,所以去创造一些令人敬畏的东西吧!
The postCreating a CLI tool with Node.jsappeared first onLogRocket Blog.