|
| 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