Skip to content

Commit 7612567

Browse files
authored
Add --shell to mix cmd (#14827)
1 parent b727a50 commit 7612567

File tree

2 files changed

+62
-13
lines changed

2 files changed

+62
-13
lines changed

lib/mix/lib/mix/tasks/cmd.ex

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,66 @@ defmodule Mix.Tasks.Cmd do
1515
1616
$ mix cmd make
1717
18-
By passing the `--cd` flag before the command, you can also force
19-
the command to run in a specific directory:
18+
## Shell expansion
2019
21-
$ mix cmd --cd "third-party" make
20+
When you invoke `mix cmd` from the command line, environment variables,
21+
glob patterns, and similar are expanded by the current shell and then
22+
passed to `mix cmd`. For example, if you invoke:
23+
24+
$ mix cmd echo lib/*
25+
26+
Your shell will expand "lib/*" and then pass multiple arguments to
27+
`mix cmd`, which in turn passes them to `echo`. Note that, `mix cmd`
28+
by itself, does not perform any shell expansion. This means that,
29+
if you invoke `mix cmd` programatically, as in:
30+
31+
Mix.Task.run("cmd", ["echo", "lib/*"])
32+
33+
or through a `mix.exs` alias:
34+
35+
alias: "cmd echo lib/*"
36+
37+
It will literally print "lib/*".
38+
39+
This behaviour is the default from Elixir v1.19. If you want
40+
`mix cmd` to behave like a shell, you can pass the `--shell`
41+
option before the command name:
42+
43+
Mix.Task.run("cmd", ["--shell", "echo", "lib/*"])
44+
45+
Keep in mind however that, if `--shell` is given, quoted arguments
46+
are no longer correctly propagated to the underlying command
47+
(as they get expanded before hand).
48+
49+
This task is automatically re-enabled, so it can be called multiple times
50+
with different arguments.
51+
52+
## Umbrella applications and directories
2253
2354
This task is also useful in umbrella applications to execute a command
2455
on each child app:
2556
2657
$ mix cmd pwd
2758
59+
In such cases, Mix will change the current working directory to the root
60+
of each umbrella application before executing the command.
61+
2862
You can limit which apps the cmd runs in by using `mix do` with the `--app`
2963
option:
3064
3165
$ mix do --app app1 --app app2 cmd pwd
3266
33-
The tasks aborts whenever a command exits with a non-zero status.
67+
Note the tasks aborts whenever a command exits with a non-zero status.
3468
35-
This task is automatically re-enabled, so it can be called multiple times
36-
with different arguments.
69+
Outside of umbrella projects, you can pass the `--cd` flag before the command,
70+
to specify a directory:
71+
72+
$ mix cmd --cd "third-party" make
3773
3874
## Command line options
3975
4076
* `--cd` *(since v1.10.4)* - the directory to run the command in
77+
* `--shell` *(since v1.19.0)* - perform shell expansion of the arguments
4178
4279
## Orphan operating system processes
4380
@@ -54,7 +91,8 @@ defmodule Mix.Tasks.Cmd do
5491

5592
@switches [
5693
app: :keep,
57-
cd: :string
94+
cd: :string,
95+
shell: :boolean
5896
]
5997

6098
@impl true
@@ -78,16 +116,21 @@ defmodule Mix.Tasks.Cmd do
78116
path = hd(args)
79117
rest = tl(args)
80118

81-
path =
82-
if String.contains?(path, ["/", "\\"]) and Path.type(path) != :absolute do
83-
Path.expand(path, Keyword.get(opts, :cd, "."))
84-
else
85-
path
119+
arg =
120+
cond do
121+
Keyword.get(opts, :shell, false) ->
122+
Enum.join([path | rest], " ")
123+
124+
String.contains?(path, ["/", "\\"]) and Path.type(path) != :absolute ->
125+
{Path.expand(path, Keyword.get(opts, :cd, ".")), rest}
126+
127+
true ->
128+
{path, rest}
86129
end
87130

88131
cmd_opts = Keyword.take(opts, [:cd])
89132

90-
case Mix.shell().cmd({path, rest}, cmd_opts) do
133+
case Mix.shell().cmd(arg, cmd_opts) do
91134
0 -> :ok
92135
status -> exit(status)
93136
end

lib/mix/test/mix/tasks/cmd_test.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ defmodule Mix.Tasks.CmdTest do
1414
assert_received {:mix_shell, :run, ["hello world\n"]}
1515
end
1616

17+
test "can be invoked as a shell" do
18+
nl = os_newline()
19+
Mix.Task.run("cmd", ["--shell", "echo", "hello"])
20+
assert_received {:mix_shell, :run, ["hello" <> ^nl]}
21+
end
22+
1723
@tag :unix
1824
test "supports relative paths" do
1925
in_tmp("cmd-relative", fn ->

0 commit comments

Comments
 (0)