How do I loop through files in npm in a way that works on Windows & linux?

5.8k views Asked by At

I'm trying to run a single command (jshint), on multiple files. My package.json contains

"lint": "jshint *.js **/*.js"

However this fails miserable on Windows. On Windows the syntax to iterate on multiple files is

 for %%f in (*.in) do (
            echo %%~nf
    )

Is there a simple, platform-agnostic way to run a single npm script (e.g. jshint) on multiple files?

(I'm interested in the general solution. There's a references here to using node-jslint instead of jshint, which does support multiple files ... but IMO jshint >> jslint).

2

There are 2 answers

2
RyanZim On

To the best of my knowledge, you can't loop in the shell in a cross-platform way.

However, you can use https://www.npmjs.com/package/catw and do something like this:

catw "**/*.js" | jshint - 

catw will expand the glob(s) itself without relying on the shell and write the files to stdout. jshint - will read from stdin. The pipe (|) works cross-platform.

0
RobC On

I'm also not aware of a platform agnostic way to loop in the shell.

However, a platform agnostic solution to running a single npm-script on multiple files with jshint, as per your example, is to:

  1. Utilize cli-glob to find the .js files.
  2. Pipe the results/paths from the globbing pattern to a custom utility node script.
  3. Then within the node script:
    1. Read the paths piped to stdin using nodes readline module.
    2. Create an Array of each path and subsequently convert that to a String.
    3. Run the jshint executable, (including the String of all paths), using nodes child_process.exec() module.

Whilst this solution is not particularly "simple", the following gists demonstrate this process:


npm-script

"scripts": {
    "jshint": "glob \"src/js/**/*.js\" | node .scripts/jshint.js"
},

Note cli-glob, (added to package.json), obtains the paths and pipes them to jshint.js.


jshint.js

#!/usr/bin/env node

'use strict';

var path = require('path');
var readline = require('readline');
var exec = require('child_process').exec;

var rl = readline.createInterface({
    input: process.stdin,
    output: null,
    terminal: false
});

// Normalise path to local jshint executable.
var jshintExec = ['.', 'node_modules', '.bin', 'jshint '].join(path.sep);

var paths = [];

function jshint(paths) {
    var command = [jshintExec, paths].join('');
    exec(command, function(error, stdout, stderr) {
        if (stdout) {
            console.log(stdout);
        }
        if (stderr) {
            console.log(stderr);
        }
    });
}

rl.on('line', function(srcPath) {
    paths.push(srcPath);
});

rl.on('close', function() {
    jshint(paths.join(' '));
});

Note

Line 16 reading:

var jshintExec = ['.', 'node_modules', '.bin', 'jshint '].join(path.sep);

The script assumes jshint has been installed locally and added to the "devDependencies": {} section of the package.json. I.e. Its pointing to the local jhint executable found in the node_modules/.bin folder and not the globally installed one.

If your preference is to run the globally installed jshint then change line 16 to:

var jshintExec = 'jshint ';

Personally, having it installed locally is the preferred option IMHO for this scenario!


Multiple globbing patterns

Your example provided includes multiple glob patterns.

"lint": "jshint *.js **/*.js"

One limitation of cli-glob is that it doesn't accept multiple glob patterns. So one workaround is to do something like this:

"scripts": {
    "jshint": "npm run jshint-a && npm run jshint-b",
    "jshint-a": "glob \"*.js\" | node .scripts/jshint.js",
    "jshint-b": "glob \"**/*.js\" | node .scripts/jshint.js"
},

Yeah, not particularly terse - but works!