From 1ed87223beaa109660fc78e3d7fc300207c9d10a Mon Sep 17 00:00:00 2001 From: sandyspiers Date: Wed, 14 May 2025 12:12:55 +0800 Subject: [PATCH 01/13] add support for nushell --- base/client.jl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/base/client.jl b/base/client.jl index 5bf658bead437..2279bd69b2067 100644 --- a/base/client.jl +++ b/base/client.jl @@ -64,7 +64,13 @@ function repl_cmd(cmd, out) cd(dir) println(out, pwd()) else - @static if !Sys.iswindows() + iswindows = @static Sys.iswindows() ? true : false + if shell_name == "nu" + # remove backticks and apostrophes that dont play nice with nushell + shell_escape_cmd = replace(shell_escape(cmd), r"`|'" => "") + shell_escape_cmd = "try { $shell_escape_cmd } catch { |err| \$err.rendered }" + cmd = `$shell -c $shell_escape_cmd` + elseif !iswindows if shell_name == "fish" shell_escape_cmd = "begin; $(shell_escape_posixly(cmd)); and true; end" else From df360887cff0e2a7475faadd6b0f2bdf944eb9ca Mon Sep 17 00:00:00 2001 From: Sandy Spiers <86579677+sandyspiers@users.noreply.github.com> Date: Wed, 14 May 2025 20:38:00 +0800 Subject: [PATCH 02/13] remove regex and @static macro Replace regex with explicit replace. Remove @static macro and use `Sys.iswindows()` instead Co-authored-by: Jakob Nybo Nissen --- base/client.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/base/client.jl b/base/client.jl index 2279bd69b2067..6409d06359c6d 100644 --- a/base/client.jl +++ b/base/client.jl @@ -64,13 +64,12 @@ function repl_cmd(cmd, out) cd(dir) println(out, pwd()) else - iswindows = @static Sys.iswindows() ? true : false if shell_name == "nu" # remove backticks and apostrophes that dont play nice with nushell - shell_escape_cmd = replace(shell_escape(cmd), r"`|'" => "") + shell_escape_cmd = replace(shell_escape(cmd), "'" => "", "`" => "") shell_escape_cmd = "try { $shell_escape_cmd } catch { |err| \$err.rendered }" cmd = `$shell -c $shell_escape_cmd` - elseif !iswindows + elseif !Sys.iswindows() if shell_name == "fish" shell_escape_cmd = "begin; $(shell_escape_posixly(cmd)); and true; end" else From 39ffeeb763fd07bb61541d460b93f3bb0374a46f Mon Sep 17 00:00:00 2001 From: sandyspiers Date: Fri, 16 May 2025 09:53:15 +0800 Subject: [PATCH 03/13] no need to remove backticks --- base/client.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/client.jl b/base/client.jl index 6409d06359c6d..7bcab0c4c0d82 100644 --- a/base/client.jl +++ b/base/client.jl @@ -65,8 +65,8 @@ function repl_cmd(cmd, out) println(out, pwd()) else if shell_name == "nu" - # remove backticks and apostrophes that dont play nice with nushell - shell_escape_cmd = replace(shell_escape(cmd), "'" => "", "`" => "") + # remove apostrophes that dont play nice with nushell + shell_escape_cmd = replace(shell_escape(cmd), "'" => "") shell_escape_cmd = "try { $shell_escape_cmd } catch { |err| \$err.rendered }" cmd = `$shell -c $shell_escape_cmd` elseif !Sys.iswindows() From 251dc132a1c4ee919c8408108396c04a4e9be36c Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 25 May 2025 12:24:33 +0100 Subject: [PATCH 04/13] modularize interface for shell commands and add nushell --- base/client.jl | 52 +++++++++++++++++++++++++++++------------ stdlib/REPL/src/REPL.jl | 1 + 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/base/client.jl b/base/client.jl index 7bcab0c4c0d82..15346e70413ad 100644 --- a/base/client.jl +++ b/base/client.jl @@ -31,7 +31,38 @@ answer_color() = text_colors[repl_color("JULIA_ANSWER_COLOR", default_color_answ stackframe_lineinfo_color() = repl_color("JULIA_STACKFRAME_LINEINFO_COLOR", :bold) stackframe_function_color() = repl_color("JULIA_STACKFRAME_FUNCTION_COLOR", :bold) -function repl_cmd(cmd, out) +""" + ShellSpecification{is_windows, shell_sym} + +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_sym::Symbol` representing the basename of the shell executable. +""" +struct ShellSpecification{is_windows,shell} end + +""" + prepare_shell_command(spec::ShellSpecification, cmd::Cmd, raw_string::String) -> Cmd + +Returns a `Cmd` object configured for execution according to `spec`, +using the provided `cmd` (parsed command) and `raw_string` (original input). +Specialized methods for `ShellSpecification` define shell- and OS-specific behavior. +""" +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 = "$(shell_escape_posixly(cmd)) && true" + return `$SHELL -c $shell_escape_cmd` +end +function prepare_shell_command(::ShellSpecification{false,:fish}, cmd, _) + shell_escape_cmd = "begin; $(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 repl_cmd(cmd, raw_string, out) shell = shell_split(get(ENV, "JULIA_SHELL", get(ENV, "SHELL", "/bin/sh"))) shell_name = Base.basename(shell[1]) @@ -64,21 +95,10 @@ function repl_cmd(cmd, out) cd(dir) println(out, pwd()) else - if shell_name == "nu" - # remove apostrophes that dont play nice with nushell - shell_escape_cmd = replace(shell_escape(cmd), "'" => "") - shell_escape_cmd = "try { $shell_escape_cmd } catch { |err| \$err.rendered }" - cmd = `$shell -c $shell_escape_cmd` - elseif !Sys.iswindows() - if shell_name == "fish" - shell_escape_cmd = "begin; $(shell_escape_posixly(cmd)); and true; end" - else - shell_escape_cmd = "($(shell_escape_posixly(cmd))) && true" - end - cmd = `$shell -c $shell_escape_cmd` - end + shell_spec = ShellSpecification{Sys.iswindows(),Symbol(shell_name)}() + prepared_cmd = prepare_shell_command(shell_spec, cmd, raw_string) try - run(ignorestatus(cmd)) + 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 @@ -90,6 +110,8 @@ function repl_cmd(cmd, out) nothing end +@deprecate repl_cmd(cmd, out) repl_cmd(cmd, string(cmd), out) + # deprecated function--preserved for DocTests.jl function ip_matches_func(ip, func::Symbol) for fr in StackTraces.lookup(ip) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index 81272ac971d40..af52c9b94038e 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -1335,6 +1335,7 @@ function setup_interface( on_done = respond(repl, julia_prompt) do line Expr(:call, :(Base.repl_cmd), :(Base.cmd_gen($(Base.shell_parse(line::String)[1]))), + line::String, outstream(repl)) end, sticky = true) From aebe20b3e512beb9a40bdca049d39bc7e674d89b Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 25 May 2025 12:24:51 +0100 Subject: [PATCH 05/13] update tests for new `repl_cmd` --- test/file.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/file.jl b/test/file.jl index a163bc07034ab..c91a26225847b 100644 --- a/test/file.jl +++ b/test/file.jl @@ -1925,15 +1925,15 @@ end cd(dir) do withenv("OLDPWD" => nothing) do io = IOBuffer() - Base.repl_cmd(@cmd("cd"), io) - Base.repl_cmd(@cmd("cd -"), io) + Base.repl_cmd(@cmd("cd"), "cd", io) + Base.repl_cmd(@cmd("cd -"), "cd -", io) @test realpath(pwd()) == realpath(dir) if !Sys.iswindows() # Delete the working directory and check we can cd out of it # Cannot delete the working directory on Windows rm(dir) @test_throws Base._UVError("pwd()", Base.UV_ENOENT) pwd() - Base.repl_cmd(@cmd("cd \\~"), io) + Base.repl_cmd(@cmd("cd \\~"), "cd \\~", io) end end end From e6cfe05c5c30fd433affc174976847f5002ecbde Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 25 May 2025 12:36:15 +0100 Subject: [PATCH 06/13] avoid `@deprecate` macro --- base/client.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/base/client.jl b/base/client.jl index 15346e70413ad..373e973216cd7 100644 --- a/base/client.jl +++ b/base/client.jl @@ -110,7 +110,8 @@ function repl_cmd(cmd, raw_string, out) nothing end -@deprecate repl_cmd(cmd, out) repl_cmd(cmd, string(cmd), out) +# For backward compatibility +repl_cmd(cmd, out) = repl_cmd(cmd, string(cmd), out) # deprecated function--preserved for DocTests.jl function ip_matches_func(ip, func::Symbol) From 1388f83a02c9fd2f4a590e7311d16f5291dbe053 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 25 May 2025 12:58:21 +0100 Subject: [PATCH 07/13] lower case type parameter --- base/client.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/base/client.jl b/base/client.jl index 373e973216cd7..54f5313d513ea 100644 --- a/base/client.jl +++ b/base/client.jl @@ -32,11 +32,11 @@ stackframe_lineinfo_color() = repl_color("JULIA_STACKFRAME_LINEINFO_COLOR", :bol stackframe_function_color() = repl_color("JULIA_STACKFRAME_FUNCTION_COLOR", :bold) """ - ShellSpecification{is_windows, shell_sym} + 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_sym::Symbol` representing the basename of the shell executable. +and `shell::Symbol` representing the basename of the shell executable. """ struct ShellSpecification{is_windows,shell} end @@ -47,12 +47,12 @@ Returns a `Cmd` object configured for execution according to `spec`, using the provided `cmd` (parsed command) and `raw_string` (original input). Specialized methods for `ShellSpecification` define shell- and OS-specific behavior. """ -function prepare_shell_command(::ShellSpecification{true,SHELL}, cmd, _) where {SHELL} +function prepare_shell_command(::ShellSpecification{true,shell}, cmd, _) where {shell} return cmd end -function prepare_shell_command(::ShellSpecification{false,SHELL}, cmd, _) where {SHELL} +function prepare_shell_command(::ShellSpecification{false,shell}, cmd, _) where {shell} shell_escape_cmd = "$(shell_escape_posixly(cmd)) && true" - return `$SHELL -c $shell_escape_cmd` + return `$shell -c $shell_escape_cmd` end function prepare_shell_command(::ShellSpecification{false,:fish}, cmd, _) shell_escape_cmd = "begin; $(shell_escape_posixly(cmd)); and true; end" From 9d27caddf7fe7607df7a84c4d4d817be6ad5b554 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 25 May 2025 18:08:44 +0100 Subject: [PATCH 08/13] avoid cmd parsing unless needed; fix cd in nushell --- base/client.jl | 141 +++++++++++++++++++++++++++------------- stdlib/REPL/src/REPL.jl | 3 +- test/file.jl | 6 +- 3 files changed, 99 insertions(+), 51 deletions(-) diff --git a/base/client.jl b/base/client.jl index 54f5313d513ea..cc22f7555c162 100644 --- a/base/client.jl +++ b/base/client.jl @@ -41,77 +41,126 @@ and `shell::Symbol` representing the basename of the shell executable. struct ShellSpecification{is_windows,shell} end """ - prepare_shell_command(spec::ShellSpecification, cmd::Cmd, raw_string::String) -> Cmd + 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) and `raw_string` (original input). +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} +function prepare_shell_command(::ShellSpecification{true,shell}, cmd) where {shell} return cmd end -function prepare_shell_command(::ShellSpecification{false,shell}, cmd, _) where {shell} +function prepare_shell_command(::ShellSpecification{false,shell}, cmd) where {shell} shell_escape_cmd = "$(shell_escape_posixly(cmd)) && true" return `$shell -c $shell_escape_cmd` end -function prepare_shell_command(::ShellSpecification{false,:fish}, cmd, _) +function prepare_shell_command(::ShellSpecification{false,:fish}, cmd) shell_escape_cmd = "begin; $(shell_escape_posixly(cmd)); and true; end" return `fish -c $shell_escape_cmd` end -function prepare_shell_command(::ShellSpecification{false,:nu}, _, raw_string) +function prepare_shell_command(::ShellSpecification{false,:nu}, raw_string) return `nu -c $raw_string` end -function repl_cmd(cmd, raw_string, out) +""" + 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, cmd::String) = false +is_cd_cmd(::ShellSpecification{false,:nu}, raw_string::String) = startswith(strip(raw_string), "cd") + +function pre_repl_cmd(raw_string, out) shell = shell_split(get(ENV, "JULIA_SHELL", get(ENV, "SHELL", "/bin/sh"))) shell_name = Base.basename(shell[1]) - - # Immediately expand all arguments, so that typing e.g. ~/bin/foo works. + shell_spec = ShellSpecification{@static(Sys.iswindows() ? true : false),Symbol(shell_name)}() + if needs_cmd(shell_spec) + cmd = Base.cmd_gen(Base.shell_parse(raw_string)[1]) + return repl_cmd(cmd, shell_spec, out) + else + return repl_cmd(raw_string, shell_spec, out) + end +end +function repl_cmd(cmd::Cmd, shell_spec, out) cmd.exec .= expanduser.(cmd.exec) - if isempty(cmd.exec) throw(ArgumentError("no cmd to execute")) - elseif cmd.exec[1] == "cd" - 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 + if is_cd_cmd(shell_spec, cmd) + return repl_cd_cmd(shell_spec, cmd, out) + end + return repl_cmd_execute(cmd, shell_spec, out) +end +function repl_cmd(raw_string::String, shell_spec, out) + if is_cd_cmd(shell_spec, raw_string) + return repl_cd_cmd(shell_spec, raw_string, out) + end + return repl_cmd_execute(raw_string, shell_spec, out) +end +function repl_cmd_execute(cmd_or_string, shell_spec, 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 + nothing +end + + +""" + repl_cd_cmd(shell_spec::ShellSpecification, cmd, 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::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 - else - dir = homedir() + dir = ENV["OLDPWD"] 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) - println(out, pwd()) else - shell_spec = ShellSpecification{Sys.iswindows(),Symbol(shell_name)}() - prepared_cmd = prepare_shell_command(shell_spec, cmd, raw_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 + dir = homedir() end - nothing + 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) + println(out, pwd()) +end +function repl_cd_cmd(shell_spec::ShellSpecification{false,:nu}, raw_string::String, out) + repl_cd_cmd(shell_spec, Base.cmd_gen(Base.shell_parse(raw_string)[1]), out) end - -# For backward compatibility -repl_cmd(cmd, out) = repl_cmd(cmd, string(cmd), out) # deprecated function--preserved for DocTests.jl function ip_matches_func(ip, func::Symbol) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index af52c9b94038e..08f69f1492df0 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -1333,8 +1333,7 @@ function setup_interface( # and pass into Base.repl_cmd for processing (handles `ls` and `cd` # special) on_done = respond(repl, julia_prompt) do line - Expr(:call, :(Base.repl_cmd), - :(Base.cmd_gen($(Base.shell_parse(line::String)[1]))), + Expr(:call, :(Base.pre_repl_cmd), line::String, outstream(repl)) end, diff --git a/test/file.jl b/test/file.jl index c91a26225847b..42827ce75f30d 100644 --- a/test/file.jl +++ b/test/file.jl @@ -1925,15 +1925,15 @@ end cd(dir) do withenv("OLDPWD" => nothing) do io = IOBuffer() - Base.repl_cmd(@cmd("cd"), "cd", io) - Base.repl_cmd(@cmd("cd -"), "cd -", io) + Base.repl_cmd("cd", io) + Base.repl_cmd("cd -", io) @test realpath(pwd()) == realpath(dir) if !Sys.iswindows() # Delete the working directory and check we can cd out of it # Cannot delete the working directory on Windows rm(dir) @test_throws Base._UVError("pwd()", Base.UV_ENOENT) pwd() - Base.repl_cmd(@cmd("cd \\~"), "cd \\~", io) + Base.repl_cmd("cd \\~", io) end end end From e6fa59131952d4fe6ed6c80cff994dde0b34347a Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 25 May 2025 18:58:43 +0100 Subject: [PATCH 09/13] forward parsed object --- base/client.jl | 4 ++-- stdlib/REPL/src/REPL.jl | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/base/client.jl b/base/client.jl index cc22f7555c162..b403b173c4c14 100644 --- a/base/client.jl +++ b/base/client.jl @@ -86,12 +86,12 @@ is_cd_cmd(::ShellSpecification, cmd::Cmd) = cmd.exec[1] == "cd" is_cd_cmd(::ShellSpecification, cmd::String) = false is_cd_cmd(::ShellSpecification{false,:nu}, raw_string::String) = startswith(strip(raw_string), "cd") -function pre_repl_cmd(raw_string, out) +function pre_repl_cmd(raw_string, parsed, out) shell = shell_split(get(ENV, "JULIA_SHELL", get(ENV, "SHELL", "/bin/sh"))) shell_name = Base.basename(shell[1]) shell_spec = ShellSpecification{@static(Sys.iswindows() ? true : false),Symbol(shell_name)}() if needs_cmd(shell_spec) - cmd = Base.cmd_gen(Base.shell_parse(raw_string)[1]) + cmd = Base.cmd_gen(parsed) return repl_cmd(cmd, shell_spec, out) else return repl_cmd(raw_string, shell_spec, out) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index 08f69f1492df0..e3a76633530bf 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -1335,6 +1335,7 @@ function setup_interface( on_done = respond(repl, julia_prompt) do line Expr(:call, :(Base.pre_repl_cmd), line::String, + Base.shell_parse(line)[1], outstream(repl)) end, sticky = true) From cd28fec33821670c33250f802e9e1436ca2e9fea Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 25 May 2025 19:05:09 +0100 Subject: [PATCH 10/13] clean up --- base/client.jl | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/base/client.jl b/base/client.jl index b403b173c4c14..027f4bbb4c4d1 100644 --- a/base/client.jl +++ b/base/client.jl @@ -92,28 +92,28 @@ function pre_repl_cmd(raw_string, parsed, out) shell_spec = ShellSpecification{@static(Sys.iswindows() ? true : false),Symbol(shell_name)}() if needs_cmd(shell_spec) cmd = Base.cmd_gen(parsed) - return repl_cmd(cmd, shell_spec, out) + return repl_cmd(shell_spec, cmd, parsed, out) else - return repl_cmd(raw_string, shell_spec, out) + return repl_cmd(shell_spec, raw_string, parsed, out) end end -function repl_cmd(cmd::Cmd, shell_spec, out) +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, out) + return repl_cd_cmd(shell_spec, cmd, parsed, out) end - return repl_cmd_execute(cmd, shell_spec, out) + return repl_cmd_execute(shell_spec, cmd, out) end -function repl_cmd(raw_string::String, shell_spec, out) +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, out) + return repl_cd_cmd(shell_spec, raw_string, parsed, out) end - return repl_cmd_execute(raw_string, shell_spec, out) + return repl_cmd_execute(shell_spec, raw_string, out) end -function repl_cmd_execute(cmd_or_string, shell_spec, out) +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)) @@ -129,12 +129,12 @@ end """ - repl_cd_cmd(shell_spec::ShellSpecification, cmd, out) + 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::Cmd, out) +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 @@ -158,8 +158,8 @@ function repl_cd_cmd(::ShellSpecification, cmd::Cmd, out) cd(dir) println(out, pwd()) end -function repl_cd_cmd(shell_spec::ShellSpecification{false,:nu}, raw_string::String, out) - repl_cd_cmd(shell_spec, Base.cmd_gen(Base.shell_parse(raw_string)[1]), out) +function repl_cd_cmd(spec::ShellSpecification{false,:nu}, _, parsed, out) + repl_cd_cmd(spec, Base.cmd_gen(parsed), parsed, out) end # deprecated function--preserved for DocTests.jl From aa1bf4808ace7dcbc36b8fe72f872d81558aa047 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 25 May 2025 19:13:45 +0100 Subject: [PATCH 11/13] update tests to use pre-cmd gen function --- test/file.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/file.jl b/test/file.jl index 42827ce75f30d..5115951e32c75 100644 --- a/test/file.jl +++ b/test/file.jl @@ -1925,15 +1925,15 @@ end cd(dir) do withenv("OLDPWD" => nothing) do io = IOBuffer() - Base.repl_cmd("cd", io) - Base.repl_cmd("cd -", io) + Base.pre_repl_cmd("cd", Base.shell_parse("cd")[1], io) + Base.pre_repl_cmd("cd -", Base.shell_parse("cd -")[1], io) @test realpath(pwd()) == realpath(dir) if !Sys.iswindows() # Delete the working directory and check we can cd out of it # Cannot delete the working directory on Windows rm(dir) @test_throws Base._UVError("pwd()", Base.UV_ENOENT) pwd() - Base.repl_cmd("cd \\~", io) + Base.pre_repl_cmd("cd \\~", Base.shell_parse("cd \\~")[1], io) end end end From 1373a7002e289f8ffedf720c0d4cc9701a84a219 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 25 May 2025 19:50:38 +0100 Subject: [PATCH 12/13] add windows support for shell REPL mode --- base/client.jl | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/base/client.jl b/base/client.jl index 027f4bbb4c4d1..c319e75302047 100644 --- a/base/client.jl +++ b/base/client.jl @@ -65,6 +65,19 @@ 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 @@ -83,13 +96,19 @@ 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, cmd::String) = false +is_cd_cmd(::ShellSpecification{true,:cmd}, cmd::Cmd) = cmd.exec[1] in ("cd", "chdir") +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{true,:busybox}, cmd::Cmd) = cmd.exec[1] == "cd" +is_cd_cmd(::ShellSpecification, cmd::String) = false # Safe default is_cd_cmd(::ShellSpecification{false,:nu}, raw_string::String) = startswith(strip(raw_string), "cd") function pre_repl_cmd(raw_string, parsed, out) - shell = shell_split(get(ENV, "JULIA_SHELL", get(ENV, "SHELL", "/bin/sh"))) - shell_name = Base.basename(shell[1]) - shell_spec = ShellSpecification{@static(Sys.iswindows() ? true : false),Symbol(shell_name)}() + is_windows = Sys.iswindows() + shell_path = 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) From 1f2cca7d1fa77aba67f9679e22518cff8888d1f7 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 25 May 2025 20:31:38 +0100 Subject: [PATCH 13/13] fix test of `pre_repl_cmd` --- test/file.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/file.jl b/test/file.jl index 5115951e32c75..d8b1c110235de 100644 --- a/test/file.jl +++ b/test/file.jl @@ -1925,15 +1925,15 @@ end cd(dir) do withenv("OLDPWD" => nothing) do io = IOBuffer() - Base.pre_repl_cmd("cd", Base.shell_parse("cd")[1], io) - Base.pre_repl_cmd("cd -", Base.shell_parse("cd -")[1], io) + Base.pre_repl_cmd("cd", eval(Base.shell_parse("cd")[1]), io) + Base.pre_repl_cmd("cd -", eval(Base.shell_parse("cd -")[1]), io) @test realpath(pwd()) == realpath(dir) if !Sys.iswindows() # Delete the working directory and check we can cd out of it # Cannot delete the working directory on Windows rm(dir) @test_throws Base._UVError("pwd()", Base.UV_ENOENT) pwd() - Base.pre_repl_cmd("cd \\~", Base.shell_parse("cd \\~")[1], io) + Base.pre_repl_cmd("cd \\~", eval(Base.shell_parse("cd \\~")[1]), io) end end end