Skip to content

Commit 12b08fc

Browse files
committed
Split Cmd out of process.jl
This change is part of #30534 (libuv disable flag), but I think it makes sense generally, since Cmd and Process are two orthogonal concepts, and I think it makes sense to have one (Cmd, a general representation of a POSIX argument list) and not the other (e.g. in cases where you want to a command to a remote, or more trivially to have the parsing macro available even when you can't spawn a process).
1 parent 710e43d commit 12b08fc

File tree

3 files changed

+385
-382
lines changed

3 files changed

+385
-382
lines changed

base/Base.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ include("asyncevent.jl")
267267
include("stream.jl")
268268
include("filesystem.jl")
269269
using .Filesystem
270+
include("cmd.jl")
270271
include("process.jl")
271272
include("grisu/grisu.jl")
272273
include("secretbuffer.jl")

base/cmd.jl

Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
# This file is a part of Julia. License is MIT: https://julialang.org/license
2+
3+
abstract type AbstractCmd end
4+
5+
# libuv process option flags
6+
const UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS = UInt8(1 << 2)
7+
const UV_PROCESS_DETACHED = UInt8(1 << 3)
8+
const UV_PROCESS_WINDOWS_HIDE = UInt8(1 << 4)
9+
10+
struct Cmd <: AbstractCmd
11+
exec::Vector{String}
12+
ignorestatus::Bool
13+
flags::UInt32 # libuv process flags
14+
env::Union{Array{String},Nothing}
15+
dir::String
16+
Cmd(exec::Vector{String}) =
17+
new(exec, false, 0x00, nothing, "")
18+
Cmd(cmd::Cmd, ignorestatus, flags, env, dir) =
19+
new(cmd.exec, ignorestatus, flags, env,
20+
dir === cmd.dir ? dir : cstr(dir))
21+
function Cmd(cmd::Cmd; ignorestatus::Bool=cmd.ignorestatus, env=cmd.env, dir::AbstractString=cmd.dir,
22+
detach::Bool = 0 != cmd.flags & UV_PROCESS_DETACHED,
23+
windows_verbatim::Bool = 0 != cmd.flags & UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS,
24+
windows_hide::Bool = 0 != cmd.flags & UV_PROCESS_WINDOWS_HIDE)
25+
flags = detach * UV_PROCESS_DETACHED |
26+
windows_verbatim * UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS |
27+
windows_hide * UV_PROCESS_WINDOWS_HIDE
28+
new(cmd.exec, ignorestatus, flags, byteenv(env),
29+
dir === cmd.dir ? dir : cstr(dir))
30+
end
31+
end
32+
33+
has_nondefault_cmd_flags(c::Cmd) =
34+
c.ignorestatus ||
35+
c.flags != 0x00 ||
36+
c.env !== nothing ||
37+
c.dir !== ""
38+
39+
"""
40+
Cmd(cmd::Cmd; ignorestatus, detach, windows_verbatim, windows_hide, env, dir)
41+
42+
Construct a new `Cmd` object, representing an external program and arguments, from `cmd`,
43+
while changing the settings of the optional keyword arguments:
44+
45+
* `ignorestatus::Bool`: If `true` (defaults to `false`), then the `Cmd` will not throw an
46+
error if the return code is nonzero.
47+
* `detach::Bool`: If `true` (defaults to `false`), then the `Cmd` will be run in a new
48+
process group, allowing it to outlive the `julia` process and not have Ctrl-C passed to
49+
it.
50+
* `windows_verbatim::Bool`: If `true` (defaults to `false`), then on Windows the `Cmd` will
51+
send a command-line string to the process with no quoting or escaping of arguments, even
52+
arguments containing spaces. (On Windows, arguments are sent to a program as a single
53+
"command-line" string, and programs are responsible for parsing it into arguments. By
54+
default, empty arguments and arguments with spaces or tabs are quoted with double quotes
55+
`"` in the command line, and `\\` or `"` are preceded by backslashes.
56+
`windows_verbatim=true` is useful for launching programs that parse their command line in
57+
nonstandard ways.) Has no effect on non-Windows systems.
58+
* `windows_hide::Bool`: If `true` (defaults to `false`), then on Windows no new console
59+
window is displayed when the `Cmd` is executed. This has no effect if a console is
60+
already open or on non-Windows systems.
61+
* `env`: Set environment variables to use when running the `Cmd`. `env` is either a
62+
dictionary mapping strings to strings, an array of strings of the form `"var=val"`, an
63+
array or tuple of `"var"=>val` pairs, or `nothing`. In order to modify (rather than
64+
replace) the existing environment, create `env` by `copy(ENV)` and then set
65+
`env["var"]=val` as desired.
66+
* `dir::AbstractString`: Specify a working directory for the command (instead
67+
of the current directory).
68+
69+
For any keywords that are not specified, the current settings from `cmd` are used. Normally,
70+
to create a `Cmd` object in the first place, one uses backticks, e.g.
71+
72+
Cmd(`echo "Hello world"`, ignorestatus=true, detach=false)
73+
"""
74+
Cmd
75+
76+
hash(x::Cmd, h::UInt) = hash(x.exec, hash(x.env, hash(x.ignorestatus, hash(x.dir, hash(x.flags, h)))))
77+
==(x::Cmd, y::Cmd) = x.exec == y.exec && x.env == y.env && x.ignorestatus == y.ignorestatus &&
78+
x.dir == y.dir && isequal(x.flags, y.flags)
79+
80+
struct OrCmds <: AbstractCmd
81+
a::AbstractCmd
82+
b::AbstractCmd
83+
OrCmds(a::AbstractCmd, b::AbstractCmd) = new(a, b)
84+
end
85+
86+
struct ErrOrCmds <: AbstractCmd
87+
a::AbstractCmd
88+
b::AbstractCmd
89+
ErrOrCmds(a::AbstractCmd, b::AbstractCmd) = new(a, b)
90+
end
91+
92+
struct AndCmds <: AbstractCmd
93+
a::AbstractCmd
94+
b::AbstractCmd
95+
AndCmds(a::AbstractCmd, b::AbstractCmd) = new(a, b)
96+
end
97+
98+
hash(x::AndCmds, h::UInt) = hash(x.a, hash(x.b, h))
99+
==(x::AndCmds, y::AndCmds) = x.a == y.a && x.b == y.b
100+
101+
shell_escape(cmd::Cmd; special::AbstractString="") =
102+
shell_escape(cmd.exec..., special=special)
103+
shell_escape_posixly(cmd::Cmd) =
104+
shell_escape_posixly(cmd.exec...)
105+
106+
function show(io::IO, cmd::Cmd)
107+
print_env = cmd.env !== nothing
108+
print_dir = !isempty(cmd.dir)
109+
(print_env || print_dir) && print(io, "setenv(")
110+
print(io, '`')
111+
join(io, map(cmd.exec) do arg
112+
replace(sprint(context=io) do io
113+
with_output_color(:underline, io) do io
114+
print_shell_word(io, arg, shell_special)
115+
end
116+
end, '`' => "\\`")
117+
end, ' ')
118+
print(io, '`')
119+
print_env && (print(io, ","); show(io, cmd.env))
120+
print_dir && (print(io, "; dir="); show(io, cmd.dir))
121+
(print_dir || print_env) && print(io, ")")
122+
nothing
123+
end
124+
125+
function show(io::IO, cmds::Union{OrCmds,ErrOrCmds})
126+
print(io, "pipeline(")
127+
show(io, cmds.a)
128+
print(io, ", ")
129+
print(io, isa(cmds, ErrOrCmds) ? "stderr=" : "stdout=")
130+
show(io, cmds.b)
131+
print(io, ")")
132+
end
133+
134+
function show(io::IO, cmds::AndCmds)
135+
show(io, cmds.a)
136+
print(io, " & ")
137+
show(io, cmds.b)
138+
end
139+
140+
const STDIN_NO = 0
141+
const STDOUT_NO = 1
142+
const STDERR_NO = 2
143+
144+
struct FileRedirect
145+
filename::String
146+
append::Bool
147+
FileRedirect(filename::AbstractString, append::Bool) = FileRedirect(convert(String, filename), append)
148+
function FileRedirect(filename::String, append::Bool)
149+
if lowercase(filename) == (@static Sys.iswindows() ? "nul" : "/dev/null")
150+
@warn "For portability use devnull instead of a file redirect" maxlog=1
151+
end
152+
return new(filename, append)
153+
end
154+
end
155+
156+
# setup_stdio ≈ cconvert
157+
# rawhandle ≈ unsafe_convert
158+
rawhandle(::DevNull) = C_NULL
159+
rawhandle(x::OS_HANDLE) = x
160+
if OS_HANDLE !== RawFD
161+
rawhandle(x::RawFD) = Libc._get_osfhandle(x)
162+
end
163+
164+
const Redirectable = Union{IO, FileRedirect, RawFD, OS_HANDLE}
165+
const StdIOSet = NTuple{3, Redirectable}
166+
167+
struct CmdRedirect <: AbstractCmd
168+
cmd::AbstractCmd
169+
handle::Redirectable
170+
stream_no::Int
171+
readable::Bool
172+
end
173+
CmdRedirect(cmd, handle, stream_no) = CmdRedirect(cmd, handle, stream_no, stream_no == STDIN_NO)
174+
175+
function show(io::IO, cr::CmdRedirect)
176+
print(io, "pipeline(")
177+
show(io, cr.cmd)
178+
print(io, ", ")
179+
if cr.stream_no == STDOUT_NO
180+
print(io, "stdout")
181+
elseif cr.stream_no == STDERR_NO
182+
print(io, "stderr")
183+
elseif cr.stream_no == STDIN_NO
184+
print(io, "stdin")
185+
else
186+
print(io, cr.stream_no)
187+
end
188+
print(io, cr.readable ? "<" : ">")
189+
show(io, cr.handle)
190+
print(io, ")")
191+
end
192+
193+
"""
194+
ignorestatus(command)
195+
196+
Mark a command object so that running it will not throw an error if the result code is non-zero.
197+
"""
198+
ignorestatus(cmd::Cmd) = Cmd(cmd, ignorestatus=true)
199+
ignorestatus(cmd::Union{OrCmds,AndCmds}) =
200+
typeof(cmd)(ignorestatus(cmd.a), ignorestatus(cmd.b))
201+
202+
"""
203+
detach(command)
204+
205+
Mark a command object so that it will be run in a new process group, allowing it to outlive the julia process, and not have Ctrl-C interrupts passed to it.
206+
"""
207+
detach(cmd::Cmd) = Cmd(cmd; detach=true)
208+
209+
# like String(s), but throw an error if s contains NUL, since
210+
# libuv requires NUL-terminated strings
211+
function cstr(s)
212+
if Base.containsnul(s)
213+
throw(ArgumentError("strings containing NUL cannot be passed to spawned processes"))
214+
end
215+
return String(s)
216+
end
217+
218+
# convert various env representations into an array of "key=val" strings
219+
byteenv(env::AbstractArray{<:AbstractString}) =
220+
String[cstr(x) for x in env]
221+
byteenv(env::AbstractDict) =
222+
String[cstr(string(k)*"="*string(v)) for (k,v) in env]
223+
byteenv(env::Nothing) = nothing
224+
byteenv(env::Union{AbstractVector{Pair{T}}, Tuple{Vararg{Pair{T}}}}) where {T<:AbstractString} =
225+
String[cstr(k*"="*string(v)) for (k,v) in env]
226+
227+
"""
228+
setenv(command::Cmd, env; dir="")
229+
230+
Set environment variables to use when running the given `command`. `env` is either a
231+
dictionary mapping strings to strings, an array of strings of the form `"var=val"`, or zero
232+
or more `"var"=>val` pair arguments. In order to modify (rather than replace) the existing
233+
environment, create `env` by `copy(ENV)` and then setting `env["var"]=val` as desired, or
234+
use `withenv`.
235+
236+
The `dir` keyword argument can be used to specify a working directory for the command.
237+
"""
238+
setenv(cmd::Cmd, env; dir="") = Cmd(cmd; env=byteenv(env), dir=dir)
239+
setenv(cmd::Cmd, env::Pair{<:AbstractString}...; dir="") =
240+
setenv(cmd, env; dir=dir)
241+
setenv(cmd::Cmd; dir="") = Cmd(cmd; dir=dir)
242+
243+
(&)(left::AbstractCmd, right::AbstractCmd) = AndCmds(left, right)
244+
redir_out(src::AbstractCmd, dest::AbstractCmd) = OrCmds(src, dest)
245+
redir_err(src::AbstractCmd, dest::AbstractCmd) = ErrOrCmds(src, dest)
246+
247+
# Stream Redirects
248+
redir_out(dest::Redirectable, src::AbstractCmd) = CmdRedirect(src, dest, STDIN_NO)
249+
redir_out(src::AbstractCmd, dest::Redirectable) = CmdRedirect(src, dest, STDOUT_NO)
250+
redir_err(src::AbstractCmd, dest::Redirectable) = CmdRedirect(src, dest, STDERR_NO)
251+
252+
# File redirects
253+
redir_out(src::AbstractCmd, dest::AbstractString) = CmdRedirect(src, FileRedirect(dest, false), STDOUT_NO)
254+
redir_out(src::AbstractString, dest::AbstractCmd) = CmdRedirect(dest, FileRedirect(src, false), STDIN_NO)
255+
redir_err(src::AbstractCmd, dest::AbstractString) = CmdRedirect(src, FileRedirect(dest, false), STDERR_NO)
256+
redir_out_append(src::AbstractCmd, dest::AbstractString) = CmdRedirect(src, FileRedirect(dest, true), STDOUT_NO)
257+
redir_err_append(src::AbstractCmd, dest::AbstractString) = CmdRedirect(src, FileRedirect(dest, true), STDERR_NO)
258+
259+
"""
260+
pipeline(command; stdin, stdout, stderr, append=false)
261+
262+
Redirect I/O to or from the given `command`. Keyword arguments specify which of the
263+
command's streams should be redirected. `append` controls whether file output appends to the
264+
file. This is a more general version of the 2-argument `pipeline` function.
265+
`pipeline(from, to)` is equivalent to `pipeline(from, stdout=to)` when `from` is a command,
266+
and to `pipeline(to, stdin=from)` when `from` is another kind of data source.
267+
268+
**Examples**:
269+
270+
```julia
271+
run(pipeline(`dothings`, stdout="out.txt", stderr="errs.txt"))
272+
run(pipeline(`update`, stdout="log.txt", append=true))
273+
```
274+
"""
275+
function pipeline(cmd::AbstractCmd; stdin=nothing, stdout=nothing, stderr=nothing, append::Bool=false)
276+
if append && stdout === nothing && stderr === nothing
277+
throw(ArgumentError("append set to true, but no output redirections specified"))
278+
end
279+
if stdin !== nothing
280+
cmd = redir_out(stdin, cmd)
281+
end
282+
if stdout !== nothing
283+
cmd = append ? redir_out_append(cmd, stdout) : redir_out(cmd, stdout)
284+
end
285+
if stderr !== nothing
286+
cmd = append ? redir_err_append(cmd, stderr) : redir_err(cmd, stderr)
287+
end
288+
return cmd
289+
end
290+
291+
pipeline(cmd::AbstractCmd, dest) = pipeline(cmd, stdout=dest)
292+
pipeline(src::Union{Redirectable,AbstractString}, cmd::AbstractCmd) = pipeline(cmd, stdin=src)
293+
294+
"""
295+
pipeline(from, to, ...)
296+
297+
Create a pipeline from a data source to a destination. The source and destination can be
298+
commands, I/O streams, strings, or results of other `pipeline` calls. At least one argument
299+
must be a command. Strings refer to filenames. When called with more than two arguments,
300+
they are chained together from left to right. For example, `pipeline(a,b,c)` is equivalent to
301+
`pipeline(pipeline(a,b),c)`. This provides a more concise way to specify multi-stage
302+
pipelines.
303+
304+
**Examples**:
305+
306+
```julia
307+
run(pipeline(`ls`, `grep xyz`))
308+
run(pipeline(`ls`, "out.txt"))
309+
run(pipeline("out.txt", `grep xyz`))
310+
```
311+
"""
312+
pipeline(a, b, c, d...) = pipeline(pipeline(a, b), c, d...)
313+
314+
315+
## implementation of `cmd` syntax ##
316+
317+
arg_gen() = String[]
318+
arg_gen(x::AbstractString) = String[cstr(x)]
319+
function arg_gen(cmd::Cmd)
320+
if has_nondefault_cmd_flags(cmd)
321+
throw(ArgumentError("Non-default environment behavior is only permitted for the first interpolant."))
322+
end
323+
cmd.exec
324+
end
325+
326+
function arg_gen(head)
327+
if isiterable(typeof(head))
328+
vals = String[]
329+
for x in head
330+
push!(vals, cstr(string(x)))
331+
end
332+
return vals
333+
else
334+
return String[cstr(string(head))]
335+
end
336+
end
337+
338+
function arg_gen(head, tail...)
339+
head = arg_gen(head)
340+
tail = arg_gen(tail...)
341+
vals = String[]
342+
for h = head, t = tail
343+
push!(vals, cstr(string(h,t)))
344+
end
345+
return vals
346+
end
347+
348+
function cmd_gen(parsed)
349+
args = String[]
350+
if length(parsed) >= 1 && isa(parsed[1], Tuple{Cmd})
351+
cmd = parsed[1][1]
352+
(ignorestatus, flags, env, dir) = (cmd.ignorestatus, cmd.flags, cmd.env, cmd.dir)
353+
append!(args, cmd.exec)
354+
for arg in tail(parsed)
355+
append!(args, arg_gen(arg...))
356+
end
357+
return Cmd(Cmd(args), ignorestatus, flags, env, dir)
358+
else
359+
for arg in parsed
360+
append!(args, arg_gen(arg...))
361+
end
362+
return Cmd(args)
363+
end
364+
end
365+
366+
"""
367+
@cmd str
368+
369+
Similar to `cmd`, generate a `Cmd` from the `str` string which represents the shell command(s) to be executed.
370+
The [`Cmd`](@ref) object can be run as a process and can outlive the spawning julia process (see `Cmd` for more).
371+
372+
# Examples
373+
```jldoctest
374+
julia> cm = @cmd " echo 1 "
375+
`echo 1`
376+
377+
julia> run(cm)
378+
1
379+
Process(`echo 1`, ProcessExited(0))
380+
```
381+
"""
382+
macro cmd(str)
383+
return :(cmd_gen($(esc(shell_parse(str, special=shell_special)[1]))))
384+
end

0 commit comments

Comments
 (0)