/*
 *  PARSER.C - command parsing.
 */

#include "precomp.h"

#define C_OP_LOWEST C_MULTI
#define C_OP_HIGHEST C_PIPE
static const TCHAR OpString[][3] = { _T("&"), _T("||"), _T("&&"), _T("|") };

static const TCHAR RedirString[][3] = { _T("<"), _T(">"), _T(">>") };

static const TCHAR *const IfOperatorString[] =
{
    _T("cmdextversion"),
    _T("defined"),
    _T("errorlevel"),
    _T("exist"),
#define IF_MAX_UNARY IF_EXIST
    _T("=="),
    _T("equ"),
    _T("gtr"),
    _T("geq"),
    _T("lss"),
    _T("leq"),
    _T("neq"),
#define IF_MAX_COMPARISON IF_NEQ
};

/* These three characters act like spaces to the parser in most contexts */
#define STANDARD_SEPS _T(",;=")

static BOOL IsSeparator(TCHAR Char)
{
    return _istspace(Char) || (Char && _tcschr(STANDARD_SEPS, Char));
}

enum
{
    TOK_END,
    TOK_NORMAL,
    TOK_OPERATOR,
    TOK_REDIRECTION,
    TOK_BEGIN_BLOCK,
    TOK_END_BLOCK
};

static BOOL bParseError;
static BOOL bLineContinuations;
static TCHAR ParseLine[CMDLINE_LENGTH];
static TCHAR *ParsePos;
static TCHAR CurChar;

static TCHAR CurrentToken[CMDLINE_LENGTH];
static int CurrentTokenType;
static int InsideBlock;

static TCHAR ParseChar(void)
{
    TCHAR Char;

    if (bParseError)
        return CurChar = 0;

restart:
    /*
     * Although CRs can be injected into a line via an environment
     * variable substitution, the parser ignores them - they won't
     * even separate tokens.
     */
    do
    {
        Char = *ParsePos++;
    }
    while (Char == _T('\r'));

    if (!Char)
    {
        ParsePos--;
        if (bLineContinuations)
        {
            if (!ReadLine(ParseLine, TRUE))
            {
                /* ^C pressed, or line was too long */
                bParseError = TRUE;
            }
            else if (*(ParsePos = ParseLine))
            {
                goto restart;
            }
        }
    }
    return CurChar = Char;
}

static void ParseError(void)
{
    error_syntax(CurrentTokenType != TOK_END ? CurrentToken : NULL);
    bParseError = TRUE;
}

/*
 * Yes, cmd has a Lexical Analyzer. Whenever the parser gives an "xxx was
 * unexpected at this time." message, it shows what the last token read was.
 */
static int ParseToken(TCHAR ExtraEnd, TCHAR *Separators)
{
    TCHAR *Out = CurrentToken;
    TCHAR Char;
    int Type;
    BOOL bInQuote = FALSE;

    for (Char = CurChar; Char && Char != _T('\n'); Char = ParseChar())
    {
        bInQuote ^= (Char == _T('"'));
        if (!bInQuote)
        {
            if (Separators != NULL)
            {
                if (_istspace(Char) || _tcschr(Separators, Char))
                {
                    /* Skip leading separators */
                    if (Out == CurrentToken)
                        continue;
                    break;
                }
            }

            /* Check for numbered redirection */
            if ((Char >= _T('0') && Char <= _T('9') &&
                   (ParsePos == &ParseLine[1] || IsSeparator(ParsePos[-2]))
                   && (*ParsePos == _T('<') || *ParsePos == _T('>'))))
            {
                break;
            }

            if (Char == ExtraEnd)
                break;
            if (InsideBlock && Char == _T(')'))
                break;
            if (_tcschr(_T("&|<>"), Char))
                break;

            if (Char == _T('^'))
            {
                Char = ParseChar();
                /* Eat up a \n, allowing line continuation */
                if (Char == _T('\n'))
                    Char = ParseChar();
                /* Next character is a forced literal */
            }
        }
        if (Out == &CurrentToken[CMDLINE_LENGTH - 1])
            break;
        *Out++ = Char;
    }

    /* Check if we got at least one character before reaching a special one.
     * If so, return them and leave the special for the next call. */
    if (Out != CurrentToken)
    {
        Type = TOK_NORMAL;
    }
    else if (Char == _T('('))
    {
        Type = TOK_BEGIN_BLOCK;
        *Out++ = Char;
        ParseChar();
    }
    else if (Char == _T(')'))
    {
        Type = TOK_END_BLOCK;
        *Out++ = Char;
        ParseChar();
    }
    else if (Char == _T('&') || Char == _T('|'))
    {
        Type = TOK_OPERATOR;
        *Out++ = Char;
        Char = ParseChar();
        /* check for && or || */
        if (Char == Out[-1])
        {
            *Out++ = Char;
            ParseChar();
        }
    }
    else if ((Char >= _T('0') && Char <= _T('9'))
             || (Char == _T('<') || Char == _T('>')))
    {
        Type = TOK_REDIRECTION;
        if (Char >= _T('0') && Char <= _T('9'))
        {
            *Out++ = Char;
            Char = ParseChar();
        }
        *Out++ = Char;
        Char = ParseChar();
        if (Char == Out[-1])
        {
            /* Strangely, the tokenizer allows << as well as >>... (it
             * will cause an error when trying to parse it though) */
            *Out++ = Char;
            Char = ParseChar();
        }
        if (Char == _T('&'))
        {
            *Out++ = Char;
            while (IsSeparator(Char = ParseChar()))
                ;
            if (Char >= _T('0') && Char <= _T('9'))
            {
                *Out++ = Char;
                ParseChar();
            }
        }
    }
    else
    {
        Type = TOK_END;
    }
    *Out = _T('\0');
    return CurrentTokenType = Type;
}

static BOOL ParseRedirection(REDIRECTION **List)
{
    TCHAR *Tok = CurrentToken;
    BYTE Number;
    REDIR_MODE RedirMode;
    REDIRECTION *Redir;

    if (*Tok >= _T('0') && *Tok <= _T('9'))
        Number = *Tok++ - _T('0');
    else
        Number = *Tok == _T('<') ? 0 : 1;

    if (*Tok++ == _T('<'))
    {
        RedirMode = REDIR_READ;
        if (*Tok == _T('<'))
            goto fail;
    }
    else
    {
        RedirMode = REDIR_WRITE;
        if (*Tok == _T('>'))
        {
            RedirMode = REDIR_APPEND;
            Tok++;
        }
    }

    if (!*Tok)
    {
        /* The file name was not part of this token, so it'll be the next one */
        if (ParseToken(0, STANDARD_SEPS) != TOK_NORMAL)
            goto fail;
        Tok = CurrentToken;
    }

    /* If a redirection for this handle number already exists, delete it */
    while ((Redir = *List))
    {
        if (Redir->Number == Number)
        {
            *List = Redir->Next;
            cmd_free(Redir);
            continue;
        }
        List = &Redir->Next;
    }

    Redir = cmd_alloc(FIELD_OFFSET(REDIRECTION, Filename[_tcslen(Tok) + 1]));
    Redir->Next = NULL;
    Redir->OldHandle = INVALID_HANDLE_VALUE;
    Redir->Number = Number;
    Redir->Mode = RedirMode;
    _tcscpy(Redir->Filename, Tok);
    *List = Redir;
    return TRUE;

fail:
    ParseError();
    FreeRedirection(*List);
    *List = NULL;
    return FALSE;
}

static PARSED_COMMAND *ParseCommandOp(int OpType);

/* Parse a parenthesized block */
static PARSED_COMMAND *ParseBlock(REDIRECTION *RedirList)
{
    PARSED_COMMAND *Cmd, *Sub, **NextPtr;
    Cmd = cmd_alloc(sizeof(PARSED_COMMAND));
    Cmd->Type = C_BLOCK;
    Cmd->Next = NULL;
    Cmd->Subcommands = NULL;
    Cmd->Redirections = RedirList;

    /* Read the block contents */
    NextPtr = &Cmd->Subcommands;
    InsideBlock++;
    while (1)
    {
        Sub = ParseCommandOp(C_OP_LOWEST);
        if (Sub)
        {
            *NextPtr = Sub;
            NextPtr = &Sub->Next;
        }
        else if (bParseError)
        {
            InsideBlock--;
            FreeCommand(Cmd);
            return NULL;
        }

        if (CurrentTokenType == TOK_END_BLOCK)
            break;

        /* Skip past the \n */
        ParseChar();
    }
    InsideBlock--;

    /* Process any trailing redirections */
    while (ParseToken(0, STANDARD_SEPS) == TOK_REDIRECTION)
    {
        if (!ParseRedirection(&Cmd->Redirections))
        {
            FreeCommand(Cmd);
            return NULL;
        }
    }
    return Cmd;
}

/* Parse an IF statement */
static PARSED_COMMAND *ParseIf(void)
{
    PARSED_COMMAND *Cmd = cmd_alloc(sizeof(PARSED_COMMAND));
    int Type;
    memset(Cmd, 0, sizeof(PARSED_COMMAND));
    Cmd->Type = C_IF;

    Type = CurrentTokenType;
    if (_tcsicmp(CurrentToken, _T("/I")) == 0)
    {
        Cmd->If.Flags |= IFFLAG_IGNORECASE;
        Type = ParseToken(0, STANDARD_SEPS);
    }
    if (_tcsicmp(CurrentToken, _T("not")) == 0)
    {
        Cmd->If.Flags |= IFFLAG_NEGATE;
        Type = ParseToken(0, STANDARD_SEPS);
    }

    if (Type != TOK_NORMAL)
    {
        FreeCommand(Cmd);
        ParseError();
        return NULL;
    }

    /* Check for unary operators */
    for (; Cmd->If.Operator <= IF_MAX_UNARY; Cmd->If.Operator++)
    {
        if (_tcsicmp(CurrentToken, IfOperatorString[Cmd->If.Operator]) == 0)
        {
            if (ParseToken(0, STANDARD_SEPS) != TOK_NORMAL)
            {
                FreeCommand(Cmd);
                ParseError();
                return NULL;
            }
            Cmd->If.RightArg = cmd_dup(CurrentToken);
            goto condition_done;
        }
    }

    /* It must be a two-argument (comparison) operator. It could be ==, so
     * the equals sign can't be treated as whitespace here. */
    Cmd->If.LeftArg = cmd_dup(CurrentToken);
    ParseToken(0, _T(",;"));

    /* The right argument can come immediately after == */
    if (_tcsnicmp(CurrentToken, _T("=="), 2) == 0 && CurrentToken[2])
    {
        Cmd->If.RightArg = cmd_dup(&CurrentToken[2]);
        goto condition_done;
    }

    for (; Cmd->If.Operator <= IF_MAX_COMPARISON; Cmd->If.Operator++)
    {
        if (_tcsicmp(CurrentToken, IfOperatorString[Cmd->If.Operator]) == 0)
        {
            if (ParseToken(0, STANDARD_SEPS) != TOK_NORMAL)
                break;
            Cmd->If.RightArg = cmd_dup(CurrentToken);
            goto condition_done;
        }
    }
    FreeCommand(Cmd);
    ParseError();
    return NULL;

condition_done:
    Cmd->Subcommands = ParseCommandOp(C_OP_LOWEST);
    if (Cmd->Subcommands == NULL)
    {
        FreeCommand(Cmd);
        return NULL;
    }
    if (_tcsicmp(CurrentToken, _T("else")) == 0)
    {
        Cmd->Subcommands->Next = ParseCommandOp(C_OP_LOWEST);
        if (Cmd->Subcommands->Next == NULL)
        {
            FreeCommand(Cmd);
            return NULL;
        }
    }

    return Cmd;
}

/*
 * Parse a FOR command.
 * Syntax is: FOR [options] %var IN (list) DO command
 */
static PARSED_COMMAND *ParseFor(void)
{
    PARSED_COMMAND *Cmd = cmd_alloc(sizeof(PARSED_COMMAND));
    TCHAR List[CMDLINE_LENGTH];
    TCHAR *Pos = List;

    memset(Cmd, 0, sizeof(PARSED_COMMAND));
    Cmd->Type = C_FOR;

    while (1)
    {
        if (_tcsicmp(CurrentToken, _T("/D")) == 0)
            Cmd->For.Switches |= FOR_DIRS;
        else if (_tcsicmp(CurrentToken, _T("/F")) == 0)
        {
            Cmd->For.Switches |= FOR_F;
            if (!Cmd->For.Params)
            {
                ParseToken(0, STANDARD_SEPS);
                if (CurrentToken[0] == _T('/') || CurrentToken[0] == _T('%'))
                    break;
                Cmd->For.Params = cmd_dup(CurrentToken);
            }
        }
        else if (_tcsicmp(CurrentToken, _T("/L")) == 0)
            Cmd->For.Switches |= FOR_LOOP;
        else if (_tcsicmp(CurrentToken, _T("/R")) == 0)
        {
            Cmd->For.Switches |= FOR_RECURSIVE;
            if (!Cmd->For.Params)
            {
                ParseToken(0, STANDARD_SEPS);
                if (CurrentToken[0] == _T('/') || CurrentToken[0] == _T('%'))
                    break;
                StripQuotes(CurrentToken);
                Cmd->For.Params = cmd_dup(CurrentToken);
            }
        }
        else
            break;
        ParseToken(0, STANDARD_SEPS);
    }

    /* Make sure there aren't two different switches specified
     * at the same time, unless they're /D and /R */
    if ((Cmd->For.Switches & (Cmd->For.Switches - 1)) != 0
        && Cmd->For.Switches != (FOR_DIRS | FOR_RECURSIVE))
    {
        goto error;
    }

    /* Variable name should be % and just one other character */
    if (CurrentToken[0] != _T('%') || _tcslen(CurrentToken) != 2)
        goto error;
    Cmd->For.Variable = CurrentToken[1];

    ParseToken(0, STANDARD_SEPS);
    if (_tcsicmp(CurrentToken, _T("in")) != 0)
        goto error;

    if (ParseToken(_T('('), STANDARD_SEPS) != TOK_BEGIN_BLOCK)
        goto error;

    while (1)
    {
        int Type;

        /* Pretend we're inside a block so the tokenizer will stop on ')' */
        InsideBlock++;
        Type = ParseToken(0, STANDARD_SEPS);
        InsideBlock--;

        if (Type == TOK_END_BLOCK)
            break;

        if (Type == TOK_END)
        {
            /* Skip past the \n */
            ParseChar();
            continue;
        }

        if (Type != TOK_NORMAL)
            goto error;

        if (Pos != List)
            *Pos++ = _T(' ');

        if (Pos + _tcslen(CurrentToken) >= &List[CMDLINE_LENGTH])
            goto error;
        Pos = _stpcpy(Pos, CurrentToken);
    }
    *Pos = _T('\0');
    Cmd->For.List = cmd_dup(List);

    ParseToken(0, STANDARD_SEPS);
    if (_tcsicmp(CurrentToken, _T("do")) != 0)
        goto error;

    Cmd->Subcommands = ParseCommandOp(C_OP_LOWEST);
    if (Cmd->Subcommands == NULL)
    {
        FreeCommand(Cmd);
        return NULL;
    }

    return Cmd;

error:
    FreeCommand(Cmd);
    ParseError();
    return NULL;
}

/* Parse a REM command */
static PARSED_COMMAND *ParseRem(void)
{
    /* Just ignore the rest of the line */
    while (CurChar && CurChar != _T('\n'))
        ParseChar();
    return NULL;
}

static DECLSPEC_NOINLINE PARSED_COMMAND *ParseCommandPart(REDIRECTION *RedirList)
{
    TCHAR ParsedLine[CMDLINE_LENGTH];
    PARSED_COMMAND *Cmd;
    PARSED_COMMAND *(*Func)(void);

    TCHAR *Pos = _stpcpy(ParsedLine, CurrentToken) + 1;
    DWORD_PTR TailOffset = Pos - ParsedLine;

    /* Check for special forms */
    if ((Func = ParseFor, _tcsicmp(ParsedLine, _T("for")) == 0) ||
        (Func = ParseIf,  _tcsicmp(ParsedLine, _T("if")) == 0)  ||
        (Func = ParseRem, _tcsicmp(ParsedLine, _T("rem")) == 0))
    {
        ParseToken(0, STANDARD_SEPS);
        /* Do special parsing only if it's not followed by /? */
        if (_tcscmp(CurrentToken, _T("/?")) != 0)
        {
            if (RedirList)
            {
                ParseError();
                FreeRedirection(RedirList);
                return NULL;
            }
            return Func();
        }
        Pos = _stpcpy(Pos, _T(" /?"));
    }

    /* Now get the tail */
    while (1)
    {
        int Type = ParseToken(0, NULL);
        if (Type == TOK_NORMAL)
        {
            if (Pos + _tcslen(CurrentToken) >= &ParsedLine[CMDLINE_LENGTH])
            {
                ParseError();
                FreeRedirection(RedirList);
                return NULL;
            }
            Pos = _stpcpy(Pos, CurrentToken);
        }
        else if (Type == TOK_REDIRECTION)
        {
            if (!ParseRedirection(&RedirList))
                return NULL;
        }
        else
        {
            break;
        }
    }
    *Pos++ = _T('\0');

    Cmd = cmd_alloc(FIELD_OFFSET(PARSED_COMMAND, Command.First[Pos - ParsedLine]));
    Cmd->Type = C_COMMAND;
    Cmd->Next = NULL;
    Cmd->Subcommands = NULL;
    Cmd->Redirections = RedirList;
    memcpy(Cmd->Command.First, ParsedLine, (Pos - ParsedLine) * sizeof(TCHAR));
    Cmd->Command.Rest = Cmd->Command.First + TailOffset;
    return Cmd;
}

static PARSED_COMMAND *ParsePrimary(void)
{
    REDIRECTION *RedirList = NULL;
    int Type;

    while (IsSeparator(CurChar))
    {
        if (CurChar == _T('\n'))
            return NULL;
        ParseChar();
    }

    if (!CurChar)
        return NULL;

    if (CurChar == _T(':'))
    {
        /* "Ignore" the rest of the line.
         * (Line continuations will still be parsed, though.) */
        while (ParseToken(0, NULL) != TOK_END)
            ;
        return NULL;
    }

    if (CurChar == _T('@'))
    {
        PARSED_COMMAND *Cmd;
        ParseChar();
        Cmd = cmd_alloc(sizeof(PARSED_COMMAND));
        Cmd->Type = C_QUIET;
        Cmd->Next = NULL;
        /* @ acts like a unary operator with low precedence,
         * so call the top-level parser */
        Cmd->Subcommands = ParseCommandOp(C_OP_LOWEST);
        Cmd->Redirections = NULL;
        return Cmd;
    }

    /* Process leading redirections and get the head of the command */
    while ((Type = ParseToken(_T('('), STANDARD_SEPS)) == TOK_REDIRECTION)
    {
        if (!ParseRedirection(&RedirList))
            return NULL;
    }

    if (Type == TOK_NORMAL)
        return ParseCommandPart(RedirList);
    else if (Type == TOK_BEGIN_BLOCK)
        return ParseBlock(RedirList);
    else if (Type == TOK_END_BLOCK && !RedirList)
        return NULL;

    ParseError();
    FreeRedirection(RedirList);
    return NULL;
}

static PARSED_COMMAND *ParseCommandOp(int OpType)
{
    PARSED_COMMAND *Cmd, *Left, *Right;

    if (OpType == C_OP_HIGHEST)
        Cmd = ParsePrimary();
    else
        Cmd = ParseCommandOp(OpType + 1);

    if (Cmd && !_tcscmp(CurrentToken, OpString[OpType - C_OP_LOWEST]))
    {
        Left = Cmd;
        Right = ParseCommandOp(OpType);
        if (!Right)
        {
            if (!bParseError)
            {
                /* & is allowed to have an empty RHS */
                if (OpType == C_MULTI)
                    return Left;
                ParseError();
            }
            FreeCommand(Left);
            return NULL;
        }

        Cmd = cmd_alloc(sizeof(PARSED_COMMAND));
        Cmd->Type = OpType;
        Cmd->Next = NULL;
        Cmd->Redirections = NULL;
        Cmd->Subcommands = Left;
        Left->Next = Right;
        Right->Next = NULL;
    }

    return Cmd;
}

PARSED_COMMAND *
ParseCommand(LPTSTR Line)
{
    PARSED_COMMAND *Cmd;

    if (Line)
    {
        if (!SubstituteVars(Line, ParseLine, _T('%')))
            return NULL;
        bLineContinuations = FALSE;
    }
    else
    {
        if (!ReadLine(ParseLine, FALSE))
            return NULL;
        bLineContinuations = TRUE;
    }
    bParseError = FALSE;
    ParsePos = ParseLine;
    CurChar = _T(' ');

    Cmd = ParseCommandOp(C_OP_LOWEST);
    if (Cmd)
    {
        if (CurrentTokenType != TOK_END)
            ParseError();
        if (bParseError)
        {
            FreeCommand(Cmd);
            Cmd = NULL;
        }
        bIgnoreEcho = FALSE;
    }
    else
    {
        bIgnoreEcho = TRUE;
    }
    return Cmd;
}


/*
 * Reconstruct a parse tree into text form; used for echoing
 * batch file commands and FOR instances.
 */
VOID
EchoCommand(PARSED_COMMAND *Cmd)
{
    TCHAR Buf[CMDLINE_LENGTH];
    PARSED_COMMAND *Sub;
    REDIRECTION *Redir;

    switch (Cmd->Type)
    {
    case C_COMMAND:
        if (SubstituteForVars(Cmd->Command.First, Buf))
            ConOutPrintf(_T("%s"), Buf);
        if (SubstituteForVars(Cmd->Command.Rest, Buf))
            ConOutPrintf(_T("%s"), Buf);
        break;
    case C_QUIET:
        return;
    case C_BLOCK:
        ConOutChar(_T('('));
        Sub = Cmd->Subcommands;
        if (Sub && !Sub->Next)
        {
            /* Single-command block: display all on one line */
            EchoCommand(Sub);
        }
        else if (Sub)
        {
            /* Multi-command block: display parenthesis on separate lines */
            ConOutChar(_T('\n'));
            do
            {
                EchoCommand(Sub);
                ConOutChar(_T('\n'));
                Sub = Sub->Next;
            } while (Sub);
        }
        ConOutChar(_T(')'));
        break;
    case C_MULTI:
    case C_IFFAILURE:
    case C_IFSUCCESS:
    case C_PIPE:
        Sub = Cmd->Subcommands;
        EchoCommand(Sub);
        ConOutPrintf(_T(" %s "), OpString[Cmd->Type - C_OP_LOWEST]);
        EchoCommand(Sub->Next);
        break;
    case C_IF:
        ConOutPrintf(_T("if"));
        if (Cmd->If.Flags & IFFLAG_IGNORECASE)
            ConOutPrintf(_T(" /I"));
        if (Cmd->If.Flags & IFFLAG_NEGATE)
            ConOutPrintf(_T(" not"));
        if (Cmd->If.LeftArg && SubstituteForVars(Cmd->If.LeftArg, Buf))
            ConOutPrintf(_T(" %s"), Buf);
        ConOutPrintf(_T(" %s"), IfOperatorString[Cmd->If.Operator]);
        if (SubstituteForVars(Cmd->If.RightArg, Buf))
            ConOutPrintf(_T(" %s "), Buf);
        Sub = Cmd->Subcommands;
        EchoCommand(Sub);
        if (Sub->Next)
        {
            ConOutPrintf(_T(" else "));
            EchoCommand(Sub->Next);
        }
        break;
    case C_FOR:
        ConOutPrintf(_T("for"));
        if (Cmd->For.Switches & FOR_DIRS)      ConOutPrintf(_T(" /D"));
        if (Cmd->For.Switches & FOR_F)         ConOutPrintf(_T(" /F"));
        if (Cmd->For.Switches & FOR_LOOP)      ConOutPrintf(_T(" /L"));
        if (Cmd->For.Switches & FOR_RECURSIVE) ConOutPrintf(_T(" /R"));
        if (Cmd->For.Params)
            ConOutPrintf(_T(" %s"), Cmd->For.Params);
        ConOutPrintf(_T(" %%%c in (%s) do "), Cmd->For.Variable, Cmd->For.List);
        EchoCommand(Cmd->Subcommands);
        break;
    }

    for (Redir = Cmd->Redirections; Redir; Redir = Redir->Next)
    {
        if (SubstituteForVars(Redir->Filename, Buf))
            ConOutPrintf(_T(" %c%s%s"), _T('0') + Redir->Number,
                RedirString[Redir->Mode], Buf);
    }
}

/*
 * "Unparse" a command into a text form suitable for passing to CMD /C.
 * Used for pipes. This is basically the same thing as EchoCommand, but
 * writing into a string instead of to standard output.
 */
TCHAR *
Unparse(PARSED_COMMAND *Cmd, TCHAR *Out, TCHAR *OutEnd)
{
    TCHAR Buf[CMDLINE_LENGTH];
    PARSED_COMMAND *Sub;
    REDIRECTION *Redir;

/*
 * Since this function has the annoying requirement that it must avoid
 * overflowing the supplied buffer, define some helper macros to make
 * this less painful.
 */
#define CHAR(Char) { \
    if (Out == OutEnd) return NULL; \
    *Out++ = Char; }
#define STRING(String) { \
    if (Out + _tcslen(String) > OutEnd) return NULL; \
    Out = _stpcpy(Out, String); }
#define PRINTF(Format, ...) { \
    UINT Len = _sntprintf(Out, OutEnd - Out, Format, __VA_ARGS__); \
    if (Len > (UINT)(OutEnd - Out)) return NULL; \
    Out += Len; }
#define RECURSE(Subcommand) { \
    Out = Unparse(Subcommand, Out, OutEnd); \
    if (!Out) return NULL; }

    switch (Cmd->Type)
    {
    case C_COMMAND:
        /* This is fragile since there could be special characters, but
         * Windows doesn't bother escaping them, so for compatibility
         * we probably shouldn't do it either */
        if (!SubstituteForVars(Cmd->Command.First, Buf)) return NULL;
        STRING(Buf)
        if (!SubstituteForVars(Cmd->Command.Rest, Buf)) return NULL;
        STRING(Buf)
        break;
    case C_QUIET:
        CHAR(_T('@'))
        RECURSE(Cmd->Subcommands)
        break;
    case C_BLOCK:
        CHAR(_T('('))
        for (Sub = Cmd->Subcommands; Sub; Sub = Sub->Next)
        {
            RECURSE(Sub)
            if (Sub->Next)
                CHAR(_T('&'))
        }
        CHAR(_T(')'))
        break;
    case C_MULTI:
    case C_IFFAILURE:
    case C_IFSUCCESS:
    case C_PIPE:
        Sub = Cmd->Subcommands;
        RECURSE(Sub)
        PRINTF(_T(" %s "), OpString[Cmd->Type - C_OP_LOWEST])
        RECURSE(Sub->Next)
        break;
    case C_IF:
        STRING(_T("if"))
        if (Cmd->If.Flags & IFFLAG_IGNORECASE)
            STRING(_T(" /I"))
        if (Cmd->If.Flags & IFFLAG_NEGATE)
            STRING(_T(" not"))
        if (Cmd->If.LeftArg && SubstituteForVars(Cmd->If.LeftArg, Buf))
            PRINTF(_T(" %s"), Buf)
        PRINTF(_T(" %s"), IfOperatorString[Cmd->If.Operator]);
        if (!SubstituteForVars(Cmd->If.RightArg, Buf)) return NULL;
        PRINTF(_T(" %s "), Buf)
        Sub = Cmd->Subcommands;
        RECURSE(Sub)
        if (Sub->Next)
        {
            STRING(_T(" else "))
            RECURSE(Sub->Next)
        }
        break;
    case C_FOR:
        STRING(_T("for"))
        if (Cmd->For.Switches & FOR_DIRS)      STRING(_T(" /D"))
        if (Cmd->For.Switches & FOR_F)         STRING(_T(" /F"))
        if (Cmd->For.Switches & FOR_LOOP)      STRING(_T(" /L"))
        if (Cmd->For.Switches & FOR_RECURSIVE) STRING(_T(" /R"))
        if (Cmd->For.Params)
            PRINTF(_T(" %s"), Cmd->For.Params)
        PRINTF(_T(" %%%c in (%s) do "), Cmd->For.Variable, Cmd->For.List)
        RECURSE(Cmd->Subcommands)
        break;
    }

    for (Redir = Cmd->Redirections; Redir; Redir = Redir->Next)
    {
        if (!SubstituteForVars(Redir->Filename, Buf)) return NULL;
        PRINTF(_T(" %c%s%s"), _T('0') + Redir->Number,
            RedirString[Redir->Mode], Buf)
    }
    return Out;
}

VOID
FreeCommand(PARSED_COMMAND *Cmd)
{
    if (Cmd->Subcommands)
        FreeCommand(Cmd->Subcommands);
    if (Cmd->Next)
        FreeCommand(Cmd->Next);
    FreeRedirection(Cmd->Redirections);
    if (Cmd->Type == C_IF)
    {
        cmd_free(Cmd->If.LeftArg);
        cmd_free(Cmd->If.RightArg);
    }
    else if (Cmd->Type == C_FOR)
    {
        cmd_free(Cmd->For.Params);
        cmd_free(Cmd->For.List);
    }
    cmd_free(Cmd);
}