Standalone bgexec

Critcl wrapper of standalone unix bgexec stripped out from BLT and enhanced with -input option (as suggested by KJN on bgexec).

Note that all windows code and all pre-Tcl 8.1 code has been stripped from the original ::blt::bgexec.

[ Daniel Steffen 24/01/05 ]


#!/bin/sh
# ----------------------------------------------------------------------------
#
# Critcl wrapper of standalone unix bgexec stripped out from BLT and enhanced
#     with -input option (C code modifications marked by //DAS).
# Note that all windows code and all pre-Tcl 8.1 code has been stripped.
#
# Process this file with 'critcl -pkg' to build a loadable package (or simply
# source this file if [package require critcl] and a compiler are available at
# deployment).
#
# In addition to the [bgexec] command, this file defines (when sourced) the
# proc [bgexec_write_manpage], run it to output the bgexec.n manpage.
#
# RCS: @(#) $Id: 13400,v 1.6 2005-02-01 07:01:00 jcw Exp $
#
# See below for original BLT copyright and license.
#
# Modifications Copyright (c) 2005, Daniel A. Steffen <[email protected]>
#
# Modifications under BSD License:
#       c.f. <http://www.opensource.org/licenses/bsd-license>
#
# ----------------------------------------------------------------------------
# \
exec tclsh "$0" "$@"

package require critcl
if {![::critcl::compiling]} {error "No compiler found"}

#-------------------------------------------------------------------------------

package provide bgexec 3.0

::critcl::ccode {

/**************************************************************************/

/* Configuration: adjust as necessary, the following works on Mac OS X 10.3 */

/* Define to 1 if you have the <ctype.h> header file. */
#define HAVE_CTYPE_H 1

/* Define to 1 if you have the <errno.h> header file. */
#define HAVE_ERRNO_H 1

/* Define to 1 if you have the <limits.h> header file. */
#define HAVE_LIMITS_H 1

/* Define to 1 if you have the <memory.h> header file. */
#define HAVE_MEMORY_H 1

/* Define to 1 if you have the <malloc.h> header file. */
/* #undef HAVE_MALLOC_H */

/* Define to 1 if you have the <stdlib.h> header file. */
#define HAVE_STDLIB_H 1

/* Define to 1 if you have the <sys/param.h> header file. */
#define HAVE_SYS_PARAM_H 1

/* Define to 1 if you have the <sys/wait.h> header file. */
#define HAVE_SYS_WAIT_H 1

/* Define if 'wait' is a union. */
#define HAVE_UNION_WAIT 1

/* Define to 1 if you have the <unistd.h> header file. */
#define HAVE_UNISTD_H 1

/* Define to 1 if you have the <waitflags.h> header file. */
/* #undef HAVE_WAITFLAGS_H */

#define USE_VFORK

#define NDEBUG

/**************************************************************************/

/*
 * bltBgexec.c --
 *
 * This module implements a background "exec" command for the BLT
 * toolkit.
 *
 *        Copyright 1993-1998 George A Howlett.
 *
 *        Permission is hereby granted, free of charge, to any person
 *        obtaining a copy of this software and associated documentation
 *        files (the "Software"), to deal in the Software without
 *        restriction, including without limitation the rights to use,
 *        copy, modify, merge, publish, distribute, sublicense, and/or
 *        sell copies of the Software, and to permit persons to whom the
 *        Software is furnished to do so, subject to the following
 *        conditions:
 *
 *        The above copyright notice and this permission notice shall be
 *        included in all copies or substantial portions of the
 *        Software.
 *
 *        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
 *        KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 *        WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 *        PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
 *        OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 *        OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 *        OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 *        SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#ifndef CONST84
#define CONST84
#endif

#include <stdio.h>
#include <assert.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <stdarg.h>

#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#endif /* HAVE_STDLIB_H */

#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif /* HAVE_ERRNO_H */

#ifdef HAVE_CTYPE_H
#include <ctype.h>
#endif /* HAVE_CTYPE_H */

#ifdef HAVE_MEMORY_H
#include <memory.h>
#endif /* HAVE_MEMORY_H */

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif /* HAVE_UNISTD_H */

#ifdef HAVE_LIMITS_H
#include <limits.h>
#endif /* HAVE_LIMITS_H */

#ifdef HAVE_SYS_PARAM_H
#include <sys/param.h>
#endif /* HAVE_SYS_PARAM_H */

#ifdef HAVE_WAITFLAGS_H
#include <waitflags.h>
#endif /* HAVE_WAITFLAGS_H */

#ifdef HAVE_SYS_WAIT_H
#include <sys/wait.h>
#endif /* HAVE_SYS_WAIT_H */

#ifdef HAVE_MALLOC_H
#include <malloc.h>
#endif /* HAVE_MALLOC_H */

#ifdef USE_VFORK
#define fork vfork
#endif

/*
 * Since the Tcl/Tk distribution doesn't perform any asserts, dynamic
 * loading can fail to find the __assert function.  As a workaround,
 * we'll include our own.
 */
#undef        assert
#ifdef        NDEBUG
#define        assert(EX) ((void)0)
#else
static void Blt_Assert (char *expr, char *file, int line);
#ifdef __STDC__
#define        assert(EX) (void)((EX) || (Blt_Assert(#EX, __FILE__, __LINE__), 0))
#else
#define        assert(EX) (void)((EX) || (Blt_Assert("EX", __FILE__, __LINE__), 0))
#endif /* __STDC__ */
#endif /* NDEBUG */

#define TRUE         1
#define FALSE         0

#define COUNT_NONNEGATIVE        0
#define COUNT_POSITIVE                1
#define COUNT_ANY                2

static char *Blt_Itoa (int value);

#define Blt_Malloc(size)        ((void*)ckalloc(size))
#define Blt_Free(ptr)           (ckfree((char*)ptr))
#define Blt_Realloc(ptr, size)        ((void*)ckrealloc((char*)ptr, size))

static char *Blt_Strdup (CONST char *ptr);

static void *Blt_Calloc (unsigned int nElem, size_t size);

#ifndef Blt_Offset
#ifdef offsetof
#define Blt_Offset(type, field) ((int) offsetof(type, field))
#else
#define Blt_Offset(type, field) ((int) ((char *) &((type *) 0)->field))
#endif
#endif /* Blt_Offset */

typedef int (Blt_SwitchParseProc) _ANSI_ARGS_((ClientData clientData,
        Tcl_Interp *interp, char *switchName, Tcl_Obj *valueObjPtr, 
        char *record, int offset, int flags));

typedef void (Blt_SwitchFreeProc) _ANSI_ARGS_((char *record, int offset,
        int flags));

typedef struct {
    Blt_SwitchParseProc *parseProc; /* Procedure to parse a switch
                                      * value and store it in its *
                                      * converted form in the data *
                                      * record. */

    Blt_SwitchFreeProc *freeProc; /* Procedure to free a switch. */

    ClientData clientData;        /* Arbitrary one-word value used by
                                  * switch parser, passed to
                                  * parseProc. */
} Blt_SwitchCustom;


/*
 * Type values for Blt_SwitchSpec structures.  See the user
 * documentation for details.
 */
typedef enum {
    BLT_SWITCH_BOOLEAN, 
    BLT_SWITCH_DOUBLE, 
    BLT_SWITCH_FLAG, 
    BLT_SWITCH_FLOAT, 
    BLT_SWITCH_INT, 
    BLT_SWITCH_INT_NONNEGATIVE, 
    BLT_SWITCH_INT_POSITIVE,
    BLT_SWITCH_LIST, 
    BLT_SWITCH_LONG, 
    BLT_SWITCH_LONG_NONNEGATIVE, 
    BLT_SWITCH_LONG_POSITIVE,
    BLT_SWITCH_OBJ,
    BLT_SWITCH_STRING, 
    BLT_SWITCH_VALUE, 
    BLT_SWITCH_CUSTOM, 
    BLT_SWITCH_END
} Blt_SwitchTypes;


typedef struct {
    Blt_SwitchTypes type;        /* Type of option, such as
                                  * BLT_SWITCH_COLOR; see definitions
                                  * below.  Last option in table must
                                  * have type BLT_SWITCH_END. */

    char *switchName;                /* Switch used to specify option in
                                  * argv.  NULL means this spec is part
                                  * of a group. */

    int offset;                        /* Where in widget record to store
                                  * value; use Blt_Offset macro to
                                  * generate values for this. */

    int flags;                        /* Any combination of the values
                                  * defined below. */

    Blt_SwitchCustom *customPtr; /* If type is BLT_SWITCH_CUSTOM then
                                  * this is a pointer to info about how
                                  * to parse and print the option.
                                  * Otherwise it is irrelevant. */
    int value;
} Blt_SwitchSpec;

#define BLT_SWITCH_DEFAULTS                (0)
#define BLT_SWITCH_ARGV_PARTIAL                (1<<1)
#define BLT_SWITCH_OBJV_PARTIAL                (1<<1)

/*
 * Possible flag values for Blt_SwitchSpec structures.  Any bits at or
 * above BLT_SWITCH_USER_BIT may be used by clients for selecting
 * certain entries.
 */
#define BLT_SWITCH_NULL_OK                (1<<0)
#define BLT_SWITCH_DONT_SET_DEFAULT        (1<<3)
#define BLT_SWITCH_SPECIFIED                (1<<4)
#define BLT_SWITCH_USER_BIT                (1<<8)

static int Blt_ParseSwitches _ANSI_ARGS_((Tcl_Interp *interp,
        Blt_SwitchSpec *specPtr, int objc, Tcl_Obj *CONST *objv, void *record, 
        int flags));

static void Blt_FreeSwitches _ANSI_ARGS_((Blt_SwitchSpec *specs, void *record,
        int flags));

/*
 * The type of the status returned by wait varies from UNIX system
 * to UNIX system.  The macro below defines it:
 */

#ifdef AIX
#   define WAIT_STATUS_TYPE pid_t
#else
#ifdef HAVE_UNION_WAIT
#   define WAIT_STATUS_TYPE union wait
#else
#   define WAIT_STATUS_TYPE int
#endif
#endif

typedef int ProcessId;

#define ENCODING_ASCII                ((Tcl_Encoding)NULL)
#define ENCODING_BINARY                ((Tcl_Encoding)1)

/*
 *        We're using our own replacement for the old Tcl_CreatePipeline
 *        call in the Tcl C library.  The prescribed workaround is
 *        Tcl_OpenCommandChannel.  But it hides the pids of the pipeline
 *        away (unless of course you pry open the undocumented structure
 *        PipeStatus as clientData).  Worse, I couldn't figure any way
 *        to make one side of the pipe to be non-blocking.
 */
static int Blt_CreatePipeline _ANSI_ARGS_((Tcl_Interp *interp, int objc,
        Tcl_Obj *CONST *objv, ProcessId **pidPtrPtr, int *inPipePtr, 
        int *outPipePtr, int *errFilePtr));

#define READ_AGAIN        (0)
#define READ_EOF        (-1)
#define READ_ERROR        (-2)

/* The wait-related definitions are taken from tclUnix.h */

#define TRACE_FLAGS (TCL_TRACE_WRITES | TCL_TRACE_UNSETS | TCL_GLOBAL_ONLY)

#define BLOCK_SIZE        1024        /* Size of allocation blocks for buffer */
#define DEF_BUFFER_SIZE        (BLOCK_SIZE * 8)
#define MAX_READS       100        /* Maximum number of successful reads
                                  * before stopping to let Tcl catch up
                                  * on events */

#ifndef NSIG
#define NSIG                 32        /* Number of signals available */
#endif /*NSIG*/

#ifndef SIGINT
#define SIGINT                2
#endif /* SIGINT */

#ifndef SIGQUIT
#define SIGQUIT                3
#endif /* SIGQUIT */

#ifndef SIGKILL
#define SIGKILL                9
#endif /* SIGKILL */

#ifndef SIGTERM
#define SIGTERM                14
#endif /* SIGTERM */

typedef struct {
    int number;
    char *name;
} SignalId;

static SignalId signalIds[] =
{
#ifdef SIGABRT
    {SIGABRT, "SIGABRT"},
#endif
#ifdef SIGALRM
    {SIGALRM, "SIGALRM"},
#endif
#ifdef SIGBUS
    {SIGBUS, "SIGBUS"},
#endif
#ifdef SIGCHLD
    {SIGCHLD, "SIGCHLD"},
#endif
#if defined(SIGCLD) && (!defined(SIGCHLD) || (SIGCLD != SIGCHLD))
    {SIGCLD, "SIGCLD"},
#endif
#ifdef SIGCONT
    {SIGCONT, "SIGCONT"},
#endif
#if defined(SIGEMT) && (!defined(SIGXCPU) || (SIGEMT != SIGXCPU))
    {SIGEMT, "SIGEMT"},
#endif
#ifdef SIGFPE
    {SIGFPE, "SIGFPE"},
#endif
#ifdef SIGHUP
    {SIGHUP, "SIGHUP"},
#endif
#ifdef SIGILL
    {SIGILL, "SIGILL"},
#endif
#ifdef SIGINT
    {SIGINT, "SIGINT"},
#endif
#ifdef SIGIO
    {SIGIO, "SIGIO"},
#endif
#if defined(SIGIOT) && (!defined(SIGABRT) || (SIGIOT != SIGABRT))
    {SIGIOT, "SIGIOT"},
#endif
#ifdef SIGKILL
    {SIGKILL, "SIGKILL"},
#endif
#if defined(SIGLOST) && (!defined(SIGIOT) || (SIGLOST != SIGIOT)) && (!defined(SIGURG) || (SIGLOST != SIGURG))
    {SIGLOST, "SIGLOST"},
#endif
#ifdef SIGPIPE
    {SIGPIPE, "SIGPIPE"},
#endif
#if defined(SIGPOLL) && (!defined(SIGIO) || (SIGPOLL != SIGIO))
    {SIGPOLL, "SIGPOLL"},
#endif
#ifdef SIGPROF
    {SIGPROF, "SIGPROF"},
#endif
#if defined(SIGPWR) && (!defined(SIGXFSZ) || (SIGPWR != SIGXFSZ))
    {SIGPWR, "SIGPWR"},
#endif
#ifdef SIGQUIT
    {SIGQUIT, "SIGQUIT"},
#endif
#ifdef SIGSEGV
    {SIGSEGV, "SIGSEGV"},
#endif
#ifdef SIGSTOP
    {SIGSTOP, "SIGSTOP"},
#endif
#ifdef SIGSYS
    {SIGSYS, "SIGSYS"},
#endif
#ifdef SIGTERM
    {SIGTERM, "SIGTERM"},
#endif
#ifdef SIGTRAP
    {SIGTRAP, "SIGTRAP"},
#endif
#ifdef SIGTSTP
    {SIGTSTP, "SIGTSTP"},
#endif
#ifdef SIGTTIN
    {SIGTTIN, "SIGTTIN"},
#endif
#ifdef SIGTTOU
    {SIGTTOU, "SIGTTOU"},
#endif
#if defined(SIGURG) && (!defined(SIGIO) || (SIGURG != SIGIO))
    {SIGURG, "SIGURG"},
#endif
#if defined(SIGUSR1) && (!defined(SIGIO) || (SIGUSR1 != SIGIO))
    {SIGUSR1, "SIGUSR1"},
#endif
#if defined(SIGUSR2) && (!defined(SIGURG) || (SIGUSR2 != SIGURG))
    {SIGUSR2, "SIGUSR2"},
#endif
#ifdef SIGVTALRM
    {SIGVTALRM, "SIGVTALRM"},
#endif
#ifdef SIGWINCH
    {SIGWINCH, "SIGWINCH"},
#endif
#ifdef SIGXCPU
    {SIGXCPU, "SIGXCPU"},
#endif
#ifdef SIGXFSZ
    {SIGXFSZ, "SIGXFSZ"},
#endif
    {-1, "unknown signal"},
};

/*
 * Sink buffer:
 *   ____________________
 *  |                    |  "size"        current allocated length of buffer.
 *  |                    |
 *  |--------------------|  "fill"      fill point (# characters in buffer).
 *  |  Raw               |
 *  |--------------------|  "mark"      Marks end of cooked characters.
 *  |                    |
 *  |  Cooked            |
 *  |                    |
 *  |                    |
 *  |--------------------|  "lastMark"  Mark end of processed characters.
 *  |                    |
 *  |                    |
 *  |  Processed         |
 *  |                    |
 *  |____________________| 0
 */
typedef struct {
    char *name;                        /* Name of the sink */

    char *doneVar;                /* Name of a Tcl variable (malloc'ed)
                                  * set to the collected data of the
                                  * last UNIX subprocess. */

    char *updateVar;                /* Name of a Tcl variable (malloc'ed)
                                  * updated as data is read from the
                                  * pipe. */

    char **updateCmd;                /* Start of a Tcl command executed
                                  * whenever data is read from the
                                  * pipe. */

    Tcl_Obj **objv;                /*  */
    int objc;                        /*  */

    int flags;                        

    Tcl_Encoding encoding;
    int fd;                        /* File descriptor of the pipe. */
    int status;

    int echo;                        /* Indicates if the pipeline's stderr stream
                                  * should be echoed */
    unsigned char *bytes;        /* Stores pipeline output (malloc-ed):
                                  * Initially points to static storage
                                  */
    size_t size;                /* Size of dynamically allocated buffer. */

    size_t fill;                /* # of bytes read into the buffer. Marks
                                  * the current fill point of the buffer. */

    size_t mark;                /* # of bytes translated (cooked). */
    size_t lastMark;                /* # of bytes as of the last read.
                                  * This indicates the start of the new
                                  * data in the buffer since the last
                                  * time the "update" variable was
                                  * set. */

    unsigned char staticSpace[DEF_BUFFER_SIZE];        /* Static space */

} Sink;

#define SINK_BUFFERED                (1<<0)
#define SINK_KEEP_NL                (1<<1)
#define SINK_NOTIFY                (1<<2)

typedef struct {
    char *statVar;                /* Name of a Tcl variable set to the
                                  * exit status of the last
                                  * process. Setting this variable
                                  * triggers the termination of all
                                  * subprocesses (regardless whether
                                  * they have already completed) */

    int signalNum;                /* If non-zero, indicates the signal
                                  * to send subprocesses when cleaning
                                  * up.*/

    int keepNewline;                /* If non-zero, indicates to set Tcl
                                  * output variables with trailing
                                  * newlines intact */

    int lineBuffered;                /* If non-zero, indicates provide data
                                  * to update variable and update proc on
                                  * a line-by-line basis. */

    int interval;                /* Interval to poll for the exiting
                                  * processes */
    char *outputEncodingName;        /* Name of decoding scheme to use when
                                  * translating output data. */
    char *errorEncodingName;        /* Name of decoding scheme to use when
                                  * translating output data. */

    char *inputVar;                /* Name of a Tcl variable set to the
                                  * tcl channel of the input pipe of the
                                  * first UNIX subprocess. */ //DAS

    /* Private */
    Tcl_Interp *interp;                /* Interpreter containing variables */

    int nProcs;                        /* Number of processes in pipeline */

    ProcessId *procIds;                /* Array of process tokens from
                                  * pipeline.  Under Unix, tokens are
                                  * pid_t, while for Win32 they're
                                  * handles. */

    int traced;                        /* Indicates that the status variable
                                  * is currently being traced. */

    int detached;                /* Indicates that the pipeline is
                                  * detached from standard I/O, running
                                  * in the background. */

    int ignoreExitCode;                /* If non-zero, don't check for 0 exit
                                  * status of the pipeline.  */

    Tcl_TimerToken timerToken;        /* Token for timer handler which polls
                                  * for the exit status of each
                                  * sub-process. If zero, there's no
                                  * timer handler queued. */

    int *exitCodePtr;                /* Pointer to a memory location to
                                  * contain the last process' exit
                                  * code. */
    int *donePtr;

    Sink sink1, sink2;

    char *inputChanName;        /* Name of input channel */ //DAS

} BgExec;


static Blt_SwitchParseProc ObjToSignal;
static Blt_SwitchCustom killSignalSwitch =
{
    ObjToSignal, NULL, (ClientData)0,
};

static Blt_SwitchSpec switchSpecs[] =
{
    {BLT_SWITCH_STRING, "-decodeoutput", Blt_Offset(BgExec, outputEncodingName),
        0},
    {BLT_SWITCH_STRING, "-decodeerror", Blt_Offset(BgExec, errorEncodingName), 
        0},
    {BLT_SWITCH_BOOLEAN, "-echo", Blt_Offset(BgExec, sink2.echo), 0},
    {BLT_SWITCH_STRING, "-error", Blt_Offset(BgExec, sink2.doneVar), 0},
    {BLT_SWITCH_STRING, "-update", Blt_Offset(BgExec, sink1.updateVar), 0},
    {BLT_SWITCH_STRING, "-output", Blt_Offset(BgExec, sink1.doneVar), 0},
    {BLT_SWITCH_STRING, "-lasterror", Blt_Offset(BgExec, sink2.updateVar), 0},
    {BLT_SWITCH_STRING, "-lastoutput", Blt_Offset(BgExec, sink1.updateVar), 0},
    {BLT_SWITCH_LIST, "-onerror", Blt_Offset(BgExec, sink2.updateCmd), 0},
    {BLT_SWITCH_LIST, "-onoutput", Blt_Offset(BgExec, sink1.updateCmd), 0},
    {BLT_SWITCH_BOOLEAN, "-keepnewline", Blt_Offset(BgExec, keepNewline), 0}, 
    {BLT_SWITCH_BOOLEAN, "-check", Blt_Offset(BgExec, interval), 0},
    {BLT_SWITCH_CUSTOM, "-killsignal", Blt_Offset(BgExec, signalNum), 0, 
        &killSignalSwitch},
    {BLT_SWITCH_BOOLEAN, "-linebuffered", Blt_Offset(BgExec, lineBuffered), 0},
    {BLT_SWITCH_BOOLEAN, "-ignorecode", Blt_Offset(BgExec, ignoreExitCode), 0},
    {BLT_SWITCH_STRING, "-input", Blt_Offset(BgExec, inputVar), 0}, //DAS
    {BLT_SWITCH_END, NULL, 0, 0}
};

static Tcl_VarTraceProc VariableProc;
static Tcl_TimerProc TimerProc;
static Tcl_FileProc StdoutProc, StderrProc;

/*
 *----------------------------------------------------------------------
 *
 * GetSignal --
 *
 *        Convert a string represent a signal number into its integer
 *        value.
 *
 * Results:
 *        The return value is a standard Tcl result.
 *
 *----------------------------------------------------------------------
 */
/*ARGSUSED*/
static int
ObjToSignal(
    ClientData clientData,        /* Contains a pointer to the tabset containing
                                  * this image. */
    Tcl_Interp *interp,                /* Interpreter to send results back to */
    char *switchName,                /* Not used. */
    Tcl_Obj *objPtr,                /* Value representation */
    char *record,                /* Structure record */
    int offset,                        /* Offset to field in structure */
    int flags)        
{
    char *string;
    int *signalPtr = (int *)(record + offset);
    int signalNum;

    string = Tcl_GetString(objPtr);
    if (string[0] == '\0') {
         *signalPtr = 0;
        return TCL_OK;
    }
    if (isdigit((unsigned char)(string[0]))) {
        if (Tcl_GetIntFromObj(interp, objPtr, &signalNum) != TCL_OK) {
            return TCL_ERROR;
        }
    } else {
        char *name;
        SignalId *sp;

        name = string;

        /*  Clip off any "SIG" prefix from the signal name */
        if ((name[0] == 'S') && (name[1] == 'I') && (name[2] == 'G')) {
            name += 3;
        }
        signalNum = -1;
        for (sp = signalIds; sp->number > 0; sp++) {
            if (strcmp(sp->name + 3, name) == 0) {
                signalNum = sp->number;
                break;
            }
        }
        if (signalNum < 0) {
            Tcl_AppendResult(interp, "unknown signal \"", string, "\"",
                (char *)NULL);
            return TCL_ERROR;
        }
    }
    if ((signalNum < 0) || (signalNum > NSIG)) {
        /* Outside range of signals */
        Tcl_AppendResult(interp, "signal number \"", string,
            "\" is out of range", (char *)NULL);
        return TCL_ERROR;
    }
        *signalPtr = signalNum;
    return TCL_OK;
}


/*
 *----------------------------------------------------------------------
 *
 * GetSinkData --
 *
 *        Returns the data currently saved in the buffer
 *
 *----------------------------------------------------------------------
 */
static void
GetSinkData(
    Sink *sinkPtr,
    unsigned char **dataPtr,
    size_t *lengthPtr)
{
    size_t length;

    sinkPtr->bytes[sinkPtr->mark] = '\0';
    length = sinkPtr->mark;
    if ((sinkPtr->mark > 0) && (sinkPtr->encoding != ENCODING_BINARY)) {
        unsigned char *last;

        last = sinkPtr->bytes + (sinkPtr->mark - 1);
        if ((!(sinkPtr->flags & SINK_KEEP_NL)) && (*last == '\n')) {
            length--;
        }
    }
        *dataPtr = sinkPtr->bytes;
        *lengthPtr = length;
}

/*
 *----------------------------------------------------------------------
 *
 * NextBlock --
 *
 *        Returns the next block of data since the last time this
 *        routine was called.
 *
 *---------------------------------------------------------------------- 
 */
static unsigned char *
NextBlock(Sink *sinkPtr, int *lengthPtr)
{
    unsigned char *string;
    size_t length;

    string = sinkPtr->bytes + sinkPtr->lastMark;
    length = sinkPtr->mark - sinkPtr->lastMark;
    sinkPtr->lastMark = sinkPtr->mark;
    if (length > 0) {
        if ((!(sinkPtr->flags & SINK_KEEP_NL)) && 
            (string[length - 1] == '\n')) {
            length--;
        }
         *lengthPtr = length;
        return string;
    }
    return NULL;
}

/*
 *----------------------------------------------------------------------
 *
 * NextLine --
 *
 *        Returns the next line of data.
 *
 *----------------------------------------------------------------------
 */
static unsigned char *
NextLine(Sink *sinkPtr, int *lengthPtr)
{
    if (sinkPtr->mark > sinkPtr->lastMark) {
        unsigned char *string;
        size_t newBytes;
        register size_t i;

        string = sinkPtr->bytes + sinkPtr->lastMark;
        newBytes = sinkPtr->mark - sinkPtr->lastMark;
        for (i = 0; i < newBytes; i++) {
            if (string[i] == '\n') {
                int length;
                
                length = i + 1;
                sinkPtr->lastMark += length;
                if (!(sinkPtr->flags & SINK_KEEP_NL)) {
                    length--;                /* Backup over the newline. */
                }
                 *lengthPtr = length;
                return string;
            }
        }
        /* Newline not found.  On errors or EOF, also return a partial line. */
        if (sinkPtr->status < 0) {
             *lengthPtr = newBytes;
            sinkPtr->lastMark = sinkPtr->mark;
            return string;
        }
    }
    return NULL;
}
/*
 *----------------------------------------------------------------------
 *
 * ResetSink --
 *
 *        Removes the bytes already processed from the buffer, possibly
 *        resetting it to empty.  This used when we don't care about
 *        keeping all the data collected from the channel (no -output
 *        flag and the process is detached).
 *
 *---------------------------------------------------------------------- 
 */
static void
ResetSink(Sink *sinkPtr)
{
    if ((sinkPtr->flags & SINK_BUFFERED) && 
        (sinkPtr->fill > sinkPtr->lastMark)) {
        register size_t i, j;

        /* There may be bytes remaining in the buffer, awaiting
          * another read before we see the next newline.  So move the
          * bytes to the front of the array. */

        for (i = 0, j = sinkPtr->lastMark; j < sinkPtr->fill; i++, j++) {
            sinkPtr->bytes[i] = sinkPtr->bytes[j];
        }
        /* Move back the fill point and processed point. */
        sinkPtr->fill -= sinkPtr->lastMark;
        sinkPtr->mark -= sinkPtr->lastMark;
    } else {
        sinkPtr->mark = sinkPtr->fill = 0;
    }
    sinkPtr->lastMark = 0;
}

/*
 *----------------------------------------------------------------------
 *
 * InitSink --
 *
 *        Initializes the buffer's storage.
 *
 * Results:
 *        None.
 *
 * Side effects:
 *        Storage is cleared.
 *
 *----------------------------------------------------------------------
 */
static void
InitSink(
    BgExec *bgPtr,
    Sink *sinkPtr,
    char *name,
    Tcl_Encoding encoding)
{
    sinkPtr->name = name;
    sinkPtr->echo = FALSE;
    sinkPtr->fd = -1;
    sinkPtr->bytes = sinkPtr->staticSpace;
    sinkPtr->size = DEF_BUFFER_SIZE;
    sinkPtr->encoding = encoding;
    if (bgPtr->keepNewline) {
        sinkPtr->flags |= SINK_KEEP_NL;
    }
    if (bgPtr->lineBuffered) {
        sinkPtr->flags |= SINK_BUFFERED;
    }        
    if ((sinkPtr->updateCmd != NULL) || 
        (sinkPtr->updateVar != NULL) ||
        (sinkPtr->echo)) {
        sinkPtr->flags |= SINK_NOTIFY;
    }
    if (sinkPtr->updateCmd != NULL) {
        Tcl_Obj **objv;
        char **p;
        int objc;
        register int i;

        objc = 0;
        for (p = sinkPtr->updateCmd; *p != NULL; p++) {
            objc++;
        }
        objv = Blt_Malloc((objc + 1) * sizeof(Tcl_Obj *));
        for (i = 0; i < objc; i++) {
            objv[i] = Tcl_NewStringObj(sinkPtr->updateCmd[i], -1);
            Tcl_IncrRefCount(objv[i]);
        }
        sinkPtr->objv = objv;
        sinkPtr->objc = objc + 1;
    }
    ResetSink(sinkPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * FreeSinkBuffer --
 *
 *        Frees the buffer's storage, freeing any malloc'ed space.
 *
 * Results:
 *        None.
 *
 *----------------------------------------------------------------------
 */
static void
FreeSinkBuffer(Sink *sinkPtr)
{
    if (sinkPtr->bytes != sinkPtr->staticSpace) {
        Blt_Free(sinkPtr->bytes);
    }
    sinkPtr->fd = -1;
    if (sinkPtr->objv != NULL) {
        int i;

        for (i = 0; i < sinkPtr->objc - 1; i++) {
            Tcl_DecrRefCount(sinkPtr->objv[i]);
        }
        Blt_Free(sinkPtr->objv);
    }
}


/*
 *----------------------------------------------------------------------
 *
 * ExtendSinkBuffer --
 *
 *        Doubles the size of the current buffer.
 *
 * Results:
 *        None.
 *
 *----------------------------------------------------------------------
 */
static int
ExtendSinkBuffer(Sink *sinkPtr)
{
    unsigned char *bytes;
      /*
        * Allocate a new array, double the old size
        */
    sinkPtr->size += sinkPtr->size;
    bytes = Blt_Malloc(sizeof(unsigned char) * sinkPtr->size);
    if (bytes != NULL) {
        unsigned char *sp, *dp, *send;

        dp = bytes;
        for (sp = sinkPtr->bytes, send = sp + sinkPtr->fill; sp < send; 
             /*empty*/) {
             *dp++ = *sp++;
        }
        if (sinkPtr->bytes != sinkPtr->staticSpace) {
            Blt_Free(sinkPtr->bytes);
        }
        sinkPtr->bytes = bytes;
        return (sinkPtr->size - sinkPtr->fill); /* Return bytes left. */
    }
    return -1;
}

/*
 *----------------------------------------------------------------------
 *
 * ReadBytes --
 *
 *        Reads and appends any available data from a given file descriptor
 *        to the buffer.
 *
 * Results:
 *        Returns TCL_OK when EOF is found, TCL_RETURN if reading
 *        data would block, and TCL_ERROR if an error occurred.
 *
 *----------------------------------------------------------------------
 */
static void
ReadBytes(Sink *sinkPtr)
{
    int nBytes;
    int bytesLeft;
    register int i;
    unsigned char *array;

      /*
        * ------------------------------------------------------------------
        *
        *         Worry about indefinite postponement.
        *
        *         Typically we want to stay in the read loop as long as it takes
        *         to collect all the data that's currently available.  But if
        *         it's coming in at a constant high rate, we need to arbitrarily
        *         break out at some point. This allows for both setting the
        *         update variable and the Tk program to handle idle events.
        *
        * ------------------------------------------------------------------
        */

    for (i = 0; i < MAX_READS; i++) {

        /* Allocate a larger buffer when the number of remaining bytes
          * is below the threshold BLOCK_SIZE.  */

        bytesLeft = sinkPtr->size - sinkPtr->fill;

        if (bytesLeft < BLOCK_SIZE) {
            bytesLeft = ExtendSinkBuffer(sinkPtr);
            if (bytesLeft < 0) {
                sinkPtr->status = READ_ERROR;
                return;
            }
        }
        array = sinkPtr->bytes + sinkPtr->fill;

        /*
          * Read into a buffer but make sure we leave room for a
          * trailing NUL byte.
          */
        nBytes = read(sinkPtr->fd, array, bytesLeft - 1);
        if (nBytes == 0) {        /* EOF: break out of loop. */
            sinkPtr->status = READ_EOF;
            return;
        }
        if (nBytes < 0) {
#ifdef O_NONBLOCK
#define BLOCKED                EAGAIN
#else
#define BLOCKED                EWOULDBLOCK
#endif /*O_NONBLOCK*/
            /* Either an error has occurred or no more data is
              * currently available to read.  */
            if (errno == BLOCKED) {
                sinkPtr->status = READ_AGAIN;
                return;
            }
            sinkPtr->bytes[0] = '\0';
            sinkPtr->status = READ_ERROR;
            return;
        }
        sinkPtr->fill += nBytes;
        sinkPtr->bytes[sinkPtr->fill] = '\0';
    }
    sinkPtr->status = nBytes;
}

#define SINKOPEN(sinkPtr)  ((sinkPtr)->fd != -1)

static void
CloseSink(Tcl_Interp *interp, Sink *sinkPtr)
{
    if (SINKOPEN(sinkPtr)) {
        close(sinkPtr->fd);
        Tcl_DeleteFileHandler(sinkPtr->fd);
        sinkPtr->fd = -1;

        if (sinkPtr->doneVar != NULL) {
            unsigned char *data;
            size_t length;
            /* 
              * If data is to be collected, set the "done" variable
              * with the contents of the buffer.  
              */
            GetSinkData(sinkPtr, &data, &length);
            if (Tcl_SetVar2Ex(interp, sinkPtr->doneVar, NULL, 
                              Tcl_NewByteArrayObj(data, length),
                              (TCL_GLOBAL_ONLY | TCL_LEAVE_ERR_MSG)) == NULL) {
                Tcl_BackgroundError(interp);
            }
        }
    }
}

/*
 *----------------------------------------------------------------------
 *
 * CookSink --
 *
 *        For Windows, translate CR/NL combinations to NL alone.
 *
 * Results:
 *        None.
 *
 * Side Effects:
 *        The size of the byte array may shrink and array contents
 *        shifted as carriage returns are found and removed.
 *
 *----------------------------------------------------------------------
 */
static void
CookSink(Tcl_Interp *interp, Sink *sinkPtr)
{
    unsigned char *srcPtr, *endPtr;
    if (sinkPtr->encoding == ENCODING_BINARY) { /* binary */
        /* No translation needed. */
        sinkPtr->mark = sinkPtr->fill; 
    } else if (sinkPtr->encoding == ENCODING_ASCII) { /* ascii */
        /* One-to-one translation. mark == fill. */
        sinkPtr->mark = sinkPtr->fill;
    } else { /* unicode. */
        int nSrcCooked, nCooked;
        int result;
        size_t cookedSize, spaceLeft, needed;
        size_t nRaw, nLeftOver;
        unsigned char *destPtr;
        unsigned char *raw, *cooked;
        unsigned char leftover[100];
        
        raw = sinkPtr->bytes + sinkPtr->mark;
        nRaw = sinkPtr->fill - sinkPtr->mark;
        /* Ideally, the cooked buffer size should be smaller */
        cookedSize = nRaw * TCL_UTF_MAX + 1;
        cooked = Blt_Malloc(cookedSize);
        result = Tcl_ExternalToUtf(interp, sinkPtr->encoding, 
                        (char *)raw, nRaw, 0, NULL, (char *)cooked, 
                        cookedSize, &nSrcCooked, &nCooked, NULL);
        nLeftOver = 0;
        if (result == TCL_CONVERT_MULTIBYTE) {
            /* 
              * Last multibyte sequence wasn't completed.  Save the
              * extra characters in a temporary buffer.  
              */
            nLeftOver = (nRaw - nSrcCooked);
            srcPtr = sinkPtr->bytes + (sinkPtr->mark + nSrcCooked); 
            endPtr = srcPtr + nLeftOver;
            destPtr = leftover;
            while (srcPtr < endPtr) {
                 *destPtr++ = *srcPtr++;
            }
        } 
        /*
          * Create a bigger 
          */
                                                 
        needed = nLeftOver + nCooked;
        spaceLeft = sinkPtr->size - sinkPtr->mark;
        if (spaceLeft >= needed) {
            spaceLeft = ExtendSinkBuffer(sinkPtr);
        }
        assert(spaceLeft > needed);
        /* 
          * Replace the characters from the mark with the translated 
          * characters.
          */
        srcPtr = cooked;
        endPtr = cooked + nCooked;
        destPtr = sinkPtr->bytes + sinkPtr->mark;
        while (srcPtr < endPtr) {
             *destPtr++ = *srcPtr++;
        }
        /* Add the number of newly translated characters to the mark */
        sinkPtr->mark += nCooked;
        
        srcPtr = leftover;
        endPtr = leftover + nLeftOver;
        while (srcPtr < endPtr) {
             *destPtr++ = *srcPtr++;
        }
        sinkPtr->fill = sinkPtr->mark + nLeftOver;
    }
}

static void
NotifyOnUpdate(
    Tcl_Interp *interp,
    Sink *sinkPtr,
    unsigned char *data,
    int nBytes)
{
    Tcl_Obj *objPtr;

    if ((nBytes == 0) || (data[0] == '\0')) {
        return;
    }
    if (sinkPtr->echo) {
        Tcl_Channel channel;
        
        channel = Tcl_GetStdChannel(TCL_STDERR);
        if (channel == NULL) {
            Tcl_AppendResult(interp, "can't get stderr channel", (char *)NULL);
            Tcl_BackgroundError(interp);
            sinkPtr->echo = FALSE;
        } else {
            if (data[nBytes] == '\n') {
                objPtr = Tcl_NewByteArrayObj(data, nBytes + 1);
            } else {
                objPtr = Tcl_NewByteArrayObj(data, nBytes);
            }
            Tcl_WriteObj(channel, objPtr);
            Tcl_Flush(channel);
        }
    }

    objPtr = Tcl_NewByteArrayObj(data, nBytes);
    Tcl_IncrRefCount(objPtr);
    if (sinkPtr->objv != NULL) {
        int result;

        sinkPtr->objv[sinkPtr->objc - 1] = objPtr;
        result = Tcl_EvalObjv(interp, sinkPtr->objc, sinkPtr->objv, 0);
        if (result != TCL_OK) {
            Tcl_BackgroundError(interp);
        }
    }
    if (sinkPtr->updateVar != NULL) {
        Tcl_Obj *result;

        result = Tcl_SetVar2Ex(interp, sinkPtr->updateVar, NULL, objPtr, 
               (TCL_GLOBAL_ONLY | TCL_LEAVE_ERR_MSG));
        if (result == NULL) {
            Tcl_BackgroundError(interp);
        }
    }
    Tcl_DecrRefCount(objPtr);
}

static int
CollectData(BgExec *bgPtr, Sink *sinkPtr)
{
    if ((bgPtr->detached) && (sinkPtr->doneVar == NULL)) {
        ResetSink(sinkPtr);
    }
    ReadBytes(sinkPtr);
    CookSink(bgPtr->interp, sinkPtr);
    if ((sinkPtr->mark > sinkPtr->lastMark) && 
        (sinkPtr->flags & SINK_NOTIFY)) {
        unsigned char *data;
        int length;

        if (sinkPtr->flags & SINK_BUFFERED) {
            /* For line-by-line updates, call NotifyOnUpdate for each
              * new complete line.  */
            while ((data = NextLine(sinkPtr, &length)) != NULL) {
                NotifyOnUpdate(bgPtr->interp, sinkPtr, data, length);
            }
        } else {
            data = NextBlock(sinkPtr, &length);
            NotifyOnUpdate(bgPtr->interp, sinkPtr, data, length);
        }
    }
    if (sinkPtr->status >= 0) {
        return TCL_OK;
    }
    if (sinkPtr->status == READ_ERROR) {
        Tcl_AppendResult(bgPtr->interp, "can't read data from ", sinkPtr->name,
            ": ", Tcl_PosixError(bgPtr->interp), (char *)NULL);
        Tcl_BackgroundError(bgPtr->interp);
        return TCL_ERROR;
    }
    return TCL_RETURN;
}

/*
 *----------------------------------------------------------------------
 *
 * CreateSinkHandler --
 *
 *        Creates a file handler for the given sink.  The file
 *        descriptor is also set for non-blocking I/O.
 *
 * Results:
 *        None.
 *
 * Side effects:
 *        The memory allocated to the BgExec structure released.
 *
 *----------------------------------------------------------------------
 */
static int
CreateSinkHandler(BgExec *bgPtr, Sink *sinkPtr, Tcl_FileProc *proc)
{
    int flags;

    flags = fcntl(sinkPtr->fd, F_GETFL);
#ifdef O_NONBLOCK
    flags |= O_NONBLOCK;
#else
    flags |= O_NDELAY;
#endif
    if (fcntl(sinkPtr->fd, F_SETFL, flags) < 0) {
        Tcl_AppendResult(bgPtr->interp, "can't set file descriptor ",
            Blt_Itoa(sinkPtr->fd), " to non-blocking:",
            Tcl_PosixError(bgPtr->interp), (char *)NULL);
        return TCL_ERROR;
    }
    Tcl_CreateFileHandler(sinkPtr->fd, TCL_READABLE, proc, bgPtr);
    return TCL_OK;
}

static void
DisableTriggers(BgExec *bgPtr) /* Background info record. */
{

    if (bgPtr->traced) {
        Tcl_UntraceVar(bgPtr->interp, bgPtr->statVar, TRACE_FLAGS, 
                VariableProc, bgPtr);
        bgPtr->traced = FALSE;
    }
    if (SINKOPEN(&bgPtr->sink1)) {
        CloseSink(bgPtr->interp, &bgPtr->sink1);
    }
    if (SINKOPEN(&bgPtr->sink2)) {
        CloseSink(bgPtr->interp, &bgPtr->sink2);
    }
    if (bgPtr->timerToken != (Tcl_TimerToken) 0) {
        Tcl_DeleteTimerHandler(bgPtr->timerToken);
        bgPtr->timerToken = 0;
    }
    if (bgPtr->donePtr != NULL) {
         *bgPtr->donePtr = TRUE;
    }
}


/*
 *----------------------------------------------------------------------
 *
 * FreeBgExec --
 *
 *        Releases the memory allocated for the backgrounded process.
 *
 *----------------------------------------------------------------------
 */
static void
FreeBgExec(BgExec *bgPtr)
{
    Blt_FreeSwitches(switchSpecs, (char *)bgPtr, 0);
    if (bgPtr->statVar != NULL) {
        Blt_Free(bgPtr->statVar);
    }
    if (bgPtr->procIds != NULL) {
        Blt_Free(bgPtr->procIds);
    }
    Blt_Free(bgPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * DestroyBgExec --
 *
 *         This procedure is invoked by Tcl_EventuallyFree or Tcl_Release
 *         to clean up the internal structure (BgExec) at a safe
 *         time (when no one is using it anymore).
 *
 * Results:
 *        None.b
 *
 * Side effects:
 *        The memory allocated to the BgExec structure released.
 *
 *----------------------------------------------------------------------
 */
/* ARGSUSED */
static void
DestroyBgExec(BgExec *bgPtr) /* Background info record. */
{
    DisableTriggers(bgPtr);
    FreeSinkBuffer(&bgPtr->sink2);
    FreeSinkBuffer(&bgPtr->sink1);
    if (bgPtr->inputChanName) {  //DAS begin
        Tcl_Channel chan = Tcl_GetChannel(bgPtr->interp, bgPtr->inputChanName, NULL);
        if (chan) {
            Tcl_UnregisterChannel(bgPtr->interp, chan);
        }
    }  //DAS end
    if (bgPtr->procIds != NULL) {
        int i;

        for (i = 0; i < bgPtr->nProcs; i++) {
            if (bgPtr->signalNum > 0) {
                kill(bgPtr->procIds[i], bgPtr->signalNum);
            }
            Tcl_DetachPids(1, (Tcl_Pid *)bgPtr->procIds[i]);
        }
    }
    FreeBgExec(bgPtr);
    Tcl_ReapDetachedProcs();
}

/*
 * ----------------------------------------------------------------------
 *
 * VariableProc --
 *
 *        Kills all currently running subprocesses (given the specified
 *        signal). This procedure is called when the user sets the status
 *        variable associated with this group of child subprocesses.
 *
 * Results:
 *        Always returns NULL.  Only called from a variable trace.
 *
 * Side effects:
 *        The subprocesses are signaled for termination using the
 *        specified kill signal.  Additionally, any resources allocated
 *        to track the subprocesses is released.
 *
 * ----------------------------------------------------------------------
 */
/* ARGSUSED */
static char *
VariableProc(
    ClientData clientData,        /* File output information. */
    Tcl_Interp *interp,
    CONST84 char *part1,
    CONST84 char *part2,                /* Not Used. */
    int flags)
{
    if (flags & TRACE_FLAGS) {
        BgExec *bgPtr = clientData;

        /* Kill all child processes that remain alive. */
        if ((bgPtr->procIds != NULL) && (bgPtr->signalNum > 0)) {
            int i;

            for (i = 0; i < bgPtr->nProcs; i++) {
                kill(bgPtr->procIds[i], bgPtr->signalNum);
            }
        }
    }
    return NULL;
}

/*
 *----------------------------------------------------------------------
 *
 * TimerProc --
 *
 *        This is a timer handler procedure which gets called
 *        periodically to reap any of the sub-processes if they have
 *        terminated.  After the last process has terminated, the
 *        contents of standard output are stored
 *        in the output variable, which triggers the cleanup proc (using
 *        a variable trace). The status the last process to exit is
 *        written to the status variable.
 *
 * Results:
 *        None.  Called from the Tcl event loop.
 *
 * Side effects:
 *        Many. The contents of procIds is shifted, leaving only those
 *        sub-processes which have not yet terminated.  If there are
 *        still subprocesses left, this procedure is placed in the timer
 *        queue again. Otherwise the output and possibly the status
 *        variables are updated.  The former triggers the cleanup
 *        routine which will destroy the information and resources
 *        associated with these background processes.
 *
 *----------------------------------------------------------------------
 */
static void
TimerProc(ClientData clientData)
{
    BgExec *bgPtr = clientData;
    register int i;
    unsigned int lastPid;
    enum PROCESS_STATUS { 
        PROCESS_EXITED, PROCESS_STOPPED, PROCESS_KILLED, PROCESS_UNKNOWN
    } pcode;
    WAIT_STATUS_TYPE waitStatus, lastStatus;
    int nLeft;                        /* Number of processes still not reaped */
    char string[200];
    Tcl_DString dString;
    int code;
    CONST char *result;

    lastPid = (unsigned int)-1;
        *((int *)&waitStatus) = 0;
        *((int *)&lastStatus) = 0;

    nLeft = 0;
    for (i = 0; i < bgPtr->nProcs; i++) {
        int pid;

        pid = waitpid(bgPtr->procIds[i], (int *)&waitStatus, WNOHANG);
        if (pid == 0) {                /*  Process has not terminated yet */
            if (nLeft < i) {
                bgPtr->procIds[nLeft] = bgPtr->procIds[i];
            }
            nLeft++;                /* Count the number of processes left */
        } else if (pid != -1) {
            /*
              * Save the status information associated with the subprocess.
              * We'll use it only if this is the last subprocess to be reaped.
              */
            lastStatus = waitStatus;
            lastPid = (unsigned int)pid;
        }
    }
    bgPtr->nProcs = nLeft;

    if ((nLeft > 0) || (SINKOPEN(&bgPtr->sink1)) || 
        (SINKOPEN(&bgPtr->sink2))) {
        /* Keep polling for the status of the children that are left */
        bgPtr->timerToken = Tcl_CreateTimerHandler(bgPtr->interval, TimerProc,
           bgPtr);
        return;
    }

      /*
        * All child processes have completed.  Set the status variable
        * with the status of the last process reaped.  The status is a
        * list of an error token, the exit status, and a message.
        */

    code = WEXITSTATUS(lastStatus);
    Tcl_DStringInit(&dString);
    if (WIFEXITED(lastStatus)) {
        Tcl_DStringAppendElement(&dString, "EXITED");
        pcode = PROCESS_EXITED;
    } else if (WIFSIGNALED(lastStatus)) {
        Tcl_DStringAppendElement(&dString, "KILLED");
        pcode = PROCESS_KILLED;
        code = -1;
    } else if (WIFSTOPPED(lastStatus)) {
        Tcl_DStringAppendElement(&dString, "STOPPED");
        pcode = PROCESS_STOPPED;
        code = -1;
    } else {
        Tcl_DStringAppendElement(&dString, "UNKNOWN");
        pcode = PROCESS_UNKNOWN;
    }
    Tcl_DStringAppendElement(&dString, Blt_Itoa(lastPid));
    Tcl_DStringAppendElement(&dString, Blt_Itoa(code));
    switch(pcode) {
    case PROCESS_EXITED:
        Tcl_DStringAppendElement(&dString, "child completed normally");
        break;
    case PROCESS_KILLED:
        Tcl_DStringAppendElement(&dString, 
                                Tcl_SignalMsg((int)(WTERMSIG(lastStatus))));
        break;
    case PROCESS_STOPPED:
        Tcl_DStringAppendElement(&dString, 
                 Tcl_SignalMsg((int)(WSTOPSIG(lastStatus))));
        break;
    case PROCESS_UNKNOWN:
        sprintf(string, "child completed with unknown status 0x%x",
             *((int *)&lastStatus));
        Tcl_DStringAppendElement(&dString, string);
        break;
    }
    if (bgPtr->exitCodePtr != NULL) {
         *bgPtr->exitCodePtr = code;
    }
    DisableTriggers(bgPtr);
    result = Tcl_SetVar(bgPtr->interp, bgPtr->statVar, 
        Tcl_DStringValue(&dString), TCL_GLOBAL_ONLY);
    Tcl_DStringFree(&dString);
    if (result == NULL) {
        Tcl_BackgroundError(bgPtr->interp);
    }
    if (bgPtr->detached) {
        DestroyBgExec(bgPtr);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * Stdoutproc --
 *
 *        This procedure is called when output from the detached pipeline
 *        is available.  The output is read and saved in a buffer in the
 *        BgExec structure.
 *
 * Results:
 *        None.
 *
 * Side effects:
 *        Data is stored in the buffer.  This character array may
 *        be increased as more space is required to contain the output
 *        of the pipeline.
 *
 *----------------------------------------------------------------------
 */
/* ARGSUSED */
static void
StdoutProc(
    ClientData clientData,        /* Background information structure. */
    int mask)                        /* Not used. */
{
    BgExec *bgPtr = clientData;

    if (CollectData(bgPtr, &bgPtr->sink1) == TCL_OK) {
        return;
    }
      /*
        * Either EOF or an error has occurred.  In either case, close the
        * sink. Note that closing the sink will also remove the file
        * handler, so this routine will not be called again.
        */
    CloseSink(bgPtr->interp, &bgPtr->sink1);

      /*
        * If both sinks (stdout and stderr) are closed, this doesn't
        * necessarily mean that the process has terminated.  Set up a
        * timer handler to periodically poll for the exit status of each
        * process.  Initially check at the next idle interval.
        */
    if (!SINKOPEN(&bgPtr->sink2)) {
        bgPtr->timerToken = Tcl_CreateTimerHandler(0, TimerProc, clientData);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * StderrProc --
 *
 *        This procedure is called when error from the detached pipeline
 *        is available.  The error is read and saved in a buffer in the
 *        BgExec structure.
 *
 * Results:
 *        None.
 *
 * Side effects:
 *        Data is stored in the buffer.  This character array may
 *        be increased as more space is required to contain the stderr
 *        of the pipeline.
 *
 *----------------------------------------------------------------------
 */
/* ARGSUSED */
static void
StderrProc(
    ClientData clientData,        /* Background information structure. */
    int mask)                        /* Not used. */
{
    BgExec *bgPtr = clientData;

    if (CollectData(bgPtr, &bgPtr->sink2) == TCL_OK) {
        return;
    }
      /*
        * Either EOF or an error has occurred.  In either case, close the
        * sink. Note that closing the sink will also remove the file
        * handler, so this routine will not be called again.
        */
    CloseSink(bgPtr->interp, &bgPtr->sink2);

      /*
        * If both sinks (stdout and stderr) are closed, this doesn't
        * necessarily mean that the process has terminated.  Set up a
        * timer handler to periodically poll for the exit status of each
        * process.  Initially check at the next idle interval.
        */
    if (!SINKOPEN(&bgPtr->sink1)) {
        bgPtr->timerToken = Tcl_CreateTimerHandler(0, TimerProc, clientData);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * OpenFile --
 *
 *        Open a file for use in a pipeline.
 *
 * Results:
 *        Returns a new TclFile handle or NULL on failure.
 *
 * Side effects:
 *        May cause a file to be created on the file system.
 *
 *----------------------------------------------------------------------
 */

static int
OpenFile(
    char *fname,                /* The name of the file to open. */
    int mode)                        /* In what mode to open the file? */
{
    int fd;

    fd = open(fname, mode, 0666);
    if (fd != -1) {
        fcntl(fd, F_SETFD, FD_CLOEXEC);

        /*
          * If the file is being opened for writing, seek to the end
          * so we can append to any data already in the file.
          */

        if (mode & O_WRONLY) {
            lseek(fd, 0, SEEK_END);
        }
        return fd;
    }
    return -1;
}

/*
 *----------------------------------------------------------------------
 *
 * CreateTempFile --
 *
 *        This function creates a temporary file initialized with an
 *        optional string, and returns a file handle with the file pointer
 *        at the beginning of the file.
 *
 * Results:
 *        A handle to a file.
 *
 * Side effects:
 *        None.
 *
 *----------------------------------------------------------------------
 */

static int
CreateTempFile(char *contents)        /* String to write into temp file, or NULL. */
{
    char fileName[L_tmpnam];
    int fd;
    size_t length = (contents == NULL) ? 0 : strlen(contents);

    mkstemp(fileName);
    fd = OpenFile(fileName, O_RDWR | O_CREAT | O_TRUNC);
    unlink(fileName);

    if ((fd >= 0) && (length > 0)) {
        for (;;) {
            if (write(fd, contents, length) != -1) {
                break;
            } else if (errno != EINTR) {
                close(fd);
                return -1;
            }
        }
        lseek(fd, 0, SEEK_SET);
    }
    return fd;
}

/*
 *----------------------------------------------------------------------
 *
 * CreatePipe --
 *
 *      Creates a pipe by simply calling the pipe() function.  
 *
 * Results:
 *      Returns 1 on success, 0 on failure.
 *
 * Side effects:
 *      Creates a pipe.
 *
 *----------------------------------------------------------------------
 */

static int
CreatePipe(
    int *inFilePtr,                /* (out) Descriptor for read side of pipe. */
    int *outFilePtr)                /* (out) Descriptor for write side of pipe. */
{
    int pipeDes[2];

    if (pipe(pipeDes) != 0) {
        return 0;
    }
    fcntl(pipeDes[0], F_SETFD, FD_CLOEXEC);
    fcntl(pipeDes[1], F_SETFD, FD_CLOEXEC);

        *inFilePtr = pipeDes[0];
        *outFilePtr = pipeDes[1];
    return 1;
}

/*
 *----------------------------------------------------------------------
 *
 * CloseFile --
 *
 *        Implements a mechanism to close a UNIX file.
 *
 * Results:
 *        Returns 0 on success, or -1 on error, setting errno.
 *
 * Side effects:
 *        The file is closed.
 *
 *----------------------------------------------------------------------
 */

static int
CloseFile(int fd)                /* File descriptor to be closed. */
{
    if ((fd == 0) || (fd == 1) || (fd == 2)) {
        return 0;                /* Don't close stdin, stdout or stderr. */
    }
    Tcl_DeleteFileHandler(fd);
    return close(fd);
}

/*
 *----------------------------------------------------------------------
 *
 * RestoreSignals --
 *
 *      This procedure is invoked in a forked child process just before
 *      exec-ing a new program to restore all signals to their default
 *      settings.
 *
 * Results:
 *      None.
 *
 * Side effects:
 *      Signal settings get changed.
 *
 *----------------------------------------------------------------------
 */

static void
RestoreSignals()
{
#ifdef SIGABRT
    signal(SIGABRT, SIG_DFL);
#endif
#ifdef SIGALRM
    signal(SIGALRM, SIG_DFL);
#endif
#ifdef SIGFPE
    signal(SIGFPE, SIG_DFL);
#endif
#ifdef SIGHUP
    signal(SIGHUP, SIG_DFL);
#endif
#ifdef SIGILL
    signal(SIGILL, SIG_DFL);
#endif
#ifdef SIGINT
    signal(SIGINT, SIG_DFL);
#endif
#ifdef SIGPIPE
    signal(SIGPIPE, SIG_DFL);
#endif
#ifdef SIGQUIT
    signal(SIGQUIT, SIG_DFL);
#endif
#ifdef SIGSEGV
    signal(SIGSEGV, SIG_DFL);
#endif
#ifdef SIGTERM
    signal(SIGTERM, SIG_DFL);
#endif
#ifdef SIGUSR1
    signal(SIGUSR1, SIG_DFL);
#endif
#ifdef SIGUSR2
    signal(SIGUSR2, SIG_DFL);
#endif
#ifdef SIGCHLD
    signal(SIGCHLD, SIG_DFL);
#endif
#ifdef SIGCONT
    signal(SIGCONT, SIG_DFL);
#endif
#ifdef SIGTSTP
    signal(SIGTSTP, SIG_DFL);
#endif
#ifdef SIGTTIN
    signal(SIGTTIN, SIG_DFL);
#endif
#ifdef SIGTTOU
    signal(SIGTTOU, SIG_DFL);
#endif
}

/*
 *----------------------------------------------------------------------
 *
 * SetupStdFile --
 *
 *        Set up stdio file handles for the child process, using the
 *        current standard channels if no other files are specified.
 *        If no standard channel is defined, or if no file is associated
 *        with the channel, then the corresponding standard fd is closed.
 *
 * Results:
 *        Returns 1 on success, or 0 on failure.
 *
 * Side effects:
 *        Replaces stdio fds.
 *
 *----------------------------------------------------------------------
 */

static int
SetupStdFile(
    int fd,                        /* File descriptor to dup, or -1. */
    int type)                        /* One of TCL_STDIN, TCL_STDOUT, TCL_STDERR */
{
    int targetFd = 0;                /* Initializations here needed only to */
    int direction = 0;                /* prevent warnings about using uninitialized
                                  * variables. */

    switch (type) {
    case TCL_STDIN:
        targetFd = 0;
        direction = TCL_READABLE;
        break;
    case TCL_STDOUT:
        targetFd = 1;
        direction = TCL_WRITABLE;
        break;
    case TCL_STDERR:
        targetFd = 2;
        direction = TCL_WRITABLE;
        break;
    }
    if (fd < 0) {
        Tcl_Channel channel;

        channel = Tcl_GetStdChannel(type);
        if (channel) {
            ClientData clientData;
            
            Tcl_GetChannelHandle(channel, direction, &clientData);
            fd = (int)clientData;
        }
    }
    if (fd >= 0) {
        if (fd != targetFd) {
            if (dup2(fd, targetFd) == -1) {
                return 0;
            }
            /*
              * Must clear the close-on-exec flag for the target FD, since
              * some systems (e.g. Ultrix) do not clear the CLOEXEC flag on
              * the target FD.
              */

            fcntl(targetFd, F_SETFD, 0);
        } else {
            /*
              * Since we aren't dup'ing the file, we need to explicitly clear
              * the close-on-exec flag.
              */
            fcntl(fd, F_SETFD, 0);
        }
    } else {
        close(targetFd);
    }
    return 1;
}

/*
 *----------------------------------------------------------------------
 *
 * CreateProcess --
 *
 *        Create a child process that has the specified files as its
 *        standard input, output, and error.  The child process runs
 *        asynchronously and runs with the same environment variables
 *        as the creating process.
 *
 *        The path is searched to find the specified executable.
 *
 * Results:
 *        The return value is TCL_ERROR and an error message is left in
 *        interp->result if there was a problem creating the child
 *        process.  Otherwise, the return value is TCL_OK and *pidPtr is
 *        filled with the process id of the child process.
 *
 * Side effects:
 *        A process is created.
 *
 *----------------------------------------------------------------------
 */

/* ARGSUSED */
static int
CreateProcess(
    Tcl_Interp *interp,                /* Interpreter in which to leave
                                  * errors that occurred when creating
                                  * the child process.  Error messages
                                  * from the child process itself are
                                  * sent to stderrFd. */
    int argc,                        /* Number of arguments in following
                                  * array. */
    char **argv,                /* Array of argument strings.  argv[0]
                                  * contains the name of the executable
                                  * converted to native format (using
                                  * the Tcl_TranslateFileName call).
                                  * Additional arguments have not been
                                  * converted. */
    int stdinFd,                /* The file to use as input for the
                                  * child process.  If stdinFd file is
                                  * -1, intput is read from the
                                  * standard input channel. If the file
                                  * isn't readable, the child will
                                  * receive no standard input. */
    int stdoutFd,                /* The file that receives output from
                                  * the child process.  If stdoutFd is -1,
                                  * output is sent to the standard output
                                  * channel.  If the file is not writeable,
                                  * output from the child will be
                                  * discarded. */
    int stderrFd,                /* The file that receives errors from
                                  * the child process.  If stderrFd
                                  * file is -1, errors will be sent to
                                  * the standard error channel. If the
                                  * file isn't writeable, errors from
                                  * the child will be discarded.
                                  * stderrFd may be the same as
                                  * stdoutFd. */
    int *pidPtr)                /* (out) If this procedure is
                                  * successful, pidPtr is filled with
                                  * the process id of the child
                                  * process. */
{
    Tcl_DString *dsArr;
    Tcl_Encoding encoding;
    char errSpace[200];
    int errPipeIn, errPipeOut;
    int i;
    int joinThisError, status, fd;
    int pid;
    size_t count;

    errPipeIn = errPipeOut = -1;
    pid = -1;

    dsArr = Blt_Malloc(argc * sizeof(Tcl_DString));
    encoding = Tcl_GetEncoding(interp, NULL);
    for(i = 0; i < argc; i++) {
        argv[i] = Tcl_UtfToExternalDString(encoding, argv[i], 
                strlen(argv[i]), dsArr + i);
    }
      /*
        * Create a pipe that the child can use to return error
        * information if anything goes wrong.
        */
    if (CreatePipe(&errPipeIn, &errPipeOut) == 0) {
        Tcl_AppendResult(interp, "can't create pipe: ",
            Tcl_PosixError(interp), (char *)NULL);
        goto error;
    }
    joinThisError = (stderrFd == stdoutFd);
    pid = fork();
    if (pid == 0) {
        fd = errPipeOut;

        /*
          * Set up stdio file handles for the child process.
          */

        if (!SetupStdFile(stdinFd, TCL_STDIN) ||
            !SetupStdFile(stdoutFd, TCL_STDOUT) ||
            (!joinThisError && !SetupStdFile(stderrFd, TCL_STDERR)) ||
            (joinThisError &&
                ((dup2(1, 2) == -1) || (fcntl(2, F_SETFD, 0) != 0)))) {
            sprintf(errSpace, "%dforked process can't set up input/output: ",
                errno);
            write(fd, errSpace, (size_t) strlen(errSpace));
            _exit(1);
        }
        /*
          * Close the input side of the error pipe.
          */

        RestoreSignals();
        execvp(argv[0], &argv[0]);
        sprintf(errSpace, "%dcan't execute \"%.150s\": ", errno, argv[0]);
        write(fd, errSpace, (size_t) strlen(errSpace));
        _exit(1);
    }
    if (pid == -1) {
        Tcl_AppendResult(interp, "can't fork child process: ",
            Tcl_PosixError(interp), (char *)NULL);
        goto error;
    }

      /*
        * Read back from the error pipe to see if the child started
        * up OK.  The info in the pipe (if any) consists of a decimal
        * errno value followed by an error message.
        */
    CloseFile(errPipeOut);
    errPipeOut = -1;

    fd = errPipeIn;
    count = read(fd, errSpace, (size_t) (sizeof(errSpace) - 1));
    if (count > 0) {
        char *end;

        errSpace[count] = 0;
        errno = strtol(errSpace, &end, 10);
        Tcl_AppendResult(interp, end, Tcl_PosixError(interp), (char *)NULL);
        goto error;
    }
    for(i = 0; i < argc; i++) {
        Tcl_DStringFree(dsArr + i);
    }
    Blt_Free(dsArr);
    CloseFile(errPipeIn);
        *pidPtr = pid;
    return TCL_OK;

  error:
    if (pid != -1) {
        /*
          * Reap the child process now if an error occurred during its
          * startup.
          */
        Tcl_WaitPid((Tcl_Pid)pid, &status, WNOHANG);
    }
    if (errPipeIn >= 0) {
        CloseFile(errPipeIn);
    }
    if (errPipeOut >= 0) {
        CloseFile(errPipeOut);
    }
    for(i = 0; i < argc; i++) {
        Tcl_DStringFree(dsArr + i);
    }
    Blt_Free(dsArr);
    return TCL_ERROR;
}

/*
 *----------------------------------------------------------------------
 *
 * FileForRedirect --
 *
 *        This procedure does much of the work of parsing redirection
 *        operators.  It handles "@" if specified and allowed, and a file
 *        name, and opens the file if necessary.
 *
 * Results:
 *        The return value is the descriptor number for the file.  If an
 *        error occurs then NULL is returned and an error message is left
 *        in interp->result.  Several arguments are side-effected; see
 *        the argument list below for details.
 *
 * Side effects:
 *        None.
 *
 *----------------------------------------------------------------------
 */

static int
FileForRedirect(
    Tcl_Interp *interp,                /* Intepreter to use for error
                                  * reporting. */
    char *spec,                        /* Points to character just after
                                  * redirection character. */
    char *arg,                        /* Pointer to entire argument
                                  * containing spec: used for error
                                  * reporting. */
    int atOK,                        /* Non-zero means that '@' notation
                                  * can be used to specify a channel,
                                  * zero means that it isn't. */
    char *nextArg,                /* Next argument in argc/argv array,
                                  * if needed for file name or channel
                                  * name.  May be NULL. */
    int flags,                        /* Flags to use for opening file or to
                                  * specify mode for channel. */
    int *skipPtr,                /* (out) Filled with 1 if redirection target
                                  * was in spec, 2 if it was in
                                  * nextArg. */
    int *closePtr)                /* (out) Filled with one if the caller
                                  * should close the file when done
                                  * with it, zero otherwise. */
{
    int writing = (flags & O_WRONLY);
    int fd;

        *skipPtr = 1;
    if ((atOK != 0) && (*spec == '@')) {
        ClientData clientData;
        int direction;
        Tcl_Channel chan;

        spec++;
        if (*spec == '\0') {
            spec = nextArg;
            if (spec == NULL) {
                goto badLastArg;
            }
             *skipPtr = 2;
        }
        chan = Tcl_GetChannel(interp, spec, NULL);
        if (chan == NULL) {
            return -1;
        }
        direction = (writing) ? TCL_WRITABLE : TCL_READABLE;
        if (Tcl_GetChannelHandle(chan, direction, &clientData) != TCL_OK) {
            fd = -1;
        }
        fd = (int)clientData;
        if (fd < 0) {
            Tcl_AppendResult(interp, "channel \"", Tcl_GetChannelName(chan),
                "\" wasn't opened for ",
                ((writing) ? "writing" : "reading"), (char *)NULL);
            return -1;
        }
        if (writing) {
            /*
              * Be sure to flush output to the file, so that anything
              * written by the child appears after stuff we've already
              * written.
              */
            Tcl_Flush(chan);
        }
    } else {
        char *name;
        Tcl_DString nameString;

        if (*spec == '\0') {
            spec = nextArg;
            if (spec == NULL) {
                goto badLastArg;
            }
             *skipPtr = 2;
        }
        name = Tcl_TranslateFileName(interp, spec, &nameString);

        if (name != NULL) {
            fd = OpenFile(name, flags);
        } else {
            fd = -1;
        }
        Tcl_DStringFree(&nameString);
        if (fd < 0) {
            Tcl_AppendResult(interp, "can't ",
                ((writing) ? "write" : "read"), " file \"", spec, "\": ",
                Tcl_PosixError(interp), (char *)NULL);
            return -1;
        }
         *closePtr = TRUE;
    }
    return fd;

  badLastArg:
    Tcl_AppendResult(interp, "can't specify \"", arg,
        "\" as last word in command", (char *)NULL);
    return -1;
}

/*
 *----------------------------------------------------------------------
 *
 * Blt_CreatePipeline --
 *
 *        Given an objc/objv array, instantiate a pipeline of processes
 *        as described by the objv.
 *
 * Results:
 *        The return value is a count of the number of new processes
 *        created, or -1 if an error occurred while creating the
 *        pipeline.  *pidArrayPtr is filled in with the address of a
 *        dynamically allocated array giving the ids of all of the
 *        processes.  
 *
 *        It is up to the caller to free this array when it isn't needed
 *        anymore.  
 *
 *        If stdinPipePtr isn't NULL, then *stdinPipePtr is filled
 *        with the file id for the input pipe for the pipeline (if any):
 *        the caller must eventually close this file.
 *
 *        If stdoutPipePtr isn't NULL, then *stdoutPipePtr is filled
 *        with the file id for the output pipe from the pipeline: the
 *        caller must close this file.  
 *
 *        If stderrPipePtr isn't NULL, then *stderrPipePtr is filled
 *        with a file id that may be used to read error output after the
 *        pipeline completes.
 *
 * Side effects:
 *        Processes and pipes are created.
 *
 *----------------------------------------------------------------------
 */

static int
Blt_CreatePipeline(
    Tcl_Interp *interp,                /* Interpreter to use for error
                                  * reporting. */
    int objc,                        /* Number of entries in objv. */
    Tcl_Obj *CONST *objv,        /* Array of strings describing
                                  * commands in pipeline plus I/O
                                  * redirection with <, <<, >, etc.
                                  * Objv[objc] must be NULL. */
    int **pidArrayPtr,                /* (out) Word at *pidArrayPtr gets
                                  * filled in with address of array of
                                  * pids for processes in pipeline
                                  * (first pid is first process in
                                  * pipeline). */
    int *stdinPipePtr,                /* (out) If non-NULL, input to the
                                  * pipeline comes from a pipe (unless
                                  * overridden by redirection in the
                                  * command).  The file id with which
                                  * to write to this pipe is stored at
                                  * *stdinPipePtr.  NULL means command
                                  * specified its own input source. */
    int *stdoutPipePtr,                /* (out) If non-NULL, output to the
                                  * pipeline goes to a pipe, unless
                                  * overriden by redirection in the
                                  * command.  The file id with which to
                                  * read frome this pipe is stored at
                                  * *stdoutPipePtr.  NULL means command
                                  * specified its own output sink. */
    int *stderrPipePtr)                /* (out) If non-NULL, all stderr
                                  * output from the pipeline will go to
                                  * a temporary file created here, and
                                  * a descriptor to read the file will
                                  * be left at *stderrPipePtr.  The file
                                  * will be removed already, so closing
                                  * this descriptor will be the end of
                                  * the file.  If this is NULL, then
                                  * all stderr output goes to our
                                  * stderr.  If the pipeline specifies
                                  * redirection then the file will
                                  * still be created but it will never
                                  * get any data. */
{
    int *pids = NULL;                /* Points to malloc-ed array holding all
                                  * the pids of child processes. */
    int nPids;                        /* Actual number of processes that exist
                                  * at *pids right now. */
    int cmdCount;                /* Count of number of distinct commands
                                  * found in objc/objv. */
    char *inputLiteral = NULL;        /* If non-null, then this points to a
                                  * string containing input data (specified
                                  * via <<) to be piped to the first process
                                  * in the pipeline. */
    int stdinFd;                /* If != NULL, gives file to use as input for
                                  * first process in pipeline (specified via <
                                  * or <@). */
    int stdoutFd;                /* Writable file for output from last command
                                  * in pipeline (could be file or pipe).  NULL
                                  * means use stdout. */
    int stderrFd;                /* Writable file for error output from all
                                  * commands in pipeline.  NULL means use
                                  * stderr. */
    int stdinIsOpen;                /* If non-zero, then stdinFd should be
                                  * closed when cleaning up. */
    int stdoutIsOpen;                /* If non-zero, then stdoutFd should be
                                  * closed when cleaning up. */
    int stderrIsOpen;                /* If non-zero, then stderrFd should be
                                  * closed when cleaning up. */
    char *p;
    int skip, lastBar, lastArg, i, j, atOK, flags, errorToOutput;
    Tcl_DString execBuffer;
    int pipeIn;
    int stdinCurrFd, stdoutCurrFd, stderrCurrFd;
    char **argv;

    stdinFd = stdoutFd = stderrFd = -1;
    stdinIsOpen = stdoutIsOpen = stderrIsOpen = FALSE;
    if (stdinPipePtr != NULL) {
         *stdinPipePtr = -1;
    }
    if (stdoutPipePtr != NULL) {
         *stdoutPipePtr = -1;
    }
    if (stderrPipePtr != NULL) {
         *stderrPipePtr = -1;
    }
    Tcl_DStringInit(&execBuffer);

    pipeIn = stdinCurrFd = stdoutCurrFd = -1;
    nPids = 0;

      /*
        * First, scan through all the arguments to figure out the
        * structure of the pipeline.  Process all of the input and output
        * redirection arguments and remove them from the argument list in
        * the pipeline.  Count the number of distinct processes (it's the
        * number of "|" arguments plus one) but don't remove the "|"
        * arguments because they'll be used in the second pass to
        * seperate the individual child processes.  
        *
        * Cannot start the child processes in this pass because the
        * redirection symbols may appear anywhere in the command line --
        * e.g., the '<' that specifies the input to the entire pipe may
        * appear at the very end of the argument list.
        */

    /* Convert all the Tcl_Objs to strings. */
    argv = Blt_Malloc((objc + 1) *  sizeof(char *));
    for (i = 0; i < objc; i++) {
        argv[i] = Tcl_GetString(objv[i]);
    }
    argv[i] = NULL;

    lastBar = -1;
    cmdCount = 1;
    for (i = 0; i < objc; i++) {
        skip = 0;
        p = argv[i];
        switch (*p++) {
        case '\\':
            p++;
            continue;

        case '|':
            if (*p == '&') {
                p++;
            }
            if (*p == '\0') {
                if ((i == (lastBar + 1)) || (i == (objc - 1))) {
                    Tcl_AppendResult(interp, 
                        "illegal use of | or |& in command", (char *)NULL);
                    goto error;
                }
            }
            lastBar = i;
            cmdCount++;
            break;

        case '<':
            if (stdinIsOpen != 0) {
                stdinIsOpen = FALSE;
                CloseFile(stdinFd);
            }
            if (*p == '<') {
                stdinFd = -1;
                inputLiteral = p + 1;
                skip = 1;
                if (*inputLiteral == '\0') {
                    inputLiteral = argv[i + 1];
                    if (inputLiteral == NULL) {
                        Tcl_AppendResult(interp, "can't specify \"", argv[i], 
                                "\" as last word in command", (char *)NULL);
                        goto error;
                    }
                    skip = 2;
                }
            } else {
                inputLiteral = NULL;
                stdinFd = FileForRedirect(interp, p, argv[i], TRUE, argv[i + 1],
                        O_RDONLY, &skip, &stdinIsOpen);
                if (stdinFd < 0) {
                    goto error;
                }
            }
            break;

        case '>':
            atOK = TRUE;
            flags = O_WRONLY | O_CREAT | O_TRUNC;
            errorToOutput = FALSE;
            if (*p == '>') {
                p++;
                atOK = FALSE;
                flags = O_WRONLY | O_CREAT;
            }
            if (*p == '&') {
                if (stderrIsOpen != 0) {
                    stderrIsOpen = FALSE;
                    CloseFile(stderrFd);
                }
                errorToOutput = TRUE;
                p++;
            }
            if (stdoutIsOpen != 0) {
                stdoutIsOpen = FALSE;
                CloseFile(stdoutFd);
            }
            stdoutFd = FileForRedirect(interp, p, argv[i], atOK, argv[i + 1], 
                flags, &skip, &stdoutIsOpen);
            if (stdoutFd < 0) {
                goto error;
            }
            if (errorToOutput) {
                stderrIsOpen = FALSE;
                stderrFd = stdoutFd;
            }
            break;

        case '2':
            if (*p != '>') {
                break;
            }
            p++;
            atOK = TRUE;
            flags = O_WRONLY | O_CREAT | O_TRUNC;
            if (*p == '>') {
                p++;
                atOK = FALSE;
                flags = O_WRONLY | O_CREAT;
            }
            if (stderrIsOpen != 0) {
                stderrIsOpen = FALSE;
                CloseFile(stderrFd);
            }
            stderrFd = FileForRedirect(interp, p, argv[i], atOK, argv[i + 1], 
                flags, &skip, &stderrIsOpen);
            if (stderrFd < 0) {
                goto error;
            }
            break;
        }

        if (skip != 0) {
            for (j = i + skip; j < objc; j++) {
                argv[j - skip] = argv[j];
            }
            objc -= skip;
            i -= 1;
        }
    }

    if (stdinFd == -1) {
        if (inputLiteral != NULL) {
            /*
              * The input for the first process is immediate data
              * coming from Tcl.  Create a temporary file for it and
              * put the data into the file.
              */
            stdinFd = CreateTempFile(inputLiteral);
            if (stdinFd < 0) {
                Tcl_AppendResult(interp,
                    "can't create input file for command: ",
                    Tcl_PosixError(interp), (char *)NULL);
                goto error;
            }
            stdinIsOpen = TRUE;
        } else if (stdinPipePtr != NULL) {
            /*
              * The input for the first process in the pipeline is to
              * come from a pipe that can be written from by the
              * caller.
              */

            if (CreatePipe(&stdinFd, stdinPipePtr) == 0) {
                Tcl_AppendResult(interp,
                    "can't create input pipe for command: ",
                    Tcl_PosixError(interp), (char *)NULL);
                goto error;
            }
            stdinIsOpen = TRUE;
        } else {
            /*
              * The input for the first process comes from stdin.
              */
            stdinFd = 0;
        }
    }
    if (stdoutFd == -1) {
        if (stdoutPipePtr != NULL) {
            /*
              * Output from the last process in the pipeline is to go
              * to a pipe that can be read by the caller.
              */

            if (CreatePipe(stdoutPipePtr, &stdoutFd) == 0) {
                Tcl_AppendResult(interp,
                    "can't create output pipe for command: ",
                    Tcl_PosixError(interp), (char *)NULL);
                goto error;
            }
            stdoutIsOpen = TRUE;
        } else {
            /*
              * The output for the last process goes to stdout.
              */
            stdoutFd = 1;
        }
    }
    if (stderrFd == -1) {
        if (stderrPipePtr != NULL) {
            /*
              * Stderr from the last process in the pipeline is to go
              * to a pipe that can be read by the caller.
              */
            if (CreatePipe(stderrPipePtr, &stderrFd) == 0) {
                Tcl_AppendResult(interp,
                    "can't create error pipe for command: ",
                    Tcl_PosixError(interp), (char *)NULL);
                goto error;
            }
            stderrIsOpen = TRUE;
        } else {
            /*
              * Errors from the pipeline go to stderr.
              */
            stderrFd = 2;
        }
    }
      /*
        * Scan through the objc array, creating a process for each group
        * of arguments between the "|" characters.
        */

    Tcl_ReapDetachedProcs();
    pids = Blt_Malloc(cmdCount * sizeof(int));
    stdinCurrFd = stdinFd;

    lastArg = 0;                /* Suppress compiler warning */
    for (i = 0; i < objc; i = lastArg + 1) {
        int joinThisError;
        int pid;

        /*
          * Convert the program name into native form.
          */

        argv[i] = Tcl_TranslateFileName(interp, argv[i], &execBuffer);
        if (argv[i] == NULL) {
            goto error;
        }
        /*
          * Find the end of the current segment of the pipeline.
          */
        joinThisError = 0;
        for (lastArg = i + 1; lastArg < objc; lastArg++) {
            if (argv[lastArg][0] == '|') {
                if (argv[lastArg][1] == '\0') {
                    break;
                }
                if ((argv[lastArg][1] == '&') && (argv[lastArg][2] == '\0')) {
                    joinThisError = 1;
                    break;
                }
            }
        }
        argv[lastArg] = NULL;

        /*
          * If this is the last segment, use the specified stdoutFd.
          * Otherwise create an intermediate pipe.  pipeIn will become the
          * curInFile for the next segment of the pipe.
          */

        if (lastArg == objc) {
            stdoutCurrFd = stdoutFd;
        } else {
            if (CreatePipe(&pipeIn, &stdoutCurrFd) == 0) {
                Tcl_AppendResult(interp, "can't create pipe: ",
                    Tcl_PosixError(interp), (char *)NULL);
                goto error;
            }
        }

        if (joinThisError != 0) {
            stderrCurrFd = stdoutCurrFd;
        } else {
            stderrCurrFd = stderrFd;
        }

        if (CreateProcess(interp, lastArg - i, argv + i, stdinCurrFd, 
                stdoutCurrFd, stderrCurrFd, &pid) != TCL_OK) {
            goto error;
        }
        Tcl_DStringFree(&execBuffer);

        pids[nPids] = pid;
        nPids++;


        /*
          * Close off our copies of file descriptors that were set up
          * for this child, then set up the input for the next child.
          */

        if ((stdinCurrFd >= 0) && (stdinCurrFd != stdinFd)) {
            CloseFile(stdinCurrFd);
        }
        stdinCurrFd = pipeIn;
        pipeIn = -1;

        if ((stdoutCurrFd >= 0) && (stdoutCurrFd != stdoutFd)) {
            CloseFile(stdoutCurrFd);
        }
        stdoutCurrFd = -1;
    }

        *pidArrayPtr = pids;

      /*
        * All done.  Cleanup open files lying around and then return.
        */

  cleanup:
    Tcl_DStringFree(&execBuffer);

    if (stdinIsOpen) {
        CloseFile(stdinFd);
    }
    if (stdoutIsOpen) {
        CloseFile(stdoutFd);
    }
    if (stderrIsOpen) {
        CloseFile(stderrFd);
    }
    if (argv != NULL) {
        Blt_Free(argv);
    }
    return nPids;

      /*
        * An error occurred.  There could have been extra files open,
        * such as pipes between children.  Clean them all up.  Detach any
        * child processes that have been created.
        */

  error:
    if (pipeIn >= 0) {
        CloseFile(pipeIn);
    }
    if ((stdoutCurrFd >= 0) && (stdoutCurrFd != stdoutFd)) {
        CloseFile(stdoutCurrFd);
    }
    if ((stdinCurrFd >= 0) && (stdinCurrFd != stdinFd)) {
        CloseFile(stdinCurrFd);
    }
    if ((stdinPipePtr != NULL) && (*stdinPipePtr >= 0)) {
        CloseFile(*stdinPipePtr);
         *stdinPipePtr = -1;
    }
    if ((stdoutPipePtr != NULL) && (*stdoutPipePtr >= 0)) {
        CloseFile(*stdoutPipePtr);
         *stdoutPipePtr = -1;
    }
    if ((stderrPipePtr != NULL) && (*stderrPipePtr >= 0)) {
        CloseFile(*stderrPipePtr);
         *stderrPipePtr = -1;
    }
    if (pids != NULL) {
        for (i = 0; i < nPids; i++) {
            if (pids[i] != -1) {
                Tcl_DetachPids(1, (Tcl_Pid *)(pids + i));
            }
        }
        Blt_Free(pids);
    }
    nPids = -1;
    goto cleanup;
}

static char stringRep[200];

static char *
Blt_Itoa(int value)
{
    sprintf(stringRep, "%d", value);
    return stringRep;
}

#ifndef NDEBUG
static void
Blt_Assert(char *testExpr, char *fileName, int lineNumber)
{
    fprintf(stderr, "line %d of %s: Assert \"%s\" failed\n",
        lineNumber, fileName, testExpr);
    fflush(stderr);
    abort();
}
#endif

static int
Blt_GetCountFromObj(
    Tcl_Interp *interp,
    Tcl_Obj *objPtr,
    int check,                        /* Can be COUNT_POSITIVE, COUNT_NONNEGATIVE,
                                  * or COUNT_ANY, */
    long *valuePtr)
{
    long count;

    if (Tcl_GetLongFromObj(interp, objPtr, &count) != TCL_OK) {
        return TCL_ERROR;
    }
    switch (check) {
    case COUNT_NONNEGATIVE:
        if (count < 0) {
            Tcl_AppendResult(interp, "bad value \"", Tcl_GetString(objPtr), 
                "\": can't be negative", (char *)NULL);
            return TCL_ERROR;
        }
        break;
    case COUNT_POSITIVE:
        if (count <= 0) {
            Tcl_AppendResult(interp, "bad value \"", Tcl_GetString(objPtr), 
                "\": must be positive", (char *)NULL);
            return TCL_ERROR;
        }
        break;
    case COUNT_ANY:
        break;
    }
        *valuePtr = count;
    return TCL_OK;
}

static void *
Blt_Calloc(unsigned int n, size_t size)
{
    void *ptr;
    size_t nBytes;

    nBytes = n * size;
    ptr = Blt_Malloc(nBytes);
    if (ptr != NULL) {
        memset(ptr, 0, nBytes);
    }
    return ptr;
}

/*
 *----------------------------------------------------------------------
 *
 * Blt_Strdup --
 *
 *      Create a copy of the string from heap storage.
 *
 * Results:
 *      Returns a pointer to the need string copy.
 *
 *----------------------------------------------------------------------
 */
char *
Blt_Strdup(CONST char *string)
{
    size_t size;
    char *ptr;

    size = strlen(string) + 1;
    ptr = Blt_Malloc(size * sizeof(char));
    if (ptr != NULL) {
        strcpy(ptr, string);
    }
    return ptr;
}


/*
 *--------------------------------------------------------------
 *
 * FindSwitchSpec --
 *
 *        Search through a table of configuration specs, looking for
 *        one that matches a given argvName.
 *
 * Results:
 *        The return value is a pointer to the matching entry, or NULL
 *        if nothing matched.  In that case an error message is left
 *        in the interp's result.
 *
 * Side effects:
 *        None.
 *
 *--------------------------------------------------------------
 */
static Blt_SwitchSpec *
FindSwitchSpec(
    Tcl_Interp *interp,                /* Used for reporting errors. */
    Blt_SwitchSpec *specs,        /* Pointer to table of configuration
                                  * specifications for a widget. */
    char *name,                        /* Name (suitable for use in a "switch"
                                  * command) identifying particular option. */
    int needFlags,                /* Flags that must be present in matching
                                  * entry. */
    int hateFlags)                /* Flags that must NOT be present in
                                  * matching entry. */
{
    Blt_SwitchSpec *sp;
    register char c;                /* First character of current argument. */
    Blt_SwitchSpec *matchPtr;        /* Matching spec, or NULL. */
    size_t length;

    c = name[1];
    length = strlen(name);
    matchPtr = NULL;
    
    for (sp = specs; sp->type != BLT_SWITCH_END; sp++) {
        if (sp->switchName == NULL) {
            continue;
        }
        if ((sp->switchName[1] != c) || 
            (strncmp(sp->switchName, name, length) != 0)) {
            continue;
        }
        if (((sp->flags & needFlags) != needFlags) || (sp->flags & hateFlags)) {
            continue;
        }
        if (sp->switchName[length] == '\0') {
            return sp;                /* Stop on a perfect match. */
        }
        if (matchPtr != NULL) {
            Tcl_AppendResult(interp, "ambiguous option \"", name, "\"", 
                (char *) NULL);
            return (Blt_SwitchSpec *) NULL;
        }
        matchPtr = sp;
    }

    if (matchPtr == NULL) {
        Tcl_AppendResult(interp, "unknown option \"", name, "\"", (char *)NULL);
        return (Blt_SwitchSpec *) NULL;
    }
    return matchPtr;
}

/*
 *--------------------------------------------------------------
 *
 * DoSwitch --
 *
 *        This procedure applies a single configuration option
 *        to a widget record.
 *
 * Results:
 *        A standard Tcl return value.
 *
 * Side effects:
 *        WidgRec is modified as indicated by specPtr and value.
 *        The old value is recycled, if that is appropriate for
 *        the value type.
 *
 *--------------------------------------------------------------
 */
static int
DoSwitch(
    Tcl_Interp *interp,                /* Interpreter for error reporting. */
    Blt_SwitchSpec *sp,                /* Specifier to apply. */
    Tcl_Obj *objPtr,                /* Value to use to fill in widgRec. */
    void *record)                /* Record whose fields are to be
                                  * modified.  Values must be properly
                                  * initialized. */
{
    do {
        char *ptr;

        ptr = (char *)record + sp->offset;
        switch (sp->type) {
        case BLT_SWITCH_BOOLEAN:
            if (Tcl_GetBooleanFromObj(interp, objPtr, (int *)ptr) != TCL_OK) {
                return TCL_ERROR;
            }
            break;

        case BLT_SWITCH_DOUBLE:
            if (Tcl_GetDoubleFromObj(interp, objPtr, (double *)ptr) != TCL_OK) {
                return TCL_ERROR;
            }
            break;

        case BLT_SWITCH_OBJ:
             *(Tcl_Obj **)ptr = objPtr;
            break;

        case BLT_SWITCH_FLOAT:
            {
                double value;

                if (Tcl_GetDoubleFromObj(interp, objPtr, &value) != TCL_OK) {
                    return TCL_ERROR;
                }
                 *(float *)ptr = (float)value;
            }
            break;

        case BLT_SWITCH_INT:
            if (Tcl_GetIntFromObj(interp, objPtr, (int *)ptr) != TCL_OK) {
                return TCL_ERROR;
            }
            break;

        case BLT_SWITCH_INT_NONNEGATIVE:
            {
                long value;
                
                if (Blt_GetCountFromObj(interp, objPtr, COUNT_NONNEGATIVE, 
                        &value) != TCL_OK) {
                    return TCL_ERROR;
                }
                 *(int *)ptr = (int)value;
            }
            break;

        case BLT_SWITCH_INT_POSITIVE:
            {
                long value;
                
                if (Blt_GetCountFromObj(interp, objPtr, COUNT_POSITIVE, 
                        &value) != TCL_OK) {
                    return TCL_ERROR;
                }
                 *(int *)ptr = (int)value;
            }
            break;

        case BLT_SWITCH_LIST:
            {
                int argc;

                if (Tcl_SplitList(interp, Tcl_GetString(objPtr), &argc, 
                                  (CONST84 char ***)ptr) != TCL_OK) {
                    return TCL_ERROR;
                }
            }
            break;

        case BLT_SWITCH_LONG:
            if (Tcl_GetLongFromObj(interp, objPtr, (long *)ptr) != TCL_OK) {
                return TCL_ERROR;
            }
            break;

        case BLT_SWITCH_LONG_NONNEGATIVE:
            {
                long value;
                
                if (Blt_GetCountFromObj(interp, objPtr, COUNT_NONNEGATIVE, 
                        &value) != TCL_OK) {
                    return TCL_ERROR;
                }
                 *(long *)ptr = value;
            }
            break;

        case BLT_SWITCH_LONG_POSITIVE:
            {
                long value;
                
                if (Blt_GetCountFromObj(interp, objPtr, COUNT_POSITIVE, &value)
                        != TCL_OK) {
                    return TCL_ERROR;
                }
                 *(long *)ptr = value;
            }
            break;

        case BLT_SWITCH_STRING: 
            {
                char *old, *new, **strPtr;
                char *string;

                string = Tcl_GetString(objPtr);
                strPtr = (char **)ptr;
                new = ((*string == '\0') && (sp->flags & BLT_SWITCH_NULL_OK))
                    ? NULL : Blt_Strdup(string);
                old = *strPtr;
                if (old != NULL) {
                    Blt_Free(old);
                }
                 *strPtr = new;
            }
            break;

        case BLT_SWITCH_CUSTOM:
            if ((*sp->customPtr->parseProc)(sp->customPtr->clientData, interp,
                sp->switchName, objPtr, (char *)record, sp->offset, sp->flags) 
                != TCL_OK) {
                return TCL_ERROR;
            }
            break;

        default: 
            Tcl_AppendResult(interp, "bad switch table: unknown type \"",
                 Blt_Itoa(sp->type), "\"", (char *)NULL);
            return TCL_ERROR;
        }
        sp++;
    } while ((sp->switchName == NULL) && (sp->type != BLT_SWITCH_END));
    return TCL_OK;
}

/*
 *--------------------------------------------------------------
 *
 * Blt_ParseSwitches --
 *
 *        Process command-line options and database options to
 *        fill in fields of a widget record with resources and
 *        other parameters.
 *
 * Results:
 *        Returns the number of arguments comsumed by parsing the
 *        command line.  If an error occurred, -1 will be returned
 *        and an error messages can be found as the interpreter
 *        result.
 *
 * Side effects:
 *        The fields of widgRec get filled in with information
 *        from argc/argv and the option database.  Old information
 *        in widgRec's fields gets recycled.
 *
 *--------------------------------------------------------------
 */
static int
Blt_ParseSwitches(
    Tcl_Interp *interp,                /* Interpreter for error reporting. */
    Blt_SwitchSpec *specs,        /* Describes legal options. */
    int objc,                        /* Number of elements in argv. */
    Tcl_Obj *CONST *objv,        /* Command-line options. */
    void *record,                /* Record whose fields are to be
                                  * modified.  Values must be properly
                                  * initialized. */
    int flags)                        /* Used to specify additional flags
                                  * that must be present in switch specs
                                  * for them to be considered.  */
{
    Blt_SwitchSpec *sp;
    int count;
    int needFlags;                /* Specs must contain this set of flags
                                  * or else they are not considered. */
    int hateFlags;                /* If a spec contains any bits here, it's
                                  * not considered. */

    needFlags = flags & ~(BLT_SWITCH_USER_BIT - 1);
    hateFlags = 0;

      /*
        * Pass 1:  Clear the change flags on all the specs so that we 
        *          can check it later.
        */
    for (sp = specs; sp->type != BLT_SWITCH_END; sp++) {
        sp->flags &= ~BLT_SWITCH_SPECIFIED;
    }
      /*
        * Pass 2:  Process the arguments that match entries in the specs.
        *                It's an error if the argument doesn't match anything.
        */
    for (count = 0; count < objc; count++) {
        char *arg;

        arg = Tcl_GetString(objv[count]);
        if (flags & BLT_SWITCH_OBJV_PARTIAL) {
            /* 
              * If the argument doesn't start with a '-' (not a switch)
              * or is '--', stop processing and return the number of
              * arguments comsumed. 
              */
            if (arg[0] != '-') {
                return count;
            }
            if ((arg[1] == '-') && (arg[2] == '\0')) {
                return count + 1; /* include the "--" in the count. */
            }
        }
        sp = FindSwitchSpec(interp, specs, arg, needFlags, hateFlags);
        if (sp == NULL) {
            return -1;
        }
        if (sp->type == BLT_SWITCH_FLAG) {
            char *ptr;
            
            ptr = (char *)record + sp->offset;
             *((int *)ptr) |= sp->value;
        } else if (sp->type == BLT_SWITCH_VALUE) {
            char *ptr;
            
            ptr = (char *)record + sp->offset;
             *((int *)ptr) = sp->value;
        } else {
            count++;
            if (count == objc) {
                Tcl_AppendResult(interp, "value for \"", arg, "\" missing", 
                                 (char *) NULL);
                return -1;
            }
            if (DoSwitch(interp, sp, objv[count], record) != TCL_OK) {
                char msg[100];

                sprintf(msg, "\n    (processing \"%.40s\" option)", 
                        sp->switchName);
                Tcl_AddErrorInfo(interp, msg);
                return -1;
            }
        }
        sp->flags |= BLT_SWITCH_SPECIFIED;
    }
    return count;
}

/*
 *----------------------------------------------------------------------
 *
 * Blt_FreeSwitches --
 *
 *        Free up all resources associated with switch options.
 *
 * Results:
 *        None.
 *
 *----------------------------------------------------------------------
 */
/* ARGSUSED */
static void
Blt_FreeSwitches(
    Blt_SwitchSpec *specs,        /* Describes legal options. */
    void *record,                /* Record whose fields contain current
                                  * values for options. */
    int needFlags)                /* Used to specify additional flags
                                  * that must be present in config specs
                                  * for them to be considered. */
{
    Blt_SwitchSpec *sp;

    for (sp = specs; sp->type != BLT_SWITCH_END; sp++) {
        if ((sp->flags & needFlags) == needFlags) {
            char *ptr;

            ptr = (char *)record + sp->offset;
            switch (sp->type) {
            case BLT_SWITCH_STRING:
            case BLT_SWITCH_LIST:
                if (*((char **) ptr) != NULL) {
                    Blt_Free(*((char **) ptr));
                     *((char **) ptr) = NULL;
                }
                break;

            case BLT_SWITCH_CUSTOM:
                if ((*(char **)ptr != NULL) && 
                    (sp->customPtr->freeProc != NULL)) {
                    (*sp->customPtr->freeProc)((char *)record, sp->offset, 
                        sp->flags);
                }
                break;

            default:
                break;
            }
        }
    }
}

}

#-------------------------------------------------------------------------------

::critcl::ccommand bgexec {ClientData interp objc objv} {
    BgExec *bgPtr;
    ProcessId *pidPtr;
    Tcl_Encoding encoding;
    char *lastArg;
    int *outFdPtr, *errFdPtr;
    int detached;
    int i;
    int nProcs;
    int inFd = -1, *inFdPtr = NULL; //DAS

    if (objc < 3) {
        Tcl_AppendResult(interp, "wrong # args: should be \"", 
                Tcl_GetString(objv[0]), " varName ?options? command ?arg...?\"",
                (char *)NULL);
        return TCL_ERROR;
    }

      /* Check if the command line is to be run detached (the last
        * argument is "&") */
    lastArg = Tcl_GetString(objv[objc - 1]);
    detached = ((lastArg[0] == '&') && (lastArg[1] == '\0'));
    if (detached) {
        objc--;                        /* Remove the '&' argument */
    }
    bgPtr = Blt_Calloc(1, sizeof(BgExec));
    assert(bgPtr);

    /* Initialize the background information record */
    bgPtr->interp = interp;
    bgPtr->signalNum = SIGKILL;
    bgPtr->nProcs = -1;
    bgPtr->interval = 1000;
    bgPtr->detached = detached;
    bgPtr->keepNewline = FALSE;
    bgPtr->statVar = Blt_Strdup(Tcl_GetString(objv[1]));
    bgPtr->inputChanName = NULL; //DAS

    /* Try to clean up any detached processes */
    Tcl_ReapDetachedProcs();

    i = Blt_ParseSwitches(interp, switchSpecs, objc - 2, objv + 2, bgPtr, 
                BLT_SWITCH_OBJV_PARTIAL);
    if (i < 0) {
        FreeBgExec(bgPtr);
        return TCL_ERROR;
    }
    i += 2;
    /* Must be at least one argument left as the command to execute. */
    if (objc <= i) {
        Tcl_AppendResult(interp, "missing command to execute: should be \"",
            Tcl_GetString(objv[0]), " varName ?options? command ?arg...?\"", 
                (char *)NULL);
        FreeBgExec(bgPtr);
        return TCL_ERROR;
    }

      /* Put a trace on the exit status variable.  The will also allow
        * the user to prematurely terminate the pipeline by simply
        * setting it.  */
    Tcl_TraceVar(interp, bgPtr->statVar, TRACE_FLAGS, VariableProc, bgPtr);
    bgPtr->traced = TRUE;

    encoding = ENCODING_ASCII;
    if (bgPtr->outputEncodingName != NULL) {
        if (strcmp(bgPtr->outputEncodingName, "binary") == 0) {
            encoding = ENCODING_BINARY;
        } else {
            encoding = Tcl_GetEncoding(interp, bgPtr->outputEncodingName);
            if (encoding == NULL) {
                goto error;
            }
        }
    }
    InitSink(bgPtr, &bgPtr->sink1, "stdout", encoding);
    if (bgPtr->errorEncodingName != NULL) {
        if (strcmp(bgPtr->errorEncodingName, "binary") == 0) {
            encoding = ENCODING_BINARY;
        } else {
            encoding = Tcl_GetEncoding(interp, bgPtr->errorEncodingName);
            if (encoding == NULL) {
                goto error;
            }
        }
    }
    InitSink(bgPtr, &bgPtr->sink2, "stderr", encoding);

    outFdPtr = errFdPtr = (int *)NULL;
    outFdPtr = &bgPtr->sink1.fd;
    if ((bgPtr->sink2.doneVar != NULL) || 
        (bgPtr->sink2.updateVar != NULL) ||
        (bgPtr->sink2.updateCmd != NULL) || 
        (bgPtr->sink2.echo)) {
        errFdPtr = &bgPtr->sink2.fd;
    }
    if (bgPtr->inputVar) { //DAS begin
        inFdPtr = &inFd;
        bgPtr->detached = 1;
    } //DAS end
    nProcs = Blt_CreatePipeline(interp, objc - i, objv + i, &pidPtr, 
        inFdPtr, outFdPtr, errFdPtr); //DAS
    if (nProcs < 0) {
        goto error;
    }
    bgPtr->procIds = pidPtr;
    bgPtr->nProcs = nProcs;

    if (bgPtr->sink1.fd == -1) {

        /* 
          * If output has been redirected, start polling immediately
          * for the exit status of each process.  Normally, this is
          * done only after stdout has been closed by the last process,
          * but here stdout has been redirected. The default polling
          * interval is every 1 second.  
          */

        bgPtr->timerToken = Tcl_CreateTimerHandler(bgPtr->interval, TimerProc,
           bgPtr);

    } else if (CreateSinkHandler(bgPtr, &bgPtr->sink1, StdoutProc) != TCL_OK) {
        goto error;
    }
    if ((bgPtr->sink2.fd != -1) &&
        (CreateSinkHandler(bgPtr, &bgPtr->sink2, StderrProc) != TCL_OK)) {
        goto error;
    }
    if (bgPtr->inputVar) { //DAS begin
        Tcl_Channel chan;
        CONST84 char *result;
        if (inFd == -1) goto error;
        chan = Tcl_MakeFileChannel((void*)inFd, TCL_WRITABLE);
        if (!chan) goto error;
        Tcl_RegisterChannel(interp, chan);
        bgPtr->inputChanName = Blt_Strdup(Tcl_GetChannelName(chan));
        result = Tcl_SetVar(interp, bgPtr->inputVar, bgPtr->inputChanName,
                TCL_GLOBAL_ONLY | TCL_LEAVE_ERR_MSG);
        if (!result) goto error;
    } //DAS end
    if (bgPtr->detached) {        
        /* If detached, return a list of the child process ids instead
          * of the output of the pipeline. */
        for (i = 0; i < nProcs; i++) {
            char string[200];
            sprintf(string, "%d", bgPtr->procIds[i]);
            Tcl_AppendElement(interp, string);
        }
    } else {
        int exitCode;
        int done;

        bgPtr->exitCodePtr = &exitCode;
        bgPtr->donePtr = &done;

        exitCode = done = 0;
        while (!done) {
            Tcl_DoOneEvent(0);
        }
        DisableTriggers(bgPtr);
        if ((bgPtr->ignoreExitCode) || (exitCode == 0)) {
            if (bgPtr->sink1.doneVar == NULL) {
                unsigned char *data;
                size_t length;
                
                /* Return the output of the pipeline. */
                GetSinkData(&bgPtr->sink1, &data, &length);
                Tcl_SetByteArrayObj(Tcl_GetObjResult(interp), data, length);
            }
        } else {
            DestroyBgExec(bgPtr);
            Tcl_AppendResult(interp, "child process exited abnormally",
                (char *)NULL);
            return TCL_ERROR;
        }
        DestroyBgExec(bgPtr);
    }
    return TCL_OK;
  error:
    DisableTriggers(bgPtr);
    DestroyBgExec(bgPtr);
    return TCL_ERROR;
}

#-------------------------------------------------------------------------------

proc bgexec_write_manpage {} {
    set n [file join [file dirname [info script]] bgexec.n]
    puts stderr "Writing manpage $n"
    puts [set f [open $n w]] {
'\"
'\" Copyright 1991-1997 by Bell Labs Innovations for Lucent Technologies.
'\"
'\" Permission to use, copy, modify, and distribute this software and its
'\" documentation for any purpose and without fee is hereby granted, provided
'\" that the above copyright notice appear in all copies and that both that the
'\" copyright notice and warranty disclaimer appear in supporting documentation,
'\" and that the names of Lucent Technologies any of their entities not be used
'\" in advertising or publicity pertaining to distribution of the software
'\" without specific, written prior permission.
'\"
'\" Lucent Technologies disclaims all warranties with regard to this software,
'\" including all implied warranties of merchantability and fitness.  In no event
'\" shall Lucent Technologies be liable for any special, indirect or
'\" consequential damages or any damages whatsoever resulting from loss of use,
'\" data or profits, whether in an action of contract, negligence or other
'\" tortuous action, arising out of or in connection with the use or performance
'\" of this software.
'\"
'\" Bgexec command created by George Howlett.
.if t .wh -1.3i ^B
.nr ^l \n(.l
.ad b
'\"        # Start an argument description
.de AP
.ie !"\\$4"" .TP \\$4
.el \{\
.   ie !"\\$2"" .TP \\n()Cu
.   el          .TP 15
.\}
.ta \\n()Au \\n()Bu
.ie !"\\$3"" \{\
\&\\$1        \\fI\\$2\\fP        (\\$3)
.\".b
.\}
.el \{\
.br
.ie !"\\$2"" \{\
\&\\$1        \\fI\\$2\\fP
.\}
.el \{\
\&\\fI\\$1\\fP
.\}
.\}
..
'\"        # define tabbing values for .AP
.de AS
.nr )A 10n
.if !"\\$1"" .nr )A \\w'\\$1'u+3n
.nr )B \\n()Au+15n
.\"
.if !"\\$2"" .nr )B \\w'\\$2'u+\\n()Au+3n
.nr )C \\n()Bu+\\w'(in/out)'u+2n
..
.AS Tcl_Interp Tcl_CreateInterp in/out
'\"        # BS - start boxed text
'\"        # ^y = starting y location
'\"        # ^b = 1
.de BS
.br
.mk ^y
.nr ^b 1u
.if n .nf
.if n .ti 0
.if n \l'\\n(.lu\(ul'
.if n .fi
..
'\"        # BE - end boxed text (draw box now)
.de BE
.nf
.ti 0
.mk ^t
.ie n \l'\\n(^lu\(ul'
.el \{\
.\"        Draw four-sided box normally, but don't draw top of
.\"        box if the box started on an earlier page.
.ie !\\n(^b-1 \{\
\h'-1.5n'\L'|\\n(^yu-1v'\l'\\n(^lu+3n\(ul'\L'\\n(^tu+1v-\\n(^yu'\l'|0u-1.5n\(ul'
.\}
.el \}\
\h'-1.5n'\L'|\\n(^yu-1v'\h'\\n(^lu+3n'\L'\\n(^tu+1v-\\n(^yu'\l'|0u-1.5n\(ul'
.\}
.\}
.fi
.br
.nr ^b 0
..
'\"        # VS - start vertical sidebar
'\"        # ^Y = starting y location
'\"        # ^v = 1 (for troff;  for nroff this doesn't matter)
.de VS
.if !"\\$2"" .br
.mk ^Y
.ie n 'mc \s12\(br\s0
.el .nr ^v 1u
..
'\"        # VE - end of vertical sidebar
.de VE
.ie n 'mc
.el \{\
.ev 2
.nf
.ti 0
.mk ^t
\h'|\\n(^lu+3n'\L'|\\n(^Yu-1v\(bv'\v'\\n(^tu+1v-\\n(^Yu'\h'-|\\n(^lu+3n'
.sp -1
.fi
.ev
.\}
.nr ^v 0
..
'\"        # Special macro to handle page bottom:  finish off current
'\"        # box/sidebar if in box/sidebar mode, then invoked standard
'\"        # page bottom macro.
.de ^B
.ev 2
'ti 0
'nf
.mk ^t
.if \\n(^b \{\
.\"        Draw three-sided box if this is the box's first page,
.\"        draw two sides but no top otherwise.
.ie !\\n(^b-1 \h'-1.5n'\L'|\\n(^yu-1v'\l'\\n(^lu+3n\(ul'\L'\\n(^tu+1v-\\n(^yu'\h'|0u'\c
.el \h'-1.5n'\L'|\\n(^yu-1v'\h'\\n(^lu+3n'\L'\\n(^tu+1v-\\n(^yu'\h'|0u'\c
.\}
.if \\n(^v \{\
.nr ^x \\n(^tu+1v-\\n(^Yu
\kx\h'-\\nxu'\h'|\\n(^lu+3n'\ky\L'-\\n(^xu'\v'\\n(^xu'\h'|0u'\c
.\}
.bp
'fi
.ev
.if \\n(^b \{\
.mk ^y
.nr ^b 2
.\}
.if \\n(^v \{\
.mk ^Y
.\}
..
'\"        # DS - begin display
.de DS
.RS
.nf
.sp
..
'\"        # DE - end display
.de DE
.fi
.RE
.sp
..
'\"        # SO - start of list of standard options
.de SO
.SH "STANDARD OPTIONS"
.LP
.nf
.ta 4c 8c 12c
.ft B
..
'\"        # SE - end of list of standard options
.de SE
.fi
.ft R
.LP
See the \\fBoptions\\fR manual entry for details on the standard options.
..
'\"        # OP - start of full description for a single option
.de OP
.LP
.nf
.ta 4c
Command-Line Name:        \\fB\\$1\\fR
Database Name:        \\fB\\$2\\fR
Database Class:        \\fB\\$3\\fR
.fi
.IP
..
'\"        # CS - begin code excerpt
.de CS
.RS
.nf
.ta .25i .5i .75i 1i
.ft CW
.sp
..
'\"        # CE - end code excerpt
.de CE
.fi
.RE
.ft R
.sp
..
.de UL
\\$1\l'|0\(ul'\\$2
..
.TH bgexec n BLT_VERSION BLT "BLT Built-In Commands"
.BS
'\" Note:  do not modify the .SH NAME line immediately below!
.SH NAME
\fBbgexec\fR \- Run programs in the background while handling Tk events.
.SH SYNOPSIS
\fBbgexec \fIvarName\fR ?\fIoption value\fR?... \fIprogram\fR ?\fIarg\fR?...
.BE
.SH DESCRIPTION
.PP
The \fBbgexec\fR command executes programs in the background,
allowing Tk to handle events.  A global Tcl variable \fIvarName\fR is
set when the program has completed.
.SH INTRODUCTION
Tcl's \fBexec\fR command is very useful for gathering information from
the operating system.  It runs a program and returns the output as its 
result.  This works well for Tcl-only applications. But
for Tk applications, a problem occurs when the program takes time to
process.  Let's say we want the get the disk usage of a
directory.  We'll use the Unix program \f(CWdu\fR to get the summary.
.CS
set out [exec du -s $dir]
puts "Disk usage for $dir is $out"
.CE
While \f(CWdu\fR is running, scrollbars won't respond.  None of the Tk
widgets will be redrawn properly.  The \fBsend\fR command won't work.
And the worst part is that the application appears hung up or dead.
The problem is that while \fBexec\fR is waiting for \fIdu\fR to
finish, Tk is not able to handle X events.
.PP
The \fBbgexec\fR command performs the same functions as \fBexec\fR,
but also allows Tk to handle events.  You can execute a long-running
program and the Tk widgets will behave normally.  When the
program finishes, its output and the exit status are written to Tcl
variables.  This makes it easy to monitor and save the output of a
program.
.SH EXAMPLE
Here is the disk usage example again, this time using \fBbgexec\fR.
The syntax to invoke "du" is exactly the same as the previous
example, when we used \fBexec\fR.
.CS
global myStatus myOutput
bgexec myStatus -output myOutput du -s $dir
puts "Disk usage for $dir is $myOutput"
.CE
Two global variables, \f(CWmyStatus\fR and \f(CWmyOutput\fR, will be set by
\fBbgexec\fR when \f(CWdu\fR has completed. \f(CWMyStatus\fR
will contain the program's exit status.  \f(CWMyOutput\fR, specified by the
\fB\-output\fR option, will store the output of the program.
.PP
You can also terminate the program by setting the variable
\f(CWmyStatus\fR.  If \f(CWmyStatus\fR is set before \f(CWdu\fR has
completed, the process is killed. Under Unix, this is done sending by
a configurable signal (by default it's SIGKILL). Under Win32, this
is done by calling \fBTerminateProcess\fR. It makes no
difference what \f(CWmyStatus\fR is set to.
.CS
set myStatus {}
.CE
There are several \fBbgexec\fR options to collect different types of
information.
.CS
global myStatus myOutput myErrs
bgexec myStatus -output myOutput -error myErrs du -s $dir
.CE
The \fB\-error\fR option is similar to \fB\-output\fR.  It sets a global
variable when the program completes.  The variable will contain
any data written to stderr by the program.
.PP
The \fB\-output\fR and \fB\-error\fR variables are set only
after the program completes.  But if the program takes a long time, to
run you may want to receive its partial output.  You can gather data
as it becomes available using the \fB\-onoutput\fR option.  It
specifies a Tcl command prefix.  Whenever new data is available, this
command is executed, with the data appended as an argument to the
command.
.CS
proc GetInfo { data } {
    puts $data
}
bgexec myStatus -onoutput GetInfo du -s $dir
.CE
When output is available, the procedure \f(CWGetInfo\fR is called.
The \fB\-onerror\fR option performs a similar function for the stderr
data stream.
.PP
Like \fBexec\fR, \fBbgexec\fR returns an error if the exit code of the
program is not zero.  If you think you may get a non-zero exit
code, you might want to invoke \fBbgexec\fR from within a \fBcatch\fR.
.CS
catch { bgexec myStatus -output myOutput du -s $dir }
.CE
By default, \fBbgexec\fR will wait for the program to finish.
But you can detach the program making ampersand (&) the last
argument on the command line.
.CS
global myStatus myOutput
bgexec myStatus -output myOutput du -s $dir &
.CE
\fBBgexec\fR will return immediately and its result will be a list of
the spawned process ids.  If at some point you need to wait for the
program to finish up, you can use \fBtkwait\fR.  When the program
finishes, the variable \f(CWmyStatus\fR will be written to, breaking
out the \fBtkwait\fR command.
.CS
global myStatus myOutput
bgexec myStatus -output myOutput du -s $dir &
        ...
tkwait variable myStatus
.CE
.SH SYNTAX
The \fBbgexec\fR command takes the following form:
.sp
\fB        bgexec \fIvarName\fR ?\fIoption value\fR?... \fIprogram\fR ?\fIarg\fR?...
.sp
\fIVarName\fR is the name of a global variable which is set when 
\fIprogram\fR has finished executing.  The exit status of
will be stored in \fIvarName\fR.  The exit status is a
list of a status token, the process-id of the program, the exit code,
and a status message.  You can also prematurely terminate the program
by setting \fIvarName\fR.  Under Unix, the program will be sent a signal to
terminate it (by default the signal is a SIGKILL; see the
\fB\-killsignal\fR option).
.PP
\fIProgram\fR is the name of the program to be executed and \fIargs\fR
are any extra arguments for \fIprogram\fR.  The syntax of
\fIprogram\fR and \fIargs\fR is the same as the \fBexec\fR command. So
you can redirect I/O, execute pipelines, etc. (see the \fBexec\fR
manual for further information) just like \fBexec\fR.  If the last
argument is an ampersand (&), the program will be run detached, and
\fBbgexec\fR will return immediately.  \fIVarName\fR will still be set
with the return status when \fIprogram\fR completes.
.SH OPTIONS
\fIOption\fR refers to the switch name always beginning with a dash (\-).
\fIValue\fR is the value of the option.  Option-value pairs are
terminated either by the program name, or double dashes (\-\-).
The following options are available for \fBbgexec\fR:
.TP 
\fB\-decodeerror \fIencodingName\fR 
.br
Specifies the encoding of the stderr channel.
This affects only data returned to the Tcl interpreter.  No translation 
is done on file redirection.
.br
For example if data is to be converted from Unicode for use in Tcl,
you would use the "unicode" encoding. The default is that no 
tranlation is performed.
.TP 
\fB\-decodeoutput \fIencodingName\fR 
.br
Specifies the encoding of the stdout channels.
This affects only data returned to the Tcl interpreter.  No translation 
is done on file redirection.
.br
For example if data is to be converted from Unicode for use in Tcl,
you would use the "unicode" encoding. The default is that no 
tranlation is performed.
.TP 
\fB\-error \fIvarName\fR 
.br
Specifies that a global variable \fIvarName\fR is to be set with the
contents of stderr after the program has completed. 
.TP 
\fB\-keepnewline \fIboolean\fR
Specifies that a trailing newline should be retained in the 
output. If \fIboolean\fR is true, the trailing newline is truncated
from the output of the \fB\-onoutput\fR and \fB\-output\fR variables.  
The default value is \f(CWtrue\fR.
.TP 
\fB\-killsignal \fIsignal\fR
Specifies the signal to be sent to the program when 
terminating. This is available only under Unix. 
\fISignal\fR can either be a number (typically 1-32) or
a mnemonic (such as SIGINT). If \fIsignal\fR is the empty string, 
then no signal is sent.  The default signal is \f(CW9\fR (SIGKILL).
.TP 
\fB\-lasterror \fIvarName\fR
Specifies a variable \fIvarName\fR that is updated whenever data
becomes available from standard error of the program.
\fIVarName\fR is a global variable. Unlike the \fB\-error\fR option,
data is available as soon as it arrives.
.TP 
\fB\-lastoutput \fIvarName\fR 
Specifies a variable \fIvarName\fR that is updated whenever data
becomes available from standard output of the program.
\fIVarName\fR is a global variable. Unlike the \fB\-output\fR option,
data is available as soon as it arrives.
.TP 
\fB\-linebuffered \fIboolean\fR
Specifies that updates should be made on a line-by-line basis.
Normally when new data is available \fBbgexec\fR will set the variable
(\fB\-lastoutput\fR and \fB\-lasterror\fR options) or invoke the
command (\fB\-onoutput\fR and \fB\-onerror\fR options) delivering all
the new data currently available.  If \fIboolean\fR is true, only one
line at a time will be delivered.  This can be useful when you want to
process the output on a line-by-line basis.  
The default value is
\f(CWfalse\fR.
.TP 
\fB\-output \fIvarName\fR
.br
Specifies that a global variable \fIvarName\fR is to be set with the
output of the program, once it has completed.  If this option 
is not set, no output will be accumulated.
.TP 
\fB\-onerror \fIcommand\fR
Specifies the start of a Tcl command that will be executed
whenever new data is available from standard error. The data
is appended to the command as an extra argument before it is
executed.
.TP 
\fB\-onoutput \fIcommand\fR 
Specifies the start of a Tcl command that will be executed
whenever new data is available from standard output. The data
is appended to the command as an extra argument before it is
executed.
.TP 
\fB\-update \fIvarName\fR 
Deprecated. This option is replaced by \fB\-lasterror\fR.
.TP 
\fB\-\|\-\fR
This marks the end of the options.  The following argument will
be considered the name of a program even if it starts with 
a dash (\fB\-\fR).
.SH PREEMPTION
Because \fBbgexec\fR allows Tk to handle events while a program is
running, it's possible for an application to preempt itself with
further user-interactions.  Let's say your application has a button
that runs the disk usage example.  And while the \f(CWdu\fR program is
running, the user accidently presses the button again.  A second
\fBbgexec\fR program will preempt the first.  What this means is that
the first program can not finish until the second program has
completed.
.PP
Care must be taken to prevent an application from preempting itself by
blocking further user-interactions (such as button clicks).  The BLT
\fBbusy\fR command is very useful for just these situations.
See the \fBbusy\fR manual for details.
.SH DIFFERENCES WITH FILEEVENT
Since Tk 4.0, a subset of \fBbgexec\fR can be also achieved using the
\fBfileevent\fR command.  The steps for running a program in the
background are:
.PP
Execute the program with the \fBopen\fR command (using the "|"
syntax) and save the file handle.
.CS
global fileId 
set fileId [open "|du -s $dir" r]
.CE
Next register a Tcl code snippet with \fBfileevent\fR to be run
whenever output is available on the file handle.  The code snippet
will read from the file handle and save the output in a variable.
.CS
fileevent fileId readable { 
    if { [gets $fileId line] < 0 } {
        close $fileId
        set output $temp
        unset fileId temp
    } else {
        append temp $line
    }
}
.CE
.PP
The biggest advantage of \fBbgexec\fR is that, unlike \fBfileevent\fR,
it requires no additional Tcl code to run a program.  It's simpler and
less error prone.  You don't have to worry about non-blocking I/O.
It's handled tranparently for you.
.PP
\fBBgexec\fR runs programs that \fBfileevent\fR can not.
\fBFileevent\fR assumes that the when stdout is closed the program has
completed.  But some programs, like the Unix \f(CWcompress\fR program,
reopen stdout, fooling \fBfileevent\fR into thinking the program has
terminated.  In the example above, we assume that the program will
write and flush its output line-by-line.  However running another
program, your application may block in the \fBgets\fR command reading
a partial line.
.PP
\fBBgexec\fR lets you get back the exit status of the program. It also
allows you to collect data from both stdout and stderr simultaneously.
Finally, since data collection is handled in C code, \fBbgexec\fR is
faster. You get back to the Tk event loop more quickly, making your
application seem more responsive.
.SH SEE ALSO
busy, exec, tkwait
.SH KEYWORDS
exec, background, busy
}
    close $f
}

#-------------------------------------------------------------------------------