/
helpers.ex
610 lines (496 loc) · 15.8 KB
/
helpers.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
defmodule IEx.Helpers do
@moduledoc """
Welcome to Interactive Elixir. You are currently
seeing the documentation for the module `IEx.Helpers`
which provides many helpers to make Elixir's shell
more joyful to work with.
This message was triggered by invoking the helper
`h()`, usually referred to as `h/0` (since it expects 0
arguments).
There are many other helpers available:
* `b/1` - prints callbacks info and docs for a given module
* `c/2` - compiles a file at the given path
* `cd/1` - changes the current directory
* `clear/0` - clears the screen
* `flush/0` - flushes all messages sent to the shell
* `h/0` - prints this help message
* `h/1` - prints help for the given module, function or macro
* `import_file/1` - evaluates the given file in the shell's context
* `l/1` - loads the given module's beam code
* `ls/0` - lists the contents of the current directory
* `ls/1` - lists the contents of the specified directory
* `pid/3` - creates a PID with the 3 integer arguments passed
* `pwd/0` - prints the current working directory
* `r/1` - recompiles and reloads the given module
* `recompile/0` - recompiles the current Mix project (requires iex -S mix)
* `respawn/0` - respawns a new IEx shell
* `s/1` - prints spec information
* `t/1` - prints type information
* `v/0` - retrieves the last value from the history
* `v/1` - retrieves the nth value from the history
Help for functions in this module can be consulted
directly from the command line, as an example, try:
h(c/2)
You can also retrieve the documentation for any module
or function. Try these:
h(Enum)
h(Enum.reverse/1)
To discover all available functions for a module, type the module name
followed by a dot, then press tab to trigger autocomplete. For example:
Enum.
To learn more about IEx as a whole, just type `h(IEx)`.
"""
import IEx, only: [dont_display_result: 0]
@doc """
Recompiles the current Mix application.
This helper only works when IEx is started with a Mix
project, for example, `iex -S mix`. Before compiling
the code, it will stop the current application, and
start it again afterwards. Stopping applications are
required so processes in the supervision tree won't
crash when code is upgraded multiple times without
going through the proper hot-code swapping mechanism.
Changes to `mix.exs` or configuration files won't be
picked up by this helper, only changes to sources.
Restarting the shell and Mix is required in such cases.
If you want to reload a single module, consider using
`r ModuleName` instead.
NOTE: This feature is experimental and may be removed
in upcoming releases.
"""
def recompile do
if mix_started? do
config = Mix.Project.config
reenable_tasks(config)
apps = stop_apps(config)
Mix.Task.run("app.start")
{:restarted, apps}
else
IO.puts IEx.color(:eval_error, "Mix is not running. Please start IEx with: iex -S mix")
:error
end
end
defp mix_started? do
List.keyfind(Application.started_applications, :mix, 0) != nil
end
defp reenable_tasks(config) do
Mix.Task.reenable("app.start")
Mix.Task.reenable("compile")
Mix.Task.reenable("compile.all")
compilers = config[:compilers] || Mix.compilers
Enum.each compilers, &Mix.Task.reenable("compile.#{&1}")
end
defp stop_apps(config) do
apps =
cond do
Mix.Project.umbrella?(config) ->
for %Mix.Dep{app: app} <- Mix.Dep.Umbrella.loaded, do: app
app = config[:app] ->
[app]
true ->
[]
end
apps |> Enum.reverse |> Enum.each(&Application.stop/1)
apps
end
@doc """
Compiles the given files.
It expects a list of files to compile and an optional path to write
the compiled code to (defaults to the current directory). When compiling
one file, there is no need to wrap it in a list.
It returns the name of the compiled modules.
If you want to recompile an existing module, check `r/1` instead.
## Examples
c ["foo.ex", "bar.ex"], "ebin"
#=> [Foo, Bar]
c "baz.ex"
#=> [Baz]
"""
def c(files, path \\ ".") when is_binary(path) do
files = List.wrap(files)
unless Enum.all?(files, &is_binary/1) do
raise ArgumentError, "expected a binary or a list of binaries as argument"
end
{found, not_found} =
files
|> Enum.map(&Path.expand(&1, path))
|> Enum.partition(&File.exists?/1)
unless Enum.empty?(not_found) do
raise ArgumentError, "could not find files #{Enum.join(not_found, ", ")}"
end
{erls, exs} = Enum.partition(found, &String.ends_with?(&1, ".erl"))
modules = Enum.map(erls, fn(source) ->
{module, binary} = compile_erlang(source)
base = source |> Path.basename |> Path.rootname
File.write!(Path.join(path, base <> ".beam"), binary)
module
end)
modules ++ Kernel.ParallelCompiler.files_to_path(exs, path)
end
@doc """
Clears the console screen.
This function only works if ANSI escape codes are enabled
on the shell, which means this function is by default
unavailable on Windows machines.
"""
def clear do
if IO.ANSI.enabled? do
IO.write [IO.ANSI.home, IO.ANSI.clear]
else
IO.puts "Cannot clear the screen because ANSI escape codes are not enabled on this shell"
end
dont_display_result
end
@doc """
Prints the documentation for `IEx.Helpers`.
"""
def h() do
IEx.Introspection.h(IEx.Helpers)
dont_display_result
end
@doc """
Prints the documentation for the given module
or for the given function/arity pair.
## Examples
h(Enum)
#=> Prints documentation for Enum
It also accepts functions in the format `fun/arity`
and `module.fun/arity`, for example:
h receive/1
h Enum.all?/2
h Enum.all?
"""
@h_modules [__MODULE__, Kernel, Kernel.SpecialForms]
defmacro h(term)
defmacro h({:/, _, [call, arity]} = term) do
args =
case Macro.decompose_call(call) do
{_mod, :__info__, []} when arity == 1 ->
[Module, :__info__, 1]
{mod, fun, []} ->
[mod, fun, arity]
{fun, []} ->
[@h_modules, fun, arity]
_ ->
[term]
end
quote do
IEx.Introspection.h(unquote_splicing(args))
end
end
defmacro h(call) do
args =
case Macro.decompose_call(call) do
{_mod, :__info__, []} ->
[Module, :__info__, 1]
{mod, fun, []} ->
[mod, fun]
{fun, []} ->
[@h_modules, fun]
_ ->
[call]
end
quote do
IEx.Introspection.h(unquote_splicing(args))
end
end
@doc """
Prints the documentation for the given callback function.
It also accepts single module argument to list
all available behaviour callbacks.
## Examples
b(Mix.Task.run/1)
b(Mix.Task.run)
b(Dict)
"""
defmacro b(term)
defmacro b({:/, _, [{{:., _, [mod, fun]}, _, []}, arity]}) do
quote do
IEx.Introspection.b(unquote(mod), unquote(fun), unquote(arity))
end
end
defmacro b({{:., _, [mod, fun]}, _, []}) do
quote do
IEx.Introspection.b(unquote(mod), unquote(fun))
end
end
defmacro b(module) do
quote do
IEx.Introspection.b(unquote(module))
end
end
@doc """
Prints the types for the given module or for the given function/arity pair.
## Examples
t(Enum)
t(Enum.t/0)
t(Enum.t)
"""
defmacro t(term)
defmacro t({:/, _, [{{:., _, [mod, fun]}, _, []}, arity]}) do
quote do
IEx.Introspection.t(unquote(mod), unquote(fun), unquote(arity))
end
end
defmacro t({{:., _, [mod, fun]}, _, []}) do
quote do
IEx.Introspection.t(unquote(mod), unquote(fun))
end
end
defmacro t(module) do
quote do
IEx.Introspection.t(unquote(module))
end
end
@doc """
Prints the specs for the given module or for the given function/arity pair.
## Examples
s(Enum)
s(Enum.all?)
s(Enum.all?/2)
s(is_atom)
s(is_atom/1)
"""
defmacro s(term)
defmacro s({:/, _, [call, arity]} = term) do
args =
case Macro.decompose_call(call) do
{mod, fun, []} -> [mod, fun, arity]
{fun, []} -> [Kernel, fun, arity]
_ -> [term]
end
quote do
IEx.Introspection.s(unquote_splicing(args))
end
end
defmacro s(call) do
args =
case Macro.decompose_call(call) do
{mod, fun, []} -> [mod, fun]
{fun, []} -> [Kernel, fun]
_ -> [call]
end
quote do
IEx.Introspection.s(unquote_splicing(args))
end
end
@doc """
Retrieves the nth expression's value from the history.
Use negative values to lookup expression values relative to the current one.
For instance, v(-1) returns the result of the last evaluated expression.
"""
def v(n \\ -1) do
IEx.History.nth(history, n) |> elem(2)
end
@doc """
Recompiles and reloads the given `module`.
Please note that all the modules defined in the same
file as `module` are recompiled and reloaded.
## In-memory reloading
When we reload the module in IEx, we recompile the module source code,
updating its contents in memory. The original `.beam` file in disk,
probably the one where the first definition of the module came from,
does not change at all.
Since typespecs and docs are loaded from the .beam file (they are not
loaded in memory with the module because there is no need for them to
be in memory), they are not reloaded when you reload the module.
"""
def r(module) when is_atom(module) do
{:reloaded, module, do_r(module)}
end
defp do_r(module) do
unless Code.ensure_loaded?(module) do
raise ArgumentError, "could not load nor find module: #{inspect module}"
end
source = source(module)
cond do
source == nil ->
raise ArgumentError, "could not find source for module: #{inspect module}"
not File.exists?(source) ->
raise ArgumentError, "could not find source (#{source}) for module: #{inspect module}"
String.ends_with?(source, ".erl") ->
[compile_erlang(source) |> elem(0)]
true ->
Enum.map(Code.load_file(source), fn {name, _} -> name end)
end
end
@doc """
Loads the given module's beam code (and ensures any previous
old version was properly purged before).
This function is useful when you know the bytecode for module
has been updated in the filesystem and you want to tell the VM
to load it.
"""
def l(module) when is_atom(module) do
:code.purge(module)
:code.load_file(module)
end
@doc """
Flushes all messages sent to the shell and prints them out.
"""
def flush do
do_flush(IEx.inspect_opts)
end
defp do_flush(inspect_opts) do
receive do
msg ->
IO.inspect(msg, inspect_opts)
do_flush(inspect_opts)
after
0 -> :ok
end
end
defp source(module) do
source = module.module_info(:compile)[:source]
case source do
nil -> nil
source -> List.to_string(source)
end
end
@doc """
Prints the current working directory.
"""
def pwd do
IO.puts IEx.color(:eval_info, System.cwd!)
end
@doc """
Changes the current working directory to the given path.
"""
def cd(directory) when is_binary(directory) do
case File.cd(expand_home(directory)) do
:ok -> pwd
{:error, :enoent} ->
IO.puts IEx.color(:eval_error, "No directory #{directory}")
end
end
@doc """
Produces a simple list of a directory's contents.
If `path` points to a file, prints its full path.
"""
def ls(path \\ ".") when is_binary(path) do
path = expand_home(path)
case File.ls(path) do
{:ok, items} ->
sorted_items = Enum.sort(items)
ls_print(path, sorted_items)
{:error, :enoent} ->
IO.puts IEx.color(:eval_error, "No such file or directory #{path}")
{:error, :enotdir} ->
IO.puts IEx.color(:eval_info, Path.absname(path))
end
end
defp expand_home(<<?~, rest :: binary>>) do
System.user_home! <> rest
end
defp expand_home(other), do: other
defp ls_print(_, []) do
:ok
end
defp ls_print(path, list) do
# print items in multiple columns (2 columns in the worst case)
lengths = Enum.map(list, &String.length(&1))
maxlen = maxlength(lengths)
width = min(maxlen, 30) + 5
ls_print(path, list, width)
end
defp ls_print(path, list, width) do
Enum.reduce(list, 0, fn(item, len) ->
if len >= 80 do
IO.puts ""
len = 0
end
IO.write format_item(Path.join(path, item), String.ljust(item, width))
len+width
end)
IO.puts ""
end
defp maxlength(list) do
Enum.reduce(list, 0, &max(&1, &2))
end
defp format_item(path, representation) do
case File.stat(path) do
{:ok, %File.Stat{type: :device}} ->
IEx.color(:ls_device, representation)
{:ok, %File.Stat{type: :directory}} ->
IEx.color(:ls_directory, representation)
_ ->
representation
end
end
@doc """
Respawns the current shell by starting a new shell process.
Returns `true` if it worked.
"""
def respawn do
if whereis = IEx.Server.whereis do
send whereis, {:respawn, self}
dont_display_result
end
end
@doc """
Evaluates the contents of the file at `path` as if it were directly typed into
the shell.
`path` has to be a literal string. `path` is automatically expanded via
`Path.expand/1`.
## Non-existent files
By default, `import_file/1` fails when the given file does not exist. However,
since this macro is expanded at compile-time, it's not possible to
conditionally import a file since the macro is always expanded:
# This raises a File.Error if ~/.iex.exs doesn't exist.
if ("~/.iex.exs" |> Path.expand |> File.exists?) do
import_file "~/.iex.exs"
end
This is why an `:optional` option can be passed to `import_file/1`. The
default value of this option is `false`, meaning that an exception will be
raised if the given file is missing. If `:optional` is set to `true`, missing
files will be ignored and `import_file/1` will just compile to `nil`.
## Examples
# ~/file.exs
value = 13
# in the shell
iex(1)> import_file "~/file.exs"
13
iex(2)> value
13
iex(3)> import_file "nonexisting.file.ex", optional: true
nil
"""
defmacro import_file(path, opts \\ [])
defmacro import_file(path, opts) when is_binary(path) do
optional? = Keyword.get(opts, :optional, false)
path = Path.expand(path)
if not optional? or File.exists?(path) do
path |> File.read! |> Code.string_to_quoted!(file: path)
end
end
defmacro import_file(_path, _opts) do
raise ArgumentError, "import_file/1 expects a literal binary as its argument"
end
# Compiles and loads an Erlang source file, returns {module, binary}
defp compile_erlang(source) do
source = Path.relative_to_cwd(source) |> String.to_char_list
case :compile.file(source, [:binary, :report]) do
{:ok, module, binary} ->
:code.purge(module)
{:module, module} = :code.load_binary(module, source, binary)
{module, binary}
_ ->
raise CompileError
end
end
defp history, do: Process.get(:iex_history)
@doc """
Creates a PID with 3 non negative integers passed as arguments
to the function.
## Examples
iex> pid(0, 21, 32)
#PID<0.21.32>
iex> pid(0, 64, 2048)
#PID<0.64.2048>
"""
def pid(x, y, z) when is_integer(x) and x >= 0 and
is_integer(y) and y >= 0 and
is_integer(z) and z >= 0 do
:c.pid(x, y, z)
end
end