Glob in NPM

JavaScript

在使用 stylelint 的时候,发现了一个有趣的问题:如果直接使用 stylelint 的 bin 文件对批量 LESS 文件进行检查,程序可以如预期的运行;但是如果把同样的命令写到 package.json 中,以 npm script 的方式进行运行,最终被检查的文件就少了很多,实际只有一个文件参与了检查。

具体来说,./node_modules/.bin/stylelint src/**/*.less 这个命令可以检查所有的 LESS 文件,但是把 stylelint src/**/*.less 写到 package.josn 中之后,再运行却只检查了一个文件。

通过检查 stylelint 的文档,发现官方在写命令的时候,写法和上述略有不同,为:stylelint "src/**/*.less"

经过排查问题,发现根源在于:npm 使用了 sh 来执行代码,而 shzsh 在解析 Glob 的时候,行为是不同的。

npm,包括其他 Linux 进程,在使用 shell 的时候,默认使用的都是 sh,除非有其他明确的指定。这意味着,即使当前正在使用的 shell 是 zsh,在运行 npm 命令的时候,还是默认使用了 sh 对脚本进行执行。也就是说,./node_modules/.bin/stylelint src/**/*.less 这个命令执行,使用的是当前打开的 shell 程序(比如 zsh);而当这个命令写到 package.json 中,并以 npm script 的方式进行运行的时候,执行 shell 的就是 sh 了。

使用不同的 shell 程序,难免就会在行为上造成不一致。这里的 Glob 解析就是一个例子。在 zsh 里面可以简单的做一个实验。执行如下的命令:

ls src/**/*.less

可以看到,zsh 给出了当前 src 目录下所有的 LESS 文件, 不管这个文件是在多深的子目录下;而如果先在 zsh 中执行 shbash 进入到 shbash 的工作环境中,再执行同样的命令,可以看到输出的结果可能就是不同的。实际上,对于 sh 来说,它本身并不识别 ** 这个语法,这个表示在 sh 中会被简单的识别为 *src/**/*.lesssh 中等价于 src/*/*.less。换句话说,在 sh 的环境中,上述命令只会寻找所有在 src 目录下一级子目录中的 LESS 文件,一旦层级大于一层,就不会被找到了。

这也是为什么同样的命令,直接执行和在 npm 中执行会有差异的原因。

最后,加上双引号 stylelint "src/**/*.less" 就可以解决这一问题的原因在于:一旦加上了双引号,这一个 Glob 就不会被 shell 直接解析,而是会以字符串的形式直接传递给 stylelint。(具体来说,如果不加双引号,shell 会先将 Glob 解析成一组具体的文件,stylelint 实际拿到的 process.env.argv 很可能会是一个很长的字符串数组,每一个元素都是一个具体的文件;而如果加上了双引号,stylelint 拿到的只有一个 Glob 表达式字符串。)有了这个 Glob 的字符串,stylelint 内部就可以使用相应的 package 来进行解析,从而得到一串具体的文件列表。因为使用了 stylelint 内部自带的 Glob 解析,就可以保证在不同的 shell 环境中都得到一致的结果了。

参考