美文网首页
命令行工具CLI以及FS API

命令行工具CLI以及FS API

作者: DHFE | 来源:发表于2018-08-22 03:35 被阅读112次

    构建首个应用:一个简单的命令行文件浏览器,其功能是允许用户读取和创建文件。

    需求:

    • 程序需要在命令行运行。这就意味着程序要么通过node命令在执行,要么直接执行,然后如通过终端提供交互给用户输入、输出。
    • 程序启动后,需要显示当前目录下列表。
    • 选择某个文件,程序需要显示该文件内容。
    • 选择一个目录时,程序需要显示该目录下的信息。
    • 运行结束后程序退出。

    根据上述需求,可以细分几个步骤。

    • 创建模块
    • 决定采用同步的fs还是异步的fs
    • 理解什么事流(Stream)
    • 实现输入输出
    • 重构
    • 使用fs进行文件交互
    • 完成

    编写首个Node程序

    开始基于上述步骤来编写一个模块。模块由几个文件组成,使用任意文本编辑器。

    创建模块

    新建目录,命名为:file-explorer

    首先定义package.json文件,这样既可以方便 NPM中注册的模块依赖进行管理,将来也能对模块进行发布。
    尽管此项目仅仅用到Node.js的核心模块API(因此不会从NPM仓库中获取模块),但是,我们还是需要一个简单的package.json文件。

    {
        "name": "file-explorer",
        "version": "0.0.1",
        "description": "A command-file file explorer!"
    ![1.jpg](https://img.haomeiwen.com/i1666407/e791bb177381a46c.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    }
    

    验证package.json文件是否有效,可以运行npm install。
    正确不会输出任何内容,否则会抛出JSON异常的错误。

    然后创建一个index.js文件。

    同步还是异步

    由于stido API是全局process对象的一部分,所以,我们的程序唯一的依赖就是fs模块。

    /**
     * Module dependencies.
     */
    
    var fs = require("fs");
    
    

    首先获取当前目录的文件列表。
    fs模块是唯一一个同时提供同步和异步的API的模块。举个例子,要想获取当前目录的文件列表,可以这样:

    console.log(fs.readdirSync(__dirname));
    

    它会立刻返回内容或者当有错误发生时抛出相应异常。


    下面是异步版本:

    function async(err,files) {
        console.log(files);
    }
    
    require("fs").readdir(".",async);
    

    我们在之前提到过,要在单线程中创建能够处理高并发的高效程序,就得采用异步、事件驱动的程序。

    未标题-1.jpg

    尽管这个命令行创建并非此类型创建(因为同一时间只会有一个人在读取文件),但是,为了学习node.js中最重要也是最具有挑战的部分,还是保持这种异步的代码风格。


    为了获取文件列表,我们需要使用fs.readdir。我们提供的回调函数首个参数是一个错误对象(如果没有错误发生,该对象为Null),另外一个参数是一个files数组:

    fs.readdir(".",function(err,files) {
        console.log(files);    
    });
    

    到现在,你知道了fs模块同时提供同步和异步的API来操作文件系统,接下来进入另一个基础概念——流。


    理解什么是流(stream)

    console.log会输出控制台。事实上,console.log内部做了这样的事情:它在指定的字符串后加上\n(换行)字符,并将其写到stdout流中。

    process全局对象中包含了三个流对象,分别对应三个UNIX标准流:

    • stdin:标准输入
    • stdout:标准输出
    • stderr:标准错误

    第一个stdin是一个可读流,而stdout和stderr都是可写流。

    stdin流默认的状态是暂停的(paused)。通常,执行一个程序,程序会做一些处理,然后退出,不过,有些时候,程序需要一直处于运行状态来接收用户输入数据。
    当回复那个流时,Node会观察对应的文件描述符(在UNIX下为0),随后保持事件循环的运行,同时保持程序不退出,等待事件触发。除非有IO等待,否则node.js总是会自动退出。

    流的另外一个属性是它默认的编码。如果在流上设置了编码,那么会得到编码后的字符串(utf-8、ascii等)而不是原始的Buffer作为事件参数。

    Steam对象和EventEmitter很像(事实上,前者继承自后者)。在Node中,你会接触到各种类型流,如TCP套接字、HTTP请求等。简而言之,当涉及持续不断地对数据进行读写时,流就出现了。

    输入和输出

    既然已经知道运行程序后大概是怎样的一个情形了,我们来尝试写第一部分,列出当前目录下的文件,然后等待用户输入。

    var fs = require("fs");
    
    fs.readdir(process.cwd(),function(err,files) {
        console.log("");
    
        if (!files.length) {
            return console.log("\033[31m No files to show!\033[39m\n");
        }
    
        console.log("     Select which file or directory you want to see\n'");
    
        function file(i) {
            var filename = files[i];
    
            fs.stat(__dirname + "/" + filename, function(err,stat) {
                if (stat.isDirectory()) {
                    console.log("         " + i + "    \033[36m" + filename + "/\033[39m");
                } else {
                    console.log("        " + "\033[90m]" + filename + "\033[39m");
                }
    
                i++;
                if (i == files.length) {
                    console.log("");
                    process.stdout.write("    \033[33mEnte your choick: \033[39m");
                    process.stdin.resume();
                } else {
                    file(i);
                }
            })
        }
    
        file(0);
    })
    
    
    

    为了输出更加友好,我们首先输出一个空行:

    console.log("");
    

    如果files数组为空,告知用户当前目录没有文件。文件周围的\033[31m\033[39m是为了让文本呈现为红色。例子中最后一个字符又是换行符\n,也是为了输出可读性更好。

        if (!files.length) {
            return console.log("\033[31m No files to show!\033[39m\n");
        }
    

    下一行则是让用户执行操作:

        console.log("     Select which file or directory you want to see\n'");
    
    

    紧接着,定义了一个函数,数组中每个元素都会执行该函数。这里也出现了第一种#异步流控制模式:串行执行。

    function file (i) {
        // ...
    }
    

    然后,先获取文件名,再查看文件名对应路径的情况。fs.stat会给出文件或者目录的元数据:

    var filename = files[i];
    fs.stat(__dirname + "/" + filename, function(err,stat) {
         // ...
    }
    

    回调函数还给出了错误对象(如果有的话)和一个Stat对象。本例中使用到的Stat对象上的方法是isDirectory

                if (stat.isDirectory()) {
                    console.log("         " + i + "    \033[36m" + filename + "/\033[39m");
    ![QQ截图20180824014417.jpg](https://img.haomeiwen.com/i1666407/9023f7e60706dd4b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
                } else {
                    console.log("        " + "\033[90m]" + filename + "\033[39m");
                }
    
    

    如果路径所代表的是目录,我们就用别于文件的颜色标识出来。

    接下来就到了流控制中的核心部分了,计数器不断递增,与此同时,检查是否还有未处理的文件:

                i++;
                if (i == files.length) {
                    console.log("");
                    process.stdout.write("    \033[33mEnte your choick: \033[39m");
                    process.stdin.resume();
                } else {
                    file(i);
                }
    
    

    如果所有文件处理完毕,此时提示用户进行选择。注意,这里使用的是process.stdout.write而不是console.log,这样就无须换行,让用户可以直接在提示语后进行输入。

    console.log("");
    process.stdout.write("    \033[33mEnte your choice: \033[39m");
    
    程序提示用户像stdin进行输入
    `process.stdin.resume()`:等待用户输入。
    `process.stdin.setEncoding('utf8')`:设置流编码为utf8,这样就能支持特殊字符了。
    

    如果还有未处理的文件,则递归调用函数来进行处理:

    file(i);
    

    直到列出所有文件、用户输入完毕后,紧接着进行下一步串行处理。

    重构

    要做重构,我们从为几个常用的变量(stdin和stdout)创建快捷变量开始:

    var fs = require("fs"),
        stdin = process.stdin,
        stdout = process.stdout
    

    由于书写的代码都是异步的,因此,会有这样的问题:随着函数量的增长(特别是流控制层的增加),许多的函数嵌套会让程序的可读性变差。

    为了避免此类问题,我们可以为每一个异步操作预先定义一个函数。
    首先,我们抽离出一个读取stdin函数:

    // called for each file walked in the directory
    function file(i) {
        var filename = files[i];
        
        fs.stat(__dirname + "/" + filename, function (err, stat) {
            if (stat.isDirectory()) { // 判定是否为一个目录还是一个文件
                console.log("    " + i + "    \033[36m" + filename + "/\033[39m");
            } else {
                console.log("    " + i + "     \033[90m" + filename + "\033[39m");
            }
    
            if (++i === files.length) {
                read();
            } else {
                file(i);
            }
        });
    }
    // read user input when files are shown
    function read() {
        console.log("");
        stdout.write("    \033[33mEnte your choice: \033[39m");
        stdin.resume();
        stdin.setEncoding("utf8");
    }
    

    注意,上述代码所使用的是新的stdin的引用和stdout的引用。

    读取用户输入后,接下来要做的就是根据用户输入做出相应处理。用户需要选择要读取的文件,所以,代码层面,设置了stdin的编码后,就开始监听其data事件:

    function read() {
        // ...
        stdin.on("data",option)
    }
    // called with the option supplied by the user
    function option(data) {
        if (!files[Number(data)]) {
            stdout.write("     \033[31mEnter your choice: \033[39m");
        } else {
            stdin.pause();
        }
    }
    

    这里检查用户的输入是否匹配files数组的下标。还记得files数组是fs.readdir回调函数中的一部分吧。另外,注意的是,上述代码中,我们将utf-8编码的字符串类型data转化为Number类型来方便做检查。

    如果检查通过,我们要确保再次将流暂停(回到默认状态),以便于之后做完fs操作后,程序顺利退出。

    现在程序能够与用户进行交互了,将当前目录的文件列表展现给用户,下面来实现读取和显示文件内容。

    用fs进行文件操作

    定位到文件,读取它:

    function option(data) {
        var filename = files[Number(data)];
        
        if (!filename) {
            stdout.write("    \033[31mEnter your choice: \033[39m");
        } else {
            stdin.pause();
            fs.readFile(__dirname + "/" + filename , "utf8" , function(err,data) {
                console.log("");
                console.log(
                    "\033[90m" + data.replace(/(.*)/g,'    $1') + "\033[39m"
                );
            });
        }
    }
    

    提醒:我们可以事先指定编码,这样得到的数据就是相应的字符串了:

    fs.raedFile(__dirname + '/' + filename, "utf8' , function(err,data) {
        // ...
    })
    

    接着,可以使用正则表达式添加一些辅助缩进后将文件内容进行输出:

    data.replace(/(.*)/g , '    $1')
    

    不过,要是选择的是目录呢?这种情况下,应当将其目录下的文件列表显示出来。

    为了避免再次执行fs.stat,我们在file函数中,将Stat对象保存了下来:

    // ...
    var stats = [];
    function file(i) {
        var filename = files[i];
    
        fs.stat(__dirname + "/" + filename , function(err,stat) {
            stats[i] = stat;
            //...
        })
    }
    

    现在可以轻松地在option函数中进行检查操作了。

    最终源码:

    // 模块依赖
    var fs = require('fs'),
        stdin = process.stdin,
        stdout = process.stdout;
    
    // 读取当前目录下的文件内容
    // 返回当前进程的目录路径
    // console.log(process.cwd());
    fs.readdir(process.cwd(), function (err, files) {
        // 将文件保存到 files 数组中
        // console.log(files);
        // 空行
        console.log('');
        // 当没有文件的提示信息
        if (!files.length) {
            return console.log('没有文件');
        }
        // 有文件的提示信息
        console.log('请选择你所看见的文件或者目录');
    
        // 保存目录
        var stats = {};
        // 遍历文件--目录还是文件
        // i 表示选择文件的索引
        function file(i) {
            var filename = files[i];
            // console.log(filename);
            // 返回文件或者目录的元数据
            fs.stat(__dirname + '/' + filename, function (err, stat) {
                stats[i] = stat;
                // 判断目录或者文件
                if (stat.isDirectory()) {
                    console.log('   \033[36m' + i + ' ' + filename + '\033[39m');
                } else {
                    console.log('   ' + i + ' ' + filename);
                }
                // 遍历完毕
                // console.log(files.length);
                if (++i == files.length) {
                    read();
                } else {
                    // 递归
                    file(i);
                }
            });
        }
    
        // 读取用户输入的信息
        function read() {
            // 空行
            console.log('');
            // 提示输入目录名称信息---不换行
            stdout.write('请输入你的选择(数字):');
            // 标准输入流默认是暂停的,我们要恢复它
            // 等待用户输入
            stdin.resume();
            // 编码
            stdin.setEncoding('utf8');
            // 监听用户的输入
            stdin.on('data', option);
        }
    
        // 用户输入的信息
        function option(data) {
            // 选择文件的名称
            var filename = files[Number(data)];
            // console.log(filename);
            if (!filename) {
                stdout.write('请输入你的选择(数字):');
            } else {
                stdin.pause();
                // 读取文件的内容
                if (stats[Number(data)].isDirectory()) {
                    fs.readdir(__dirname + '/' + filename, function (err, files) {
                        console.log('');
                        console.log('(' + files.length + ' 个文件)');
                        // 遍历文件名称
                        files.forEach(function (file) {
                            console.log(' - ' + file);
                        });
                        console.log('');
                    })
                } else {
                    fs.readFile(__dirname + '/' + filename, 'utf8', function (err, data) {
                        console.log('');
                        // 行缩进
                        console.log(data.replace(/(.*)/g, ' $1'));
                    })
                }
    
            }
        }
        file(0);
    })
    

    注意点:

    process.cwd()__dirname的区别:

    process.cwd():运行当前脚本的工作目录的路径process.cwd()
    __dirname:是被执行的js 文件的地址 ——文件所在目录__dirname


    argv
    process.argv包含了所有Node程序运行时的参数值:


    返回一个数组,第一个元素为process.execPath,第二个元素为当前执行的JavaScript文件路径。剩余的元素为其他命令行参数。

    --

    退出
    要让一个应用退出,可以调用process.exit并提供一个退出代码。比如,当发生错误时,要退出程序,这个时候最好使用退出代码。

    console.error("An error occurred");
    process.exit();
    

    ANSI转义码

    要在文本终端下控制格式、颜色以及其他输出选项,可以使用ANSI转义码。
    在文本周围添加的明显不用于输出的字符,称为非打印字符

    console.log('\033[90m' + data.replace(/(.*)/g , '    $1') + '\033[39m')
    
    • \033表示转义序列开始。
    • [表示开始颜色设置。
    • 90表示前景色为亮灰色。
    • m表示颜色设置结束。

    结尾的39用来将颜色再设置回去。


    fs
    fs模块允许通过Stream API来对数据进行读写操作。与readFile及writeFile方法不同,它对内存的分配不是一次完成的。

    比如,有一个大文件,文件内容上百万行逗号分隔文本组成。要完成的读取该文件进行解析,意味着一次性分配很大的内存。更好的方式应当是一次只读取一块内容,以行尾结束符("\n")来切分,然后再逐块进行解析。

    fs.createReadStream方法允许为一个文件创建一个可读的Stream对象。
    来看例子:

    fs.readFile("my-file.txt" , function(err,contents) {
        // 对文件进行处理
    })
    

    上述例子中,回调函数必须要等到整个文件读取完毕、载入到RAM、 可用的情况下才会触发

    而下面的例子,每次会读取可变大小的内容块,并且每次读取之后会触发回调函数:

    var stream = fs.createReadStream("my-file.txt");
    stream.on("data",function(chunk) {
        // 处理文件部分内容
    });
    stream.on("end",function(chunk) {
        //  文件读取完毕
    })    
    
    

    为什么这种能力很重要呢?假设有个很大的视频文件需要上传到某个Web服务。这时,你无须在读取完整的视频内容后开始上传,使用Stream就可以大大提速上传过程。

    这对日志纪录的例子也一样,特别是使用可写stream。假设有个应用需要纪录网站上的访问情况,这时,为了将纪录写到文件中,让操作系统进行打开/关闭文件的操作可能就很低效(每次都要在磁盘上进行查找文件操作)。

    所以,这就是一个很好的使用fs.WriteStream的例子。打开文件操作只做一次,然后写入每个日志项时都调用.write方法。

    监视
    Node允许监视文件或目录的变化。监视意味着当文件系统中文件(或者目录)发生变化时,会分发一个事件,然后触发指定的回调函数。

    该功能在Node生态系统中被广泛使用。举例来说,有人喜欢用一种可以编译为CSS的语言来书写CSS样式。这个时候,就可以使用监视功能,当源文件发生改变时,就将其编译为CSS文件。


    相关文章

      网友评论

          本文标题:命令行工具CLI以及FS API

          本文链接:https://www.haomeiwen.com/subject/faejiftx.html