转自:https://developer.telerik.com/featured/creating-node-js-command-line-utilities-improve-workflow/ 类似的oclif

Once upon a time, the command line seemed scary and intimidating to me. I felt as if it stared back at me blankly like the price tag on something extremely expensive saying, “If you have to ask, it’s not for you.” I preferred the comfort of the buttons and menus laying out for me what the possibilities were.

Today, the command line is all about completing tasks as fast as you can type them, without the overhead of a GUI program. I’ve even spoken before about how npm offers a wealth of command-line utilities that can improve a developer’s workflow. And since then, the list of tools has only grown tremendously.

However, in every job there are repetetive tasks that both unique to that position but also ripe for automating…if only it were easy to build yourself a tool to do so.

Today, I’m going to explore how, using an npm library named Vorpal, it is relatively easy to create your own command line applications. And I do mean applications.

What differentiates Vorpal from rolling your own command line tools is that it makes it easy to build much more complex and immersive command line applications rather than simple, single-command utilities. These command line applications run within their own context that offers built-in help, command history, tabbed auto-completion and more. Combining Vorpal with the abundance of modules available on npm opens up a ton of possibilities.

A Simple Example

Most tutorials start with “Hello World.” That’s boring. I want something with a little more attitude. I’m sure you’re already thinking what I’m thinking. Right, let’s build a “Hello Daffy Duck.”

My sample command line application is going to be based off the Rabbit Fire episode from Looney Tunes. If you don’t know which one I am talking about, then you are missing out. It’s a classic.

Of course, first you need to install Vorpal.

npm install vorpal

Now, let’s create an daffy.js file and require Vorpal.

var vorpal = require('vorpal')();

DELIMITER

One of the cool things about Vorpal is that it offers a variety of methods for you to customize the “UI” of your utility. For example, let’s start by making our command line delimiter “daffy.”

vorpal
.delimiter('daffy$')
.show();

Go ahead and run the program via node daffy.js and you’ll see something like this:

At this point our utility doesn’t do anything, so let’s add some functionality.

COMMANDS

The primary way of interacting with a command line utility is via commands. Commands in Vorpal have a lot of flexibility – for instance, they can be a single word or multiple words and have required arguments as well as optional arguments.

Let’s start with a simple one word command – when we say “duck” then Daffy will respond with “Wabbit.”

// duck
vorpal
.command('duck', 'Outputs "rabbit"')
.action(function(args, callback) {
this.log('Wabbit');
callback();
});

The command() method defines the text of the command as well as the help text (i.e. what will show up when I use --help in the command line). We then use Vorpal’s log() method rather than the console.log() to output to the console. Calling the callback method will return the user to our custom console, as opposed to exiting the program.

COMPLETED CODE

The complete code for the Daffy command line application is below. It includes several multi-word commands that all work fairly similarly.

var vorpal = require('vorpal')(),
duckCount = 0,
wabbitCount = 0; // duck
vorpal
.command('duck', 'Outputs "rabbit"')
.action(function(args, callback) {
this.log('Wabbit');
callback();
});
vorpal
.command('duck season', 'Outputs "rabbit season"')
.action(function(args, callback) {
duckCount++;
this.log('Wabbit season');
callback();
}); // wabbit
vorpal
.command('wabbit', 'Outputs "duck"')
.action(function(args, callback) {
this.log('Duck');
callback();
});
vorpal
.command('wabbit season', 'Outputs "duck season"')
.action(function(args, callback) {
// no cheating
if (duckCount < 2) {
duckCount = 0;
this.log('You\'re despicable');
callback();
}
else if (wabbitCount === 0) {
wabbitCount++;
this.log('Duck season');
callback();
}
// doh!
else {
this.log('I say it\'s duck season. And I say fire!');
vorpal.ui.cancel();
}
}); vorpal
.delimiter('daffy$')
.show();

Since the methods all work similarly, this should be pretty straightforward. You’ll notice the use of vorpal.ui.cancel(), this exits the application and returns to the main command line console (one note, this can’t be followed by additional code or it will fail to function).

Now that the application is built, a help command is built in, without requiring any additional code. Notice that the multiword commands are automatically grouped.

Let’s see our application in action!

A Real World Example

While I’m certain you will find hours of entertainment in the Daffy sample, let’s try something a little more complex. In this example application, we’ll explore things like required and optional arguments as well as getting feedback from the user via prompts.

The utility we are going to start building will allow us to quickly resize and reformat images individually or in bulk. In my role managing this site (i.e. the Telerik Developer Network), I often receive articles with very numerous large PNG formatted images. These images need to be resized and reformatted before posting to reduce the weight of the images on the page. Being able to quickly resize every image in a folder would make this a much simpler and less tedious task.

SHARP

Of course, on its own, Vorpal can’t do image resizing. And while Node.js has a filesystem module, it doesn’t have the capability to manipulate images.

Luckily, there are a number of image resizing modules on npm. The one I chose to use is called Sharp(documentation).

Sharp works fast, but, unfortunately, the setup is a little complex depending on what platform you are running on. Sharp depends on a library called libvips, which we’ll need to install first. I am working on a Mac with Homebrew installed, which offered the easiest route for me to get libvips.

brew install homebrew/science/vips --with-webp --with-graphicsmagick

Once libvips is installed, then install Sharp.

npm install sharp

Of course, to use Sharp within our application, we’ll need to require it.

var sharp = require('sharp');

MMMAGIC

Since we’ll be bulk modifying any images in a given directory, we’ll need to be able to tell that any given file is an image or not. Again, the filesystem module, while powerful, doesn’t quite meet our needs.

For this purpose, I chose a npm module called MMMagic. MMMagic allows you to pass a file and it will get the MIME type, encoding and other metadata for you.

There are no complicated prerequisites for MMMagic, simply…

npm install mmmagic

Now require it…

var mmm = require('mmmagic');

We’ll also need an instance of it to work with.

var Magic = mmm.Magic;

The reason for having both the mmm and Magic variables is because later on we’ll need to access some constants that are in the root (mmm in this case).

PUTTING ALL THE PIECES TOGETHER

We’ll start by creating a single resize command that will take both a required argument (the path of the directory or image that you want to resize) as well as some optional arguments. The optional arguments are available as a shortcut for the user to bypass some of the prompts and make our task even faster to complete.

vorpal
.command('resize <path> [width] [height]', 'Resize an image or all images in a folder')
.action(function(args, cb) {
const self = this;
path = args.path; // if a width and height are defined when the command is called, set them
if (args.width)
width = args.width;
if (args.height)
height = args.height; // resize all images in a directory to a set width
if (fs.lstatSync(path).isDirectory()) {
this.prompt({
type: 'confirm',
name: 'continue',
default: false,
message: 'Resize all images in the folder? ',
},
function(result){
if (result.continue) {
files = fs.readdirSync(args.path);
// skip the prompts if a width was supplied
if (width)
doResize(self);
else
getWidth(self);
}
else {
cb();
}
});
}
// resize a single image
else if (fs.lstatSync(args.path).isFile()) {
// get the file name without the path
files = [args.path.split("/").pop()];
//get the path without the file name
path = args.path.substr(0, args.path.lastIndexOf('/'))
// skip the questions if a width was supplied
if (width)
doResize(self);
else
getWidth(self);
}
});

Let’s walk through the code a bit. The required argument, path, is denoted with angle brackets (i.e. <and >). The optional arguments, width and height, are denoted by square brackets (i.e. [ and ]). The arguments are available in the args object.

After setting our variables, we determine whether the user has passed a filename or a directory and respond accordingly (note that a future improvement would be to handle situations where the user passes an invalid file or directory). If the user passed a directory, we display a corfirm-type prompt to ensure that they intend to resize every image in the directory.

Finally, in both cases we either move on to complete the resizing or move to the next prompt if the user did not specify a width argument in the command. Let’s look at the prompts to get the width and height when the user doesn’t specify them.

INPUT PROMPTS

The below prompts request additional information for the maximum width and height via an input-type prompt on the command line. The way the application currently works is that it will shrink anything over these sizes to the specified dimensions, but will not enlarge items that are already less than the specified dimensions.

// ask for a width
function getWidth(v) {
self = v;
self.prompt({
type: 'input',
name: 'width',
default: false,
message: 'Max width? ',
},
function(result){
if (result.width)
width = result.width;
getHeight(self);
});
} // ask for a height
function getHeight(v) {
self = v;
self.prompt({
type: 'input',
name: 'height',
default: false,
message: 'Max height? ',
},
function(result){
if (result.height)
height = result.height;
doResize(self);
});
}

Both inputs have a default if no value is entered (i.e. the user simply hits the enter key).

RESIZING THE IMAGES

The next step is to loop through the specified files and resize them. Note that in the case where a user specifies a single file, it is still placed within the same array of files, it just only contains a single file.

function doResize(v) {
self = v;
// create a folder to dump the resized images into
if (!fs.existsSync('optimized'))
fs.mkdirSync('optimized') for (var i in files) {
detectFileType(files[i]);
}
}

You’ll note above that we create a folder to place the optimized images.

Within the loop, we call a function to detect the file type to prevent errors when the folder contains files other than images.

function detectFileType(filename) {
var fullpath = path + "/" + filename,
filenameNoExt = filename.substr(0, filename.lastIndexOf('.'));
magic = new Magic(mmm.MAGIC_MIME_TYPE); // make sure this is an appropriate image file type
magic.detectFile(fullpath, function(err, result) {
if (!err) {
if (result.split('/')[0] == 'image')
// resize to a jpeg without enlarging it beyond the specified width/height
sharp(fullpath)
.resize(parseInt(width),parseInt(height))
.max()
.withoutEnlargement()
.jpeg()
.toFile('optimized/'+filenameNoExt+'.jpg', function(err) {
if (err)
self.log(err);
else
self.log('Resize of ' + filename + ' complete');
});
}
else
self.log(err);
});
}

In the detectFileType method we use the MMMagic module to determine if the current file is actually an image. Sharp can handle most image types, so at this point we’re simply assuming that if it is an image, Sharp can handle it (plus, if the image processing fails for some reason, we do note it in the console output).

Lastly, we finally put Sharp to use to perform the resize. The resize() method can take a width and/or height, and if only one is passed, it will resize while maintaining the same aspect ratio. The max() and withoutEnlargement() methods ensure that we only resize images larger than the specified sizes, though all images will be processed and converted into a JPEG regardless.

If the resize is successful, we note it in the console. If there is an error, we note that as well.

Let’s see the utility in action. In the example below, I am passing just the directory so that you can see the prompts (note that I do not supply a height). As you can see by the results, the images in the optimized folder are all JPEG (including the one PNG from the source) and are all 600px width (except the one image that was already less than 600px wide).

We could bypass the prompts by using the command resize images 600 which would have specified the width. We also could have simply resized a single image using resize images\test3.png 600.

FUTURE IMPROVEMENTS

While this utility is already useful, I would love to add some improvements. First, of course, even though this is a personal utility, the code could almost certainly be improved (I’m relatively new to using Node.js, so I’m sure there are things I can be doing better).

Second, I should package the command line utility so that it is installable. This would allow me to call it much more easily from whichever directory I happen to be working in.

Also, at this point, it really is a single command utility, but there are a number of ways I would like to see it expand into a full application. For example, I didn’t specify an output quality for my JPEG, which is something I will need to do in the long run. I could also create additional commands to allow different kinds of resizing or even cropping – Sharp offers a good number of operations that can be performed on images that we didn’t cover here. In the long run, it could become a full command-line image tool rather than just a resizer.

Conclusion

I’m sure that there are a ton of tasks in your own job that you could potentially automate. Creating a complete command-line application for these purposes is easy using Vorpal. These applications can get very complex – we only touched on commands and prompts, but Vorpal offers a fairly extensive API.

So get busy automating those time-consuming and repetetive tasks with custom command-line applications. You don’t even need to tell your boss. Perhaps you can use the spare time to hunt wabbits – it’s wabbit season after all!

Creating Node.js Command Line Utilities to Improve Your Workflow的更多相关文章

  1. NPM(Node.js) 使用介绍

    前言:express 推出了4.X,自己尝试了一下,出现了各种问题.结果查看了各种文档和问题,现在在这个给大家分享下4.X版本的安装. NPM 使用介绍 NPM是随同NodeJS一起安装的包管理工具, ...

  2. window环境下安装node.js

    在使用sublime text 3 过程中,node.js装了好几次都没有成功,今天终于成功了,现将安装过程整理一下. 安装过程中主要参考了以下代码: 第一,下载文件 https://nodejs.o ...

  3. windows中安装node.js和测试

    首先下载node.js安装包:下载页面:http://down.keleyi.com/goto/node.js.htm 选择windows msi安装包,根据自己操作系统选择32位或者64位安装包.然 ...

  4. Node.js学习

    1. 下载 网址:https://nodejs.org/download/ 2. 添加express框架 如下图,运行Node.js command prompt 在命令行中输入:npm instal ...

  5. 开始学习node.js了,第一节,fs文件系统 【fs.rename】重命名文件/文件夹

    var fs=require('fs');fs.rename('c:\\a','c:\\a2',function(err){ if(err) console.log('error:'+err);}); ...

  6. [Node.js] Broswerify -- 1

    Browserify is a tool that brings node.js style development to the browser. The thing you can see on ...

  7. 深入浅出Node.js (附录D) - 搭建局域NPM仓库

    D.1 NPM仓库的安装 D.1.1 安装Erlang和CouchDB D.1.2 搭建NPM仓库 D.2 高阶应用 D.2.1 镜像仓库 D.2.2 私有模块应用 D.2.3 纯私有仓库 D.3 总 ...

  8. node.js及相关组件安装

    第一步:下载安装文件(下载地址:官网http://www.nodejs.org/download/ )第二步:安装nodejs(双击直接安装) 安装完成后使用命令行查看版本信息,出现版本号说明安装成功 ...

  9. APNs功能之Node.js和Mysql应用总结

    APNs功能之Node.js和Mysql应用总结 这篇文档主要是总结Node.js和Mysql的学习心得体会.当然也可以看作是此前所写的消息推送服务的续篇. 简单描述下应用背景,我们的应用需要实现苹果 ...

随机推荐

  1. linux下Mysql多实例实现

    什么是MySQL多实例 MySQL多实例就是在一台机器上开启多个不同的服务端口(如:3306,3307),运行多个MySQL服务进程,通过不同的socket监听不同的服务端口来提供各自的服务:: My ...

  2. L1-053 电子汪

    据说汪星人的智商能达到人类 4 岁儿童的水平,更有些聪明汪会做加法计算.比如你在地上放两堆小球,分别有 1 只球和 2 只球,聪明汪就会用“汪!汪!汪!”表示 1 加 2 的结果是 3. 本题要求你为 ...

  3. DevExpress v18.1新版亮点——Analytics Dashboard篇(二)

    用户界面套包DevExpress v18.1日前正式发布,本站将以连载的形式为大家介绍各版本新增内容.本文将介绍了DevExpress Analytics Dashboard v18.1 的新功能,快 ...

  4. Centos7部署kubernetes API服务(四)

    1.准备软件包 [root@linux-node1 bin]# pwd /usr/local/src/kubernetes/server/bin [root@linux-node1 bin]# cp ...

  5. SQL--数据--基本操作

    数据操作 新增数据 有两种方案方案1:给全表字段插入数据,不需要指定字段列表:要求数据的值出现的顺序必须与表中设计的字段出现的顺序一致:凡是非数值数据,都需要使用引号(建议是单引号)包裹 insert ...

  6. python学习 day01 基础介绍

    一.编程的目的 1.什么是语言?编程语言又为何? 语言是一种事物与另外一种事物沟通的介质.编程语言是程序员和计算机沟通的介质. 2.什么是编程? 程序员把自己想要计算机做的事用编程语言表达出来,编程的 ...

  7. Python实现简单的udp打洞(P2P)

    UDP穿越NAT的具体设计 首先,Client A登录服务器,NAT 1为这次的Session分配了一个端口60000,那么Server S收到的Client A的地址是200.0.0.132:600 ...

  8. 莫烦tensorflow(3)-Variable

    import tensorflow as tf state = tf.Variable(0,name='counter') one = tf.constant(1) new_value = tf.ad ...

  9. Ubuntu 16.04安装vsftpd 并开启ftp服务

    1. 安装 sudo apt-get install vsftpd 2.可以使用下列命令来打开,关闭,重启ftp服务 sudo /etc/init.d/vsftpd start sudo /etc/i ...

  10. meson 中调用shell script

    meson 中有时需要调用其他脚本语言,加之对meson build system接口和原理不熟悉,无奈只有静心学习meson 官方文档,终于皇天不负有心人让我找到了: run_command() 只 ...