/* CmdSweep.c
 *
 * Author:   Bill DuPree (with thanks to Jeff Lydiatt for PatMatch.c)
 * Language: Lattice C version 5.04
 * Date:     January 16, 1990
 * Version:  1.00
 *
 * Usage:  CmdSweep [options] <starting_directory> "Command"
 *
 * CmdSweep executes a given command in every subdirectory under the
 * specified starting directory including the starting directory.  It
 * can also be used to execute the command for every matching file
 * encountered in the directory tree.  If the current directory is the
 * desired starting directory, then it may be specified by coding the
 * argument as "".
 *
 * The "Command" argument specifies a template for the command you wish
 * to execute.  A favorite application of mine is to check the contents
 * of a diskette for viral infestations by using CmdSweep with the command
 * template "kv *", i.e. the full entry on the command line is:
 *
 *      CmdSweep df0: "kv *"
 *
 * The command template may contain the escape sequences %p and %f.  These
 * will be replaced with the current directory path and current file 
 * respectively, in command generation.  If you need to use the % character
 * in the command, then escape it with an additional % character.
 *
 * The "-A" option will force CmdSweep to process directories (and files)
 * in alphabetical order.  While this may cause a slight performance penalty,
 * it is useful in that you have some indication of how much processing
 * remains to be done by the visual inspection of the current directory
 * and/or file name.
 *
 * The use of the "-F" (Files) option will cause CmdSweep to execute the
 * command for every file it encounters in the directory tree.  If coded
 * as "-F=pattern"  then the command is executed only for files matching
 * the AmigaDOS pattern.
 *
 * Deferred execution may be had through the use of the "-S" (Script) option
 * which will cause CmdSweep to generate an AmigaDOS script file.  This
 * option is coded as "-S=script_file_name" where script_file_name is the
 * name of the file to receive the list of script commands.  If coded as
 * "-SE=script_file_name" then ECHO commands will be placed into the
 * script to provide a visible progress report when executing the script.
 *
 * You may use the "-X=pattern" (eXclude) option to exclude from 
 * consideration all files that match the supplied AmigaDOS pattern.
 *
 * The "-H" (Here) option will inhibit the directory tree traversal causing
 * CmdSweep to operate only in the supplied directory.  None of its
 * subdirectories will be entered.
 *
 * To compile and link:
 *
 *      lc -b -r -v CmdSweep.c
 *      lc -b -r -v PatMatch.c
 *      blink lib:c.o,CmdSweep.o,PatMatch.o to CmdSweep lib lib:lc.lib SD SC ND
 *
 * Examples:
 *
 * (1) To browse every C source and header file on a diskette in drive df1:
 *
 *      CmdSweep -f=#?.((c)|(h)) df1: "More %f"
 *
 * (2) To create a script file in RAM: to do the same as example one:
 *
 *      CmdSweep -s=ram:Browse.Script -f=#?.((c)|(h)) df1: "More %f"
 *
 * (3) To delete all files ending in the ".bak" extension on dh0:
 *
 *      CmdSweep -f=#?.bak dh0: "delete %f"
 * or
 *      CmdSweep dh0: "delete #?.bak"
 *
 * (4) To rid a diskette in drive df1: from viruses using the public domain
 *     KV program:
 *
 *      CmdSweep df1: "kv *"
 * or
 *      CmdSweep df1: "kv %p/"
 * or
 *      CmdSweep -f df1: "kv %p/%f"
 *
 * I'm sure that you will find many other uses.
 * 
 *--------------------------------------------------------------------------
 * DISCLAIMER:
 *
 * THIS PROGRAM AND ITS ASSOCIATED DOCUMENTATION ARE PROVIDED "AS IS"
 * WITHOUT REPRESENTATION OR WARRANTY OF ANY KIND, EITHER EXPRESS OR
 * IMPLIED, INCLUDING WITHOUT LIMITATION, ANY REPRESENTATIONS OR
 * ENDORSEMENTS REGARDING THE USE OF, THE RESULTS OF, OR PERFORMANCE OF
 * THE PROGRAM OR ITS ASSOCIATED DOCUMENTATION, ITS APPROPRIATENESS,
 * ACCURACY, RELIABILTY, OR CURRENTNESS.  THE ENTIRE RISK AS TO THE USE OF
 * THIS PROGRAM OR ITS ASSOCIATED DOCUMENTATION IS ASSUMED BY THE USER.
 *
 * IN NO EVENT WILL BILL DUPREE, NOR ANY OTHER PARTY INVOLVED IN THE
 * CREATION OR DISSEMINATION OF THIS PROGRAM AND ITS ASSOCIATED
 * DOCUMENTATION, BE LIABLE FOR ANY DAMAGES, DIRECT, INDIRECT,
 * INCIDENTAL OR CONSEQUENTIAL, RESULTING FROM ANY DEFECT IN THIS PROGRAM,
 * EVEN IF ANY OF THE AFOREMENTIONED PARTIES HAS BEEN ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGES.
 *
 * THIS DISCLAIMER SHALL SUPERCEDE ANY VERBAL OR WRITTEN STATEMENT TO
 * THE CONTRARY.
 *
 * Enough of the CYA stuff already.  This program is hereby placed into the
 * public domain.  Please use it freely for whatever purposes you deem
 * necessary.  However, if you incorporate it into any wondrous creations
 * of your own, give credit where credit is due, i.e. leave the names of
 * the author(s) intact in the code.  We all want our moments in the sun.
 * It would probably also be wise to continue including the disclaimer given
 * the state of affairs in our litigious society.  If you feel compelled to
 * contact me for any reason, or if you want to pass on constructive
 * criticism, suggestions, ideas for new features, or monetary rewards
 * (not necessary), or if you are a young, pretty, and single female Amiga
 * enthusiast in the Chicagoland area (I'm single), you can contact me at:
 *
 *      Bill DuPree
 *      7449 W. Washington #402
 *      Forest Park, IL. 60130
 */

#include "libraries/dos.h"
#include "libraries/dosextens.h"
#include "exec/memory.h"
#include "string.h"
#include "stdio.h"
#include "stdlib.h"
#include "proto/dos.h"
#include "proto/exec.h"

#define FALSE 0
#define TRUE  1
#define _MAX_PATH 256
#define ERROR 10

/* These two arrays are required for AmigaDOS style regular */
/* expression pattern matching.                             */
static WORD Aux[128], xAux[128];
static char *Pat, *xPat;

/* These declarations are for the pattern matching routines */
extern int CmplPat(char *, WORD *);
extern int Match(char *, WORD *, char *);

/* A couple of ANSI prototypes for our own routines */
static char *show_locked_dir(BPTR, char *);
static int traverse_tree(char *, BPTR);
static void my_exit(int);

struct dir_entry {
        struct dir_entry *next; /* pointer to next instance */
        char *name;             /* pointer to a string containing dir name */
};

/* Some global flags which tell what options were selected */
static int do_files, x_files, here_only, sorted, echo;

/* The script file stream */
static FILE *script;
char *ScriptName;

/* We only need one FileInfoBlock, not one for every level of recursion */
/* like most tree traversal algorithms.  This is because we defer       */
/* recursion until we have exhausted the current directory contents.    */
/* Subdirectories are saved in an ordered, dynamically allocated,       */
/* linked list for use later.  See traverse_tree().                     */

struct FileInfoBlock *myfib;

/* This program is VERY!!! loosely based on a non-working example given in */
/* the book "Programmer's Guide to the Amiga" authored by Rob Peck and     */
/* published by Sybex.  (See example opta.c on pp. 34)                     */

int main(int argc, char **argv)
{
        int i, rc, got_dir, batch;
        BPTR oldlock, newlock;
        char *whichdir, *command;
        static char work[_MAX_PATH];
        static char *signon =
                "\x1b[33mCmdSweep\x1b[31m v. 1.00 - January 16, 1990\n"
                "Author:  Bill DuPree";
        static char *usage =
                "Usage: CmdSweep [options] <starting_dir> \"command\"\n"
                "Enter \"CmdSweep -help\" for more information";

        /* Start variables off with known values */
	batch = rc = 0;
	sorted = got_dir = do_files = x_files = here_only = echo = FALSE;
        oldlock = NULL;
        myfib = NULL;
	script = NULL;
        command = whichdir = "";
        *work = (char) 0;
        Pat = "#?";

        puts(signon);

        /* make sure command line is worth parsing */
        if (argc < 2) {
                puts("Command line error");
                puts(usage);
                my_exit(ERROR);
        }

        /* allocate space for a FileInfoBlock */

        if ((myfib = (struct FileInfoBlock *)
           AllocMem(sizeof(struct FileInfoBlock), MEMF_CLEAR)) == NULL) {
                fputs("\n\aAlloc of FileInfoBlock failed\n", stderr);
                my_exit(ERROR);
        }

        /* Usage and/or help requested? */
        if (argc == 2)
                if (!strcmp(argv[1], "?")) {
                        puts(usage);
                        my_exit(0);
                }
		else if (!stricmp(argv[1], "-HELP")) {
                        puts("\fUsage: CmdSweep [options] <starting_dir> \"command\"");
			puts("where options are:\n");

			puts("\t-A : Process directories (and files) in alphabetical order.\n");

			puts("\t-F : will generate the given command for every file in the");
                        puts("\t     traversed directory tree and \"-F=pattern\" will generate.");
			puts("\t     the command for every file that matches the AmigaDOS pattern.\n");

                        puts("\t-S : coded as \"-S=script_file_name\", where script_file_name");
                        puts("\t     is any valid AmigaDOS file name.  The given file name");
                        puts("\t     will be opened to receive the list of commands that would");
			puts("\t     have otherwise been executed immediately.  The -SE variant");
			puts("\t     will place ECHO commands in the script for a visible progress");
			puts("\t     report.\n");

			puts("\t-X : coded as \"-X=pattern\" excludes files matching the pattern.\n");

			puts("\t-H : causes CmdSweep to operate only in the specified directory.\n");

			fputs("Press return to continue:", stdout);
			fflush(stdout);
			fgets(work, 1, stdin);

			puts("\fCmdSweep help continued:\n");
			puts("Options are not case sensitive and can be used in any combination to");
                        puts("achieve the desired effect.  All command line options and arguments must");
                        puts("be separated by space(s).  If they contain embedded space(s), then they");
			puts("must be surrounded by quotes.\n");

			puts("The <starting_dir> argument is not optional and must be a valid directory");
                        puts("on a mounted device.  If the current directory is desired then it should be");
			puts("coded as \"\".\n");

			puts("The \"command\" argument is also mandatory.  If you embed the escape");
                        puts("sequences \"%p\" or \"%f\" (no quotes) in the command, then the current");
                        puts("path and file will replace them in the resulting command, respectively.");
                        puts("The sequence \"%%\" escapes the '%' character in the generated commands.\n");
                        my_exit(0);
                }

        /* process command line */
        for (i = 1; i < argc; i++) {
                if (*argv[i] == (char) '-') {
			if ((stricmp(argv[i], "-A") == 0) && !sorted)
				sorted = TRUE;
			else if ((stricmp(argv[i], "-F") == 0) && !do_files)
				do_files = TRUE;
                        else if ((strnicmp(argv[i], "-F=", 3) == 0) && !do_files) { 
                                do_files = TRUE;
                                strcpy(Pat, &argv[i][3]);
                        }
                        else if ((stricmp(argv[i], "-H") == 0) && !here_only) 
                                here_only = TRUE;
                        else if ((strnicmp(argv[i], "-S=", 3) == 0) && !batch)
                                batch = i;
                        else if ((strnicmp(argv[i], "-SE=", 4) == 0) && !batch) {
				echo = TRUE;
                                batch = i;
			}
                        else if ((strnicmp(argv[i], "-X=", 3) == 0) && !x_files) { 
                                x_files = TRUE;
                                xPat = &argv[i][3];
                        }
                        else {
                                fputs("\n\aExtraneous options\n", stderr);
                                my_exit(ERROR);
                        }
                }
                else {
                        if ((*whichdir == (char) 0) && !got_dir) {
                                got_dir = TRUE;
                                whichdir = argv[i];
                        }
                        else if (*command == (char) 0)
                                command = argv[i];
                        else {
                                fputs("\n\aExtraneous command line args\n", stderr);
                                my_exit(ERROR);
                        }
                }
        }

        /* we have to examine files if we are excluding them */
        do_files |= x_files;

        /* make sure command was specified */
        if (*command == (char) 0) {
                fputs("\n\aCommand argument is required\n", stderr);
                my_exit(ERROR);
        }

        /* Compile and validate the inclusive pattern */
        if (do_files && !CmplPat(Pat, Aux)) {
                fputs("\n\aBad wild card expression in inclusion\n", stderr);
                my_exit(ERROR);
        }

        /* Compile and validate the pattern for exclusion */
        if (x_files && !CmplPat(xPat, xAux)) {
                fputs("\n\aBad wild card expression in exclusion\n", stderr);
                my_exit(ERROR);
        }

        if (batch) {
		if ((script = fopen(ScriptName = &argv[batch][echo ? 4 : 3], "w")) == NULL) {
                        fputs("\n\aCannot open script file\n", stderr);
                        my_exit(ERROR);
                }
        }

        /* get a lock on the specified directory */
        newlock = Lock(whichdir, ACCESS_READ);
	if (newlock != 0) {
		Examine(newlock, myfib);
		if (myfib->fib_DirEntryType > 0) {
			/* get lock on current dir so we can come back to it*/
			oldlock = CurrentDir(newlock);

			/* process new dir and subtree recursively */
			rc = (traverse_tree(command, newlock) == FALSE) ? ERROR : 0;

			/* return to original directory */
			(void) CurrentDir(oldlock);
			if (batch) {
				fputs("\nCD ", script);
				fputs(show_locked_dir(oldlock, work), script);
				fputs("\n", script);
			}

			/* unlock what we locked */
			UnLock(newlock);
		}
		else {
			fputs("\n\aError - \"", stderr);
			fputs(whichdir, stderr);
			fputs("\" is not a directory\n", stderr);
			my_exit(ERROR);
		}
	}
        else {
                fputs("\n\aCan't lock selected dir\n", stderr);
		my_exit(ERROR);
        }
        fputs("\n", stdout);
        my_exit(rc);
}

/*
 * Function to cleanup and exit.
 */
static void my_exit(int rc)
{
	if (script) fclose(script);

	/* Set script attribute if we generated a script */
	if (script && (rc == 0))
		(void) SetProtection(ScriptName, (FIBF_SCRIPT | FIBF_EXECUTE));

	if (myfib) FreeMem(myfib, sizeof(struct FileInfoBlock));
        exit(rc);
}

/*
 * This routine will construct the full path for a given lock.
 */
static char *show_locked_dir(BPTR lock, char *cd)
{
        BPTR mylock, oldlock;
        struct FileInfoBlock *finfo;
        static char work[_MAX_PATH];

        /* allocate space for a FileInfoBlock */

        if ((finfo = (struct FileInfoBlock *)
           AllocMem(sizeof(struct FileInfoBlock), MEMF_CLEAR)) == NULL) {
                fputs("\n\aAlloc of FileInfoBlock failed\n", stderr);
                return NULL;
        }

        /* construct the path by examining all parent directories */
        mylock = lock;
        *cd = *work = (char) 0;
        while (mylock) {
                (void)Examine(mylock, finfo);
                strcpy(work, cd);
                strcpy(cd, finfo->fib_FileName);
                oldlock = mylock;
                mylock = ParentDir(oldlock);

                /* This is not well documented, but you have to unlock */
                /* the locks obtained from ParentDir().                */
                if (oldlock != lock)
                        UnLock(oldlock);

                if (mylock == 0)        /* did we hit the root? */
                        strcat(cd,":");

                if (work[0]) {          /* concatenate names */
                        if (mylock != 0)                        
                                strcat(cd, "/");
                        strcat(cd, work);
                }
        }

        /* return used resources to OS */
        FreeMem(finfo, sizeof(struct FileInfoBlock));
        return cd;
}

/*
 * This routine expands the command template by replacing %p escape
 * sequences with the path and %f escape sequences with the file.
 */
static int expand_cmd(char *new, char *old, char *path, char *file)
{
        char *percent, *lastpos;

        *new = (char) 0;
        lastpos = old;
        while (percent = strchr(lastpos, '%')) {
                *percent = (char) 0;
                strcat(new, lastpos);
                *percent = (char) '%';
                percent++;
                if (toupper(*percent) == 'F') {
                        percent++;
                        strcat(new, file);
                }
                else if (toupper(*percent) == 'P') {
                        percent++;
                        strcat(new, path);
                }
                else if (*percent == '%') {
                        strcat(new, "%");
                        percent++;
                }
                else
                        return FALSE;

                lastpos = percent;
        }
        strcat(new, lastpos);
        return TRUE;
}

struct dir_entry *make_entry(char *name)
{
	struct dir_entry *new;

	/* allocate a dir_entry structure for deferred processing */
	if ((new = malloc(sizeof(struct dir_entry))) == NULL) {
		fputs("\n\aAlloc of dir_entry failed\n", stderr);
		return NULL;
	}

	/* fill in name */
	if ((new->name = strdup(name)) == NULL) {
		fputs("\n\aOut of memory\n", stderr);
		free(new);
		return NULL;
	}
	return new;
}


/* Now follow the tree... might hit a directory, might hit a file.   */
/* If a directory, list it and then follow it down (recursively).    */

static int traverse_tree(char *cmd, BPTR lock)
{
        int rc;
        BPTR newlock, oldlock;
        struct dir_entry *dirlist, *filelist, *new, *current, *prev;
        static char path[_MAX_PATH];
        static char command[_MAX_PATH];

        /* start with empty list of files and directories */
        filelist = dirlist = NULL;

        rc = TRUE;

        /* The first call to Examine fills in the FileInfoBlock */
        /* with information about the directory.  If it is      */
        /* called at the root level, it contains the volume     */
        /* name of the disk.                                    */

        Examine(lock, myfib);
        if (show_locked_dir(lock, path) == NULL) {
                rc = FALSE;
                goto clean_exit;
        }               
        fputs("\n\n\x1b[32;41mCmdSweep:  Directory ==> ", stdout);
        fputs(path, stdout);
        fputs("\x1b[31;40m\n", stdout);
        fflush(stdout);

        /* Expand command if we are executing directory by directory */
        if (!do_files && !expand_cmd(command, cmd, path, "")) {
                fputs("\n\aCommand format is invalid\n", stderr);
                rc = FALSE;
                goto clean_exit;
        }

        /* Write out appropriate script commands if necessary */
        if (script) {
		if (echo) {
	                fputs("\nECHO \"*e[32;41mDirectory ==> ", script);
        	        fputs(path, script);
                	fputs("*e[31;40m\"\n", script);
		}
		else
			fputs("\n", script);

                fputs("CD ", script);
                fputs(path, script);
                fputs("\n", script);
                if (!do_files) {
			if (echo) {
	                        fputs("ECHO \"*e[32;41mCommand:   ", script);
        	                fputs(command, script);
                	        fputs("*e[31;40m\"\n", script);
			}
                        fputs(command, script);
                        fputs("\n", script);
                }
        }
        else if (!do_files) {
                fputs("\x1b[32;41mCommand:   ", stdout);
                fputs(command, stdout);
                fputs("\x1b[31;40m\n", stdout);
                fflush(stdout);

                if (Execute(command, 0, 0) == FALSE) {
                        fputs("\n\aSystem command processor failure\n", stderr);
                        rc = FALSE;
                        goto clean_exit;
                }
        }

        /* loop until last directory entry */
        while (ExNext(lock, myfib)) {
		new = NULL;
                if (myfib->fib_DirEntryType > 0) {
                        /* got a subdirectory */
                        if (!here_only) {
				if ((new = make_entry(myfib->fib_FileName)) == NULL)
					goto clean_exit;
				current = dirlist;
                                prev = (struct dir_entry *) &dirlist;
                        }
                }
                else {  /* process matching files if needed */
                        if ((do_files && Match(Pat, Aux, myfib->fib_FileName)) &&
                           (!x_files || !Match(xPat, xAux, myfib->fib_FileName))) {
				if ((new = make_entry(myfib->fib_FileName)) == NULL)
					goto clean_exit;
                                current = filelist;
                                prev = (struct dir_entry *) &filelist;
                        }
                }

		if (!sorted && new) {
			prev->next = new;
			new->next = current;
		}
		else while (new) { /* maintain list in alpha order */
                        if ((current == NULL) ||
                           (stricmp(new->name, current->name) < 0)) {
                                prev->next = new;
                                new->next = current;
                                break;
                        }
                        else {
                                prev = current;
                                current = current->next;
                        }
                }
	} /* while (ExNext()) */

	/* Process list of files */
	while (filelist) {

		/* Format the current command */
		if (!expand_cmd(command, cmd, path, filelist->name)) {
                        fputs("\n\aCommand format is invalid\n", stderr);
                        rc = FALSE;
                        goto clean_exit;
                        }

		/* write the command to the AmigaDOS script if necessary */
		if (script) {
			if (echo) {
	                        fputs("ECHO \"*e[32;41mCommand:   ", script);
        	                fputs(command, script);
                	        fputs("*e[31;40m\"\n", script);
			}
                        fputs(command, script);
                        fputs("\n", script);
                }
		else {  /* not a script -- execute the command immediately */
                        fputs("\x1b[32;41mCommand:   ", stdout);
                        fputs(command, stdout);
                        fputs("\x1b[31;40m\n", stdout);
                        fflush(stdout);

                        if (Execute(command, 0, 0) == FALSE) {
                                fputs("\n\aSystem command processor failure\n", stderr);
                                rc = FALSE;
                                goto clean_exit;
                        }
                }
		/* Get next file to process and free processed element */
		free(filelist->name);
		prev = filelist;
		filelist = filelist->next;
		free(prev);
        }

        /* recursively process subdirectories */
	while (dirlist) {

		/* Get a lock on the directory and     */
		/* go into it to process its contents. */
		newlock = Lock(dirlist->name, ACCESS_READ);
		oldlock = CurrentDir(newlock);

                /* Recursively follow the tree down every branch */
                if (!(rc = traverse_tree(cmd, newlock))) break;

		/* After processing the contents of the new directory */
		/* return to old parent directory.                    */
		(void) CurrentDir(oldlock);
                UnLock(newlock);

		/* Get next directory in list and free processed element */
		free(dirlist->name);
		prev = dirlist;
		dirlist = dirlist->next;
		free(prev);
        }

        /* free all used resources */
clean_exit:
        while (filelist) {
                free(filelist->name);
		prev = filelist;
                filelist = filelist->next;
		free(prev);
        }

        while (dirlist) {
                free(dirlist->name);
		prev = dirlist;
                dirlist = dirlist->next;
		free(prev);
        }
        return rc;
}
