launch-program.lisp - clic - Clic is an command line interactive client for gopher written in Common LISP (HTM) git clone git://bitreich.org/clic/ git://enlrupgkhuxnvlhsf6lc3fziv5h2hhfrinws65d7roiv6bfj7d652fid.onion/clic/ (DIR) Log (DIR) Files (DIR) Refs (DIR) Tags (DIR) README (DIR) LICENSE --- launch-program.lisp (33643B) --- 1 ;;;; ------------------------------------------------------------------------- 2 ;;;; launch-program - semi-portably spawn asynchronous subprocesses 3 4 (uiop/package:define-package :uiop/launch-program 5 (:use :uiop/common-lisp :uiop/package :uiop/utility 6 :uiop/pathname :uiop/os :uiop/filesystem :uiop/stream) 7 (:export 8 ;;; Escaping the command invocation madness 9 #:easy-sh-character-p #:escape-sh-token #:escape-sh-command 10 #:escape-windows-token #:escape-windows-command 11 #:escape-shell-token #:escape-shell-command 12 #:escape-token #:escape-command 13 14 ;;; launch-program 15 #:launch-program 16 #:close-streams #:process-alive-p #:terminate-process #:wait-process 17 #:process-info-error-output #:process-info-input #:process-info-output #:process-info-pid)) 18 (in-package :uiop/launch-program) 19 20 ;;;; ----- Escaping strings for the shell ----- 21 (with-upgradability () 22 (defun requires-escaping-p (token &key good-chars bad-chars) 23 "Does this token require escaping, given the specification of 24 either good chars that don't need escaping or bad chars that do need escaping, 25 as either a recognizing function or a sequence of characters." 26 (some 27 (cond 28 ((and good-chars bad-chars) 29 (parameter-error "~S: only one of good-chars and bad-chars can be provided" 30 'requires-escaping-p)) 31 ((typep good-chars 'function) 32 (complement good-chars)) 33 ((typep bad-chars 'function) 34 bad-chars) 35 ((and good-chars (typep good-chars 'sequence)) 36 #'(lambda (c) (not (find c good-chars)))) 37 ((and bad-chars (typep bad-chars 'sequence)) 38 #'(lambda (c) (find c bad-chars))) 39 (t (parameter-error "~S: no good-char criterion" 'requires-escaping-p))) 40 token)) 41 42 (defun escape-token (token &key stream quote good-chars bad-chars escaper) 43 "Call the ESCAPER function on TOKEN string if it needs escaping as per 44 REQUIRES-ESCAPING-P using GOOD-CHARS and BAD-CHARS, otherwise output TOKEN, 45 using STREAM as output (or returning result as a string if NIL)" 46 (if (requires-escaping-p token :good-chars good-chars :bad-chars bad-chars) 47 (with-output (stream) 48 (apply escaper token stream (when quote `(:quote ,quote)))) 49 (output-string token stream))) 50 51 (defun escape-windows-token-within-double-quotes (x &optional s) 52 "Escape a string token X within double-quotes 53 for use within a MS Windows command-line, outputing to S." 54 (labels ((issue (c) (princ c s)) 55 (issue-backslash (n) (loop :repeat n :do (issue #\\)))) 56 (loop 57 :initially (issue #\") :finally (issue #\") 58 :with l = (length x) :with i = 0 59 :for i+1 = (1+ i) :while (< i l) :do 60 (case (char x i) 61 ((#\") (issue-backslash 1) (issue #\") (setf i i+1)) 62 ((#\\) 63 (let* ((j (and (< i+1 l) (position-if-not 64 #'(lambda (c) (eql c #\\)) x :start i+1))) 65 (n (- (or j l) i))) 66 (cond 67 ((null j) 68 (issue-backslash (* 2 n)) (setf i l)) 69 ((and (< j l) (eql (char x j) #\")) 70 (issue-backslash (1+ (* 2 n))) (issue #\") (setf i (1+ j))) 71 (t 72 (issue-backslash n) (setf i j))))) 73 (otherwise 74 (issue (char x i)) (setf i i+1)))))) 75 76 (defun easy-windows-character-p (x) 77 "Is X an \"easy\" character that does not require quoting by the shell?" 78 (or (alphanumericp x) (find x "+-_.,@:/="))) 79 80 (defun escape-windows-token (token &optional s) 81 "Escape a string TOKEN within double-quotes if needed 82 for use within a MS Windows command-line, outputing to S." 83 (escape-token token :stream s :good-chars #'easy-windows-character-p :quote nil 84 :escaper 'escape-windows-token-within-double-quotes)) 85 86 (defun escape-sh-token-within-double-quotes (x s &key (quote t)) 87 "Escape a string TOKEN within double-quotes 88 for use within a POSIX Bourne shell, outputing to S; 89 omit the outer double-quotes if key argument :QUOTE is NIL" 90 (when quote (princ #\" s)) 91 (loop :for c :across x :do 92 (when (find c "$`\\\"") (princ #\\ s)) 93 (princ c s)) 94 (when quote (princ #\" s))) 95 96 (defun easy-sh-character-p (x) 97 "Is X an \"easy\" character that does not require quoting by the shell?" 98 (or (alphanumericp x) (find x "+-_.,%@:/="))) 99 100 (defun escape-sh-token (token &optional s) 101 "Escape a string TOKEN within double-quotes if needed 102 for use within a POSIX Bourne shell, outputing to S." 103 (escape-token token :stream s :quote #\" :good-chars #'easy-sh-character-p 104 :escaper 'escape-sh-token-within-double-quotes)) 105 106 (defun escape-shell-token (token &optional s) 107 "Escape a token for the current operating system shell" 108 (os-cond 109 ((os-unix-p) (escape-sh-token token s)) 110 ((os-windows-p) (escape-windows-token token s)))) 111 112 (defun escape-command (command &optional s 113 (escaper 'escape-shell-token)) 114 "Given a COMMAND as a list of tokens, return a string of the 115 spaced, escaped tokens, using ESCAPER to escape." 116 (etypecase command 117 (string (output-string command s)) 118 (list (with-output (s) 119 (loop :for first = t :then nil :for token :in command :do 120 (unless first (princ #\space s)) 121 (funcall escaper token s)))))) 122 123 (defun escape-windows-command (command &optional s) 124 "Escape a list of command-line arguments into a string suitable for parsing 125 by CommandLineToArgv in MS Windows" 126 ;; http://msdn.microsoft.com/en-us/library/bb776391(v=vs.85).aspx 127 ;; http://msdn.microsoft.com/en-us/library/17w5ykft(v=vs.85).aspx 128 (escape-command command s 'escape-windows-token)) 129 130 (defun escape-sh-command (command &optional s) 131 "Escape a list of command-line arguments into a string suitable for parsing 132 by /bin/sh in POSIX" 133 (escape-command command s 'escape-sh-token)) 134 135 (defun escape-shell-command (command &optional stream) 136 "Escape a command for the current operating system's shell" 137 (escape-command command stream 'escape-shell-token))) 138 139 140 (with-upgradability () 141 ;;; Internal helpers for run-program 142 (defun %normalize-io-specifier (specifier &optional role) 143 "Normalizes a portable I/O specifier for LAUNCH-PROGRAM into an implementation-dependent 144 argument to pass to the internal RUN-PROGRAM" 145 (declare (ignorable role)) 146 (typecase specifier 147 (null (or #+(or allegro lispworks) (null-device-pathname))) 148 (string (parse-native-namestring specifier)) 149 (pathname specifier) 150 (stream specifier) 151 ((eql :stream) :stream) 152 ((eql :interactive) 153 #+(or allegro lispworks) nil 154 #+clisp :terminal 155 #+(or abcl clozure cmucl ecl mkcl sbcl scl) t 156 #-(or abcl clozure cmucl ecl mkcl sbcl scl allegro lispworks clisp) 157 (not-implemented-error :interactive-output 158 "On this lisp implementation, cannot interpret ~a value of ~a" 159 specifier role)) 160 ((eql :output) 161 (cond ((eq role :error-output) 162 #+(or abcl allegro clozure cmucl ecl lispworks mkcl sbcl scl) 163 :output 164 #-(or abcl allegro clozure cmucl ecl lispworks mkcl sbcl scl) 165 (not-implemented-error :error-output-redirect 166 "Can't send ~a to ~a on this lisp implementation." 167 role specifier)) 168 (t (parameter-error "~S IO specifier invalid for ~S" specifier role)))) 169 (otherwise 170 (parameter-error "Incorrect I/O specifier ~S for ~S" 171 specifier role)))) 172 173 (defun %interactivep (input output error-output) 174 (member :interactive (list input output error-output))) 175 176 (defun %signal-to-exit-code (signum) 177 (+ 128 signum)) 178 179 (defun %code-to-status (exit-code signal-code) 180 (cond ((null exit-code) :running) 181 ((null signal-code) (values :exited exit-code)) 182 (t (values :signaled signal-code)))) 183 184 #+mkcl 185 (defun %mkcl-signal-to-number (signal) 186 (require :mk-unix) 187 (symbol-value (find-symbol signal :mk-unix))) 188 189 (defclass process-info () 190 (;; The process field is highly platform-, implementation-, and 191 ;; even version-dependent. 192 ;; Prior to LispWorks 7, the only information that 193 ;; `sys:run-shell-command` with `:wait nil` was certain to return 194 ;; is a PID (e.g. when all streams are nil), hence we stored it 195 ;; and used `sys:pid-exit-status` to obtain an exit status 196 ;; later. That is still what we do. 197 ;; From LispWorks 7 on, if `sys:run-shell-command` does not 198 ;; return a proper stream, we are instead given a dummy stream. 199 ;; We can thus always store a stream and use 200 ;; `sys:pipe-exit-status` to obtain an exit status later. 201 ;; The advantage of dealing with streams instead of PID is the 202 ;; availability of functions like `sys:pipe-kill-process`. 203 (process :initform nil) 204 (input-stream :initform nil) 205 (output-stream :initform nil) 206 (bidir-stream :initform nil) 207 (error-output-stream :initform nil) 208 ;; For backward-compatibility, to maintain the property (zerop 209 ;; exit-code) <-> success, an exit in response to a signal is 210 ;; encoded as 128+signum. 211 (exit-code :initform nil) 212 ;; If the platform allows it, distinguish exiting with a code 213 ;; >128 from exiting in response to a signal by setting this code 214 (signal-code :initform nil))) 215 216 ;;;--------------------------------------------------------------------------- 217 ;;; The following two helper functions take care of handling the IF-EXISTS and 218 ;;; IF-DOES-NOT-EXIST arguments for RUN-PROGRAM. In particular, they process the 219 ;;; :ERROR, :APPEND, and :SUPERSEDE arguments *here*, allowing the master 220 ;;; function to treat input and output files unconditionally for reading and 221 ;;; writing. 222 ;;;--------------------------------------------------------------------------- 223 224 (defun %handle-if-exists (file if-exists) 225 (when (or (stringp file) (pathnamep file)) 226 (ecase if-exists 227 ((:append :supersede :error) 228 (with-open-file (dummy file :direction :output :if-exists if-exists) 229 (declare (ignorable dummy))))))) 230 231 (defun %handle-if-does-not-exist (file if-does-not-exist) 232 (when (or (stringp file) (pathnamep file)) 233 (ecase if-does-not-exist 234 ((:create :error) 235 (with-open-file (dummy file :direction :probe 236 :if-does-not-exist if-does-not-exist) 237 (declare (ignorable dummy))))))) 238 239 (defun process-info-error-output (process-info) 240 (slot-value process-info 'error-output-stream)) 241 (defun process-info-input (process-info) 242 (or (slot-value process-info 'bidir-stream) 243 (slot-value process-info 'input-stream))) 244 (defun process-info-output (process-info) 245 (or (slot-value process-info 'bidir-stream) 246 (slot-value process-info 'output-stream))) 247 248 (defun process-info-pid (process-info) 249 (let ((process (slot-value process-info 'process))) 250 (declare (ignorable process)) 251 #+abcl (symbol-call :sys :process-pid process) 252 #+allegro process 253 #+clozure (ccl:external-process-id process) 254 #+ecl (ext:external-process-pid process) 255 #+(or cmucl scl) (ext:process-pid process) 256 #+lispworks7+ (sys:pipe-pid process) 257 #+(and lispworks (not lispworks7+)) process 258 #+mkcl (mkcl:process-id process) 259 #+sbcl (sb-ext:process-pid process) 260 #-(or abcl allegro clozure cmucl ecl mkcl lispworks sbcl scl) 261 (not-implemented-error 'process-info-pid))) 262 263 (defun %process-status (process-info) 264 (if-let (exit-code (slot-value process-info 'exit-code)) 265 (return-from %process-status 266 (if-let (signal-code (slot-value process-info 'signal-code)) 267 (values :signaled signal-code) 268 (values :exited exit-code)))) 269 #-(or allegro clozure cmucl ecl lispworks mkcl sbcl scl) 270 (not-implemented-error '%process-status) 271 (if-let (process (slot-value process-info 'process)) 272 (multiple-value-bind (status code) 273 (progn 274 #+allegro (multiple-value-bind (exit-code pid signal-code) 275 (sys:reap-os-subprocess :pid process :wait nil) 276 (assert pid) 277 (%code-to-status exit-code signal-code)) 278 #+clozure (ccl:external-process-status process) 279 #+(or cmucl scl) (let ((status (ext:process-status process))) 280 (if (member status '(:exited :signaled)) 281 ;; Calling ext:process-exit-code on 282 ;; processes that are still alive 283 ;; yields an undefined result 284 (values status (ext:process-exit-code process)) 285 status)) 286 #+ecl (ext:external-process-status process) 287 #+lispworks 288 ;; a signal is only returned on LispWorks 7+ 289 (multiple-value-bind (exit-code signal-code) 290 (symbol-call :sys 291 #+lispworks7+ :pipe-exit-status 292 #-lispworks7+ :pid-exit-status 293 process :wait nil) 294 (%code-to-status exit-code signal-code)) 295 #+mkcl (let ((status (mk-ext:process-status process))) 296 (if (eq status :exited) 297 ;; Only call mk-ext:process-exit-code when 298 ;; necessary since it leads to another waitpid() 299 (let ((code (mk-ext:process-exit-code process))) 300 (if (stringp code) 301 (values :signaled (%mkcl-signal-to-number code)) 302 (values :exited code))) 303 status)) 304 #+sbcl (let ((status (sb-ext:process-status process))) 305 (if (eq status :running) 306 :running 307 ;; sb-ext:process-exit-code can also be 308 ;; called for stopped processes to determine 309 ;; the signal that stopped them 310 (values status (sb-ext:process-exit-code process))))) 311 (case status 312 (:exited (setf (slot-value process-info 'exit-code) code)) 313 (:signaled (let ((%code (%signal-to-exit-code code))) 314 (setf (slot-value process-info 'exit-code) %code 315 (slot-value process-info 'signal-code) code)))) 316 (if code 317 (values status code) 318 status)))) 319 320 (defun process-alive-p (process-info) 321 "Check if a process has yet to exit." 322 (unless (slot-value process-info 'exit-code) 323 #+abcl (sys:process-alive-p (slot-value process-info 'process)) 324 #+(or cmucl scl) (ext:process-alive-p (slot-value process-info 'process)) 325 #+sbcl (sb-ext:process-alive-p (slot-value process-info 'process)) 326 #-(or abcl cmucl sbcl scl) (find (%process-status process-info) 327 '(:running :stopped :continued :resumed)))) 328 329 (defun wait-process (process-info) 330 "Wait for the process to terminate, if it is still running. 331 Otherwise, return immediately. An exit code (a number) will be 332 returned, with 0 indicating success, and anything else indicating 333 failure. If the process exits after receiving a signal, the exit code 334 will be the sum of 128 and the (positive) numeric signal code. A second 335 value may be returned in this case: the numeric signal code itself. 336 Any asynchronously spawned process requires this function to be run 337 before it is garbage-collected in order to free up resources that 338 might otherwise be irrevocably lost." 339 (if-let (exit-code (slot-value process-info 'exit-code)) 340 (if-let (signal-code (slot-value process-info 'signal-code)) 341 (values exit-code signal-code) 342 exit-code) 343 (let ((process (slot-value process-info 'process))) 344 #-(or abcl allegro clozure cmucl ecl lispworks mkcl sbcl scl) 345 (not-implemented-error 'wait-process) 346 (when process 347 ;; 1- wait 348 #+clozure (ccl::external-process-wait process) 349 #+(or cmucl scl) (ext:process-wait process) 350 #+sbcl (sb-ext:process-wait process) 351 ;; 2- extract result 352 (multiple-value-bind (exit-code signal-code) 353 (progn 354 #+abcl (sys:process-wait process) 355 #+allegro (multiple-value-bind (exit-code pid signal) 356 (sys:reap-os-subprocess :pid process :wait t) 357 (assert pid) 358 (values exit-code signal)) 359 #+clozure (multiple-value-bind (status code) 360 (ccl:external-process-status process) 361 (if (eq status :signaled) 362 (values nil code) 363 code)) 364 #+(or cmucl scl) (let ((status (ext:process-status process)) 365 (code (ext:process-exit-code process))) 366 (if (eq status :signaled) 367 (values nil code) 368 code)) 369 #+ecl (multiple-value-bind (status code) 370 (ext:external-process-wait process t) 371 (if (eq status :signaled) 372 (values nil code) 373 code)) 374 #+lispworks (symbol-call :sys 375 #+lispworks7+ :pipe-exit-status 376 #-lispworks7+ :pid-exit-status 377 process :wait t) 378 #+mkcl (let ((code (mkcl:join-process process))) 379 (if (stringp code) 380 (values nil (%mkcl-signal-to-number code)) 381 code)) 382 #+sbcl (let ((status (sb-ext:process-status process)) 383 (code (sb-ext:process-exit-code process))) 384 (if (eq status :signaled) 385 (values nil code) 386 code))) 387 (if signal-code 388 (let ((%exit-code (%signal-to-exit-code signal-code))) 389 (setf (slot-value process-info 'exit-code) %exit-code 390 (slot-value process-info 'signal-code) signal-code) 391 (values %exit-code signal-code)) 392 (progn (setf (slot-value process-info 'exit-code) exit-code) 393 exit-code))))))) 394 395 ;; WARNING: For signals other than SIGTERM and SIGKILL this may not 396 ;; do what you expect it to. Sending SIGSTOP to a process spawned 397 ;; via LAUNCH-PROGRAM, e.g., will stop the shell /bin/sh that is used 398 ;; to run the command (via `sh -c command`) but not the actual 399 ;; command. 400 #+os-unix 401 (defun %posix-send-signal (process-info signal) 402 #+allegro (excl.osi:kill (slot-value process-info 'process) signal) 403 #+clozure (ccl:signal-external-process (slot-value process-info 'process) 404 signal :error-if-exited nil) 405 #+(or cmucl scl) (ext:process-kill (slot-value process-info 'process) signal) 406 #+sbcl (sb-ext:process-kill (slot-value process-info 'process) signal) 407 #-(or allegro clozure cmucl sbcl scl) 408 (if-let (pid (process-info-pid process-info)) 409 (symbol-call :uiop :run-program 410 (format nil "kill -~a ~a" signal pid) :ignore-error-status t))) 411 412 ;;; this function never gets called on Windows, but the compiler cannot tell 413 ;;; that. [2016/09/25:rpg] 414 #+os-windows 415 (defun %posix-send-signal (process-info signal) 416 (declare (ignore process-info signal)) 417 (values)) 418 419 (defun terminate-process (process-info &key urgent) 420 "Cause the process to exit. To that end, the process may or may 421 not be sent a signal, which it will find harder (or even impossible) 422 to ignore if URGENT is T. On some platforms, it may also be subject to 423 race conditions." 424 (declare (ignorable urgent)) 425 #+abcl (sys:process-kill (slot-value process-info 'process)) 426 ;; On ECL, this will only work on versions later than 2016-09-06, 427 ;; but we still want to compile on earlier versions, so we use symbol-call 428 #+ecl (symbol-call :ext :terminate-process (slot-value process-info 'process) urgent) 429 #+lispworks7+ (sys:pipe-kill-process (slot-value process-info 'process)) 430 #+mkcl (mk-ext:terminate-process (slot-value process-info 'process) 431 :force urgent) 432 #-(or abcl ecl lispworks7+ mkcl) 433 (os-cond 434 ((os-unix-p) (%posix-send-signal process-info (if urgent 9 15))) 435 ((os-windows-p) (if-let (pid (process-info-pid process-info)) 436 (symbol-call :uiop :run-program 437 (format nil "taskkill ~:[~;/f ~]/pid ~a" urgent pid) 438 :ignore-error-status t))) 439 (t (not-implemented-error 'terminate-process)))) 440 441 (defun close-streams (process-info) 442 "Close any stream that the process might own. Needs to be run 443 whenever streams were requested by passing :stream to :input, :output, 444 or :error-output." 445 (dolist (stream 446 (cons (slot-value process-info 'error-output-stream) 447 (if-let (bidir-stream (slot-value process-info 'bidir-stream)) 448 (list bidir-stream) 449 (list (slot-value process-info 'input-stream) 450 (slot-value process-info 'output-stream))))) 451 (when stream (close stream)))) 452 453 (defun launch-program (command &rest keys 454 &key 455 input (if-input-does-not-exist :error) 456 output (if-output-exists :supersede) 457 error-output (if-error-output-exists :supersede) 458 (element-type #-clozure *default-stream-element-type* 459 #+clozure 'character) 460 (external-format *utf-8-external-format*) 461 directory 462 #+allegro separate-streams 463 &allow-other-keys) 464 "Launch program specified by COMMAND, 465 either a list of strings specifying a program and list of arguments, 466 or a string specifying a shell command (/bin/sh on Unix, CMD.EXE on 467 Windows) _asynchronously_. 468 469 If OUTPUT is a pathname, a string designating a pathname, or NIL (the 470 default) designating the null device, the file at that path is used as 471 output. 472 If it's :INTERACTIVE, output is inherited from the current process; 473 beware that this may be different from your *STANDARD-OUTPUT*, and 474 under SLIME will be on your *inferior-lisp* buffer. If it's T, output 475 goes to your current *STANDARD-OUTPUT* stream. If it's :STREAM, a new 476 stream will be made available that can be accessed via 477 PROCESS-INFO-OUTPUT and read from. Otherwise, OUTPUT should be a value 478 that the underlying lisp implementation knows how to handle. 479 480 IF-OUTPUT-EXISTS, which is only meaningful if OUTPUT is a string or a 481 pathname, can take the values :ERROR, :APPEND, and :SUPERSEDE (the 482 default). The meaning of these values and their effect on the case 483 where OUTPUT does not exist, is analogous to the IF-EXISTS parameter 484 to OPEN with :DIRECTION :OUTPUT. 485 486 ERROR-OUTPUT is similar to OUTPUT. T designates the *ERROR-OUTPUT*, 487 :OUTPUT means redirecting the error output to the output stream, 488 and :STREAM causes a stream to be made available via 489 PROCESS-INFO-ERROR-OUTPUT. 490 491 IF-ERROR-OUTPUT-EXISTS is similar to IF-OUTPUT-EXIST, except that it 492 affects ERROR-OUTPUT rather than OUTPUT. 493 494 INPUT is similar to OUTPUT, except that T designates the 495 *STANDARD-INPUT* and a stream requested through the :STREAM keyword 496 would be available through PROCESS-INFO-INPUT. 497 498 IF-INPUT-DOES-NOT-EXIST, which is only meaningful if INPUT is a string 499 or a pathname, can take the values :CREATE and :ERROR (the 500 default). The meaning of these values is analogous to the 501 IF-DOES-NOT-EXIST parameter to OPEN with :DIRECTION :INPUT. 502 503 ELEMENT-TYPE and EXTERNAL-FORMAT are passed on to your Lisp 504 implementation, when applicable, for creation of the output stream. 505 506 LAUNCH-PROGRAM returns a PROCESS-INFO object." 507 #-(or abcl allegro clozure cmucl ecl (and lispworks os-unix) mkcl sbcl scl) 508 (progn command keys input output error-output directory element-type external-format 509 if-input-does-not-exist if-output-exists if-error-output-exists ;; ignore 510 (not-implemented-error 'launch-program)) 511 #+allegro 512 (when (some #'(lambda (stream) 513 (and (streamp stream) 514 (not (file-stream-p stream)))) 515 (list input output error-output)) 516 (parameter-error "~S: Streams passed as I/O parameters need to be file streams on this lisp" 517 'launch-program)) 518 #+(or abcl clisp lispworks) 519 (when (some #'streamp (list input output error-output)) 520 (parameter-error "~S: I/O parameters cannot be foreign streams on this lisp" 521 'launch-program)) 522 #+clisp 523 (unless (eq error-output :interactive) 524 (parameter-error "~S: The only admissible value for ~S is ~S on this lisp" 525 'launch-program :error-output :interactive)) 526 #+ecl 527 (when (some #'(lambda (stream) 528 (and (streamp stream) 529 (not (file-or-synonym-stream-p stream)))) 530 (list input output error-output)) 531 (parameter-error "~S: Streams passed as I/O parameters need to be (synonymous with) file streams on this lisp" 532 'launch-program)) 533 #+(or abcl allegro clozure cmucl ecl (and lispworks os-unix) mkcl sbcl scl) 534 (nest 535 (progn ;; see comments for these functions 536 (%handle-if-does-not-exist input if-input-does-not-exist) 537 (%handle-if-exists output if-output-exists) 538 (%handle-if-exists error-output if-error-output-exists)) 539 #+ecl (let ((*standard-input* *stdin*) 540 (*standard-output* *stdout*) 541 (*error-output* *stderr*))) 542 (let ((process-info (make-instance 'process-info)) 543 (input (%normalize-io-specifier input :input)) 544 (output (%normalize-io-specifier output :output)) 545 (error-output (%normalize-io-specifier error-output :error-output)) 546 #+(and allegro os-windows) (interactive (%interactivep input output error-output)) 547 (command 548 (etypecase command 549 #+os-unix (string `("/bin/sh" "-c" ,command)) 550 #+os-unix (list command) 551 #+os-windows 552 (string 553 ;; NB: On other Windows implementations, this is utterly bogus 554 ;; except in the most trivial cases where no quoting is needed. 555 ;; Use at your own risk. 556 #-(or allegro clisp clozure ecl) 557 (nest 558 #+(or ecl sbcl) (unless (find-symbol* :escape-arguments #+ecl :ext #+sbcl :sb-impl nil)) 559 (parameter-error "~S doesn't support string commands on Windows on this Lisp" 560 'launch-program command)) 561 ;; NB: We add cmd /c here. Behavior without going through cmd is not well specified 562 ;; when the command contains spaces or special characters: 563 ;; IIUC, the system will use space as a separator, 564 ;; but the C++ argv-decoding libraries won't, and 565 ;; you're supposed to use an extra argument to CreateProcess to bridge the gap, 566 ;; yet neither allegro nor clisp provide access to that argument. 567 #+(or allegro clisp) (strcat "cmd /c " command) 568 ;; On ClozureCL for Windows, we assume you are using 569 ;; r15398 or later in 1.9 or later, 570 ;; so that bug 858 is fixed http://trac.clozure.com/ccl/ticket/858 571 ;; On ECL, commit 2040629 https://gitlab.com/embeddable-common-lisp/ecl/issues/304 572 ;; On SBCL, we assume the patch from fcae0fd (to be part of SBCL 1.3.13) 573 #+(or clozure ecl sbcl) (cons "cmd" (strcat "/c " command))) 574 #+os-windows 575 (list 576 #+allegro (escape-windows-command command) 577 #-allegro command))))) 578 #+(or abcl (and allegro os-unix) clozure cmucl ecl mkcl sbcl) 579 (let ((program (car command)) 580 #-allegro (arguments (cdr command)))) 581 #+(and (or ecl sbcl) os-windows) 582 (multiple-value-bind (arguments escape-arguments) 583 (if (listp arguments) 584 (values arguments t) 585 (values (list arguments) nil))) 586 #-(or allegro mkcl sbcl) (with-current-directory (directory)) 587 (multiple-value-bind 588 #+(or abcl clozure cmucl sbcl scl) (process) 589 #+allegro (in-or-io out-or-err err-or-pid pid-or-nil) 590 #+ecl (stream code process) 591 #+lispworks (io-or-pid err-or-nil #-lispworks7+ pid-or-nil) 592 #+mkcl (stream process code) 593 #.`(apply 594 #+abcl 'sys:run-program 595 #+allegro ,@'('excl:run-shell-command 596 #+os-unix (coerce (cons program command) 'vector) 597 #+os-windows command) 598 #+clozure 'ccl:run-program 599 #+(or cmucl ecl scl) 'ext:run-program 600 #+lispworks ,@'('system:run-shell-command `("/usr/bin/env" ,@command)) ; full path needed 601 #+mkcl 'mk-ext:run-program 602 #+sbcl 'sb-ext:run-program 603 #+(or abcl clozure cmucl ecl mkcl sbcl) ,@'(program arguments) 604 #+(and (or ecl sbcl) os-windows) ,@'(:escape-arguments escape-arguments) 605 :input input :if-input-does-not-exist :error 606 :output output :if-output-exists :append 607 ,(or #+(or allegro lispworks) :error-output :error) error-output 608 ,(or #+(or allegro lispworks) :if-error-output-exists :if-error-exists) :append 609 :wait nil :element-type element-type :external-format external-format 610 :allow-other-keys t 611 #+allegro ,@`(:directory directory 612 #+os-windows ,@'(:show-window (if interactive nil :hide))) 613 #+lispworks ,@'(:save-exit-status t) 614 #+mkcl ,@'(:directory (native-namestring directory)) 615 #-sbcl keys ;; on SBCL, don't pass :directory nil but remove it from the keys 616 #+sbcl ,@'(:search t (if directory keys (remove-plist-key :directory keys))))) 617 (labels ((prop (key value) (setf (slot-value process-info key) value))) 618 #+allegro 619 (cond 620 (separate-streams 621 (prop 'process pid-or-nil) 622 (when (eq input :stream) (prop 'input-stream in-or-io)) 623 (when (eq output :stream) (prop 'output-stream out-or-err)) 624 (when (eq error-output :stream) (prop 'error-stream err-or-pid))) 625 (t 626 (prop 'process err-or-pid) 627 (ecase (+ (if (eq input :stream) 1 0) (if (eq output :stream) 2 0)) 628 (0) 629 (1 (prop 'input-stream in-or-io)) 630 (2 (prop 'output-stream in-or-io)) 631 (3 (prop 'bidir-stream in-or-io))) 632 (when (eq error-output :stream) 633 (prop 'error-stream out-or-err)))) 634 #+(or abcl clozure cmucl sbcl scl) 635 (progn 636 (prop 'process process) 637 (when (eq input :stream) 638 (nest 639 (prop 'input-stream) 640 #+abcl (symbol-call :sys :process-input) 641 #+clozure (ccl:external-process-input-stream) 642 #+(or cmucl scl) (ext:process-input) 643 #+sbcl (sb-ext:process-input) 644 process)) 645 (when (eq output :stream) 646 (nest 647 (prop 'output-stream) 648 #+abcl (symbol-call :sys :process-output) 649 #+clozure (ccl:external-process-output-stream) 650 #+(or cmucl scl) (ext:process-output) 651 #+sbcl (sb-ext:process-output) 652 process)) 653 (when (eq error-output :stream) 654 (nest 655 (prop 'error-output-stream) 656 #+abcl (symbol-call :sys :process-error) 657 #+clozure (ccl:external-process-error-stream) 658 #+(or cmucl scl) (ext:process-error) 659 #+sbcl (sb-ext:process-error) 660 process))) 661 #+(or ecl mkcl) 662 (let ((mode (+ (if (eq input :stream) 1 0) (if (eq output :stream) 2 0)))) 663 code ;; ignore 664 (unless (zerop mode) 665 (prop (case mode (1 'input-stream) (2 'output-stream) (3 'bidir-stream)) stream)) 666 (prop 'process process)) 667 #+lispworks 668 ;; See also the comments on the process-info class 669 (let ((mode (+ (if (eq input :stream) 1 0) (if (eq output :stream) 2 0)))) 670 (cond 671 ((or (plusp mode) (eq error-output :stream)) 672 (prop 'process #+lispworks7+ io-or-pid #-lispworks7+ pid-or-nil) 673 (when (plusp mode) 674 (prop (ecase mode (1 'input-stream) (2 'output-stream) (3 'bidir-stream)) 675 io-or-pid)) 676 (when (eq error-output :stream) 677 (prop 'error-stream err-or-nil))) 678 ;; Prior to Lispworks 7, this returned (pid); now it 679 ;; returns (io err pid) of which we keep io. 680 (t (prop 'process io-or-pid))))) 681 process-info))) 682