/*** Exec.C - Contains routines that have do to with execing programs ********
*
*       Copyright (c) 1988-1991, Microsoft Corporation. All Rights Reserved.
*
* Purpose:
*  Contains routines that spawn programs ...
*
* Revision History:
*  15-Nov-1993 JdR Major speed improvements
*  15-Oct-1993 HV Use tchar.h instead of mbstring.h directly, change STR*() to _ftcs*()
*  10-May-1993 HV Add include file mbstring.h
*                 Change the str* functions to STR*
*  06-Oct-1992 GBS Removed extern for _pgmptr
*  10-Aug-1992 GBS Change file parsing in execLine to use splitpath
*  19-Aug-1992 SS  Remove Quotes from cd argument.
*  08-Jun-1992 SS  add IDE feedback support
*  08-Jun-1992 SS  Port to DOSX32
*  16-May-1991 SB  Created from routines that existed elsewhere
*
* Notes:
*
* Notes:
*  Functions currently in this module ...
*
*  buildArgumentVector - local
*  doCommands          - public (build.c)
*  execLine            - public (rpn.c)
*  execCommand         - local (undone, currently common to do & iterate)
*  expandCommandLine   - local
*  fDoRedirection      - local
*  fEmulateCommand     - local
*  getComSpec          - local
*  iterateCommand      - local
*  redirect            - local
*  removeQuotes        - local
*  touch               - local
*
*****************************************************************************/

/* INCLUDEs */

#include "nmake.h"
#include "nmmsg.h"
#include "proto.h"
#include "globals.h"
#include "grammar.h"


/* Constant DEFINEs */

#define numInternals    (sizeof(internals) / sizeof(char *))
#define SLASH '\\'
#define PUBLIC
#define QUOTE '\"'

/* Extern PROTOTYPEs */

#ifndef NO_OPTION_Z
extern STRINGLIST * NEAR canonCmdLine(char *);
#endif
extern void NEAR copyMacroTable(MACRODEF *old[], MACRODEF *new[]);

#if defined(DOS) && !defined(FLAT)
extern int  NEAR doSuperSpawn(char *, char **);
#endif

extern void NEAR freeEnviron(char **);
extern void NEAR freeMacroTable(MACRODEF *table[]);
extern BOOL NEAR processInline(char *, char **, STRINGLIST **);
extern char * NEAR SearchRunPath(char *, char *);
extern void CDECL NEAR  makeIdeMessage (unsigned, unsigned,...);

#if defined(FLAT)
extern UCHAR fRunningUnderTNT;
#endif

/* Local PROTOTYPEs */

LOCAL void   NEAR buildArgumentVector(unsigned*, char**, char *);
LOCAL char * NEAR expandCommandLine(void);
#if defined(DOS)
LOCAL BOOL   NEAR fDoRedirection(char*, int*, int*);
LOCAL BOOL       NEAR redirect(char*, unsigned);
#endif
LOCAL BOOL   NEAR fEmulateCommand(int argc, char **argv, int *pStatus);
LOCAL char * NEAR getComSpec(void);
LOCAL BOOL   NEAR iterateCommand(char*, STRINGLIST*, UCHAR, UCHAR, char *, unsigned*);
LOCAL void   NEAR removeQuotes(int, char **);
LOCAL void   NEAR touch(char*, BOOL);


/* Extern VARIABLEs */

//buffer for path of .cmd/.bat
extern char   NEAR bufPath[];
extern char   NEAR fileStr[MAXNAME];
extern char * NEAR initSavPtr;
extern char * NEAR makeStr;
#ifdef DEBUG_MEMORY
extern FILE *memory;
#endif
extern char * NEAR progName;
extern unsigned NEAR saveBytes;
extern char * NEAR shellName;


/* Local VARIABLEs */

#ifndef NO_OPTION_Z
LOCAL char batchIfCmd[] = "@if errorlevel %3d @goto NMAKEEXIT";
#endif
//cmd.exe and command.com internal commands
LOCAL char *internals[] = {
    "BREAK", "CD", "CHDIR", "CLS", "COPY", "CTTY", "DATE", "DEL", "DIR",
    "DIR.", "ECHO", "ECHO.", "ERASE", "EXIT", "FOR", "GOTO", "IF", "MD",
    "MKDIR", "PATH", "PAUSE", "PROMPT", "RD", "REM", "REN", "RENAME",
    "RMDIR", "SET", "SHIFT", "TIME", "TYPE", "VER", "VERIFY", "VOL"
};
LOCAL char szCmdLineBuf[MAXCMDLINELENGTH];

/* FUNCTIONs in Alphabetical order */

/*** buildArgumentVector -- builds an argument vector from a command line ****
*
* Scope:
*  Local.
*
* Purpose:
*  It builds an argument vector for a command line. This argument vector can
*  be used by spawnvX routines. The algorithm is explained in the notes below.
*
* Input:
*  argc    -- The number of arguments created in the argument vector
*  argv    -- The actual argument vector created
*  cmdline -- The command line whose vector is required
*
* Output:
*  Returns the number of arguments and the argument vector as parameters
*
* Errors/Warnings:
* Assumes:
*  That the behaviour of cmd.exe i.e. parses quotes but does not disturb them.
*  Assumes that the SpawnVX routines will handle quotes as well as escaped
*  chars.
*
* Modifies Globals:
* Uses Globals:
* Notes:
*  Scan the cmdline from left to the end building the argument vector along
*  the way. Whitespace delimits arguments except for the first argument for
*  which the switch char '/' is also allowed. Backslash can be used to escape
*  a char and so ignore the character following it. Parse the quotes along
*  the way. If an argument begins with a double-quote then all characters till
*  an unescaped double-quote are part of that argument. Likewise, if an
*  unescaped Doublequote occurs within an argument then the above follows. If
*  the end of the command line comes before the closing quote then the
*  argument goes as far as that.
*
*****************************************************************************/
LOCAL void NEAR
buildArgumentVector(argc, argv, cmdline)
unsigned *argc;
char **argv;
char *cmdline;
{
    char *p;                                /* current loc in cmdline */
    char *end;                              /* end of command line    */
    BOOL    fFirstTime = TRUE;              /* true if 1st argument   */

    // 11-May-1993 HV _mbschr() bug: return NULL
    // end = _ftcschr(p = cmdline, '\0');
    // Work around:
    end = p = cmdline;
    while (*end)
        end++;

    for (*argc = 0; *argc < MAXARG && p < end; ++*argc) {
        p += _ftcsspn(p, " \t");                    /* skip whitespace*/
        if (p >= end)
            break;
        *argv++ = p;
        if (*p == '\"') {
            /* If the word begins with double-quote, find the next
             * occurrence of double-quote which is not preceded by backslash
             * (same escape as C runtime), or end of string, whichever is
             * first.  From there, find the next whitespace character.
             */
            for (++p; p < end; ++p) {
                if (*p == '\\')
                    ++p;                    //skip escaped character
                else if (*p == '\"')
                    break;
            }
            if (p >= end)
                continue;
            ++p;
            p = _ftcspbrk(p, " \t");
        }
        else {
            /* For the first word on the command line, accept the switch
             * character and whitespace as terminators.  Otherwise, just
             * whitespace.
             */
            p = _ftcspbrk(p, " \t\"/");
            for (;p && p < end;p = _ftcspbrk(p+1, " \t\"/")) {
                if (*p == '/' && !fFirstTime)
                    continue;               //after 1st word '/' is !terminator
                else break;
            }
            if (p && *p == '\"') {
                for (p++;p < end;p++) {     //inside quote so skip to next one
                    if (*p == '\"')
                        break;
                }
                p = _ftcspbrk(p, " \t");            //after quote go to first whitespace
            }
            if (fFirstTime) {
                fFirstTime = FALSE;
                /* If switch char terminates the word, replace it with 0,
                 * re-allocate the word on the heap, restore the switch and
                 * set p just before the switch.  It would be easier to
                 * shift everything right but then we have to worry about
                 * overflow.
                 */
                if (p && *p == '/') {
                    *p = '\0';
                    argv[-1] = makeString(argv[-1]);
                    *p-- = '/';
                }

            }
        }
        if (!p)
            p = end;
        /* Now, p points to end of command line argument */
        *p++ = '\0';
    }
    *argv = NULL;
}

PUBLIC int NEAR
doCommands(
char *name,
STRINGLIST *s,
STRINGLIST *t,
UCHAR buildFlags,
char *pFirstDep
) {
    char *u,
         *v;
    UCHAR cFlags;
    unsigned status = 0;
    char c;
    char *Cmd;
    char *pLine;
    BOOL fExpanded;
    char *pCmd;

#ifndef NO_OPTION_Z
    STRINGLIST *z, *zList;          //For -z option
#endif

#ifdef DEBUG_ALL
    if (fDebug)
        {
        printf("* doCommands: %s,\n", name);
        DumpList(s);
        DumpList(t);
        }
#endif

#ifdef DEBUG_ALL
    printf ("DEBUG: doCommands 1\n");
#endif
    ++numCommands;
    if (ON(gFlags, F1_QUESTION_STATUS))
        return(0);

    makeIdeMessage (3, MSG_IDE_BUILD, name);

    if (ON(gFlags, F1_TOUCH_TARGETS)) {
        touch(name, (USHORT) ON(buildFlags, F2_NO_EXECUTE));
        return(0);
    }

#ifdef DEBUG_ALL
    printf ("DEBUG: doCommands 2\n");
#endif

    for (; s; s = s->next) {
        fExpanded = processInline(s->text, &Cmd, &t);
        cFlags = 0;
        errorLevel = 0L;
        u = Cmd;
        for (v = u; *v; ++v) {
            if (*v == ESCH) ++v;
            else if (*v == '$') {
                if (*++v == '$') continue;
// commented out 15-Apr-93 by JonM.  This code forces recursive nmake to be
// executed even if -n, but it's hosed (the -n is not passed to the recursive
// nmake), and the whole thing sounds like a bad idea anyway, so I'm going to
// turn it off.
//              if (!_ftcsncmp(v, "(MAKE)", 6)) {
//                  SET(cFlags, C_EXECUTE);
//                  break;
//              }
            }
        }
#ifdef DEBUG_ALL
    printf ("DEBUG: doCommands 2.1\n");
#endif
        for (c = *u; c == '!'
                     || c == '-'
                     || c == '@'
                     || c == ESCH
                     || WHITESPACE(c); c = *++u) {
            switch (c) {
                case ESCH:  if (c = *++u, WHITESPACE(c)) c = ' '; /*keep going*/
                            else c = ESCH;
                            break;
                case '!':   SET(cFlags, C_ITERATE);
                            break;
                case '-':   SET(cFlags, C_IGNORE);
                            ++u;
                            if (_istdigit(*u)) {
                                char *pNumber = u;

                                errorLevel = strtol(u, &u, 10);
                                if (errno == ERANGE) {
                                    *u = '\0';
                                    makeError(line, CONST_TOO_BIG, pNumber);
                                }
                                while(_istspace(*u))
                                      u++;
                            }
                            else errorLevel = 255;
                            --u;
                            break;
                case '@':   if (
#ifndef NO_OPTION_Z
                                OFF(gFlags, F1_REVERSE_BATCH_FILE) ||
#endif
                                    OFF(flags, F2_NO_EXECUTE))
                                SET(cFlags, C_SILENT);
                            break;
            }
            if (c == ESCH) break;        /* stop parsing for cmd-line options */
        }
#ifdef DEBUG_ALL
    printf ("DEBUG: doCommands 2.2\n");
#endif
        if (ON(cFlags, C_ITERATE) &&
              iterateCommand(u, t, buildFlags, cFlags, pFirstDep, &status)) {
            //The macros used by the command have to be freed & so we do so
            v = u;
#ifdef DEBUG_ALL
    printf ("DEBUG: doCommands 2.21\n");
#endif
            if (_ftcschr(u, '$'))
                u = expandMacros(u, &t);
#ifdef DEBUG_ALL
    printf ("DEBUG: doCommands 2.22\n");
#endif
            if (v != u) FREE(u);
            if (OFF(buildFlags, F2_IGNORE_EXIT_CODES) && fOptionK && status &&
                    status > (unsigned)errorLevel)
                break;
            continue;
        }
        v = u;
#ifdef DEBUG_ALL
    printf ("DEBUG: doCommands 2.23\n");
#endif
        if (!fExpanded && _ftcschr(u, '$'))
            u = expandMacros(u, &t);
#ifdef DEBUG_ALL
    printf ("DEBUG: doCommands 2.24\n");
#endif

        expandExtmake(CmdLine, u, pFirstDep);
#ifndef NO_OPTION_Z
        if (ON(gFlags, F1_REVERSE_BATCH_FILE))
            zList = canonCmdLine(CmdLine);
        else {
            zList = makeNewStrListElement();
            zList->text = CmdLine;
        }

#ifdef DEBUG_ALL
    printf ("DEBUG: doCommands 2.3\n");
#endif
        for (z = zList; z; z = z->next) {
            pLine = z->text;
#else
            pLine = CmdLine;
#endif
            status = execLine(pLine,
                              (BOOL)(ON(buildFlags, F2_NO_EXECUTE)
                                  || (OFF(buildFlags,F2_NO_ECHO)
                                     && OFF(cFlags,C_SILENT))),
                              (BOOL)((OFF(buildFlags, F2_NO_EXECUTE)
#ifndef NO_OPTION_Z
                                      && OFF(gFlags, F1_REVERSE_BATCH_FILE)
#endif
                                     )
                                     || ON(cFlags, C_EXECUTE)),
                              (BOOL)ON(cFlags, C_IGNORE), &pCmd);
            if (OFF(buildFlags, F2_IGNORE_EXIT_CODES)) {
#ifndef NO_OPTION_Z
                if (ON(gFlags, F1_REVERSE_BATCH_FILE)) {
                    STRINGLIST *revCmd;
                    revCmd = makeNewStrListElement();
                    revCmd->text = (char *)rallocate(_ftcslen(batchIfCmd) + 1);
                    sprintf(revCmd->text, batchIfCmd,
                        (errorLevel == 255 ? errorLevel: errorLevel + 1));
                    prependItem(&revList, revCmd);
                }
                else
#endif
                    if (status && status > (unsigned)errorLevel) {
                        if (!fOptionK)
                            makeError(0, BAD_RETURN_CODE, pCmd, status);
#ifndef NO_OPTION_Z
                        else
                            break;
#endif
                }
            }
#ifndef NO_OPTION_Z
        }
#endif
        if (v != u)
            FREE(u);
#ifndef NO_OPTION_Z
        if (ON(gFlags, F1_REVERSE_BATCH_FILE))
            freeList(zList);
        else
            FREE_STRINGLIST(zList);
#endif
        FREE(Cmd);
        if (OFF(buildFlags, F2_IGNORE_EXIT_CODES) && fOptionK && status &&
                status > (unsigned)errorLevel)
            break;
    }
#ifdef DEBUG_ALL
    printf ("DEBUG: doCommands 3\n");
#endif
    if (OFF(buildFlags, F2_IGNORE_EXIT_CODES) && fOptionK)
        return(status);
    else
        return(0);
}





/*** execLine -- execute a command line **************************************
*
* Scope:
*  Global (build.c, rpn.c)
*
* Purpose:
*  Parses the command line for redirection characters and redirects stdin and
*  stdout if "<", ">", or ">>" are seen.  If any of the following occur,
*  restore the original stdin and stdout, pass the command to the shell, and
*  invoke the shell:
*     - the command line contains "|" (pipe)
*     - a syntax error occurs in parsing the command line
*     - an error occurs in redirection
*  Otherwise, attempt to invoke the command directly, then restore the
*  original stdin and stdout.  If this invocation failed because of
*  file-not-found then pass the command to the shell and invoke the shell.
*
* Input:
*  line         -- The command line to be executed
*  echoCmd      -- determines if the command line is to be echoed
*  doCmd        -- determines if the command is to be actually executed
*  ignoreReturn -- determines if NMAKE is to ignore the return code on
*                  execution
*  ppCmd        -- if non-null then on error returns command executed
*
* Output:
*  Returns ... return code from child process
*          ... -1 if error occurs
*
* Assumes:
*  Whatever it assumes
*
* Modifies Globals:
*  global  --  how/what
*
* Uses Globals:
*  global used and why
*
* Notes:
*  1/ Quoted strings can have redir chars "<>" which will be skipped over.
*  2/ Unmatched quotes cause error; redir chars are replaced by space char.
*  3/ Dup stdin file handle then redirect it. If we have to use the shell,
*     restore the original command line.
*  4/ Emulate certain commands such as "cd" to help prevent some makefiles
*     from breaking when ported from DOS to OS/2.
*
* Algorithm for spawning commands:
*  If we can't handle the syntax, let the shell do everything.  Otherwise,
*  first check to see if the command (without extension) is a DOS built-in &
*  if it is, call the shell to execute it (this is how cmd.exe behaves)
*  If it's not a built-in, we check to see if it has a .cmd or a .bat
*  extension (depending on whether we're in DOS or OS/2). If it does, we
*  call system() to execute it.
*  If it has some other extension, we ignore the extension and go looking for
*  a .cmd or .bat file.  If we find it, we execute it with system().
*  Otherwise, we try to spawn it (without extension). If the spawn fails,
*  we issue an unknown program error.
*
*****************************************************************************/
int NEAR
execLine(
char *line,
BOOL echoCmd,
BOOL doCmd,
BOOL ignoreReturn,
char **ppCmd
) {
    char *argv[3+MAXNAME/2];
    BOOL fUseShell;
    int oldIn = -1,     //old stdin file handle
        oldOut = -1,    //old stdout file handle
        status;
    unsigned argc;
    static char bufName[MAXNAME] = { 0 };       //Buffer for program name
    BOOL fInternalCmd = FALSE;
    static char szDrive[_MAX_DRIVE], szDir[_MAX_DIR], szFileName[_MAX_FNAME];

    progName = NULL;
    if (!shellName)
        shellName = getComSpec();

    switch (*line)
    {
        case '@':
            // Turn off echo if it was on.  This handles the case where the "@"
            // was in a macro.
            //
            line++;
            if (doCmd)
                echoCmd = 0;
            break;

        case '-':
            ignoreReturn = TRUE;
            ++line;
            if (_istdigit(*line))
            {
                char * pNumber = line;
                errorLevel = strtol(line, &line, 10);
                if (errno == ERANGE) {
                    *line = '\0';
                    makeError(0, CONST_TOO_BIG, pNumber);       // Todo: replace 0 with line number
                }
                while(_istspace(*line))
                      line++;
            }
            else
                errorLevel = 255;
            break;
    }

    //handle null command ...
    if (!line[0])
        return(0);
    //copy command line into buffer
    if (_ftcslen(line) < MAXCMDLINELENGTH)
        _ftcscpy(szCmdLineBuf, line);
    else
        makeError(0, COMMAND_TOO_LONG, line);
#ifndef NO_OPTION_Z
    //If -z and '$(MAKE)' then echo it
    if (echoCmd && ON(gFlags, F1_REVERSE_BATCH_FILE)
          && !_ftcsnicmp(szCmdLineBuf, makeStr, _ftcslen(makeStr))) {
      STRINGLIST *revCmd;
      revCmd = makeNewStrListElement();
      revCmd->text = (char *)rallocate(1 + _ftcslen(szCmdLineBuf) + 3 + 1);

      sprintf(revCmd->text, "\t%s /Z%s", makeStr, szCmdLineBuf + _ftcslen(makeStr));
      prependItem(&revList, revCmd);
      return(0);
    }
#endif
    //If -n then echo command if not '$(MAKE)'
    if (echoCmd
// 15-Apr-93 JonM ... we are no longer executing recursive makes if -n, so
// we want to echo them.
//        && (_strnicmp(szCmdLineBuf, makeStr, strlen(makeStr)) ||
//            OFF(flags, F2_NO_EXECUTE))
        )
    {
        printf("\t%s\n", szCmdLineBuf);
        fflush(stdout);
    }
#if defined(DOS)
    //for DOS use shell only if we have to because COMMAND.COM does not
    //return child return codes; redirect, except for -n
    fUseShell =
#if defined(FLAT)
         !fRunningUnderTNT ||   // use shell only if TNT, not NT
#endif
         (BOOL) (OFF(flags, F2_NO_EXECUTE) &&
         fDoRedirection(szCmdLineBuf, &oldIn, &oldOut))
  #ifndef NO_OPTION_Z
              || ON(gFlags, F1_REVERSE_BATCH_FILE)
  #endif
              ;
#else
    //for OS/2 let the shell do the work
    fUseShell = TRUE;
#endif

    /* Allocate a copy of the command line on the heap because in a
     * recursive call to doMake(), argv pointers will be allocated from
     * the static buffer which will then be trashed.  For buildArg...().
     */
    pCmdLineCopy = makeString(szCmdLineBuf);
    /* Build arg vector.  This is a waste on OS/2 since we're probably
     * going to use the shell, except we have to check for cd, $(MAKE),
     * etc. so we take advantage of the parsing code.
     */

    buildArgumentVector(&argc, argv, pCmdLineCopy);

    // 11-May-1993 HV The _mbsicmp() does not like NULL pointer
    //                so I have to check before calling it.
    if (argv[0] && makeStr && !_ftcsicmp(argv[0], makeStr))
        *argv = _pgmptr;

    /* Copy program name into buffer.  Can't just use argv[0] since this is
     * from heap and will be freed before it may be used in an error message.
     */
    if (argc)
        progName = _ftcsncpy(bufName, argv[0], sizeof(bufName) - 1);
    else
        return(0);  // for case when macro command is null

    if (!doCmd) {                           /* don't execute command if doCmd false*/
#ifndef NO_OPTION_Z
        if (ON(gFlags, F1_REVERSE_BATCH_FILE)) {
            STRINGLIST *revCmd;
            char *echoStr;
            revCmd = makeNewStrListElement();
            revCmd->text = (char *)rallocate(1 + _ftcslen(szCmdLineBuf) + 1);
            echoStr = echoCmd ? "\t" : "@";
            _ftcscat(_ftcscpy(revCmd->text, echoStr), szCmdLineBuf);
            prependItem(&revList, revCmd);
        }
#endif
        //For -n, emulate if possible.
        if (fEmulateCommand(argc, argv, &status)) {
            if (status && ppCmd)
                *ppCmd = makeString(*argv);
            return(status);                          /* return status */
        }
        else
            return(0);
    }
    /* Try emulating the command if appropriate.  If not, and we should not
     * use the shell, try spawning command directly.
     */
    //Check status when emulating
    if (fEmulateCommand(argc, argv, &status))
        fUseShell = FALSE;
#if defined(DOS)
    else if (!fUseShell) {
        int lo = 0, mid, result, hi = numInternals;     //for binary search
        errno = 0;
        /* Do binary search of *argv in internal commands.  */
        for (mid = (hi+lo) / 2; hi - lo > 1; mid = (hi+lo) / 2) {
            if (!(result = _ftcsicmp(*argv, internals[mid]))) {
                fUseShell = TRUE;
                break;
            }
            else if (result < 0) hi = mid;
            else lo = mid;
        }
        fInternalCmd = TRUE;
        if (!fUseShell) {
            char *p;

            /* Ignore any given extention.  This is what DOS does.  */
            _splitpath( progName, szDrive, szDir, szFileName, NULL );
            _makepath( progName, szDrive, szDir, szFileName, NULL );

            // p = _ftcsrchr(progName, '.');
            // if (p && p[1] != '\\' && p[1] != '/')
            //  *p = 0;

            /* Search for the program in the search path.  If found,
             * p points to extention else NULL.
             */
            p = SearchRunPath(progName, bufPath);
            if (!p) {
                /* If not found, set up an error since COMMAND will
                 * return 0.  This risks future incompatibility if new
                 * DOS built-in commands are added.
                 */
                errno = ENOENT;
                status = -1;
            } else if (p[1] == 'b' || _ftcsicmp(p, ".cmd") == 0)
                //If .bat extention, use COMMAND.COM.
                fUseShell = TRUE;
            else {
                //Spawn command directly.  Capitalize argv[0] since
                //COMMAND.COM does.
                for (p = *argv; *p; p++)
                    *p = (char)_totupper(*p);
#if defined(DOS)
    #ifdef CHECK_CMD_LIMIT
                if (_ftcslen(line) >= DOSCMDLINELIMIT)
                    makeError(0, COMMAND_TOO_LONG, line);
    #endif
#endif
#ifdef USE_SUPER
                status = doSuperSpawn(bufPath, argv);
#else
                {
                char *    t = argv[0];
                argv[0] = bufPath;
                status = SPAWNVP(P_WAIT, bufPath, argv);
                argv[0] = t;
                }
#endif
            }
        }
    }
#endif

    if (oldIn != -1) {
        if (_dup2(oldIn, _fileno(stdin)) == -1)
            makeError(0, BUILD_INTERNAL);
        _close(oldIn);
    }
    if (oldOut != -1) {
        if (_dup2(oldOut, _fileno(stdout)) == -1)
            makeError(0, BUILD_INTERNAL);
        _close(oldOut);
    }

    if (fUseShell) {
        _ftcscpy(szCmdLineBuf, line);
#if defined(DOS)
        if (_ftcslen(line) >= DOSCMDLINELIMIT)
            makeError(0, COMMAND_TOO_LONG, line);
        for (p = szCmdLineBuf; *p && *p != ' ' && *p != '\t'; p++)
            *p = (char)_totupper(*p);
#endif

#ifdef DEBUG_MEMORY
        if (fDebug) {
            mem_status();
            fprintf(memory, "Spawning '%s'\n", szCmdLineBuf);
        }
#endif
        if (fInternalCmd)
            status = SYSTEM(szCmdLineBuf);
        else {
            int i;

            for (i=argc; i >= 0 ; i--) {
                argv[i+2] = argv[i];
            }
            argv[0] = shellName;
            argv[1] = "/c";

#ifdef USE_SUPER
                    status = doSuperSpawn(argv[0], argv);
#else
                    status = SPAWNVP(P_WAIT, argv[0], argv);
#endif
        }

#ifdef DEBUG_MEMORY
        if (fDebug)
            mem_status();
#endif
    }

    //BUGBUG: NT version 262 has a problem with the way the run-time execs
    //        a process.  When the run-time posts a WaitForSingleObject to
    //        make sure the process has finished, the kernal occasionally sets
    //        the exit status of the process to STATUS_THREAD_IS_TERMINATING
    //        (0xC000004B).  We test here to make sure that case doesn't cause
    //        problems later on...

    if (status == 0xc000004b)
    {
        fprintf(stderr, "spawn returned 0xc000004b ... Benign\n");
        status = 0;
    }

    /* Check for errors spawning command (distinct from errors *returned*
     * from a successfully spawned command).
     */
    if (status == -1) {
        if (ignoreReturn) {
            status = 0;
        } else {
            switch (errno) {
                case 0:
                    // We (ie: nmake) didn't fail, but the spawned program did.
                    break;

                case ENOENT:
                    makeError(0, CANT_FIND_PROGRAM, argv[0]);
                    break;

                case ENOMEM:
                    makeError(0, EXEC_NO_MEM, fUseShell? argv[2]: argv[0]);
                    break;

                default:
                    /* Done to flag possibly erroneous decision made here [SB] */
                    makeError(0, SPAWN_FAILED_ERROR, _strerror(NULL));
            }
        }
    }

    if (status && ppCmd)
        *ppCmd = makeString(*argv);

    FREE(pCmdLineCopy);
    return(status);
}


/*** expandCommandLine -- expands %name% strings in the Command Line *******
*
* Purpose:
*  The function expands '%name%' type strings in the Command Line. Its main
*  job is to assist fEmulateCommand() in emulating set for OS/2.
*
* Modifies:
*  buf -- The Command Line available globally
*
* Output:
*  Returns -- the position of 'name=value' part in the Command Line.
*          -- Null when no '=' is found so that fEmulateCommand() can pass the
*              line to the shell to signal syntax error.
* Note:
*  The shell does not give a syntax error for unmatched '%' and assumes it
*  as just another character in this case. This behaviour is duplicated
*  by expandCommandLine()
*
************************************************************************/

LOCAL char * NEAR
expandCommandLine(
void
) {
    char Buf[MAXCMDLINELENGTH];             //Buffer for expanded string
    char *pBuf;
    char EnvBuf[MAXCMDLINELENGTH];          //getenv returned string copy
    char *posName,                          //position of 'name=string' in Buf or buf
         *p,                                //points into buf
         *pEnv;                             //points into Env
    char ExpandName[MAXNAME];               //%name% string
    char *pExpandName;


    pBuf = Buf;
    _ftcscpy(pBuf, "set");
    p = szCmdLineBuf + 3;                   // go beyond 'set'
    pBuf +=3;
    /* Skip whitespace */
    for (;;p++) {
        if (!(WHITESPACE(*p)))
            break;                          // argc>1 � this will happen
        else *pBuf++ = *p;
    }
    if (!_ftcschr(p, '='))
        return("");                         //Syntax error so pass to the shell
    else
        posName = pBuf;                     //fixes position of Name in Buf
    /* Now we look for environment variables and expand if required */
    for (;*p != '=';p++)
        *pBuf++ = (char)_totupper(*p);

    for (;*p;) {
        if (*p == '%') {
            pExpandName = &ExpandName[0];
            while (*++p != '%' && *p)
                *pExpandName++ = (char)_totupper(*p);
            *pExpandName = '\0';
            if (!*p++) {                           //unmatched %;so don't expand
                *pBuf='\0';                 //from the environment; like set
                _ftcscat(Buf, ExpandName);
                pBuf += _ftcslen(ExpandName);
            }
            else {                    //matched %;so expand from the environment
                EnvBuf[0] = '\0';
                if ((pEnv = getenv(ExpandName)) != (char *)NULL) {
                    _ftcscat(EnvBuf, pEnv);
                    *pBuf='\0';
                    _ftcscat(Buf,EnvBuf);
                    pBuf += _ftcslen(EnvBuf);
                }
            }
        }
        else
            *pBuf++ = *p++;
    }
    *pBuf = '\0';
    _ftcscpy(szCmdLineBuf, Buf);
    *posName = '\0';
    posName = szCmdLineBuf + _ftcslen(Buf);          //Offset into buf
    return(posName);
}

#if defined(DOS)
/*
 * fDoRedirection -- handle redirection if possible, else return TRUE
 *
 */
LOCAL BOOL NEAR
fDoRedirection(p, oldIn, oldOut)
char *p;
int *oldIn;
int *oldOut;
{
    BOOL in = FALSE,
         out = FALSE;
    BOOL fReturn = FALSE;
    char *q;
    unsigned which;
    //save original string
    char *t = p;
    char *save = NULL;

    while (q = _ftcspbrk(p, "\"<>|")) {
        switch (*q) {
            case '\"':
                if (!(q = _ftcschr(q+1, '\"'))) {
                    fReturn = TRUE;
                    break;
                }
                p = ++q;
                break;
            case '<':
                if (in) {
                    fReturn = TRUE;
                    break;
                }
                if (!save)
                    save = makeString(p);
                *q++ = ' ';
                p = q;
                in = TRUE;
                *oldIn = _dup(_fileno(stdin));
                if ((*oldIn == -1)
                    || !redirect(q, READ)) {
                    fReturn = TRUE;
                    break;
                }
                break;
            case '>':
                if (out) {
                    fReturn = TRUE;
                    break;
                }
                if (!save)
                    save = makeString(p);
                *q++ = ' ';
                p = q;
                out = TRUE;
                if ((*q) == '>') {
                    *q++ = ' ';
                    which = APPEND;
                }
                else
                    which = WRITE;
                *oldOut = _dup(_fileno(stdout));
                if ((*oldOut == -1)
                    || !redirect(q, which)) {
                    fReturn = TRUE;
                    break;
                }
                break;
            case '|':
                fReturn = TRUE;
                break;
            default :
                makeError(0, BUILD_INTERNAL);
        }
        if (fReturn)
            break;
    }
    if (fReturn) {
        if (save) {
            _ftcscpy(p, save);
            FREE(save);
        }
        if (in && *oldIn != -1) {
            if (_dup2(*oldIn, _fileno(stdout)) == -1)
                makeError(0, BUILD_INTERNAL);
            _close(*oldIn);
            *oldIn = -1;
        }
        if (out && *oldOut != -1) {
            if (_dup2(*oldOut, _fileno(stdout)) == -1)
                makeError(0, BUILD_INTERNAL);
            _close(*oldOut);
            *oldOut = -1;
        }

    }
    return(fReturn);
}

#endif // DOS


/***  fEmulateCommand - look for certain commands and emulate them
*
* Emulate $(MAKE), cd, chdir, and <drive letter>:.
* Also emulates 'set'.
*
* RETURNS:    TRUE if command emulated, FALSE if not.
*
* Note:
*  In set emulation if a syntax error is discovered then it lets the
*  shell handle it. It does this by returning FALSE.
*/
LOCAL BOOL NEAR
fEmulateCommand(
int argc,
char **argv,
int *pStatus
) {
    char *pArg0 = *argv;
    char *pArg1 = argv[1];
#if defined(SELF_RECURSE)
    char *parentPtr;
    MACRODEF **oldTable;
    int i;
    /* use local because global gets overwritten by second memmove */
    BOOL fInhMacs = fInheritMacros;
#endif

    /*
     * If $(MAKE), save memory on recursive make's by saving the current
     * state of the world and recursively calling doMake().  This saves
     * the amount of memory taken up by NMAKE itself.
     */

#if !defined(SELF_RECURSE)
    if (0) {}
#else
    if (pArg0 == _pgmptr) { // if this is a recursive invocation
        char **oldEnv;

  #if defined(HEAP) && defined(TEST_RECURSION)
        printf("\n**** BEFORE RECURSION ****\n");
        heapdump(__FILE__, __LINE__);
  #endif

        parentPtr = (char *)rallocate(saveBytes);
        memmove(parentPtr, &startOfSave, saveBytes);
        if (fInhMacs) {
            oldTable = (MACRODEF **)rallocate(MAXMACRO * sizeof(MACRODEF *));
            copyMacroTable(macroTable, oldTable);
        }

        memmove(&startOfSave, initSavPtr, saveBytes);

        /* UNDONE: Need to inherit /K and /O */

        if (fInhMacs) {
            for (i = 0; i < MAXMACRO; i++)
                macroTable[i] = oldTable[i];
        }
#ifndef NO_OPTION_Z
        /* reinitialize makeflags variable */
        if (ON(gFlags, F1_REVERSE_BATCH_FILE)) {
  #ifdef PWB_HELP
            char *p;
            p = _ftcschr(getenv("MAKEFLAGS"), ' ');
            *p = 'Z';
  #else
            return(TRUE);
  #endif
        }
#endif
        removeQuotes(argc, argv);
        // save old environ
        oldEnv = environ;
        // get new environ
        environ = copyEnviron(environ);

        *pStatus = doMake(argc, argv, parentPtr);

        // free new environ; not needed anymore
        freeEnviron(environ);
        // restore old environ
        environ = oldEnv;

        if (fInhMacs) {
            freeMacroTable(oldTable);
            FREE(oldTable);
        }

        // make the heap less clustered by returning cleared area to the OS
        _heapmin();

#if defined(HEAP) && defined(TEST_RECURSION)
        printf("\n**** AFTER RECURSION ****\n");
        heapdump(__FILE__, __LINE__);
#endif

        return(TRUE);
    }
#endif //of self-recursive section
    else
    /* If "<drive letter>:" then change drives.  Ignore everything after
     * the drive letter, just like the shell does.
     */
    if (_istalpha(*pArg0) && pArg0[1] == ':' && !pArg0[2]) {
        setdrive(_totupper(*pArg0) - 'A' + 1);
        *pStatus = 0;
        return(TRUE);
    }
    /* If "set" then pass it to the shell and if "set string" then put it
     * into the environment. Let the shell handle the syntax errors.
     */
    else if (!_ftcsicmp(pArg0, "set")) {
        if (argc == 1)
            return(FALSE);                  // pass it to the shell
        else {
            char *pNameVal;                 // the "name=value" string
            pNameVal = expandCommandLine();
            /* if there is a syntax error let the shell handle it */
            if (!*pNameVal)
                return(FALSE);
            if ((*pStatus = PutEnv(makeString(pNameVal))) == -1)
                makeError(currentLine, OUT_OF_ENV_SPACE);
        }
    }
    /* If "cd foo" or "chdir foo", do a chdir() else in protect mode this
     * would be a no-op.  Ignore everything after 1st arg, just like the
     * shell does.
     */
    else {
        if (!_ftcsnicmp(pArg0, "cd", 2))
            pArg0 += 2;
        else if (!_ftcsnicmp(pArg0, "chdir", 5))
            pArg0 += 5;
        else
            return(FALSE);
        /* At this point, a prefix of argv[0] matches cd or chdir and pArg0
         * points to the next char.  Check for a path separator in argv[0]
         * (e.g., cd..\foo) or else use the next arg if present.
         */
        // Remove quotes, if any from the argument
        removeQuotes(argc, argv);

        //if there are more than two arguments then let the shell handle it
        if (argc > 2)
            return(FALSE);
        else if (!*pArg0 && pArg1) {
            //Under certain circumstances the C RunTime does not help us
            //e.g. 'd:', in this case let the shell do it ...
            if (isalpha(*pArg1) && pArg1[1] == ':' && !pArg1[2])
                return(FALSE);
            *pStatus = _chdir(pArg1);
        }
        else if (*pArg0 == '.' || PATH_SEPARATOR(*pArg0))
            *pStatus = _chdir(pArg0);
        else
            /* Unrecognized syntax--we can't emulate.  */
            return(FALSE);
    }
    /* If error, simulate a return code of 1.  */
    if (*pStatus != 0)
        *pStatus = 1;
    return(TRUE);
}


/*  ----------------------------------------------------------------------------
 *  getComSpec()
 *
 *  actions:        Attempts to find system shell.
 *
 *  First look for COMSPEC.  If not found, look for COMMAND.COM or CMD.EXE
 *  in the current directory then the path.  If not found, fatal error.
 *  It would make sense to give an error if COMSPEC is not defined but
 *  test suites are easier if no user-defined environment variables are
 *  required.
 */
LOCAL char * NEAR
getComSpec()
{
    void *findBuf = _alloca(resultbuf_size);
    NMHANDLE searchHandle;
    char *p;
    char *shell;

    if ((shell = getenv("COMSPEC")) != NULL) {
        return(shell);
    }
    if ((p = getenv("PATH")) == NULL)
        p = "";
#ifdef DOS
        shell = searchPath(p, "COMMAND.COM", findBuf, &searchHandle);
#else
        shell = searchPath(p, "CMD.EXE", findBuf, &searchHandle);
#endif
    if (shell == NULL)
        makeError(0, NO_COMMAND_COM);
    return(shell);
}


LOCAL BOOL NEAR
iterateCommand(
char *u,
STRINGLIST *t,
UCHAR buildFlags,
UCHAR cFlags,
char *pFirstDep,
unsigned *status
) {
    BOOL parens;
    char c = '\0';
    char *v;
    STRINGLIST *p = NULL,
               *q;
    char *pLine;
#ifndef NO_OPTION_Z
    STRINGLIST *z, *zList;          //For -z option
#endif
    char *pCmd;

    for (v = u; *v ; ++v) {
        parens = FALSE;
        if (*v == '$') {
            if (*(v+1) == '(') {
                ++v;
                parens = TRUE;
            }
            if (*(v+1) == '?') {
                if (parens
                    && !(_ftcschr("DFBR", *(v+2)) && *(v+3) == ')')
                    && *(v+2) != ')')
                    continue;
                p = dollarQuestion;
                c = '?';
                break;
            }
            if (*++v == '*' && *(v+1) == '*') {
                if (parens
                    && !(_ftcschr("DFBR", *(v+2)) && *(v+3) == ')')
                    && *(v+2) != ')')
                    continue;
                p = dollarStarStar;
                c = '*';
                break;
            }
        }
    }
    if (!*v) return(FALSE);
    v = u;
    q = p;
    while (p) {
        macros = t;
        if (c == '*') {
            p = dollarStarStar->next;
            dollarStarStar->next = NULL;
        }
        else {
            p = dollarQuestion->next;
            dollarQuestion->next = NULL;
        }
        u = expandMacros(v, &macros);

        expandExtmake(CmdLine, u, pFirstDep);
#ifndef NO_OPTION_Z
        if (ON(gFlags, F1_REVERSE_BATCH_FILE))
            zList = canonCmdLine(CmdLine);
        else {
            zList = makeNewStrListElement();
            zList->text = CmdLine;
        }

        for (z = zList; z; z = z->next) {
            pLine = z->text;
#else
            pLine = CmdLine;
#endif
            *status = execLine(pLine,
                              (BOOL)(ON(buildFlags, F2_NO_EXECUTE)
                                  || (OFF(buildFlags,F2_NO_ECHO)
                                     && OFF(cFlags,C_SILENT))),
                              (BOOL)((OFF(buildFlags, F2_NO_EXECUTE)
#ifndef NO_OPTION_Z
                                      && OFF(gFlags, F1_REVERSE_BATCH_FILE)
#endif
                                     )
                                     || ON(cFlags, C_EXECUTE)),
                              (BOOL)ON(cFlags, C_IGNORE), &pCmd);
            if (OFF(buildFlags, F2_IGNORE_EXIT_CODES)) {
#ifndef NO_OPTION_Z
                if (ON(gFlags, F1_REVERSE_BATCH_FILE)) {
                    STRINGLIST *revCmd;
                    revCmd = makeNewStrListElement();
                    revCmd->text = (char *)rallocate(_ftcslen(batchIfCmd) + 1);
                    sprintf(revCmd->text, batchIfCmd,
                        (errorLevel == 255 ? errorLevel: errorLevel + 1));
                    prependItem(&revList, revCmd);
                }
                else
#endif
                    if (*status && *status > (unsigned)errorLevel)
                        if (!fOptionK)
                            makeError(0, BAD_RETURN_CODE, pCmd, *status);
            }
#ifndef NO_OPTION_Z
        }
#endif
        if (c == '*')
            dollarStarStar = dollarStarStar->next = p;
        else dollarQuestion = dollarQuestion->next = p;
        FREE(u);
#ifndef NO_OPTION_Z
        if (ON(gFlags, F1_REVERSE_BATCH_FILE))
            freeList(zList);
        else
            FREE_STRINGLIST(zList);
#endif
        if (OFF(buildFlags, F2_IGNORE_EXIT_CODES) && fOptionK && *status &&
                *status > (unsigned)errorLevel)
            break;
    }
    if (c == '*') dollarStarStar = q;
    else dollarQuestion = q;
    return(TRUE);
}

#if defined(DOS)

/* redirect -- handles redirection of input or output.
 *
 * arguments:   dir - READ => input,
 *                    WRITE => output,
 *                    APPEND => append to end of the file.
 *
 *              p - pointer to buffer that has the filename as
 *                  well as the rest of the command string.
 *
 * return value     FALSE => error (freopen fails)
 *                  TRUE => normal return.
 *
 * the freopen() call sets up the redirection. the rest of the
 * command string is then copied forward.
 *
 */

LOCAL BOOL NEAR
redirect(name, which)
char *name;
unsigned  which;
{
    char *p,
          c = '\0';
    BOOL fStatus;
    char *mode;
    FILE *stream;
    FILE *new;

    while (WHITESPACE(*name)) ++name;
    if (p = _ftcspbrk(name, " \t<>\r")) {
        c = *p;
        *p = '\0';
    }
    if (which == READ) {
        mode = "r";
        stream = stdin;
    }
    else {
        stream = stdout;
        if (which == WRITE)
            mode = "w";
        else
            mode = "a";
    }

    new = freopen(name, mode, stream);

//  if (!new) {                 // REVIEW: consider notifying the user
//      perror(name);           // REVIEW: that we failed here?
//  }                           // REVIEW: this could save grief later...

    fStatus = (BOOL)(new ? TRUE : FALSE);
    if (fStatus && which == APPEND)
        _lseek(_fileno(new), 0L, SEEK_END);

    while(*name)
        *name++ = ' ';
    if (p)
        *p = c;
    return(fStatus);
}

#endif  // DOS

LOCAL void NEAR
removeQuotes(argc, argv)
int argc;
char **argv;
{
    char *t,
         *string;

    for (; argc--; argv++) {
        string = *argv;
        for (t = string; *t;) {
            if (*t == SLASH || *t == ESCH) {
                if (t[1] == QUOTE)
                   *(string)++ = *(t++);
                *(string++) = *(t++);
                continue;
            }
            if (*t == QUOTE)
                ++t;
            else
                *(string++) = *(t++);
        }
        *string = '\0';
    }
}

LOCAL void NEAR
touch(s, minusN)
char *s;
BOOL minusN;
{
    int fd;
    char c;
    FILE * file;

    makeMessage(TOUCHING_TARGET, s);
    if (!minusN &&
            ((file = FILEOPEN(s, "r+b")) != NULL)) {
        fd = _fileno(file);
        if (_read(fd, &c, 1) > 0) {
            _lseek(fd, 0L, SEEK_SET);
            _write(fd, &c, 1);
        }
        _close(fd);
    }
}