Skip to content

Instantly share code, notes, and snippets.

@MilesCranmer
Created May 28, 2025 18:36
Show Gist options
  • Save MilesCranmer/0b530cf4602905d548acdfb3bb54ded0 to your computer and use it in GitHub Desktop.
Save MilesCranmer/0b530cf4602905d548acdfb3bb54ded0 to your computer and use it in GitHub Desktop.
Julia startup file with expanded shell mode compatibility
# ~/.julia/config/startup.jl
### REPL CUSTOMIZATION
isinteractive() && VERSION < v"1.13.0-" && @eval begin
"""
ShellSpecification{is_windows, shell}
A type used for dispatch to select the appropriate shell command preparation logic.
It is parameterized by `is_windows::Bool` indicating the operating system,
and `shell::Symbol` representing the basename of the shell executable.
"""
struct ShellSpecification{is_windows,shell} end
"""
prepare_shell_command(spec::ShellSpecification, cmd::Cmd) -> Cmd
prepare_shell_command(spec::ShellSpecification, raw_string::String) -> Cmd
Returns a `Cmd` object configured for execution according to `spec`,
using the provided `cmd` (parsed command) or `raw_string` (original input).
Specialized methods for `ShellSpecification` define shell- and OS-specific behavior.
Define `Base.needs_cmd(::ShellSpecification)` to `false` for shells that do not require a `Cmd` as input.
They will then be passed the raw string instead.
"""
function prepare_shell_command(::ShellSpecification{true,shell}, cmd) where {shell}
return cmd
end
function prepare_shell_command(::ShellSpecification{false,shell}, cmd) where {shell}
shell_escape_cmd = "$(Base.shell_escape_posixly(cmd)) && true"
return `$shell -c $shell_escape_cmd`
end
function prepare_shell_command(::ShellSpecification{false,:fish}, cmd)
shell_escape_cmd = "begin; $(Base.shell_escape_posixly(cmd)); and true; end"
return `fish -c $shell_escape_cmd`
end
function prepare_shell_command(::ShellSpecification{false,:nu}, raw_string)
return `nu -c $raw_string`
end
function prepare_shell_command(::ShellSpecification{true,:cmd}, cmd)
return Cmd(`cmd /s /c $(string('"', cmd, '"'))`; windows_verbatim=true)
end
function prepare_shell_command(::ShellSpecification{true,:powershell}, cmd)
return `powershell -Command $cmd`
end
function prepare_shell_command(::ShellSpecification{true,:pwsh}, cmd)
return `pwsh -Command $cmd`
end
function prepare_shell_command(::ShellSpecification{true,:busybox}, cmd)
return `busybox sh -c $(shell_escape_posixly(cmd))`
end
"""
needs_cmd(::ShellSpecification) -> Bool
This trait is used to determine if the shell specification requires `Cmd` as input.
Setting this to `false` for a shell can help avoid specific parsing errors.
"""
needs_cmd(::ShellSpecification) = true
needs_cmd(::ShellSpecification{false,:nu}) = false
"""
is_cd_cmd(::ShellSpecification, cmd::Cmd) -> Bool
is_cd_cmd(::ShellSpecification, cmd::String) -> Bool
Determines if a command is a `cd` command. Overload this for
shells that have a different syntax for `cd`.
"""
is_cd_cmd(::ShellSpecification, cmd::Cmd) = cmd.exec[1] == "cd"
is_cd_cmd(::ShellSpecification{true,:cmd}, cmd::Cmd) = cmd.exec[1] in ("cd", "chdir")
is_cd_cmd(::ShellSpecification{true,:busybox}, cmd::Cmd) = cmd.exec[1] == "cd"
is_cd_cmd(::ShellSpecification{true,:powershell}, cmd::Cmd) = cmd.exec[1] ∈ ("Set-Location", "cd", "sl", "chdir")
is_cd_cmd(::ShellSpecification{true,:pwsh}, cmd::Cmd) = cmd.exec[1] ∈ ("Set-Location", "cd", "sl", "chdir")
is_cd_cmd(::ShellSpecification, cmd::String) = false # Safe default
function is_cd_cmd(::ShellSpecification{false,:nu}, raw_string::String)
startswith(strip(raw_string), "cd")
end
# function is_cd_cmd(::ShellSpecification{true,:powershell}, raw_string::String)
# any(prefix -> startswith(raw_string, prefix), ("Set-Location", "cd", "sl", "chdir"))
# end
# function is_cd_cmd(::ShellSpecification{true,:pwsh}, raw_string::String)
# any(prefix -> startswith(raw_string, prefix), ("Set-Location", "cd", "sl", "chdir"))
# end
function pre_repl_cmd(raw_string, parsed, out)
is_windows = Sys.iswindows()
shell_path = Base.shell_split(get(ENV, "JULIA_SHELL", is_windows ? "cmd" : get(ENV, "SHELL", "/bin/sh")))
shell_name = Base.basename(shell_path[1])
normalized_shell_name = is_windows ? lowercase(splitext(shell_name)[1]) : shell_name
shell_spec = ShellSpecification{is_windows,Symbol(normalized_shell_name)}()
if needs_cmd(shell_spec)
cmd = Base.cmd_gen(parsed)
return repl_cmd(shell_spec, cmd, parsed, out)
else
return repl_cmd(shell_spec, raw_string, parsed, out)
end
end
function repl_cmd(shell_spec, cmd::Cmd, parsed, out)
cmd.exec .= expanduser.(cmd.exec)
if isempty(cmd.exec)
throw(ArgumentError("no cmd to execute"))
end
if is_cd_cmd(shell_spec, cmd)
return repl_cd_cmd(shell_spec, cmd, parsed, out)
end
return repl_cmd_execute(shell_spec, cmd, out)
end
function repl_cmd(shell_spec, raw_string::String, parsed, out)
if is_cd_cmd(shell_spec, raw_string)
return repl_cd_cmd(shell_spec, raw_string, parsed, out)
end
return repl_cmd_execute(shell_spec, raw_string, out)
end
function repl_cmd_execute(shell_spec, cmd_or_string, out)
prepared_cmd = prepare_shell_command(shell_spec, cmd_or_string)
try
run(ignorestatus(prepared_cmd))
catch
# Windows doesn't shell out right now (complex issue), so Julia tries to run the program itself
# Julia throws an exception if it can't find the program, but the stack trace isn't useful
lasterr = current_exceptions()
lasterr = ExceptionStack([(exception=e[1], backtrace=[]) for e in lasterr])
invokelatest(display_error, lasterr)
end
return nothing
end
"""
repl_cd_cmd(shell_spec::ShellSpecification, cmd, parsed, out)
Parses a `cd` command and executes it. Overload this for
shells that have a different syntax for `cd`.
"""
function repl_cd_cmd(::ShellSpecification, cmd, _, out)
if length(cmd.exec) > 2
throw(ArgumentError("cd method only takes one argument"))
elseif length(cmd.exec) == 2
dir = cmd.exec[2]
if dir == "-"
if !haskey(ENV, "OLDPWD")
error("cd: OLDPWD not set")
end
dir = ENV["OLDPWD"]
end
else
dir = homedir()
end
try
ENV["OLDPWD"] = pwd()
catch ex
ex isa IOError || rethrow()
# if current dir has been deleted, then pwd() will throw an IOError: pwd(): no such file or directory (ENOENT)
delete!(ENV, "OLDPWD")
end
cd(dir)
return println(out, pwd())
end
function repl_cd_cmd(spec::ShellSpecification{false,:nu}, _, parsed, out)
return repl_cd_cmd(spec, Base.cmd_gen(parsed), parsed, out)
end
# function repl_cd_cmd(spec::ShellSpecification{true,:powershell}, _, parsed, out)
# return repl_cd_cmd(spec, Base.cmd_gen(parsed), parsed, out)
# end
# function repl_cd_cmd(spec::ShellSpecification{true,:pwsh}, _, parsed, out)
# return repl_cd_cmd(spec, Base.cmd_gen(parsed), parsed, out)
# end
using REPL:
Prompt, SHELL_PROMPT, ShellCompletionProvider, respond,
outstream, REPL, position, edit_insert, LineEdit
@async begin
# This needs to be `@async`, because it has to run
# AFTER the REPL is initialized, and can't do it
# during `atreplinit` (since the interface gets overwritten!)
while true
if (
isdefined(Base, :active_repl) &&
isdefined(Base.active_repl, :interface) &&
isdefined(Base.active_repl.interface, :modes) &&
!isempty(Base.active_repl.interface.modes)
)
break
end
sleep(0.001)
end
repl = Base.active_repl
julia_prompt = repl.interface.modes[1]
old_shell_prompt = repl.interface.modes[2]
hascolor = true
new_shell_prompt = Prompt(
SHELL_PROMPT;
prompt_prefix=hascolor ? repl.shell_color : "",
prompt_suffix=if hascolor
(repl.envcolors ? Base.input_color : repl.input_color)
else
""
end,
repl=repl,
complete=ShellCompletionProvider(),
on_done=respond(repl, julia_prompt) do line
return Expr(
:call,
pre_repl_cmd,
line::String,
Base.shell_parse(line, false)[1],
outstream(repl),
)
end,
keymap_dict=old_shell_prompt.keymap_dict,
hist=old_shell_prompt.hist,
on_enter=old_shell_prompt.on_enter,
sticky=true,
)
repl.interface.modes[2] = new_shell_prompt
julia_on_enter = julia_prompt.on_done
julia_prompt.keymap_dict[';'] = function (s, rest...)
if isempty(s) || position(LineEdit.buffer(s)) == 0
buf = copy(LineEdit.buffer(s))
LineEdit.transition(s, new_shell_prompt) do
LineEdit.state(s, new_shell_prompt).input_buffer = buf
end
else
edit_insert(s, ';')
LineEdit.check_for_hint(s) && LineEdit.refresh_line(s)
end
end
end
end ### END REPL CUSTOMIZATION
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment