Merge pull request #830 from zielmicha/osproc

osproc: introduce poEvalCommand, poUsePath, fix remaining quoting issues.
This commit is contained in:
Andreas Rumpf 2014-02-01 16:11:09 -08:00
commit 087448d167
2 changed files with 536 additions and 534 deletions

View file

@ -2085,6 +2085,7 @@ proc execv*(a1: cstring, a2: cstringArray): cint {.importc, header: "<unistd.h>"
proc execve*(a1: cstring, a2, a3: cstringArray): cint {. proc execve*(a1: cstring, a2, a3: cstringArray): cint {.
importc, header: "<unistd.h>".} importc, header: "<unistd.h>".}
proc execvp*(a1: cstring, a2: cstringArray): cint {.importc, header: "<unistd.h>".} proc execvp*(a1: cstring, a2: cstringArray): cint {.importc, header: "<unistd.h>".}
proc execvpe*(a1: cstring, a2: cstringArray, a3: cstringArray): cint {.importc, header: "<unistd.h>".}
proc fchown*(a1: cint, a2: Tuid, a3: TGid): cint {.importc, header: "<unistd.h>".} proc fchown*(a1: cint, a2: Tuid, a3: TGid): cint {.importc, header: "<unistd.h>".}
proc fchdir*(a1: cint): cint {.importc, header: "<unistd.h>".} proc fchdir*(a1: cint): cint {.importc, header: "<unistd.h>".}
proc fdatasync*(a1: cint): cint {.importc, header: "<unistd.h>".} proc fdatasync*(a1: cint): cint {.importc, header: "<unistd.h>".}
@ -2565,5 +2566,3 @@ proc poll*(a1: ptr TPollfd, a2: Tnfds, a3: int): cint {.
proc realpath*(name, resolved: cstring): cstring {. proc realpath*(name, resolved: cstring): cstring {.
importc: "realpath", header: "<stdlib.h>".} importc: "realpath", header: "<stdlib.h>".}

View file

@ -13,7 +13,7 @@
include "system/inclrtl" include "system/inclrtl"
import import
strutils, os, strtabs, streams strutils, os, strtabs, streams, sequtils
when defined(windows): when defined(windows):
import winlean import winlean
@ -36,11 +36,17 @@ type
TProcessOption* = enum ## options that can be passed `startProcess` TProcessOption* = enum ## options that can be passed `startProcess`
poEchoCmd, ## echo the command before execution poEchoCmd, ## echo the command before execution
poUseShell, ## use the shell to execute the command; NOTE: This poUsePath, ## Asks system to search for executable using PATH environment
## often creates a security hole! ## variable.
## On Windows, this is the default.
poEvalCommand, ## Pass `command` directly to the shell, without quoting.
## Use it only if `command` comes from trused source.
poStdErrToStdOut, ## merge stdout and stderr to the stdout stream poStdErrToStdOut, ## merge stdout and stderr to the stdout stream
poParentStreams ## use the parent's streams poParentStreams ## use the parent's streams
template poUseShell*: TProcessOption {.deprecated.} = poUsePath
## Deprecated alias for poUsePath.
proc quoteShellWindows*(s: string): string {.noSideEffect, rtl, extern: "nosp$1".} = proc quoteShellWindows*(s: string): string {.noSideEffect, rtl, extern: "nosp$1".} =
## Quote s, so it can be safely passed to Windows API. ## Quote s, so it can be safely passed to Windows API.
## Based on Python's subprocess.list2cmdline ## Based on Python's subprocess.list2cmdline
@ -94,12 +100,17 @@ proc quoteShell*(s: string): string {.noSideEffect, rtl, extern: "nosp$1".} =
{.error:"quoteShell is not supported on your system".} {.error:"quoteShell is not supported on your system".}
proc execProcess*(command: string, proc execProcess*(command: string,
args: openarray[string] = [],
env: PStringTable = nil,
options: set[TProcessOption] = {poStdErrToStdOut, options: set[TProcessOption] = {poStdErrToStdOut,
poUseShell}): TaintedString {. poUsePath,
poEvalCommand}): TaintedString {.
rtl, extern: "nosp$1", rtl, extern: "nosp$1",
tags: [FExecIO, FReadIO].} tags: [FExecIO, FReadIO].}
## A convenience procedure that executes ``command`` with ``startProcess`` ## A convenience procedure that executes ``command`` with ``startProcess``
## and returns its output as a string. ## and returns its output as a string.
## WARNING: this function uses poEvalCommand by default for backward compatibility.
## Make sure to pass options explicitly.
proc execCmd*(command: string): int {.rtl, extern: "nosp$1", tags: [FExecIO].} proc execCmd*(command: string): int {.rtl, extern: "nosp$1", tags: [FExecIO].}
## Executes ``command`` and returns its error code. Standard input, output, ## Executes ``command`` and returns its error code. Standard input, output,
@ -127,16 +138,10 @@ proc startProcess*(command: string,
## but ``EOS`` is raised in case of an error. ## but ``EOS`` is raised in case of an error.
proc startCmd*(command: string, options: set[TProcessOption] = { proc startCmd*(command: string, options: set[TProcessOption] = {
poStdErrToStdOut, poUseShell}): PProcess {. poStdErrToStdOut, poUsePath}): PProcess {.
tags: [FExecIO, FReadEnv].} = tags: [FExecIO, FReadEnv], deprecated.} =
## a simpler version of `startProcess` that parses the command line into ## Deprecated - use `startProcess` directly.
## program and arguments and then calls `startProcess` with the empty string result = startProcess(command=command, options=options + {poEvalCommand})
## for `workingDir` and the nil string table for `env`.
var c = parseCmdLine(command)
var a: seq[string]
newSeq(a, c.len-1) # avoid slicing for now (still unstable)
for i in 1 .. c.len-1: a[i-1] = c[i]
result = startProcess(command=c[0], args=a, options=options)
proc close*(p: PProcess) {.rtl, extern: "nosp$1", tags: [].} proc close*(p: PProcess) {.rtl, extern: "nosp$1", tags: [].}
## When the process has finished executing, cleanup related handles ## When the process has finished executing, cleanup related handles
@ -246,7 +251,7 @@ proc countProcessors*(): int {.rtl, extern: "nosp$1".} =
proc execProcesses*(cmds: openArray[string], proc execProcesses*(cmds: openArray[string],
options = {poStdErrToStdOut, poParentStreams}, options = {poStdErrToStdOut, poParentStreams},
n = countProcessors()): int {.rtl, extern: "nosp$1", n = countProcessors()): int {.rtl, extern: "nosp$1",
tags: [FExecIO, FTime, FReadEnv].} = tags: [FExecIO, FTime, FReadEnv]} =
## executes the commands `cmds` in parallel. Creates `n` processes ## executes the commands `cmds` in parallel. Creates `n` processes
## that execute in parallel. The highest return value of all processes ## that execute in parallel. The highest return value of all processes
## is returned. ## is returned.
@ -307,13 +312,17 @@ proc select*(readfds: var seq[PProcess], timeout = 500): int
when not defined(useNimRtl): when not defined(useNimRtl):
proc execProcess(command: string, proc execProcess(command: string,
args: openarray[string] = [],
env: PStringTable = nil,
options: set[TProcessOption] = {poStdErrToStdOut, options: set[TProcessOption] = {poStdErrToStdOut,
poUseShell}): TaintedString = poUsePath,
var p = startCmd(command, options=options) poEvalCommand}): TaintedString =
var p = startProcess(command, args=args, env=env, options=options)
var outp = outputStream(p) var outp = outputStream(p)
result = TaintedString"" result = TaintedString""
var line = newStringOfCap(120).TaintedString var line = newStringOfCap(120).TaintedString
while true: while true:
# FIXME: converts CR-LF to LF.
if outp.readLine(line): if outp.readLine(line):
result.string.add(line.string) result.string.add(line.string)
result.string.add("\n") result.string.add("\n")
@ -427,8 +436,9 @@ when defined(Windows) and not defined(useNimRtl):
result.errHandle = TFileHandle(si.hStdError) result.errHandle = TFileHandle(si.hStdError)
var cmdl: cstring var cmdl: cstring
when false: # poUseShell in options: if poEvalCommand in options:
cmdl = buildCommandLine(getEnv("COMSPEC"), @["/c", command] & args) cmdl = command
assert args.len == 0
else: else:
cmdl = buildCommandLine(command, args) cmdl = buildCommandLine(command, args)
var wd: cstring = nil var wd: cstring = nil
@ -455,7 +465,6 @@ when defined(Windows) and not defined(useNimRtl):
fileClose(si.hStdError) fileClose(si.hStdError)
if e != nil: dealloc(e) if e != nil: dealloc(e)
dealloc(cmdl)
if success == 0: osError(lastError) if success == 0: osError(lastError)
# Close the handle now so anyone waiting is woken: # Close the handle now so anyone waiting is woken:
discard closeHandle(procInfo.hThread) discard closeHandle(procInfo.hThread)
@ -561,22 +570,7 @@ elif not defined(useNimRtl):
readIdx = 0 readIdx = 0
writeIdx = 1 writeIdx = 1
proc addCmdArgs(command: string, args: openArray[string]): string = proc envToCStringArray(t: PStringTable): cstringArray =
result = quoteShell(command)
for i in 0 .. high(args):
add(result, " ")
add(result, quoteShell(args[i]))
proc toCStringArray(b, a: openArray[string]): cstringArray =
result = cast[cstringArray](alloc0((a.len + b.len + 1) * sizeof(cstring)))
for i in 0..high(b):
result[i] = cast[cstring](alloc(b[i].len+1))
copyMem(result[i], cstring(b[i]), b[i].len+1)
for i in 0..high(a):
result[i+b.len] = cast[cstring](alloc(a[i].len+1))
copyMem(result[i+b.len], cstring(a[i]), a[i].len+1)
proc toCStringArray(t: PStringTable): cstringArray =
result = cast[cstringArray](alloc0((t.len + 1) * sizeof(cstring))) result = cast[cstringArray](alloc0((t.len + 1) * sizeof(cstring)))
var i = 0 var i = 0
for key, val in pairs(t): for key, val in pairs(t):
@ -610,6 +604,21 @@ elif not defined(useNimRtl):
pipe(pStderr) != 0'i32: pipe(pStderr) != 0'i32:
osError(osLastError()) osError(osLastError())
var sys_command: string
var sys_args_raw: seq[string]
if poEvalCommand in options:
sys_command = "/bin/sh"
sys_args_raw = @[sys_command, "-c", command]
assert args.len == 0
else:
sys_command = command
sys_args_raw = @[command]
for arg in args.items:
sys_args_raw.add arg
var sys_args = allocCStringArray(sys_args_raw)
finally: deallocCStringArray(sys_args)
var pid: TPid var pid: TPid
when defined(posix_spawn) and not defined(useFork): when defined(posix_spawn) and not defined(useFork):
var attr: Tposix_spawnattr var attr: Tposix_spawnattr
@ -639,27 +648,22 @@ elif not defined(useNimRtl):
if poStdErrToStdOut in options: if poStdErrToStdOut in options:
chck posix_spawn_file_actions_adddup2(fops, pStdout[writeIdx], 2) chck posix_spawn_file_actions_adddup2(fops, pStdout[writeIdx], 2)
else: else:
chck posix_spawn_file_actions_adddup2(fops, pStderr[writeIdx], 2) chck posix_spawn_file_actions_adddup2(fops, p_stderr[writeIdx], 2)
var e = if env == nil: envToCStringArray() else: toCStringArray(env) var sys_env = if env == nil: envToCStringArray() else: envToCStringArray(env)
var a: cstringArray
var res: cint var res: cint
# This is incorrect!
if workingDir.len > 0: os.setCurrentDir(workingDir) if workingDir.len > 0: os.setCurrentDir(workingDir)
if poUseShell notin options: if poUsePath in options:
a = toCStringArray([extractFilename(command)], args) res = posix_spawnp(pid, sys_command, fops, attr, sys_args, sys_env)
res = posix_spawn(pid, command, fops, attr, a, e)
else: else:
var x = addCmdArgs(command, args) res = posix_spawn(pid, sys_command, fops, attr, sys_args, sys_env)
a = toCStringArray(["sh", "-c"], [x]) deallocCStringArray(sys_env)
res = posix_spawn(pid, "/bin/sh", fops, attr, a, e)
deallocCStringArray(a)
deallocCStringArray(e)
discard posix_spawn_file_actions_destroy(fops) discard posix_spawn_file_actions_destroy(fops)
discard posix_spawnattr_destroy(attr) discard posix_spawnattr_destroy(attr)
chck res chck res
else: else:
pid = fork() pid = fork()
if pid < 0: osError(osLastError()) if pid < 0: osError(osLastError())
if pid == 0: if pid == 0:
@ -680,19 +684,18 @@ elif not defined(useNimRtl):
if setpgid(0, 0) == -1: quit("setpgid call failed: " & $strerror(errno)) if setpgid(0, 0) == -1: quit("setpgid call failed: " & $strerror(errno))
if workingDir.len > 0: os.setCurrentDir(workingDir) if workingDir.len > 0: os.setCurrentDir(workingDir)
if poUseShell notin options:
var a = toCStringArray([extractFilename(command)], args) if env == nil:
if env == nil: if poUsePath in options:
discard execv(command, a) discard execvp(sys_command, sys_args)
else: else:
discard execve(command, a, toCStringArray(env)) discard execv(sys_command, sys_args)
else: else:
var x = addCmdArgs(command, args) var c_env = envToCStringArray(env)
var a = toCStringArray(["sh", "-c"], [x]) if poUsePath in options:
if env == nil: discard execvpe(sys_command, sys_args, c_env)
discard execv("/bin/sh", a)
else: else:
discard execve("/bin/sh", a, toCStringArray(env)) discard execve(sys_command, sys_args, c_env)
# too risky to raise an exception here: # too risky to raise an exception here:
quit("execve call failed: " & $strerror(errno)) quit("execve call failed: " & $strerror(errno))
# Parent process. Copy process information. # Parent process. Copy process information.
@ -808,7 +811,7 @@ elif not defined(useNimRtl):
setLen(s, L) setLen(s, L)
proc select(readfds: var seq[PProcess], timeout = 500): int = proc select(readfds: var seq[PProcess], timeout = 500): int =
var tv: Ttimeval var tv: TTimeVal
tv.tv_sec = 0 tv.tv_sec = 0
tv.tv_usec = timeout * 1000 tv.tv_usec = timeout * 1000
@ -825,7 +828,7 @@ elif not defined(useNimRtl):
proc execCmdEx*(command: string, options: set[TProcessOption] = { proc execCmdEx*(command: string, options: set[TProcessOption] = {
poStdErrToStdOut, poUseShell}): tuple[ poStdErrToStdOut, poUsePath}): tuple[
output: TaintedString, output: TaintedString,
exitCode: int] {.tags: [FExecIO, FReadIO].} = exitCode: int] {.tags: [FExecIO, FReadIO].} =
## a convenience proc that runs the `command`, grabs all its output and ## a convenience proc that runs the `command`, grabs all its output and