Skip to content

Commit 5e42900

Browse files
author
José Valim
committed
Support aliases in Mix.Task.run/2
1 parent 707b810 commit 5e42900

File tree

7 files changed

+184
-89
lines changed

7 files changed

+184
-89
lines changed

lib/mix/lib/mix/cli.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ defmodule Mix.CLI do
6060
# If the task is not available, let's try to
6161
# compile the repository and then run it again.
6262
cond do
63-
match?({:ok, _}, Mix.Task.get(name)) ->
63+
Mix.Task.get(name) ->
6464
Mix.Task.run(name, args)
6565
Mix.Project.get ->
6666
Mix.Task.run("compile")

lib/mix/lib/mix/project.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ defmodule Mix.Project do
5757
:ok
5858
{:error, other} when is_binary(other) ->
5959
Mix.raise "Trying to load #{inspect atom} from #{inspect file}" <>
60-
" but another project with the same name was already defined at #{inspect other}"
60+
" but another project with the same name was already defined at #{inspect other}"
6161
end
6262
end
6363

@@ -359,7 +359,8 @@ defmodule Mix.Project do
359359
end
360360

361361
defp default_config do
362-
[build_per_environment: true,
362+
[aliases: [],
363+
build_per_environment: true,
363364
default_task: "run",
364365
deps: [],
365366
deps_path: "deps",

lib/mix/lib/mix/task.ex

Lines changed: 84 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,11 @@ defmodule Mix.Task do
150150
Otherwise returns `{:error, :invalid}` in case the module
151151
exists but it isn't a task or `{:error, :not_found}`.
152152
"""
153-
@spec get(task_name) :: {:ok, task_module} | {:error, :invalid} | {:error, :not_found}
154-
155-
def get(task) when is_binary(task) or is_atom(task) do
156-
case Mix.Utils.command_to_module(to_string(task), Mix.Tasks) do
157-
{:module, module} ->
158-
if task?(module), do: {:ok, module}, else: {:error, :invalid}
159-
{:error, _} ->
160-
{:error, :not_found}
153+
@spec get(task_name) :: task_module | nil
154+
def get(task) do
155+
case fetch(task) do
156+
{:ok, module} -> module
157+
{:error, _} -> nil
161158
end
162159
end
163160

@@ -172,7 +169,7 @@ defmodule Mix.Task do
172169
"""
173170
@spec get!(task_name) :: task_module | no_return
174171
def get!(task) do
175-
case get(task) do
172+
case fetch(task) do
176173
{:ok, module} ->
177174
module
178175
{:error, :invalid} ->
@@ -182,6 +179,15 @@ defmodule Mix.Task do
182179
end
183180
end
184181

182+
defp fetch(task) when is_binary(task) or is_atom(task) do
183+
case Mix.Utils.command_to_module(to_string(task), Mix.Tasks) do
184+
{:module, module} ->
185+
if task?(module), do: {:ok, module}, else: {:error, :invalid}
186+
{:error, _} ->
187+
{:error, :not_found}
188+
end
189+
end
190+
185191
@doc """
186192
Runs a `task` with the given `args`.
187193
@@ -197,62 +203,101 @@ defmodule Mix.Task do
197203
@spec run(task_name, [any]) :: any
198204
def run(task, args \\ []) when is_binary(task) or is_atom(task) do
199205
task = to_string(task)
206+
proj = Mix.Project.get
207+
208+
alias = Mix.Project.config[:aliases][String.to_atom(task)]
209+
210+
cond do
211+
alias && Mix.TasksServer.run({:alias, task, proj}) ->
212+
res = run_alias(List.wrap(alias), args, :ok)
213+
Mix.TasksServer.put({:task, task, proj})
214+
res
215+
Mix.TasksServer.run({:task, task, proj}) ->
216+
run_task(task, args)
217+
true ->
218+
:noop
219+
end
220+
end
200221

201-
if Mix.TasksServer.run_task(task, Mix.Project.get) do
202-
module = get!(task)
222+
defp run_task(task, args) do
223+
module = get!(task)
203224

204-
recur module, fn proj ->
205-
Mix.TasksServer.put_task(task, proj)
206-
module.run(args)
207-
end
225+
fun = fn proj ->
226+
Mix.TasksServer.put({:task, task, proj})
227+
module.run(args)
228+
end
229+
230+
if recursive(module) and Mix.Project.umbrella? and Mix.ProjectStack.enable_recursion do
231+
res = recur(fun)
232+
Mix.ProjectStack.disable_recursion
233+
res
208234
else
209-
:noop
235+
fun.(Mix.Project.get)
210236
end
211237
end
212238

239+
defp run_alias([h|t], alias_args, _res) when is_binary(h) do
240+
[task|args] = OptionParser.split(h)
241+
res = Mix.Task.run task, join_args(args, alias_args, t)
242+
run_alias(t, alias_args, res)
243+
end
244+
245+
defp run_alias([h|t], alias_args, _res) when is_function(h, 1) do
246+
res = h.(join_args([], alias_args, t))
247+
run_alias(t, alias_args, res)
248+
end
249+
250+
defp run_alias([], _alias_task, res) do
251+
res
252+
end
253+
254+
defp join_args(args, alias_args, []), do: args ++ alias_args
255+
defp join_args(args, _alias_args, _), do: args
256+
213257
@doc """
214258
Clears all invoked tasks, allowing them to be reinvoked.
259+
260+
This operation is not recursive.
215261
"""
216262
@spec clear :: :ok
217263
def clear do
218-
Mix.TasksServer.clear_tasks
264+
Mix.TasksServer.clear
219265
end
220266

221267
@doc """
222268
Reenables a given task so it can be executed again down the stack.
223269
224-
If an umbrella project reenables a task it is reenabled for all sub projects.
270+
Both alias and the regular stack are reenabled when this function
271+
is called.
272+
273+
If an umbrella project reenables a task, it is reenabled for all
274+
children projects.
225275
"""
226276
@spec reenable(task_name) :: :ok
227277
def reenable(task) when is_binary(task) or is_atom(task) do
228-
task = to_string(task)
229-
module = get!(task)
278+
task = to_string(task)
279+
proj = Mix.Project.get
230280

231-
Mix.TasksServer.delete_task(task, Mix.Project.get)
281+
Mix.TasksServer.delete_many([{:task, task, proj},
282+
{:alias, task, proj}])
232283

233-
recur module, fn project ->
234-
Mix.TasksServer.delete_task(task, project)
284+
if (module = get(task)) && recursive(module) && Mix.Project.umbrella? do
285+
recur fn proj ->
286+
Mix.TasksServer.delete_many([{:task, task, proj},
287+
{:alias, task, proj}])
288+
end
235289
end
236290

237291
:ok
238292
end
239293

240-
defp recur(module, fun) do
241-
umbrella? = Mix.Project.umbrella?
242-
recursive = recursive(module)
243-
244-
if umbrella? && recursive && Mix.ProjectStack.enable_recursion do
245-
# Get all dependency configuration but not the deps path
246-
# as we leave the control of the deps path still to the
247-
# umbrella child.
248-
config = Mix.Project.deps_config |> Keyword.delete(:deps_path)
249-
res = for %Mix.Dep{app: app, opts: opts} <- Mix.Dep.Umbrella.loaded do
250-
Mix.Project.in_project(app, opts[:path], config, fun)
251-
end
252-
Mix.ProjectStack.disable_recursion
253-
res
254-
else
255-
fun.(Mix.Project.get)
294+
defp recur(fun) do
295+
# Get all dependency configuration but not the deps path
296+
# as we leave the control of the deps path still to the
297+
# umbrella child.
298+
config = Mix.Project.deps_config |> Keyword.delete(:deps_path)
299+
for %Mix.Dep{app: app, opts: opts} <- Mix.Dep.Umbrella.loaded do
300+
Mix.Project.in_project(app, opts[:path], config, fun)
256301
end
257302
end
258303

lib/mix/lib/mix/tasks_server.ex

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,22 @@ defmodule Mix.TasksServer do
55
Agent.start_link(fn -> HashSet.new end, name: __MODULE__)
66
end
77

8-
def clear_tasks() do
8+
def clear() do
99
update fn _ -> HashSet.new end
1010
end
1111

12-
def run_task(task, proj) do
13-
run_item = { task, proj }
12+
def run(tuple) do
1413
get_and_update fn set ->
15-
{ not(run_item in set), Set.put(set, run_item) }
14+
{not(tuple in set), Set.put(set, tuple)}
1615
end
1716
end
1817

19-
def put_task(task, proj) do
20-
update &Set.put(&1, { task, proj })
18+
def put(tuple) do
19+
update &Set.put(&1, tuple)
2120
end
2221

23-
def delete_task(task, proj) do
24-
update &Set.delete(&1, { task, proj })
22+
def delete_many(many) do
23+
update &Enum.reduce(many, &1, fn x, acc -> Set.delete(acc, x) end)
2524
end
2625

2726
defp get_and_update(fun) do

lib/mix/test/mix/aliases_test.exs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Code.require_file "../test_helper.exs", __DIR__
2+
3+
defmodule Mix.AliasesTest do
4+
use MixTest.Case
5+
6+
defmodule Aliases do
7+
def project do
8+
[aliases: [h: "hello",
9+
p: &inspect/1,
10+
compile: "hello",
11+
"nested.h": [&Mix.shell.info(inspect(&1)), "h foo bar"]]]
12+
end
13+
end
14+
15+
setup do
16+
Mix.Project.push Aliases
17+
:ok
18+
end
19+
20+
test "runs string aliases" do
21+
assert Mix.Task.run("h", []) == "Hello, World!"
22+
assert Mix.Task.run("h", []) == :noop
23+
assert Mix.Task.run("hello", []) == :noop
24+
25+
Mix.Task.reenable "h"
26+
Mix.Task.reenable "hello"
27+
assert Mix.Task.run("h", ["foo", "bar"]) == "Hello, foo bar!"
28+
end
29+
30+
test "runs function aliases" do
31+
assert Mix.Task.run("p", []) == "[]"
32+
assert Mix.Task.run("p", []) == :noop
33+
34+
Mix.Task.reenable "p"
35+
assert Mix.Task.run("p", ["foo", "bar"]) == "[\"foo\", \"bar\"]"
36+
end
37+
38+
test "runs list aliases" do
39+
assert Mix.Task.run("nested.h", ["baz"]) == "Hello, foo bar baz!"
40+
assert_received {:mix_shell, :info, ["[]"]}
41+
end
42+
43+
test "run alias override" do
44+
assert Mix.Task.run("compile", []) == "Hello, World!"
45+
assert Mix.Task.run("compile", []) == :noop
46+
end
47+
end

lib/mix/test/mix/task_test.exs

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,9 @@
11
Code.require_file "../test_helper.exs", __DIR__
22

3-
path = MixTest.Case.tmp_path("beams")
4-
File.rm_rf!(path)
5-
File.mkdir_p!(path)
6-
7-
write_beam = fn {:module, name, bin, _} ->
8-
path
9-
|> Path.join(Atom.to_string(name) <> ".beam")
10-
|> File.write!(bin)
11-
end
12-
13-
defmodule Mix.Tasks.Hello do
14-
use Mix.Task
15-
@shortdoc "This is short documentation, see"
16-
17-
@moduledoc """
18-
A test task.
19-
"""
20-
21-
def run(_) do
22-
"Hello, World!"
23-
end
24-
end |> write_beam.()
25-
26-
defmodule Mix.Tasks.Invalid do
27-
end |> write_beam.()
28-
293
defmodule Mix.TaskTest do
304
use MixTest.Case
315

32-
setup do
33-
Code.prepend_path unquote(path)
34-
:ok
35-
end
36-
37-
test :run do
6+
test "run/1" do
387
assert Mix.Task.run("hello") == "Hello, World!"
398
assert Mix.Task.run("hello") == :noop
409

@@ -47,19 +16,19 @@ defmodule Mix.TaskTest do
4716
end
4817
end
4918

50-
test :clear do
19+
test "clear/0" do
5120
assert Mix.Task.run("hello") == "Hello, World!"
5221
Mix.Task.clear
5322
assert Mix.Task.run("hello") == "Hello, World!"
5423
end
5524

56-
test :reenable do
25+
test "reenable/1" do
5726
assert Mix.Task.run("hello") == "Hello, World!"
5827
Mix.Task.reenable("hello")
5928
assert Mix.Task.run("hello") == "Hello, World!"
6029
end
6130

62-
test "reenable for umbrella" do
31+
test "reenable/1 for umbrella" do
6332
in_fixture "umbrella_dep/deps/umbrella", fn ->
6433
Mix.Project.in_project(:umbrella, ".", fn _ ->
6534
assert [:ok, :ok] = Mix.Task.run "clean"
@@ -72,7 +41,7 @@ defmodule Mix.TaskTest do
7241
end
7342
end
7443

75-
test :get! do
44+
test "get!" do
7645
assert Mix.Task.get!("hello") == Mix.Tasks.Hello
7746

7847
assert_raise Mix.NoTaskError, "The task unknown could not be found", fn ->
@@ -84,18 +53,19 @@ defmodule Mix.TaskTest do
8453
end
8554
end
8655

87-
test :all_modules do
56+
test "all_modules/0" do
8857
Mix.Task.load_all
8958
modules = Mix.Task.all_modules
9059
assert Mix.Tasks.Hello in modules
9160
assert Mix.Tasks.Compile in modules
9261
end
9362

94-
test :moduledoc do
63+
test "moduledoc/1" do
64+
Code.prepend_path MixTest.Case.tmp_path("beams")
9565
assert Mix.Task.moduledoc(Mix.Tasks.Hello) == "A test task.\n"
9666
end
9767

98-
test :shortdoc do
68+
test "shortdoc/1" do
9969
assert Mix.Task.shortdoc(Mix.Tasks.Hello) == "This is short documentation, see"
10070
end
10171
end

0 commit comments

Comments
 (0)