Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

JsGrep - an OO-ish implementation of grep in JScript

0.00/5 (No votes)
4 Dec 2003 1  
Shows how the object features of JScript can be put to use in a useful utility script

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 // (c)ount

,   'i' : false // (i)gnore case

,   'l' : false // (l)ist files

,   'n' : false // show line (n)umbers

,   'r' : false // (r)ecursively match file names

,   'v' : false // in(v)ert match (i.e. show non-matching lines)

};

var i, cArgs = WScript.Arguments.length;
var nFirstNonOptArg = cArgs;

for (i = 0; i < cArgs; ++i)
{
    var strArg = WScript.Arguments(i);
    if (strArg == '--')
    {
        // end of args flag (to allow REs beginning with '-')

        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)
{
    // No RE

    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] = '-'; // stdin

        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:

// Counter


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)
{
    // properties

    this.bCountReqd     = bCountReqd;
    this.cFileMatches   = 0;
    this.cTotalMatches  = 0;

    // methods

    this.FilePreamble   = Count_FilePreamble;
    this.Action         = Count_Action;
    this.FilePostamble  = Count_FilePostamble;
    this.ProgPostamble  = Count_ProgPostamble;
}

// Printer


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)
{
    // properties

    this.bNumbered      = bNumbered;

    // methods

    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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here