diff --git a/jim-exec.c b/jim-exec.c index 343d5e1e..03011811 100644 --- a/jim-exec.c +++ b/jim-exec.c @@ -97,8 +97,8 @@ struct WaitInfoTable; static char **JimOriginalEnviron(void); static char **JimSaveEnv(char **env); static void JimRestoreEnv(char **env); -static int JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, - phandle_t **pidArrayPtr, int *inPipePtr, int *outPipePtr, int *errFilePtr); +static int JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t **pidArrayPtr, + int *outPipePtr, int *errFilePtr); static void JimDetachPids(struct WaitInfoTable *table, int numPids, const phandle_t *pidPtr); static int JimCleanupChildren(Jim_Interp *interp, int numPids, phandle_t *pidPtr, Jim_Obj *errStrObj); static int Jim_WaitCommand(Jim_Interp *interp, int argc, Jim_Obj *const *argv); @@ -383,7 +383,7 @@ static int Jim_ExecCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv) int i; argc--; - numPids = JimCreatePipeline(interp, argc - 1, argv + 1, &pidPtr, NULL, NULL, NULL); + numPids = JimCreatePipeline(interp, argc - 1, argv + 1, &pidPtr, NULL, NULL); if (numPids < 0) { return JIM_ERR; } @@ -402,7 +402,7 @@ static int Jim_ExecCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv) * Create the command's pipeline. */ numPids = - JimCreatePipeline(interp, argc - 1, argv + 1, &pidPtr, NULL, &outputId, &errorId); + JimCreatePipeline(interp, argc - 1, argv + 1, &pidPtr, &outputId, &errorId); if (numPids < 0) { return JIM_ERR; @@ -636,37 +636,200 @@ static int Jim_PidCommand(Jim_Interp *interp, int argc, Jim_Obj *const *argv) return JIM_OK; } -/* - *---------------------------------------------------------------------- - * - * JimCreatePipeline -- - * - * Given an argc/argv array, instantiate a pipeline of processes - * as described by the argv. - * - * 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 inPipePtr is non-NULL, *inPipePtr is filled in - * with the file id for the input pipe for the pipeline (if any): - * the caller must eventually close this file. If outPipePtr - * isn't NULL, then *outPipePtr is filled in with the file id - * for the output pipe from the pipeline: the caller must close - * this file. If errFilePtr isn't NULL, then *errFilePtr 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. +#define JIM_ETT_IN 0x0001 /* < */ +#define JIM_ETT_OUT 0x0002 /* > */ +#define JIM_ETT_ERR 0x0004 /* 2> */ +#define JIM_ETT_PIPE 0x0008 /* | */ + +#define JIM_ETT_NOARG 0x0010 /* does not accept an additional argument */ +#define JIM_ETT_APPEND 0x0020 /* append to output */ +#define JIM_ETT_STR 0x0040 /* arg is a literal */ +#define JIM_ETT_DUPERR 0x0080 /* dup output to err */ +#define JIM_ETT_HANDLE 0x0100 /* arg is a filehandle */ + +#define JIM_ETT_CMD 0xF000 +#define JIM_ETT_BAD 0xF001 + +struct redir_type_t { + const char *prefix; + unsigned flags; +}; + +/* These need to be sorted by length, most specific first */ +static const struct redir_type_t redir_types[] = { + { "<<@", JIM_ETT_IN | JIM_ETT_HANDLE | JIM_ETT_STR }, + { "<<", JIM_ETT_IN | JIM_ETT_STR }, + { "<@", JIM_ETT_IN | JIM_ETT_HANDLE }, + { "<", JIM_ETT_IN }, + + { "2>>", JIM_ETT_ERR | JIM_ETT_APPEND }, + { "2>@", JIM_ETT_ERR | JIM_ETT_HANDLE }, + { "2>", JIM_ETT_ERR }, + + { ">>&", JIM_ETT_OUT | JIM_ETT_APPEND | JIM_ETT_DUPERR }, + { ">>", JIM_ETT_OUT | JIM_ETT_APPEND }, + { ">&@", JIM_ETT_OUT | JIM_ETT_HANDLE | JIM_ETT_DUPERR }, + { ">@", JIM_ETT_OUT | JIM_ETT_HANDLE }, + { ">&", JIM_ETT_OUT | JIM_ETT_DUPERR }, + { ">", JIM_ETT_OUT }, + + { "|&", JIM_ETT_PIPE | JIM_ETT_DUPERR }, + { "|", JIM_ETT_PIPE }, + { NULL } +}; + +static unsigned JimExecClassifyArg(const char *arg) +{ + int i; + for (i = 0; redir_types[i].prefix; i++) { + int len = strlen(redir_types[i].prefix); + if (strncmp(arg, redir_types[i].prefix, len) == 0) { + if (strlen(arg) > len) { + if (redir_types[i].flags & JIM_ETT_NOARG) { + /* error - no arg expected */ + return JIM_ETT_BAD; + } + return redir_types[i].flags; + } + /* Token doesn't contain an arg */ + return redir_types[i].flags | JIM_ETT_NOARG; + } + } + return JIM_ETT_CMD; +} + +/** + * Parses the exec pipeline in TIP424 format into two lists, cmdList and redirectList. + * (These must start as empty lists) + * + * Returns JIM_OK if ok or JIM_ERR on error. + */ +static int JimParsePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, Jim_Obj *cmdList, Jim_Obj *redirectList) +{ + int i; + /* Add an initial empty commandlist */ + int first = 1; + const char *arg = NULL; + + for (i = 0; i < argc; i++) { + unsigned ett; + if (first) { + if (Jim_ListLength(interp, argv[i]) == 0) { + Jim_SetResultString(interp, "empty command list", -1); + return JIM_ERR; + } + Jim_ListAppendElement(interp, cmdList, argv[i]); + first = 0; + continue; + } + /* Remaining items should be redirections or | */ + arg = Jim_String(argv[i]); + ett = JimExecClassifyArg(arg); + if (ett == JIM_ETT_BAD || ett == JIM_ETT_CMD) { + Jim_SetResultFormatted(interp, "invalid redirection %s", arg); + return JIM_ERR; + } + if (ett & JIM_ETT_PIPE) { + Jim_ListAppendElement(interp, cmdList, argv[i]); + first = 1; + continue; + } + Jim_ListAppendElement(interp, redirectList, argv[i]); + if ((ett & JIM_ETT_NOARG)) { + /* This means we need an arg */ + if (i >= argc - 1) { + /* This is an error */ + Jim_SetResultFormatted(interp, "can't specify \"%#s\" as last word in command", argv[i]); + return -1; + } + i++; + Jim_ListAppendElement(interp, redirectList, argv[i]); + } + } + + if (first) { + if (Jim_ListLength(interp, cmdList)) { + Jim_SetResultFormatted(interp, "cmdlist required after %s", arg); + } + else { + Jim_SetResultString(interp, "cmdlist is required", -1); + } + return JIM_ERR; + } + + return JIM_OK; +} + +/** + * Parses the exec pipeline in legacy format into two lists, cmdList and redirectList. + * (These must start as empty lists) + * + * cmdList contains a list of {cmdlist ?sep cmdlist ...? } + * i.e. pairs of cmdlist (a list of {command arg...}) and a separator: | or |& + * with the separator missing after the last command list. * - *---------------------------------------------------------------------- + * Returns JIM_OK if ok or JIM_ERR on error. */ -static int -JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t **pidArrayPtr, - int *inPipePtr, int *outPipePtr, int *errFilePtr) +static int JimParsePipelineLegacy(Jim_Interp *interp, int argc, Jim_Obj *const *argv, Jim_Obj *cmdList, Jim_Obj *redirectList) +{ + int i; + /* Add an initial empty commandlist */ + Jim_Obj *cmdObj = Jim_NewListObj(interp, NULL, 0); + Jim_ListAppendElement(interp, cmdList, cmdObj); + const char *arg = NULL; + + for (i = 0; i < argc; i++) { + arg = Jim_String(argv[i]); + unsigned ett = JimExecClassifyArg(arg); + if (ett == JIM_ETT_BAD) { + Jim_SetResultFormatted(interp, "invalid: %s", arg); + return JIM_ERR; + } + if (ett == JIM_ETT_CMD) { + /* Add to the current command */ + Jim_ListAppendElement(interp, cmdObj, argv[i]); + continue; + } + if (ett & JIM_ETT_PIPE) { + if (Jim_ListLength(interp, cmdObj) == 0) { + goto missing_cmd; + } + /* Add this separator */ + Jim_ListAppendElement(interp, cmdList, argv[i]); + /* Now start a new command list */ + cmdObj = Jim_NewListObj(interp, NULL, 0); + Jim_ListAppendElement(interp, cmdList, cmdObj); + continue; + } + Jim_ListAppendElement(interp, redirectList, argv[i]); + if ((ett & JIM_ETT_NOARG)) { + /* This means we need an arg */ + if (i >= argc - 1) { + /* This is an error */ + Jim_SetResultFormatted(interp, "can't specify \"%#s\" as last word in command", argv[i]); + return -1; + } + i++; + Jim_ListAppendElement(interp, redirectList, argv[i]); + } + } + + if (Jim_ListLength(interp, cmdObj) == 0) { +missing_cmd: + if (arg && *arg == '|') { + Jim_SetResultString(interp, "illegal use of | or |& in command", -1); + } + else { + Jim_SetResultString(interp, "didn't specify command to execute", -1); + } + return JIM_ERR; + } + + return JIM_OK; +} + +static int JimExecPipeline(Jim_Interp *interp, Jim_Obj *cmdList, Jim_Obj *redirectList, + phandle_t **pidArrayPtr, int *outPipePtr, int *errFilePtr) { phandle_t *pidPtr = NULL; /* Points to alloc-ed array holding all * the pids of child processes. */ @@ -679,9 +842,9 @@ JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t * from stdin/pipe. */ int input_len = 0; /* Length of input, if relevant */ -#define FILE_NAME 0 /* input/output: filename */ +#define FILE_NAME 0 /* input/output: filename or @filehandle */ #define FILE_APPEND 1 /* output only: filename, append */ -#define FILE_HANDLE 2 /* input/output: filehandle */ +#define FILE_HANDLE 2 /* input/output: @ filehandle */ #define FILE_TEXT 3 /* input only: input is actual text */ int inputFile = FILE_NAME; /* 1 means input is name of input file. @@ -720,9 +883,6 @@ JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t * in pipeline (could be file or pipe). * -1 means use stdout. */ int pipeIds[2]; /* File ids for pipe that's being created. */ - int firstArg, lastArg; /* Indexes of first and last arguments in - * current command. */ - int lastBar; int i; phandle_t phandle; char **save_environ; @@ -731,13 +891,6 @@ JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t #endif struct WaitInfoTable *table = Jim_CmdPrivData(interp); - /* Holds the args which will be used to exec */ - char **arg_array = Jim_Alloc(sizeof(*arg_array) * (argc + 1)); - int arg_count = 0; - - if (inPipePtr != NULL) { - *inPipePtr = -1; - } if (outPipePtr != NULL) { *outPipePtr = -1; } @@ -746,105 +899,72 @@ JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t } pipeIds[0] = pipeIds[1] = -1; - /* - * First, scan through all the arguments to figure out the structure - * of the pipeline. Count the number of distinct processes (it's the - * number of "|" arguments). If there are "<", "<<", or ">" arguments - * then make note of input and output redirection and remove these - * arguments and the arguments that follow them. + /* Now interpet the redirection list */ - cmdCount = 1; - lastBar = -1; - for (i = 0; i < argc; i++) { - const char *arg = Jim_String(argv[i]); - - if (arg[0] == '<') { - inputFile = FILE_NAME; - input = arg + 1; - if (*input == '<') { - inputFile = FILE_TEXT; - input_len = Jim_Length(argv[i]) - 2; - input++; - } - else if (*input == '@') { - inputFile = FILE_HANDLE; - input++; - } - - if (!*input && ++i < argc) { - input = Jim_GetString(argv[i], &input_len); - } - } - else if (arg[0] == '>') { - int dup_error = 0; - - outputFile = FILE_NAME; - - output = arg + 1; - if (*output == '>') { - outputFile = FILE_APPEND; - output++; - } - if (*output == '&') { - /* Redirect stderr too */ - output++; - dup_error = 1; - } - if (*output == '@') { - outputFile = FILE_HANDLE; - output++; - } - if (!*output && ++i < argc) { - output = Jim_String(argv[i]); + int redir_len = Jim_ListLength(interp, redirectList); + for (i = 0; i < redir_len; i++) { + int len; + int item_len; + Jim_Obj *redirObj = Jim_ListGetIndex(interp, redirectList, i); + const char *arg = Jim_GetString(redirObj, &len); + unsigned ett = JimExecClassifyArg(arg); + const char *item; + int type = FILE_NAME; + if ((ett & JIM_ETT_NOARG) == 0) { + /* No separate arg. Need to skip over the appropriate number or redirection chars */ + item = arg + 1; + if ((ett & JIM_ETT_HANDLE)) { + item++; } - if (dup_error) { - errorFile = outputFile; - error = output; + if ((ett & JIM_ETT_APPEND)) { + item++; } - } - else if (arg[0] == '2' && arg[1] == '>') { - error = arg + 2; - errorFile = FILE_NAME; - - if (*error == '@') { - errorFile = FILE_HANDLE; - error++; + if ((ett & JIM_ETT_DUPERR)) { + item++; } - else if (*error == '>') { - errorFile = FILE_APPEND; - error++; + if ((ett & JIM_ETT_ERR)) { + item++; } - if (!*error && ++i < argc) { - error = Jim_String(argv[i]); + if ((ett & JIM_ETT_STR)) { + type = FILE_TEXT; + item++; } + item_len = len - (item - arg); } else { - if (strcmp(arg, "|") == 0 || strcmp(arg, "|&") == 0) { - if (i == lastBar + 1 || i == argc - 1) { - Jim_SetResultString(interp, "illegal use of | or |& in command", -1); - goto badargs; - } - lastBar = i; - cmdCount++; + /* separate arg, so fetch it */ + i++; + item = Jim_GetString(Jim_ListGetIndex(interp, redirectList, i), &item_len); + } + /* Figure out the type */ + if ((ett & JIM_ETT_HANDLE)) { + type = FILE_HANDLE; + } + if ((ett & JIM_ETT_APPEND)) { + type = FILE_APPEND; + } + if ((ett & JIM_ETT_STR)) { + type = FILE_TEXT; + } + if (ett & JIM_ETT_IN) { + input = item; + input_len = item_len; + inputFile = type; + } + else if (ett & JIM_ETT_OUT) { + output = item; + outputFile = type; + if (ett & JIM_ETT_DUPERR) { + error = output; + errorFile = outputFile; } - /* Either |, |& or a "normal" arg, so store it in the arg array */ - arg_array[arg_count++] = (char *)arg; - continue; } - - if (i >= argc) { - Jim_SetResultFormatted(interp, "can't specify \"%s\" as last word in command", arg); - goto badargs; + else if (ett & JIM_ETT_ERR) { + error = item; + errorFile = type; } } - if (arg_count == 0) { - Jim_SetResultString(interp, "didn't specify command to execute", -1); -badargs: - Jim_Free(arg_array); - return -1; - } - /* Must do this before vfork(), so do it now */ save_environ = JimSaveEnv(JimBuildEnv(interp)); @@ -869,7 +989,8 @@ JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t } Jim_Lseek(inputId, 0L, SEEK_SET); } - else if (inputFile == FILE_HANDLE) { + else if (inputFile == FILE_HANDLE || *input == '@') { + input += (inputFile == FILE_NAME); int fd = JimGetChannelFd(interp, input); if (fd < 0) { @@ -888,15 +1009,6 @@ JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t } } } - else if (inPipePtr != NULL) { - if (pipe(pipeIds) != 0) { - Jim_SetResultErrno(interp, "couldn't create input pipe for command"); - goto error; - } - inputId = pipeIds[0]; - *inPipePtr = pipeIds[1]; - pipeIds[0] = pipeIds[1] = -1; - } /* * Set up the redirected output sink for the pipeline from one @@ -936,6 +1048,7 @@ JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t /* If we are redirecting stderr with 2>filename or 2>@fileId, then we ignore errFilePtr */ if (error != NULL) { if (errorFile == FILE_HANDLE) { + error += (errorFile == FILE_NAME); if (strcmp(error, "1") == 0) { /* Special 2>@1 */ if (lastOutputId != -1) { @@ -982,59 +1095,59 @@ JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t } /* - * Scan through the argc array, forking off a process for each - * group of arguments between "|" arguments. + * Iterate over cmdList, forking off a process for each + * cmdlist */ - + int cmd_list_size = Jim_ListLength(interp, cmdList); + cmdCount = (cmd_list_size + 1) / 2; pidPtr = Jim_Alloc(cmdCount * sizeof(*pidPtr)); - for (firstArg = 0; firstArg < arg_count; numPids++, firstArg = lastArg + 1) { - int pipe_dup_err = 0; - int origErrorId = errorId; - for (lastArg = firstArg; lastArg < arg_count; lastArg++) { - if (strcmp(arg_array[lastArg], "|") == 0) { - break; - } - if (strcmp(arg_array[lastArg], "|&") == 0) { - pipe_dup_err = 1; - break; - } + for (i = 0; i < cmd_list_size; ) { + char **arg_array; + int j; + int origErrorId = errorId; + Jim_Obj *cmdObj = Jim_ListGetIndex(interp, cmdList, i++); + int cmd_len = Jim_ListLength(interp, cmdObj); + Jim_Obj *sepObj = NULL; + if (i < cmd_list_size - 1) { + sepObj = Jim_ListGetIndex(interp, cmdList, i++); } - if (lastArg == firstArg) { - Jim_SetResultString(interp, "missing command to exec", -1); - goto error; + /* Build exec array */ + arg_array = Jim_Alloc((cmd_len + 1) * sizeof(*arg_array)); + for (j = 0; j < cmd_len; j++) { + arg_array[j] = (char *)Jim_String(Jim_ListGetIndex(interp, cmdObj, j)); } + arg_array[j] = NULL; - /* Replace | with NULL for execv() */ - arg_array[lastArg] = NULL; - if (lastArg == arg_count) { + if (sepObj == NULL) { outputId = lastOutputId; lastOutputId = -1; } else { if (pipe(pipeIds) != 0) { Jim_SetResultErrno(interp, "couldn't create pipe"); + Jim_Free(arg_array); goto error; } outputId = pipeIds[1]; } /* Need to do this before vfork() */ - if (pipe_dup_err) { + if (sepObj && Jim_CompareStringImmediate(interp, sepObj, "|&")) { errorId = outputId; } /* Now fork the child */ #ifdef __MINGW32__ - phandle = JimStartWinProcess(interp, &arg_array[firstArg], save_environ, inputId, outputId, errorId); + phandle = JimStartWinProcess(interp, &arg_array[0], save_environ, inputId, outputId, errorId); if (phandle == JIM_BAD_PHANDLE) { - Jim_SetResultFormatted(interp, "couldn't exec \"%s\"", arg_array[firstArg]); + Jim_SetResultFormatted(interp, "couldn't exec \"%s\"", arg_array[0]); goto error; } #else - i = strlen(arg_array[firstArg]); + int argv0_len = strlen(arg_array[0]); #ifdef HAVE_EXECVPE child_environ = Jim_GetEnviron(); @@ -1083,10 +1196,10 @@ JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t close(lastOutputId); } - execvpe(arg_array[firstArg], &arg_array[firstArg], child_environ); + execvpe(arg_array[0], arg_array, child_environ); if (write(fileno(stderr), "couldn't exec \"", 15) && - write(fileno(stderr), arg_array[firstArg], i) && + write(fileno(stderr), arg_array[0], argv0_len) && write(fileno(stderr), "\"\n", 2)) { /* nothing */ } @@ -1102,6 +1215,7 @@ JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t #endif /* parent */ + Jim_Free(arg_array); /* * Enlarge the wait table if there isn't enough space for a new @@ -1116,7 +1230,7 @@ JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t table->info[table->used].flags = 0; table->used++; - pidPtr[numPids] = phandle; + pidPtr[numPids++] = phandle; /* Restore in case of pipe_dup_err */ errorId = origErrorId; @@ -1151,7 +1265,6 @@ JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t if (errorId != -1) { close(errorId); } - Jim_Free(arg_array); JimRestoreEnv(save_environ); @@ -1164,10 +1277,6 @@ JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t */ error: - if ((inPipePtr != NULL) && (*inPipePtr != -1)) { - close(*inPipePtr); - *inPipePtr = -1; - } if ((outPipePtr != NULL) && (*outPipePtr != -1)) { close(*outPipePtr); *outPipePtr = -1; @@ -1194,6 +1303,61 @@ JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t goto cleanup; } +/* + *---------------------------------------------------------------------- + * + * JimCreatePipeline -- + * + * Given an argc/argv array, instantiate a pipeline of processes + * as described by the argv. + * + * 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 outPipePtr + * isn't NULL, then *outPipePtr is filled in with the file id + * for the output pipe from the pipeline: the caller must close + * this file. If errFilePtr isn't NULL, then *errFilePtr 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 +JimCreatePipeline(Jim_Interp *interp, int argc, Jim_Obj *const *argv, phandle_t **pidArrayPtr, + int *outPipePtr, int *errFilePtr) +{ + int rc = -1; + int ret; + + Jim_Obj *cmdList = Jim_NewListObj(interp, NULL, 0); + Jim_Obj *redirectList = Jim_NewListObj(interp, NULL, 0); + Jim_IncrRefCount(cmdList); + Jim_IncrRefCount(redirectList); + + if (argc > 1 && Jim_CompareStringImmediate(interp, argv[0], "|")) { + /* TIP424 exec format */ + ret = JimParsePipeline(interp, argc - 1, argv + 1, cmdList, redirectList); + } + else { + /* legacy exec format */ + ret = JimParsePipelineLegacy(interp, argc, argv, cmdList, redirectList); + } + if (ret == JIM_OK) { + /* OK, try to exec */ + rc = JimExecPipeline(interp, cmdList, redirectList, pidArrayPtr, outPipePtr, errFilePtr); + } + Jim_DecrRefCount(interp, cmdList); + Jim_DecrRefCount(interp, redirectList); + return rc; +} + /* *---------------------------------------------------------------------- * diff --git a/jim_tcl.txt b/jim_tcl.txt index 83dd9c8c..28598916 100644 --- a/jim_tcl.txt +++ b/jim_tcl.txt @@ -62,6 +62,7 @@ Changes since 0.82 6. `socket` , `open` and `aio accept` now support '-noclose' 7. Add support for hinting with `history hints` 8. Support for `proc` statics by reference (lexical closure) rather than by value +9. `exec` TIP 424 - support safer +exec | + syntax (also +open "|| pipeline..."+) Changes between 0.81 and 0.82 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -2309,6 +2310,8 @@ evaluation (or any error generated by it). exec ~~~~ +*exec* 'arg ?arg\...?'+ +or ++*exec* | '{cmdlist ..} ?redirection? ...+ This command treats its arguments as the specification of one or more UNIX commands to execute as subprocesses. diff --git a/tests/exec-tip424.test b/tests/exec-tip424.test new file mode 100644 index 00000000..291dc3e7 --- /dev/null +++ b/tests/exec-tip424.test @@ -0,0 +1,420 @@ +# The same tests as exec.test, but changed to TIP424 exec syntax + +source [file dirname [info script]]/testing.tcl + +needs cmd exec +needs cmd flush + +# Need [pipe] to implement [open |command] +constraint cmd pipe +constraint expr unix {$tcl_platform(platform) eq "unix"} + +# Sleep which supports fractions of a second +if {[info commands sleep] eq {}} { + proc sleep {n} { + exec {*}$::sleepx $n + } +} + +set f [open sleepx w] +puts $f { + sleep "$@" +} +close $f +#catch {exec chmod +x sleepx} +set sleepx [list sh sleepx] + +# Basic operations. + +test exec-1.1 {basic exec operation} { + exec | {echo a b c} +} "a b c" +test exec-1.2 {pipelining} { + exec | {echo a b c d} | cat | cat +} "a b c d" +test exec-1.3 {pipelining} { + set a [exec | {echo a b c d} | cat | wc] + list [scan $a "%d %d %d" b c d] $b $c +} {3 1 4} +set arg {12345678901234567890123456789012345678901234567890} +set arg "$arg$arg$arg$arg$arg$arg" +test exec-1.4 {long command lines} { + exec | [list echo $arg] +} $arg +set arg {} + +# I/O redirection: input from Tcl command. + +test exec-2.1 {redirecting input from immediate source} { + exec | cat << "Sample text" +} {Sample text} +test exec-2.2 {redirecting input from immediate source} { + exec | cat << "Sample text" | cat +} {Sample text} +test exec-2.4 {redirecting input from immediate source} { + exec | cat | cat << "Sample text" +} {Sample text} +test exec-2.5 {redirecting input from immediate source} { + exec | cat "< external conversion did not + # occur before writing out the temp file. + exec | cat << "\uE9\uE0\uFC\uF1" +} "\uE9\uE0\uFC\uF1" +test exec-2.7 {redirecting input from immediate source with nulls} { + exec | cat << "Sample\0text" +} "Sample\0text" + +# I/O redirection: output to file. + +file delete gorp.file +test exec-3.1 {redirecting output to file} { + exec | {echo "Some simple words"} > gorp.file + exec | {cat gorp.file} +} "Some simple words" +test exec-3.2 {redirecting output to file} { + exec | {echo "More simple words"} | cat >gorp.file | cat + exec | {cat gorp.file} +} "More simple words" +test exec-3.3 {redirecting output to file} { + exec | {echo "Different simple words"} > gorp.file | cat | cat + exec | {cat gorp.file} +} "Different simple words" +test exec-3.4 {redirecting output to file} { + exec | {echo "Some simple words"} >gorp.file + exec | {cat gorp.file} +} "Some simple words" +test exec-3.5 {redirecting output to file} { + exec | {echo "First line"} >gorp.file + exec | {echo "Second line"} >> gorp.file + exec | {cat gorp.file} +} "First line\nSecond line" +test exec-3.7 {redirecting output to file} { + set f [open gorp.file w] + puts $f "Line 1" + flush $f + exec | {echo "More text"} >@ $f + exec | {echo "Even more"} >@$f + puts $f "Line 3" + close $f + exec | {cat gorp.file} +} "Line 1\nMore text\nEven more\nLine 3" + +# I/O redirection: output and stderr to file. + +file delete gorp.file +test exec-4.1 {redirecting output and stderr to file} { + exec | {echo "test output"} >& gorp.file + exec | {cat gorp.file} +} "test output" +test exec-4.2 {redirecting output and stderr to file} { + list [exec | {sh -c "echo foo bar 1>&2"} >&gorp.file] \ + [exec | {cat gorp.file}] +} {{} {foo bar}} +test exec-4.3 {redirecting output and stderr to file} { + exec | {echo "first line"} > gorp.file + list [exec | {sh -c "echo foo bar 1>&2"} >>&gorp.file] \ + [exec | {cat gorp.file}] +} "{} {first line\nfoo bar}" +test exec-4.4 {redirecting output and stderr to file} { + set f [open gorp.file w] + puts $f "Line 1" + flush $f + exec | {echo "More text"} >&@ $f + exec | {echo "Even more"} >&@$f + puts $f "Line 3" + close $f + exec | {cat gorp.file} +} "Line 1\nMore text\nEven more\nLine 3" +test exec-4.5 {redirecting output and stderr to file} { + set f [open gorp.file w] + puts $f "Line 1" + flush $f + exec | {sh -c "echo foo bar 1>&2"} >&@ $f + exec | {sh -c "echo xyzzy 1>&2"} >&@$f + puts $f "Line 3" + close $f + exec | {cat gorp.file} +} "Line 1\nfoo bar\nxyzzy\nLine 3" + +# I/O redirection: input from file. + +exec | {echo "Just a few thoughts"} > gorp.file + +test exec-5.1 {redirecting input from file} { + exec | cat < gorp.file +} {Just a few thoughts} +test exec-5.2 {redirecting input from file} { + exec | cat | cat < gorp.file +} {Just a few thoughts} +test exec-5.3 {redirecting input from file} { + exec | cat < gorp.file | cat +} {Just a few thoughts} +test exec-5.5 {redirecting input from file} { + exec | cat &2"} |& cat +} "foo bar" +test exec-6.3 {redirecting stderr through a pipeline} { + exec | {sh -c "echo foo bar 1>&2"} \ + |& cat |& cat +} "foo bar" + +# I/O redirection: combinations. + +file delete gorp.file2 +test exec-7.1 {multiple I/O redirections} { + exec | cat << "command input" > gorp.file2 < gorp.file + exec | {cat gorp.file2} +} {Just a few thoughts} +test exec-7.2 {multiple I/O redirections} { + exec cat < gorp.file << "command input" +} {command input} + +# Long input to command and output from command. + +set a [string repeat a 1000000] +test exec-8.1 {long input and output} { + string length [exec | cat << $a] +} 1000000 + +# More than 20 arguments to exec. + +test exec-8.1 {long input and output} { + exec | {echo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23} +} {1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23} + +# Commands that return errors. + +test exec-9.1 {commands returning errors} { + catch {exec | gorp456} +} {1} +test exec-9.2 {commands returning errors} { + catch {exec | {echo foo} | foo123} msg +} {1} +test exec-9.3 {commands returning errors} { + list [catch {exec | [list {*}$sleepx 0.1] | false | [list {*}$sleepx 0.1]} msg] +} {1} +test exec-9.4 {commands returning errors} jim { + list [catch {exec | false | {echo "foo bar"}} msg] $msg +} {1 {foo bar}} +test exec-9.5 {commands returning errors} { + list [catch {exec | gorp456 | {echo a b c}} msg] +} {1} +test exec-9.6 {commands returning errors} jim { + list [catch {exec | {sh -c "echo error msg 1>&2"}} msg] $msg +} {0 {error msg}} +test exec-9.7 {commands returning errors} jim { + # Note: Use sleep here to ensure the order + list [catch {exec | {sh -c "echo error msg 1 1>&2"} \ + | {sh -c "sleep 0.1; echo error msg 2 1>&2"}} msg] $msg +} {0 {error msg 1 +error msg 2}} + +# Errors in executing the Tcl command, as opposed to errors in the +# processes that are invoked. + +test exec-10.1 {errors in exec invocation} { + list [catch {exec |} msg] +} {1} +test exec-10.3 {errors in exec invocation} { + list [catch {exec | cat |} msg] $msg +} {1 {cmdlist required after |}} +test exec-10.4 {errors in exec invocation} { + list [catch {exec | cat | | cat} msg] $msg +} {1 {invalid redirection cat}} +test exec-10.5 {errors in exec invocation} { + list [catch {exec | cat | |& cat} msg] $msg +} {1 {invalid redirection cat}} +test exec-10.6 {errors in exec invocation} { + list [catch {exec | cat |&} msg] $msg +} {1 {cmdlist required after |&}} +test exec-10.7 {errors in exec invocation} { + list [catch {exec | cat <} msg] $msg +} {1 {can't specify "<" as last word in command}} +test exec-10.8 {errors in exec invocation} { + list [catch {exec | cat >} msg] $msg +} {1 {can't specify ">" as last word in command}} +test exec-10.9 {errors in exec invocation} { + list [catch {exec | cat <<} msg] $msg +} {1 {can't specify "<<" as last word in command}} +test exec-10.10 {errors in exec invocation} { + list [catch {exec | cat >>} msg] $msg +} {1 {can't specify ">>" as last word in command}} +test exec-10.11 {errors in exec invocation} { + list [catch {exec | cat >&} msg] $msg +} {1 {can't specify ">&" as last word in command}} +test exec-10.12 {errors in exec invocation} { + list [catch {exec | cat >>&} msg] $msg +} {1 {can't specify ">>&" as last word in command}} +test exec-10.13 {errors in exec invocation} { + list [catch {exec | cat >@} msg] $msg +} {1 {can't specify ">@" as last word in command}} +test exec-10.14 {errors in exec invocation} { + list [catch {exec | cat <@} msg] $msg +} {1 {can't specify "<@" as last word in command}} +test exec-10.15 {errors in exec invocation} { + list [catch {exec | cat < a/b/c} msg] [string tolower $msg] +} {1 {couldn't read file "a/b/c": no such file or directory}} +test exec-10.16 {errors in exec invocation} { + list [catch {exec | cat << foo > a/b/c} msg] [string tolower $msg] +} {1 {couldn't write file "a/b/c": no such file or directory}} +test exec-10.17 {errors in exec invocation} { + list [catch {exec | cat << foo > a/b/c} msg] [string tolower $msg] +} {1 {couldn't write file "a/b/c": no such file or directory}} +set f [open gorp.file w] +test exec-10.18 {errors in exec invocation} { + list [catch {exec | cat <@ $f} msg] +} 1 +close $f + +# Commands in background. + +test exec-11.1 {commands in background} { + set x [lindex [time {exec | [list {*}$sleepx 0.2] &}] 0] + expr $x<1000000 +} 1 +test exec-11.2 {commands in background} { + list [catch {exec | {echo a &b}} msg] $msg +} {0 {a &b}} +test exec-11.3 {commands in background} { + llength [exec | [list {*}$sleepx 0.1] &] +} 1 +test exec-11.4 {commands in background} { + llength [exec | [list {*}$sleepx 0.1] | [list {*}$sleepx 0.1] | [list {*}$sleepx 0.1] &] +} 3 + +# Make sure that background commands are properly reaped when +# they eventually die. + +exec | [list {*}$sleepx 0.3] + +test exec-12.1 {reaping background processes} -constraints unix -body { + for {set i 0} {$i < 20} {incr i} { + exec | {echo foo} > exec.tmp1 & + } + exec | [list {*}$sleepx 0.1] + catch {exec | ps | {fgrep "echo foo"} | {fgrep -v grep} | wc} msg + lindex $msg 0 +} -cleanup { + file delete exec.tmp1 +} -result 0 + +# Redirecting standard error separately from standard output + +test exec-15.1 {standard error redirection} { + exec | {echo "First line"} > gorp.file + list [exec | {sh -c "echo foo bar 1>&2"} 2> gorp.file] \ + [exec | {cat gorp.file}] +} {{} {foo bar}} +test exec-15.2 {standard error redirection} { + list [exec | {sh -c "echo foo bar 1>&2"} \ + | {echo biz baz} >gorp.file 2> gorp.file2] \ + [exec | {cat gorp.file}] \ + [exec | {cat gorp.file2}] +} {{} {biz baz} {foo bar}} +test exec-15.3 {standard error redirection} { + list [exec | {sh -c "echo foo bar 1>&2"} \ + | {echo biz baz} 2>gorp.file > gorp.file2] \ + [exec | {cat gorp.file}] \ + [exec | {cat gorp.file2}] +} {{} {foo bar} {biz baz}} +test exec-15.4 {standard error redirection} { + set f [open gorp.file w] + puts $f "Line 1" + flush $f + exec | {sh -c "echo foo bar 1>&2"} 2>@ $f + puts $f "Line 3" + close $f + exec | {cat gorp.file} +} {Line 1 +foo bar +Line 3} +test exec-15.5 {standard error redirection} { + exec | {echo "First line"} > gorp.file + exec | {sh -c "echo foo bar 1>&2"} 2>> gorp.file + exec | {cat gorp.file} +} {First line +foo bar} +test exec-15.6 {standard error redirection} { + exec | {sh -c "echo foo bar 1>&2"} > gorp.file2 2> gorp.file \ + >& gorp.file 2> gorp.file2 | {echo biz baz} + list [exec | {cat gorp.file}] [exec | {cat gorp.file2}] +} {{biz baz} {foo bar}} +test exec-15.7 {combine standard output/standard error} -body { + exec | {sh -c "echo foo bar 1>&2"} > gorp.file 2>@1 + exec | {cat gorp.file} +} -cleanup { + file delete gorp.file gorp.file2 +} -result {foo bar} + +test exec-16.1 {flush output before exec} -body { + set f [open gorp.file w] + puts $f "First line" + exec | {echo "Second line"} >@ $f + puts $f "Third line" + close $f + exec | {cat gorp.file} +} -cleanup { + file delete gorp.file +} -result {First line +Second line +Third line} + +test exec-17.1 {redirecting from command pipeline} -setup { + makeFile "abc\nghi\njkl" gorp.file +} -constraints pipe -body { + set f [open "|| {cat gorp.file} | {wc -l}" r] + set result [lindex [exec | cat <@$f] 0] + close $f + set result +} -cleanup { + file delete gorp.file +} -result {3} + +test exec-17.2 {redirecting to command pipeline} -setup { + makeFile "abc\nghi\njkl" gorp.file +} -constraints pipe -body { + set f [open "|| {wc -l} >gorp2.file" w] + exec | {cat gorp.file} >@$f + flush $f + close $f + lindex [exec | {cat gorp2.file}] 0 +} -cleanup { + file delete gorp.file gorp2.file +} -result {3} + +file delete sleepx + +# Now we probably have a lot of unreaped zombies at this point +# so reap them to avoid confusing further tests +wait + +testreport diff --git a/tests/exec.test b/tests/exec.test index 85014a7e..e957ec30 100644 --- a/tests/exec.test +++ b/tests/exec.test @@ -268,9 +268,10 @@ error msg 2}} test exec-10.1 {errors in exec invocation} { list [catch {exec} msg] } {1} -test exec-10.2 {errors in exec invocation} { - list [catch {exec | cat} msg] $msg -} {1 {illegal use of | or |& in command}} +# This is no longer an error - it is TIP424 syntax +#test exec-10.2 {errors in exec invocation} { +# list [catch {exec | cat} msg] $msg +#} {1 {illegal use of | or |& in command}} test exec-10.3 {errors in exec invocation} { list [catch {exec cat |} msg] $msg } {1 {illegal use of | or |& in command}}