Introduction
This article showcases JScript's ability to allow useful scripts to be written in a relatively object-oriented manner.
Background
This utility was first "published" via my blog.
Using the code
The code should be run under using the Windows Scripting Host (WSH) Console client, cscript.exe, e.g.:
cscript jsgrep.js -il "MyHeader\.h" *.cpp
To set the console client to be the default, use:
cscript /H:cscript
You can then run the utility simply with:
jsgrep -il "MyHeader\.h" *.cpp
To run the utility under the Windows Script Debugger, if installed, use:
cscript //x jsgrep.js -il "MyHeader\.h" *.cpp
Understanding the code
As is often the way with these things, the code is easier to understand in a different order to that in which it appears in the source file. In this particular case, it's easiest to look at the main program logic before the classes which implement it. The first section of this declares a map of command line switches (all initially false), then parses the command line arguments to determine which are set:
var mapOpts =
{
'c' : false
, 'i' : false
, 'l' : false
, 'n' : false
, 'r' : false
, 'v' : false
};
var i, cArgs = WScript.Arguments.length;
var nFirstNonOptArg = cArgs;
for (i = 0; i < cArgs; ++i)
{
var strArg = WScript.Arguments(i);
if (strArg == '--')
{
nFirstNonOptArg = ++i;
break;
}
else if (strArg.charAt(0) == '-')
{
var j, cChars = strArg.length;
for (j = 1; j < cChars; ++j)
mapOpts[strArg.charAt(j)] = true;
}
else
{
nFirstNonOptArg = i;
break;
}
}
The next chunk of code deals with the non-switch command line arguments: exactly one regular expression followed by zero or more files specs. If the latter contain any wildcard characters (* or ?), they are expanded by shelling a DIR /B
command, a process which I believe is known as globbing.
if (nFirstNonOptArg == cArgs)
{
WScript.Echo('usage: jsgrep [-cilnrv] regexp [file ...]');
WScript.Quit(1);
}
var arrFiles = new Array();
var oShell = null;
for (i = nFirstNonOptArg + 1; i < cArgs; ++i)
{
var strFileArg = WScript.Arguments(i);
if (strFileArg.indexOf('*') >= 0 || strFileArg.indexOf('?') >= 0)
{
if (oShell == null)
oShell = new ActiveXObject('WScript.Shell');
var nLastSep = strFileArg.lastIndexOf('\\');
var strPath = (nLastSep >= 0) ?
strFileArg.slice(0, ++nLastSep) : '';
var strCmd = 'cmd.exe /c dir /b ';
if (mapOpts['r'])
strCmd += '/s ';
var procGlob = oShell.Exec(strCmd + strFileArg);
while (!procGlob.StdOut.AtEndOfStream)
{
if (mapOpts['r'])
arrFiles[arrFiles.length] = procGlob.StdOut.ReadLine();
else
arrFiles[arrFiles.length] = strPath +
procGlob.StdOut.ReadLine();
}
}
else
arrFiles[arrFiles.length] = strFileArg;
}
var cFiles = arrFiles.length;
if (cFiles == 0)
{
if (nFirstNonOptArg < cArgs - 1)
{
WScript.Echo('no matching files found');
WScript.Quit(1)
}
else
{
arrFiles[0] = '-';
cFiles = 1;
}
}
var re = new RegExp(WScript.Arguments(nFirstNonOptArg),
mapOpts['i'] ? 'i' : '');
var bNegateMatch = mapOpts['v'];
var bSingleFile = (cFiles == 1);
The main logic of the script is expressed in terms of a "helper" object, oAction
. Its instantiation and various implementations will be covered later, but for now all we need to know is that it must implement the following "interface":
void FilePreamble(string strFile, bool bIsOnlyFile);
bool Action();
void FilePostamble(string strFile, bool bIsOnlyFile);
void ProgPostamble(bool bSingleFileOnly);
The logic itself is then relatively simple iteration and helper method invocation, though there is a disproportionately large amount of logic dedicated to properly handling stdin
as an explicit or implicit source of input:
var strCurrFile = '', nLineNumber = 1;
var oFSO = new ActiveXObject('Scripting.FileSystemObject');
var ts;
for (i = 0; i < cFiles; ++i)
{
strCurrFile = arrFiles[i];
var bNeedClose = false;
if (strCurrFile == '-')
{
ts = WScript.stdin;
}
else
{
try
{
ts = oFSO.OpenTextFile(strCurrFile);
bNeedClose = true;
}
catch(e)
{
WScript.Echo('Failed to open "' + strCurrFile + '" - ' +
e.description);
continue;
}
}
nLineNumber = 1;
oAction.FilePreamble(strCurrFile, bSingleFile);
var bContinue = true;
while (bContinue && !ts.AtEndOfStream)
{
var strLine = ts.ReadLine();
if ((strLine.match(re) == null) == bNegateMatch)
bContinue = oAction.Action(strLine);
++nLineNumber;
}
oAction.FilePostamble(strCurrFile, bSingleFile);
if (bNeedClose) ts.Close();
}
oAction.ProgPostamble(bSingleFile);
Next in the tour of the code comes the instantiation of the helper object. There is a choice of implementation: either a Printer, which shows each matching line and is the default (no switches) behaviour; or a Counter, which is badly named as it might not be required to actually keep the count, but just to identify that there is at least one match:
var oAction = mapOpts['c'] ? new Counter(true) : mapOpts['l'] ?
new Counter(false) : new Printer(mapOpts['n']);
Finally comes the implementations of the helper object. All that is of note here is the fact that JScript objects are implemented as expando ones (i.e. they implement COM's IDispatchEx
interface), so the methods can be attached to the object at construction time. I'm convinced there must be a more elegant way of doing this, with prototypes perhaps, but it works fine in this simple example:
function Count_FilePreamble(strFile, bIsOnlyFile)
{
this.cFileMatches = 0;
}
function Count_Action(strLine)
{
++this.cFileMatches;
++this.cTotalMatches;
return this.bCountReqd;
}
function Count_FilePostamble(strFile, bIsOnlyFile)
{
if (bIsOnlyFile)
{
if (this.bCountReqd)
WScript.Echo(this.cFileMatches);
else
WScript.Echo('Match found');
}
else if (this.cFileMatches > 0)
{
if (this.bCountReqd)
WScript.Echo(strFile + ": " + this.cFileMatches);
else
WScript.Echo(strFile);
}
}
function Count_ProgPostamble(bOneFileOnly)
{
if (this.bCountReqd && !bOneFileOnly)
WScript.Echo('Total: ' + this.cTotalMatches);
}
function Counter(bCountReqd)
{
this.bCountReqd = bCountReqd;
this.cFileMatches = 0;
this.cTotalMatches = 0;
this.FilePreamble = Count_FilePreamble;
this.Action = Count_Action;
this.FilePostamble = Count_FilePostamble;
this.ProgPostamble = Count_ProgPostamble;
}
function Print_Action(strLine)
{
if (bSingleFile)
WScript.Echo((this.bNumbered ? nLineNumber : '') + strLine);
else
WScript.Echo(strCurrFile + (this.bNumbered ? '('
+ nLineNumber + ')' : '') + ': ' + strLine);
return true;
}
function Printer(bNumbered)
{
this.bNumbered = bNumbered;
this.FilePreamble = DoNothing2;
this.Action = Print_Action;
this.FilePostamble = DoNothing2;
this.ProgPostamble = DoNothing1;
}
History
- 28 November 2003 - Initial version
- 03 December 2003 - Correct small, but important, typo in matching logic (code error, so affects article and source), and added support for recursive expansion of file name wildcards