From 97e6961d6fc2928dee43b198a1dd1389d6b36527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Gnudi?= Date: Sun, 19 Oct 2025 01:06:18 +0200 Subject: [PATCH 1/3] fix: support Fish shell's space-separated PATH format Addresses https://github.com/elixir-lang/expert/issues/171 --- apps/expert/lib/expert/port.ex | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/expert/lib/expert/port.ex b/apps/expert/lib/expert/port.ex index 62f564f4..c7d3149f 100644 --- a/apps/expert/lib/expert/port.ex +++ b/apps/expert/lib/expert/port.ex @@ -41,7 +41,15 @@ defmodule Expert.Port do # managed programs. shell = System.get_env("SHELL") - {path, 0} = System.cmd(shell, ["-i", "-l", "-c", "cd #{root_path} && echo $PATH"]) + # Ideally, it should contain the path to fish shell (e.g. `/usr/bin/fish`), + # but it might contain only the name of the shell (e.g. `fish`). + is_fish? = String.contains?(shell, "fish") + + # Fish uses space-separated PATH, so we use the built-in `string join` command + # to join the entries with colons and have a standard PATH output as in bash. + path_command = if is_fish?, do: "string join ':' $PATH", else: "echo $PATH" + + {path, 0} = System.cmd(shell, ["-i", "-l", "-c", "cd #{root_path} && #{path_command}"]) case :os.find_executable(~c"elixir", to_charlist(path)) do false -> From bce3abb9b68dd6b81d7ed24a41f6bbec5c03c369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Gnudi?= Date: Sun, 19 Oct 2025 01:21:45 +0200 Subject: [PATCH 2/3] chore: improve comments --- apps/expert/lib/expert/port.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/expert/lib/expert/port.ex b/apps/expert/lib/expert/port.ex index c7d3149f..62bdc7f1 100644 --- a/apps/expert/lib/expert/port.ex +++ b/apps/expert/lib/expert/port.ex @@ -41,12 +41,13 @@ defmodule Expert.Port do # managed programs. shell = System.get_env("SHELL") - # Ideally, it should contain the path to fish shell (e.g. `/usr/bin/fish`), + # Ideally, it should contain the path to shell (e.g. `/usr/bin/fish`), # but it might contain only the name of the shell (e.g. `fish`). is_fish? = String.contains?(shell, "fish") # Fish uses space-separated PATH, so we use the built-in `string join` command - # to join the entries with colons and have a standard PATH output as in bash. + # to join the entries with colons and have a standard colon-separated PATH output + # as in bash, which is expected by `:os.find_executable/2`. path_command = if is_fish?, do: "string join ':' $PATH", else: "echo $PATH" {path, 0} = System.cmd(shell, ["-i", "-l", "-c", "cd #{root_path} && #{path_command}"]) From 167d9ea252279c5654becce14dee1e4f601d6e98 Mon Sep 17 00:00:00 2001 From: doorgan Date: Mon, 20 Oct 2025 15:08:45 -0300 Subject: [PATCH 3/3] refactor: move path finding to private function --- apps/expert/lib/expert/port.ex | 42 +++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/apps/expert/lib/expert/port.ex b/apps/expert/lib/expert/port.ex index 62bdc7f1..877339a7 100644 --- a/apps/expert/lib/expert/port.ex +++ b/apps/expert/lib/expert/port.ex @@ -34,23 +34,8 @@ defmodule Expert.Port do def elixir_executable(%Project{} = project) do root_path = Project.root_path(project) - # We run a shell in interactive mode to populate the PATH with the right value - # at the project root. Otherwise, we either can't find an elixir executable, - # we use the wrong version if the user uses a version manager like asdf/mise, - # or we get an incomplete PATH not including erl or any other version manager - # managed programs. shell = System.get_env("SHELL") - - # Ideally, it should contain the path to shell (e.g. `/usr/bin/fish`), - # but it might contain only the name of the shell (e.g. `fish`). - is_fish? = String.contains?(shell, "fish") - - # Fish uses space-separated PATH, so we use the built-in `string join` command - # to join the entries with colons and have a standard colon-separated PATH output - # as in bash, which is expected by `:os.find_executable/2`. - path_command = if is_fish?, do: "string join ':' $PATH", else: "echo $PATH" - - {path, 0} = System.cmd(shell, ["-i", "-l", "-c", "cd #{root_path} && #{path_command}"]) + path = path_env_at_directory(root_path, shell) case :os.find_executable(~c"elixir", to_charlist(path)) do false -> @@ -72,6 +57,31 @@ defmodule Expert.Port do end end + defp path_env_at_directory(directory, shell) do + # We run a shell in interactive mode to populate the PATH with the right value + # at the project root. Otherwise, we either can't find an elixir executable, + # we use the wrong version if the user uses a version manager like asdf/mise, + # or we get an incomplete PATH not including erl or any other version manager + # managed programs. + + case Path.basename(shell) do + # Ideally, it should contain the path to shell (e.g. `/usr/bin/fish`), + # but it might contain only the name of the shell (e.g. `fish`). + "fish" -> + # Fish uses space-separated PATH, so we use the built-in `string join` command + # to join the entries with colons and have a standard colon-separated PATH output + # as in bash, which is expected by `:os.find_executable/2`. + {path, 0} = + System.cmd(shell, ["-i", "-l", "-c", "cd #{directory} && string join ':' $PATH"]) + + path + + _ -> + {path, 0} = System.cmd(shell, ["-i", "-l", "-c", "cd #{directory} && echo $PATH"]) + path + end + end + @doc """ Launches an executable in the project context via a port. """