Index: library/tcltest/tcltest.tcl ================================================================== --- library/tcltest/tcltest.tcl +++ library/tcltest/tcltest.tcl @@ -1240,10 +1240,14 @@ # Some tests must be skipped if the interpreter is not in # interactive mode ConstraintInitializer interactive \ {expr {[info exists ::tcl_interactive] && $::tcl_interactive}} + + # Skip slow tests (to enable slow tests add parameter `-constraints slowTest`) + + ConstraintInitializer slowTest {format 0} # Some tests can only be run if the installation came from a CD # image instead of a web image. Some tests must be skipped if you # are running as root on Unix. Other tests can only be run if you # are running as root on Unix. Index: tests/winPipe.test ================================================================== --- tests/winPipe.test +++ tests/winPipe.test @@ -306,14 +306,54 @@ after 100 { lappend x timeout } vwait x lappend x [catch {close $f} msg] $msg } {writable timeout 0 {}} -set path(echoArgs.tcl) [makeFile { - puts "[list $argv0 $argv]" -} echoArgs.tcl] - +proc _testExecArgs {single args} { + variable path + if {![info exists path(echoArgs.tcl)] || ![file exists $path(echoArgs.tcl)]} { + set path(echoArgs.tcl) [makeFile { + puts "[list [file tail $argv0] {*}$argv]" + } echoArgs.tcl] + } + if {![info exists path(echoArgs.bat)] || ![file exists $path(echoArgs.bat)]} { + set path(echoArgs.bat) [makeFile "@[file native [interpreter]] $path(echoArgs.tcl) %*" "echoArgs.bat"] + } + set cmds [list [list [interpreter] $path(echoArgs.tcl)]] + if {!($single & 2)} { + lappend cmds [list $path(echoArgs.bat)] + } else { + if {![info exists path(echoArgs2.bat)] || ![file exists $path(echoArgs2.bat)]} { + file mkdir [file join [temporaryDirectory] test(Dir)Check] + set path(echoArgs2.bat) [makeFile "@[file native [interpreter]] $path(echoArgs.tcl) %*" \ + "test(Dir)Check/echo(Cmd)Test Args & Batch.bat"] + } + lappend cmds [list $path(echoArgs2.bat)] + } + set broken {} + foreach args $args { + if {$single & 1} { + # enclose single test-arg between 1st/3rd to be sure nothing is truncated + # (e. g. to cover unexpected trim by nts-zero case, and args don't recombined): + set args [list "1st" $args "3rd"] + } + set args [list {*}$args]; # normalized canonical list + foreach cmd $cmds { + set e [linsert $args 0 [file tail $path(echoArgs.tcl)]] + tcltest::DebugPuts 4 " ## test exec [file extension [lindex $cmd 0]] ($cmd) for\n ## $args" + if {[catch { + exec {*}$cmd {*}$args + } r]} { + set r "ERROR: $r" + } + if {$r ne $e} { + append broken "\[ERROR\]: exec [file extension [lindex $cmd 0]] on $args\n -- result:\n$r\n -- expected:\n$e\n" + } + } + } + return $broken +} ### validate the raw output of BuildCommandLine(). ### test winpipe-7.1 {BuildCommandLine: null arguments} {win exec} { exec $env(COMSPEC) /c echo foo "" bar @@ -367,70 +407,157 @@ exec $env(COMSPEC) /c echo foo \{ bar } "foo \{ bar" test winpipe-7.18 {BuildCommandLine: special chars #5} {win exec} { exec $env(COMSPEC) /c echo foo \} bar } "foo \} bar" + +set injectList { + {test"whoami} {test""whoami} + {test"""whoami} {test""""whoami} + + "test\"whoami\\" "test\"\"whoami\\" + "test\"\"\"whoami\\" "test\"\"\"\"whoami\\" + + {test\\&\\test} {test"\\&\\test} + {"test\\&\\test} {"test"\\&\\"test"} + {test\\"&"\\test} {test"\\"&"\\test} + {"test\\"&"\\test} {"test"\\"&"\\"test"} + + {test\"&whoami} {test"\"&whoami} + {test""\"&whoami} {test"""\"&whoami} + {test\"\&whoami} {test"\"\&whoami} + {test""\"\&whoami} {test"""\"\&whoami} + + {test&whoami} {test|whoami} + {"test&whoami} {"test|whoami} + {test"&whoami} {test"|whoami} + {"test"&whoami} {"test"|whoami} + {""test"&whoami} {""test"|whoami} + + {test&echo "} {test|echo "} + {"test&echo "} {"test|echo "} + {test"&echo "} {test"|echo "} + {"test"&echo "} {"test"|echo "} + {""test"&echo "} {""test"|echo "} + + {test&echo ""} {test|echo ""} + {"test&echo ""} {"test|echo ""} + {test"&echo ""} {test"|echo ""} + {"test"&echo ""} {"test"|echo ""} + {""test"&echo ""} {""test"|echo ""} + + {test>whoami} {testwhoami} {"testwhoami} {test"whoami} {"test"whoami} {""test"!()%} + {\&|^<>!()% } + {"\&|^<>!()%} + {"\&|^<>!()% } + {"""""\\\\\&|^<>!()%} + {"""""\\\\\&|^<>!()% } + } + set i 0 + time { + set args {[incr i].} + time { + set map [lindex $maps [expr {int(rand()*[llength $maps])}]] + # be sure arg has some prefix (avoid special handling, like |& etc) + set a {x} + while {[string length $a] < 50} { + append a [string index $map [expr {int(rand()*[string length $map])}]] + } + lappend args $a + } 20 + lappend lst $args + } 10 + _testExecArgs 0 {*}$lst +} -result {} -cleanup { + unset -nocomplain lst args a map maps +} + +rename _testExecArgs {} # restore old values for env(TMP) and env(TEMP) if {[catch {set env(TMP) $env_tmp}]} { unset env(TMP) @@ -438,8 +565,9 @@ if {[catch {set env(TEMP) $env_temp}]} { unset env(TEMP) } # cleanup -file delete big little stdout stderr nothing echoArgs.tcl +file delete big little stdout stderr nothing echoArgs.tcl echoArgs.bat +file delete -force [file join [temporaryDirectory] test(Dir)Check] ::tcltest::cleanupTests return Index: win/tclWinPipe.c ================================================================== --- win/tclWinPipe.c +++ win/tclWinPipe.c @@ -1490,11 +1490,11 @@ Tcl_AppendResult(interp, "couldn't execute \"", originalName, "\": ", Tcl_PosixError(interp), (char *) NULL); return APPL_NONE; } - if ((applType == APPL_DOS) || (applType == APPL_WIN3X)) { + if (applType == APPL_WIN3X) { /* * Replace long path name of executable with short path name for * 16-bit applications. Otherwise the application may not be able to * correctly parse its own command line to separate off the * application name from the arguments. @@ -1524,10 +1524,90 @@ * Side effects: * None. * *---------------------------------------------------------------------- */ + +static const char * +BuildCmdLineBypassBS( + const char *current, + const char **bspos +) { + /* mark first backslash possition */ + if (!*bspos) { + *bspos = current; + } + do { + current++; + } while (*current == '\\'); + return current; +} + +static void +QuoteCmdLineBackslash( + Tcl_DString *dsPtr, + const char *start, + const char *current, + const char *bspos +) { + if (!bspos) { + if (current > start) { /* part before current (special) */ + Tcl_DStringAppend(dsPtr, start, (int) (current - start)); + } + } else { + if (bspos > start) { /* part before first backslash */ + Tcl_DStringAppend(dsPtr, start, (int) (bspos - start)); + } + while (bspos++ < current) { /* each backslash twice */ + Tcl_DStringAppend(dsPtr, "\\\\", 2); + } + } +} + +static const char * +QuoteCmdLinePart( + Tcl_DString *dsPtr, + const char *start, + const char *special, + const char *specMetaChars, + const char **bspos +) { + if (!*bspos) { + /* rest before special (before quote) */ + QuoteCmdLineBackslash(dsPtr, start, special, NULL); + start = special; + } else { + /* rest before first backslash and backslashes into new quoted block */ + QuoteCmdLineBackslash(dsPtr, start, *bspos, NULL); + start = *bspos; + } + /* + * escape all special chars enclosed in quotes like `"..."`, note that here we + * don't must escape `\` (with `\`), because it's outside of the main quotes, + * so `\` remains `\`, but important - not at end of part, because results as + * before the quote, so `%\%\` should be escaped as `"%\%"\\`). + */ + Tcl_DStringAppend(dsPtr, "\"", 1); /* opening escape quote-char */ + do { + *bspos = NULL; + special++; + if (*special == '\\') { + /* bypass backslashes (and mark first backslash possition)*/ + special = BuildCmdLineBypassBS(special, bspos); + if (*special == '\0') break; + } + } while (*special && strchr(specMetaChars, *special)); + if (!*bspos) { + /* unescaped rest before quote */ + QuoteCmdLineBackslash(dsPtr, start, special, NULL); + } else { + /* unescaped rest before first backslash (rather belongs to the main block) */ + QuoteCmdLineBackslash(dsPtr, start, *bspos, NULL); + } + Tcl_DStringAppend(dsPtr, "\"", 1); /* closing escape quote-char */ + return special; +} static void BuildCommandLine( const char *executable, /* Full path of executable (including * extension). Replacement for argv[0]. */ @@ -1534,13 +1614,25 @@ int argc, /* Number of arguments. */ const char **argv, /* Argument strings in UTF. */ Tcl_DString *linePtr) /* Initialized Tcl_DString that receives the * command line (TCHAR). */ { - const char *arg, *start, *special; - int quote, i; + const char *arg, *start, *special, *bspos; + int quote = 0, i; Tcl_DString ds; + + /* characters to enclose in quotes if unpaired quote flag set */ + const static char *specMetaChars = "&|^<>!()%"; + /* characters to enclose in quotes in any case (regardless unpaired-flag) */ + const static char *specMetaChars2 = "%"; + + /* Quote flags: + * CL_ESCAPE - escape argument; + * CL_QUOTE - enclose in quotes; + * CL_UNPAIRED - previous arguments chain contains unpaired quote-char; + */ + enum {CL_ESCAPE = 1, CL_QUOTE = 2, CL_UNPAIRED = 4}; Tcl_DStringInit(&ds); /* * Prime the path. Add a space separator if we were primed with something. @@ -1557,64 +1649,102 @@ } else { arg = argv[i]; Tcl_DStringAppend(&ds, " ", 1); } - quote = 0; + quote &= ~(CL_ESCAPE|CL_QUOTE); /* reset escape flags */ + bspos = NULL; if (arg[0] == '\0') { - quote = 1; + quote = CL_QUOTE; } else { int count; Tcl_UniChar ch; - for (start = arg; *start != '\0'; start += count) { - count = Tcl_UtfToUniChar(start, &ch); - if (Tcl_UniCharIsSpace(ch)) { /* INTL: ISO space. */ - quote = 1; - break; - } - } - } - if (quote) { - Tcl_DStringAppend(&ds, "\"", 1); - } - start = arg; - for (special = arg; ; ) { - if ((*special == '\\') && (special[1] == '\\' || - special[1] == '"' || (quote && special[1] == '\0'))) { - Tcl_DStringAppend(&ds, start, (int) (special - start)); - start = special; - while (1) { - special++; - if (*special == '"' || (quote && *special == '\0')) { - /* - * N backslashes followed a quote -> insert N * 2 + 1 - * backslashes then a quote. - */ - - Tcl_DStringAppend(&ds, start, - (int) (special - start)); - break; - } - if (*special != '\\') { - break; - } - } - Tcl_DStringAppend(&ds, start, (int) (special - start)); - start = special; - } - if (*special == '"') { - Tcl_DStringAppend(&ds, start, (int) (special - start)); - Tcl_DStringAppend(&ds, "\\\"", 2); - start = special + 1; - } - if (*special == '\0') { - break; - } - special++; - } - Tcl_DStringAppend(&ds, start, (int) (special - start)); - if (quote) { + for (start = arg; + *start != '\0' && + (quote & (CL_ESCAPE|CL_QUOTE)) != (CL_ESCAPE|CL_QUOTE); + start += count + ) { + count = Tcl_UtfToUniChar(start, &ch); + if (count > 1) continue; + if (Tcl_UniCharIsSpace(ch)) { + quote |= CL_QUOTE; /* quote only */ + if (bspos) { /* if backslash found - escape & quote */ + quote |= CL_ESCAPE; + break; + } + continue; + } + if (strchr(specMetaChars, *start)) { + quote |= (CL_ESCAPE|CL_QUOTE); /*escape & quote */ + break; + } + if (*start == '"') { + quote |= CL_ESCAPE; /* escape only */ + continue; + } + if (*start == '\\') { + bspos = start; + if (quote & CL_QUOTE) { /* if quote - escape & quote */ + quote |= CL_ESCAPE; + break; + } + continue; + } + } + bspos = NULL; + } + if (quote & CL_QUOTE) { + /* start of argument (main opening quote-char) */ + Tcl_DStringAppend(&ds, "\"", 1); + } + if (!(quote & CL_ESCAPE)) { + /* nothing to escape */ + Tcl_DStringAppend(&ds, arg, -1); + } else { + start = arg; + for (special = arg; *special != '\0'; ) { + /* position of `\` is important before quote or at end (equal `\"` because quoted) */ + if (*special == '\\') { + /* bypass backslashes (and mark first backslash possition)*/ + special = BuildCmdLineBypassBS(special, &bspos); + if (*special == '\0') break; + } + /* ["] */ + if (*special == '"') { + quote ^= CL_UNPAIRED; /* invert unpaired flag - observe unpaired quotes */ + /* add part before (and escape backslashes before quote) */ + QuoteCmdLineBackslash(&ds, start, special, bspos); + bspos = NULL; + /* escape using backslash */ + Tcl_DStringAppend(&ds, "\\\"", 2); + start = ++special; + continue; + } + /* unpaired (escaped) quote causes special handling on meta-chars */ + if ((quote & CL_UNPAIRED) && strchr(specMetaChars, *special)) { + special = QuoteCmdLinePart(&ds, start, special, specMetaChars, &bspos); + /* start to current or first backslash */ + start = !bspos ? special : bspos; + continue; + } + /* special case for % - should be enclosed always (paired also) */ + if (strchr(specMetaChars2, *special)) { + special = QuoteCmdLinePart(&ds, start, special, specMetaChars2, &bspos); + /* start to current or first backslash */ + start = !bspos ? special : bspos; + continue; + } + /* other not special (and not meta) character */ + bspos = NULL; /* reset last backslash possition (not interesting) */ + special++; + } + /* rest of argument (and escape backslashes before closing main quote) */ + QuoteCmdLineBackslash(&ds, start, special, + (quote & CL_QUOTE) ? bspos : NULL); + } + if (quote & CL_QUOTE) { + /* end of argument (main closing quote-char) */ Tcl_DStringAppend(&ds, "\"", 1); } } Tcl_DStringFree(linePtr); Tcl_WinUtfToTChar(Tcl_DStringValue(&ds), Tcl_DStringLength(&ds), linePtr);