Skip to content

Commit

Permalink
Distributed: add C shell support (fixes #32690) (#41485)
Browse files Browse the repository at this point in the history
The Unix C Shell (csh, tcsh) remains in common use as a login shell in
some communities. It is not compatible with POSIX shells (bash, dash,
ksh, zsh, etc.) and requires different syntax and escaping. This patch
adds the function `Base.shell_escape_csh()`, a C shell equivalent of
`Base.shell_escape_posixly()`, along with a new option `:csh` for the
`shell` argument of `Distributed.addprocs()`, for use if the login
shell on worker accounts is a C shell.
  • Loading branch information
mgkuhn authored Jul 9, 2021
1 parent 2dd34c1 commit 156b0be
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 2 deletions.
2 changes: 2 additions & 0 deletions base/cmd.jl
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ shell_escape(cmd::Cmd; special::AbstractString="") =
shell_escape(cmd.exec..., special=special)
shell_escape_posixly(cmd::Cmd) =
shell_escape_posixly(cmd.exec...)
shell_escape_csh(cmd::Cmd) =
shell_escape_csh(cmd.exec...)
escape_microsoft_c_args(cmd::Cmd) =
escape_microsoft_c_args(cmd.exec...)
escape_microsoft_c_args(io::IO, cmd::Cmd) =
Expand Down
47 changes: 47 additions & 0 deletions base/shell.jl
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,53 @@ julia> Base.shell_escape_posixly("echo", "this", "&&", "that")
shell_escape_posixly(args::AbstractString...) =
sprint(print_shell_escaped_posixly, args...)

"""
shell_escape_csh(args::Union{Cmd,AbstractString...})
shell_escape_csh(io::IO, args::Union{Cmd,AbstractString...})
This function quotes any metacharacters in the string arguments such
that the string returned can be inserted into a command-line for
interpretation by the Unix C shell (csh, tcsh), where each string
argument will form one word.
In contrast to a POSIX shell, csh does not support the use of the
backslash as a general escape character in double-quoted strings.
Therefore, this function wraps strings that might contain
metacharacters in single quotes, except for parts that contain single
quotes, which it wraps in double quotes instead. It switches between
these types of quotes as needed. Linefeed characters are escaped with
a backslash.
This function should also work for a POSIX shell, except if the input
string contains a linefeed (`"\\n"`) character.
See also: [`shell_escape_posixly`](@ref)
"""
function shell_escape_csh(io::IO, args::AbstractString...)
first = true
for arg in args
first || write(io, ' ')
first = false
i = 1
while true
for (r,e) = (r"^[A-Za-z0-9/\._-]+\z" => "",
r"^[^']*\z" => "'", r"^[^\$\`\"]*\z" => "\"",
r"^[^']+" => "'", r"^[^\$\`\"]+" => "\"")
if ((m = match(r, SubString(arg, i))) !== nothing)
write(io, e)
write(io, replace(m.match, '\n' => "\\\n"))
write(io, e)
i += ncodeunits(m.match)
break
end
end
i <= lastindex(arg) || break
end
end
end
shell_escape_csh(args::AbstractString...) =
sprint(shell_escape_csh, args...;
sizehint = sum(sizeof.(args)) + length(args) * 3)

"""
shell_escape_wincmd(s::AbstractString)
Expand Down
3 changes: 2 additions & 1 deletion stdlib/Distributed/src/Distributed.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import Base: getindex, wait, put!, take!, fetch, isready, push!, length,
using Base: Process, Semaphore, JLOptions, buffer_writes, @sync_add,
VERSION_STRING, binding_module, atexit, julia_exename,
julia_cmd, AsyncGenerator, acquire, release, invokelatest,
shell_escape_posixly, shell_escape_wincmd, escape_microsoft_c_args,
shell_escape_posixly, shell_escape_csh,
shell_escape_wincmd, escape_microsoft_c_args,
uv_error, something, notnothing, isbuffered, mapany
using Base.Threads: Event

Expand Down
21 changes: 20 additions & 1 deletion stdlib/Distributed/src/managers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ Keyword arguments:
* `shell`: specifies the type of shell to which ssh connects on the workers.
+ `shell=:posix`: a POSIX-compatible Unix/Linux shell (bash, sh, etc.). The default.
+ `shell=:posix`: a POSIX-compatible Unix/Linux shell
(sh, ksh, bash, dash, zsh, etc.). The default.
+ `shell=:csh`: a Unix C shell (csh, tcsh).
+ `shell=:wincmd`: Microsoft Windows `cmd.exe`.
Expand Down Expand Up @@ -281,6 +284,22 @@ function launch_on_machine(manager::SSHManager, machine::AbstractString, cnt, pa
# shell login (-l) with string command (-c) to launch julia process
remotecmd = shell_escape_posixly(`sh -l -c $cmds`)

elseif shell == :csh
# ssh connects to (t)csh

remotecmd = "$(shell_escape_csh(exename)) $(shell_escape_csh(exeflags))"

# set environment variables
for (var, val) in env
occursin(r"^[a-zA-Z_][a-zA-Z_0-9]*\z", var) ||
throw(ArgumentError("invalid env key $var"))
remotecmd = "setenv $(var) $(shell_escape_csh(val))\n$remotecmd"
end
# change working directory
if dir !== nothing && dir != ""
remotecmd = "cd $(shell_escape_csh(dir))\n$remotecmd"
end

elseif shell == :wincmd
# ssh connects to Windows cmd.exe

Expand Down
14 changes: 14 additions & 0 deletions test/spawn.jl
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,20 @@ if Sys.iswindows()
end


# test (t)csh escaping if tcsh is installed
cshcmd = "/bin/tcsh"
if isfile(cshcmd)
csh_echo(s) = chop(read(Cmd([cshcmd, "-c",
"echo " * Base.shell_escape_csh(s)]), String))
csh_test(s) = csh_echo(s) == s
@testset "shell_escape_csh" begin
for s in ["", "-a/b", "'", "\"", join(' ':'~') ^ 2,
"\t", "\n", "'\n", "\"\n", "'\n\n\""]
@test csh_test(s)
end
end
end

@testset "shell escaping on Windows" begin
# Note argument A can be parsed both as A or "A".
# We do not test that the parsing satisfies either of these conditions.
Expand Down

0 comments on commit 156b0be

Please sign in to comment.