diff --git a/collects/mzlib/process.rkt b/collects/mzlib/process.rkt index 67cbc647d8..35c713fe53 100644 --- a/collects/mzlib/process.rkt +++ b/collects/mzlib/process.rkt @@ -1,4 +1,4 @@ -#lang mzscheme +#lang racket/base (provide process process* process/ports @@ -33,15 +33,20 @@ (format "~a: don't know what shell to use for platform: " who) (system-type))])) -(define (if-stream-out p) - (cond [(or (not p) (file-stream-port? p)) p] +(define (if-stream-out who p [sym-ok? #f]) + (cond [(and sym-ok? (eq? p 'stdout)) p] + [(or (not p) (and (output-port? p) (file-stream-port? p))) p] [(output-port? p) #f] - [else (raise-type-error 'subprocess "output port" p)])) + [else (raise-type-error who + (if sym-ok? + "output port, #f, or 'stdout" + "output port or #f") + p)])) -(define (if-stream-in p) - (cond [(or (not p) (file-stream-port? p)) p] +(define (if-stream-in who p) + (cond [(or (not p) (and (input-port? p) (file-stream-port? p))) p] [(input-port? p) #f] - [else (raise-type-error 'subprocess "input port" p)])) + [else (raise-type-error who "input port or #f" p)])) (define (streamify-in cin in ready-for-break) (if (and cin (not (file-stream-port? cin))) @@ -58,22 +63,52 @@ in)) (define (streamify-out cout out) - (if (and cout (not (file-stream-port? cout))) - (thread (lambda () - (dynamic-wind - void - (lambda () (copy-port out cout)) - (lambda () (close-input-port out))))) - out)) + (if (and cout + (not (eq? cout 'stdout)) + (not (file-stream-port? cout))) + (thread (lambda () + (dynamic-wind + void + (lambda () (copy-port out cout)) + (lambda () (close-input-port out))))) + out)) + +(define (check-exe who exe) + (unless (or (path-string? exe) + (eq? exe 'exact)) + (raise-type-error who "path, string, or 'exact" exe)) + exe) + +(define (check-args who exe args) + (cond + [(eq? exe 'exact) + (unless (and (= 1 (length args)) + (string? (car args)) + (path-string? (car args))) + (raise-mismatch-error "expected a single string argument with 'exact, given: " + args))] + [else + (for ([s (in-list args)]) + (unless (or (path-string? s) + (and (bytes? s) + (for/and ([b (in-bytes s)]) (positive? b)))) + (raise-type-error who "path, string, or byte string (no with nuls)" s)))]) + args) + +(define (check-command who str) + (unless (or (string? str) + (bytes? str)) + (raise-type-error who "string or byte string" str))) ;; Old-style functions: ---------------------------------------- -(define (process*/ports cout cin cerr exe . args) +(define (do-process*/ports who cout cin cerr exe . args) (let-values ([(subp out in err) (apply subprocess - (if-stream-out cout) - (if-stream-in cin) - (if-stream-out cerr) - exe args)] + (if-stream-out who cout) + (if-stream-in who cin) + (if-stream-out who cerr #t) + (check-exe who exe) + (check-args who exe args))] [(it-ready) (make-semaphore)]) (let ([so (streamify-out cout out)] [si (streamify-in cin in (lambda (ok?) @@ -121,27 +156,32 @@ (aport se) control))))) +(define (process*/ports cout cin cerr exe . args) + (apply do-process*/ports 'process*/ports cout cin cerr exe args)) + (define (process/ports out in err str) - (apply process*/ports out in err (shell-path/args 'process/ports str))) + (apply do-process*/ports 'process/ports out in err (shell-path/args 'process/ports str))) (define (process* exe . args) - (apply process*/ports #f #f #f exe args)) + (apply do-process*/ports 'process* #f #f #f exe args)) (define (process str) - (apply process* (shell-path/args 'process str))) + (check-command 'process str) + (apply do-process*/ports 'process #f #f #f (shell-path/args 'process str))) ;; Note: these always use current ports -(define (system*/exit-code exe . args) +(define (do-system*/exit-code who exe . args) (let ([cout (current-output-port)] [cin (current-input-port)] [cerr (current-error-port)] [it-ready (make-semaphore)]) (let-values ([(subp out in err) (apply subprocess - (if-stream-out cout) - (if-stream-in cin) - (if-stream-out cerr) - exe args)]) + (if-stream-out who cout) + (if-stream-in who cin) + (if-stream-out who cerr #t) + (check-exe who exe) + (check-args who exe args))]) (let ([ot (streamify-out cout out)] [it (streamify-in cin in (lambda (ok?) (if ok? @@ -161,11 +201,16 @@ (when in (close-output-port in))) (subprocess-status subp)))) +(define (system*/exit-code exe . args) + (apply do-system*/exit-code 'system*/exit-code exe args)) + (define (system* exe . args) - (zero? (apply system*/exit-code exe args))) + (zero? (apply do-system*/exit-code 'system* exe args))) (define (system str) - (apply system* (shell-path/args 'system str))) + (check-command 'system str) + (zero? (apply do-system*/exit-code 'system (shell-path/args 'system str)))) (define (system/exit-code str) - (apply system*/exit-code (shell-path/args 'system/exit-code str))) + (check-command 'system/exit-code str) + (apply do-system*/exit-code 'system/exit-code (shell-path/args 'system/exit-code str))) diff --git a/collects/scribblings/reference/subprocess.scrbl b/collects/scribblings/reference/subprocess.scrbl index 482a499e42..d00caebb8e 100644 --- a/collects/scribblings/reference/subprocess.scrbl +++ b/collects/scribblings/reference/subprocess.scrbl @@ -6,9 +6,9 @@ @defproc*[([(subprocess [stdout (or/c (and/c output-port? file-stream-port?) #f)] [stdin (or/c (and/c input-port? file-stream-port?) #f)] - [stderr (or/c (and/c output-port? file-stream-port?) #f)] + [stderr (or/c (and/c output-port? file-stream-port?) #f 'stdout)] [command path-string?] - [arg string?] ...) + [arg (or/c path? string? bytes?)] ...) (values subprocess? (or/c (and/c input-port? file-stream-port?) #f) (or/c (and/c output-port? file-stream-port?) #f) @@ -30,8 +30,11 @@ Creates a new process in the underlying operating system to execute The @racket[command] argument is a path to a program executable, and the @racket[arg]s are command-line arguments for the program. Under -Unix and Mac OS X, command-line arguments are passed as byte strings -using the current locale's encoding (see @secref["encodings"]). +Unix and Mac OS X, command-line arguments are passed as byte strings, +and string @racket[args] are converted using the current locale's +encoding (see @secref["encodings"]). Under Windows, command-line +arguments are passed as strings, and bytes strings are converted using +UTF-8. Under Windows, the first @racket[arg] can be replaced with @indexed-racket['exact], which triggers a Windows-specific behavior: @@ -46,12 +49,15 @@ the @exnraise[exn:fail:contract]. search for ``command line parsing'' at @tt{http://msdn.microsoft.com/}.} -Unless it is @racket[#f], @racket[stdout] is used for the launched +When provided as a port, @racket[stdout] is used for the launched process's standard output, @racket[stdin] is used for the process's standard input, and @racket[stderr] is used for the process's standard error. All provided ports must be file-stream ports. Any of the ports can be @racket[#f], in which case a system pipe is created and -returned by @racket[subprocess]. For each port that is provided, no +returned by @racket[subprocess]. The @racket[stderr] argument can be +@racket['stdout], in which case the same file-stream port or system pipe +that is supplied as standard output is also used for standard error. +For each port or @racket['stdout] that is provided, no pipe is created and the corresponding returned value is @racket[#f]. The @racket[subprocess] procedure returns four values: @@ -67,7 +73,7 @@ The @racket[subprocess] procedure returns four values: @racket[#f] if @racket[stdin-input-port] was a port;} @item{an input port piped from the process's standard error, or - @racket[#f] if @racket[stderr-output-port] was a port.} + @racket[#f] if @racket[stderr-output-port] was a port or @racket['stdout].} ] @@ -311,12 +317,12 @@ real process ID).} @note-lib[racket/system] -@defproc[(system [command string?]) boolean?]{ +@defproc[(system [command (or/c string? bytes?)]) boolean?]{ Executes a Unix, Mac OS X, or Windows shell command synchronously (i.e., the call to @racket[system] does not return until the subprocess has ended). The @racket[command] argument is a string -containing no nul characters. If the command succeeds, the return +or byte string containing no nul characters. If the command succeeds, the return value is @racket[#t], @racket[#f] otherwise. See also @racket[current-subprocess-custodian-mode] and @@ -324,7 +330,7 @@ See also @racket[current-subprocess-custodian-mode] and implement @racket[system].} -@defproc*[([(system* [command path-string?] [arg string?] ...) boolean?] +@defproc*[([(system* [command path-string?] [arg (or/c path? string? bytes?)] ...) boolean?] [(system* [command path-string?] [exact 'exact] [arg string?]) boolean?])]{ Like @racket[system], except that @racket[command] is a filename that @@ -344,7 +350,7 @@ Like @racket[system], except that the result is the exit code returned by the subprocess. A @racket[0] result normally indicates success.} -@defproc*[([(system*/exit-code [command path-string?] [arg string?] ...) (integer-in 0 255)] +@defproc*[([(system*/exit-code [command path-string?] [arg (or/c path? string? bytes?)] ...) (integer-in 0 255)] [(system*/exit-code [command path-string?] [exact 'exact] [arg string?]) (integer-in 0 255)])]{ Like @racket[system*], but returns the exit code like @@ -420,7 +426,7 @@ implement @racket[process]. In particular, the @racket['interrupt] and of a single process.} -@defproc*[([(process* [command path-string?] [arg string?] ...) list?] +@defproc*[([(process* [command path-string?] [arg (or/c path? string? bytes?)] ...) list?] [(process* [command path-string?] [exact 'exact] [arg string?]) list?])]{ Like @racket[process], except that @racket[command] is a filename that @@ -431,7 +437,7 @@ replaced with @racket['exact].} @defproc[(process/ports [out (or/c #f output-port?)] [in (or/c #f input-port?)] - [error-out (or/c #f output-port?)] + [error-out (or/c #f output-port? 'stdout)] [command string?]) list?]{ @@ -439,19 +445,21 @@ Like @racket[process], except that @racket[out] is used for the process's standard output, @racket[in] is used for the process's standard input, and @racket[error-out] is used for the process's standard error. Any of the ports can be @racket[#f], in which case a -system pipe is created and returned, as in @racket[process]. For each -port that is provided, no pipe is created, and the corresponding value -in the returned list is @racket[#f].} +system pipe is created and returned, as in @racket[process]. If +@racket[error-out] is @racket['stdout], then standard error is +redirected to standard output. For each port or @racket['stdout] that +is provided, no pipe is created, and the corresponding value in the +returned list is @racket[#f].} @defproc*[([(process*/ports [out (or/c #f output-port?)] [in (or/c #f input-port?)] - [error-out (or/c #f output-port?)] + [error-out (or/c #f output-port? 'stdout)] [command path-string?] - [arg string?] ...) + [arg (or/c path? string? bytes?)] ...) list?] [(process*/ports [out (or/c #f output-port?)] [in (or/c #f input-port?)] - [error-out (or/c #f output-port?)] + [error-out (or/c #f output-port? 'stdout)] [command path-string?] [exact 'exact] [arg string?]) diff --git a/collects/tests/racket/subprocess.rktl b/collects/tests/racket/subprocess.rktl index 1d85c93f9a..e13a961b36 100644 --- a/collects/tests/racket/subprocess.rktl +++ b/collects/tests/racket/subprocess.rktl @@ -37,20 +37,43 @@ ;; Generate output to stderr as well as stdout -(let ([p (process* cat "-" "nosuchfile")]) +(define (nosuchfile-test dash nosuchfile) + (let ([p (process* cat dash nosuchfile)]) + (fprintf (cadr p) "Hello\n") + (close-output-port (cadr p)) + (test "Hello" read-line (car p)) + (test eof read-line (car p)) + (test '("nosuchfile") + regexp-match "nosuchfile" (read-line (cadddr p))) + (test eof read-line (cadddr p)) + + ((list-ref p 4) 'wait) + (test 'done-error (list-ref p 4) 'status) + + (close-input-port (car p)) + (close-input-port (cadddr p)))) +(nosuchfile-test "-" "nosuchfile") +(nosuchfile-test #"-" (string->path "nosuchfile")) +(nosuchfile-test (string->path "-") "nosuchfile") + +;; Redirect stderr to stdout: + +(let ([p (process*/ports #f #f 'stdout cat "-" "nosuchfile")]) + (test #t file-stream-port? (car p)) + (test #t file-stream-port? (cadr p)) + (test #f cadddr p) + (fprintf (cadr p) "Hello\n") (close-output-port (cadr p)) (test "Hello" read-line (car p)) - (test eof read-line (car p)) (test '("nosuchfile") - regexp-match "nosuchfile" (read-line (cadddr p))) - (test eof read-line (cadddr p)) + regexp-match "nosuchfile" (read-line (car p))) + (test eof read-line (car p)) ((list-ref p 4) 'wait) (test 'done-error (list-ref p 4) 'status) - - (close-input-port (car p)) - (close-input-port (cadddr p))) + + (close-input-port (car p))) ;; Supply file for stdout @@ -73,20 +96,23 @@ ;; Supply file for stdout & stderr, only stdout writes -(let ([f (open-output-file tmpfile #:exists 'truncate/replace)]) - (let ([p (process*/ports f #f f cat)]) - (test #f car p) - (test #f cadddr p) +(define (stderr-to-stdout-test stderr-filter) + (let ([f (open-output-file tmpfile #:exists 'truncate/replace)]) + (let ([p (process*/ports f #f (stderr-filter f) cat)]) + (test #f car p) + (test #f cadddr p) - (fprintf (cadr p) "Hello\n") - (close-output-port (cadr p)) + (fprintf (cadr p) "Hello\n") + (close-output-port (cadr p)) - ((list-ref p 4) 'wait) - (test 'done-ok (list-ref p 4) 'status) + ((list-ref p 4) 'wait) + (test 'done-ok (list-ref p 4) 'status) - (close-output-port f) - (test 6 file-size tmpfile) - (test 'Hello with-input-from-file tmpfile read))) + (close-output-port f) + (test 6 file-size tmpfile) + (test 'Hello with-input-from-file tmpfile read)))) +(stderr-to-stdout-test values) +(stderr-to-stdout-test (lambda (x) 'stdout)) ;; Supply file for stderr @@ -246,6 +272,56 @@ (test f2 f2 f2) (test f2 f f))) +;; system* ------------------------------------------------------ + +(let ([out (open-output-string)]) + (test #t 'system* + (parameterize ([current-input-port (open-input-string "Hi\n")] + [current-output-port out]) + (system* cat "-"))) + (test "Hi\n" get-output-string out)) + +(let ([out (open-output-string)]) + (test 0 'system*/exit-code + (parameterize ([current-input-port (open-input-string "Hi\n")] + [current-output-port out]) + (system*/exit-code cat "-"))) + (test "Hi\n" get-output-string out)) + +;; shells ------------------------------------------------------ + +(let ([go + (lambda (path->string) + (let ([p (process (path->string cat))]) + (fprintf (cadr p) "Hi\n") + (close-output-port (cadr p)) + (test "Hi" read-line (car p) 'any) + (close-input-port (car p)) + (close-input-port (cadddr p)) + (test 'done-ok (list-ref p 4) 'status)))]) + (go path->string) + (go path->bytes)) + +(let ([p (process/ports #f (open-input-string "Hi\n") 'stdout (path->string cat))]) + (test "Hi" read-line (car p) 'any) + (close-input-port (car p)) + (test #f cadddr p) + (test 'done-ok (list-ref p 4) 'status)) + +(let ([out (open-output-string)]) + (test #t 'system + (parameterize ([current-input-port (open-input-string "Hi\n")] + [current-output-port out]) + (system (path->string cat)))) + (test "Hi\n" get-output-string out)) + +(let ([out (open-output-string)]) + (test 0 'system + (parameterize ([current-input-port (open-input-string "Hi\n")] + [current-output-port out]) + (system/exit-code (path->string cat)))) + (test "Hi\n" get-output-string out)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; nested tests ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -310,9 +386,10 @@ (equal? (list (number->string sub-pid)) (regexp-match - (format "(?m:^~a(?=[^0-9]))" sub-pid) + (format "(?m:^ *~a(?=[^0-9]))" sub-pid) (let ([s (open-output-string)]) - (parameterize ([current-output-port s]) + (parameterize ([current-output-port s] + [current-input-port (open-input-string "")]) (system (format "ps x"))) (get-output-string s)))))]) (let ([sub-pid (read (car l))]) @@ -323,7 +400,8 @@ (test 'done-error (list-ref l 4) 'status) (test post-shutdown? running? sub-pid) (when post-shutdown? - (system (format "kill ~a" sub-pid))))))]) + (parameterize ([current-input-port (open-input-string "")]) + (system (format "kill ~a" sub-pid)))))))]) (try #t) (try #f))) diff --git a/src/racket/src/port.c b/src/racket/src/port.c index da6ade6055..38fbc345c7 100644 --- a/src/racket/src/port.c +++ b/src/racket/src/port.c @@ -7782,6 +7782,7 @@ static Scheme_Object *subprocess(int c, Scheme_Object *args[]) Scheme_Object *inport; Scheme_Object *outport; Scheme_Object *errport; + int stderr_is_stdout = 0; Scheme_Object *a[4]; Scheme_Subprocess *subproc; Scheme_Object *cust_mode; @@ -7817,7 +7818,7 @@ static Scheme_Object *subprocess(int c, Scheme_Object *args[]) mzCOPY_FILE_HANDLE(from_subprocess, 1); #endif } else - scheme_wrong_type(name, "file-stream-output-port", 0, c, args); + scheme_wrong_type(name, "file-stream-output-port or #f", 0, c, args); } else outport = NULL; @@ -7841,11 +7842,15 @@ static Scheme_Object *subprocess(int c, Scheme_Object *args[]) mzCOPY_FILE_HANDLE(to_subprocess, 0); #endif } else - scheme_wrong_type(name, "file-stream-input-port", 1, c, args); + scheme_wrong_type(name, "file-stream-input-port or #f", 1, c, args); } else inport = NULL; - if (SCHEME_TRUEP(args[2])) { + if (SCHEME_SYMBOLP(args[2]) && !SCHEME_SYM_WEIRDP(args[2]) + && !strcmp("stdout", SCHEME_SYM_VAL(args[2]))) { + errport = NULL; + stderr_is_stdout = 1; + } else if (SCHEME_TRUEP(args[2])) { errport = args[2]; if (SCHEME_OUTPUT_PORTP(errport) && SCHEME_TRUEP(scheme_file_stream_port_p(1, &errport))) { #ifdef PROCESS_FUNCTION @@ -7865,7 +7870,7 @@ static Scheme_Object *subprocess(int c, Scheme_Object *args[]) mzCOPY_FILE_HANDLE(err_subprocess, 1); #endif } else - scheme_wrong_type(name, "file-stream-output-port", 2, c, args); + scheme_wrong_type(name, "file-stream-output-port, #f, or 'stdout", 2, c, args); } else errport = NULL; @@ -7913,11 +7918,16 @@ static Scheme_Object *subprocess(int c, Scheme_Object *args[]) #endif } else { for (i = 4; i < c; i++) { - if (!SCHEME_CHAR_STRINGP(args[i]) || scheme_any_string_has_null(args[i])) - scheme_wrong_type(name, CHAR_STRING_W_NO_NULLS, i, c, args); + if (((!SCHEME_CHAR_STRINGP(args[i]) && !SCHEME_BYTE_STRINGP(args[i])) + || scheme_any_string_has_null(args[i])) + && !SCHEME_PATHP(args[i])) + scheme_wrong_type(name, "path, string, or byte string (without nuls)", i, c, args); + { Scheme_Object *bs; - bs = scheme_char_string_to_byte_string_locale(args[i]); + bs = args[i]; + if (SCHEME_CHAR_STRINGP(args[i])) + bs = scheme_char_string_to_byte_string_locale(bs); argv[i - 3] = SCHEME_BYTE_STR_VAL(bs); } } @@ -7952,7 +7962,10 @@ static Scheme_Object *subprocess(int c, Scheme_Object *args[]) if (errport) { mzCLOSE_FILE_HANDLE(err_subprocess, 1); } scheme_raise_exn(MZEXN_FAIL, "%s: pipe failed (%e)", name, errno); } - if (!errport && PIPE_FUNC(err_subprocess, 0 _EXTRA_PIPE_ARGS)) { + if (!errport && stderr_is_stdout) { + err_subprocess[0] = from_subprocess[0]; + err_subprocess[1] = from_subprocess[1]; + } else if (!errport && PIPE_FUNC(err_subprocess, 0 _EXTRA_PIPE_ARGS)) { if (!inport) { MSC_IZE(close)(to_subprocess[0]); MSC_IZE(close)(to_subprocess[1]); @@ -8113,8 +8126,10 @@ static Scheme_Object *subprocess(int c, Scheme_Object *args[]) mzCLOSE_FILE_HANDLE(from_subprocess, 1); } if (!errport) { - MSC_IZE(close)(err_subprocess[0]); - MSC_IZE(close)(err_subprocess[1]); + if (!stderr_is_stdout) { + MSC_IZE(close)(err_subprocess[0]); + MSC_IZE(close)(err_subprocess[1]); + } } else { mzCLOSE_FILE_HANDLE(err_subprocess, 1); } @@ -8139,8 +8154,10 @@ static Scheme_Object *subprocess(int c, Scheme_Object *args[]) MSC_IZE(close)(from_subprocess[1]); } if (!errport) { - MSC_IZE(close)(err_subprocess[0]); - MSC_IZE(close)(err_subprocess[1]); + if (!stderr_is_stdout) { + MSC_IZE(close)(err_subprocess[0]); + MSC_IZE(close)(err_subprocess[1]); + } } #ifdef CLOSE_ALL_FDS_AFTER_FORK @@ -8232,7 +8249,8 @@ static Scheme_Object *subprocess(int c, Scheme_Object *args[]) in = scheme_false; } if (!errport) { - mzCLOSE_PIPE_END(err_subprocess[1]); + if (!stderr_is_stdout) + mzCLOSE_PIPE_END(err_subprocess[1]); err = NULL; } else { mzCLOSE_FILE_HANDLE(err_subprocess, 1); @@ -8245,7 +8263,10 @@ static Scheme_Object *subprocess(int c, Scheme_Object *args[]) in = (in ? in : make_fd_input_port(from_subprocess[0], scheme_intern_symbol("subprocess-stdout"), 0, 0, NULL, 0)); out = (out ? out : make_fd_output_port(to_subprocess[1], scheme_intern_symbol("subprocess-stdin"), 0, 0, 0, -1)); - err = (err ? err : make_fd_input_port(err_subprocess[0], scheme_intern_symbol("subprocess-stderr"), 0, 0, NULL, 0)); + if (stderr_is_stdout) + err = scheme_false; + else + err = (err ? err : make_fd_input_port(err_subprocess[0], scheme_intern_symbol("subprocess-stderr"), 0, 0, NULL, 0)); /*--------------------------------------*/ /* Return result info */