@run macro?

How could a run macro be defined that executes a script?

It should just include the file. Would something like:

@run myscript.jl

be possible as replacement for

include("myscript.jl")

?

That would save 7 keystrokes in the REPL.

macro run(script)
    return :(include($script))
end
@run "scripts/tmp.jl"

I don’t see a way of avoiding the quotation marks in the invocation, however.

1 Like

Function APIs are way more flexible, from my standpoint.

Just like JLD2.@load "a.jld2" can be replaced with first(JLD2.load("a.jld2")).second.

It’s mostly possible if we painstakingly validate an Expr with “variables” and “operators” like /, \, . etc, but it seems like a lot of trouble just to hit a ParseError at a very common C:\. If the point is to save keystrokes, then a single-character alias for include or a nonstandard string literal saves more. To edit your macro, the latter looks like:

julia> macro run_str(script)
         esc( :(include($script)) )
       end

julia> @macroexpand run"C:\blah.jl"
:(include("C:\\blah.jl"))

Note that non-standard string literals are parsed like raw strings (which are actually implemented with no processing) unlike normal Strings, so this doesn’t allow string interpolation or most unescaping.

1 Like

@run my_file.jl wouldn’t TAB-complete in the REPL, which saves far more keystrokes and errors than this macro would IMHO.

4 Likes

I like the custom string literal most. Nice: After TAB completion, you do not need to type the closing bracket.

macro run_str(script)
    esc( :(include($script)) )
end

I put it in my startup.jl file in the .julia/config folder.

1 Like

With cheating you can save 9 instead of the 7 (isn’t it 6?) keystrokes by just entering the file name in the REPL and hitting alt+i:

using REPL: LineEdit
@async Base.active_repl.interface.modes[1].keymap_dict['\e']['i'] = function (state, _, _)
    io = LineEdit.buffer(state)
    write(io, "include(\"", io |> take! |> String, "\")")
    LineEdit.commit_line(state)
    return :done
end

Replace ['i'] by ['r'] if you want to indicate run instead of include. The @async supports adding this to startup.jl. And obviously includet might be more useful than include. However, @cstjean’s point of missing TAB-complete still stands.

This is tested with Julia 1.11.7 and 1.12.1. Expect breakage with other Julia versions (as e.g. seen on nightly) due to using private identifiers.

I am not sure how seriously I am suggesting this. But I do think we should consider using REPL shortcuts more often in non-composable use cases like this.

3 Likes

This sounded too much of a challenge to let it pass. So with

using REPL
using REPL.REPLCompletions: completions, complete_path, PathCompletion, ModuleCompletion

const completions_orig_world = Base.get_world_counter()

function REPL.REPLCompletions.completions(string::String, pos::Int, context_module::Module=Main, shift::Bool=true, hint::Bool=false)
    # Get original completions.
    ret, range, should_complete = Base.invoke_in_world(completions_orig_world, completions, string, pos, context_module, shift, hint)

    # If we don't already have path completions, add them.
    has_paths = any(x -> x isa PathCompletion, ret)
    if !has_paths
        paths, dir, path_success = complete_path(string[1:pos])
        if path_success
            # In subfolder completion, do not suggest anything else than paths.
            contains(string[1:pos], '/') && filter!(x -> x isa PathCompletion, ret)

            # In subfolder completion, do not suggest strange entries.
            filter!(x -> !(x isa ModuleCompletion && x.parent == Main && x.mod in ("/", "//")), ret)

            # Join directory with paths.
            paths = (PathCompletion(joinpath(dir, p.path)) for p in paths)
            append!(ret, paths)
            range = 1:pos
        end
    end

    return ret, range, should_complete
end

it also TAB-completes here in 1.12.1. There are many reasons why this is dirty and yet I find it fascinating. Maybe we should think about making more of completions a public API? It would be interesting to see more packages adding specific completions.

2 Likes

Just to list the ones that come to my mind:

  • It’s wholly interactive, though it’s not much of a problem in this case to write out the include call in a file. Generally, a simple public function could be provided for scripts, and the REPL’s generated code would just call it.
  • As type piracy, this is hard to distribute through packages without clashes. Maybe a different package decides that ALT + i does something different.
  • ALT + i is too quick of a decision to generate and execute code, which is vulnerable to typos (ALT + o could do something else we’d rather not) or misremembering (was it ALT + t, nope, that swaps the file name and extension for some reason).

I don’t know of a way to address all those at once to save keystrokes, and maybe there isn’t one. However, the last one could be improved with a custom prompt mode. We still wouldn’t see the generated code, but there is at least a visible indicator of its intention and we decide to do it separately.

julia> # ALT+i or whatever keystrokes are necessary

include> helloworld.jl # ENTER to go through with it
hello world

include> # ready for more files or backspace to return to julia> prompt

It’d be nice if custom TAB-completion can be isolated to a custom prompt mode instead of changing the builtin prompt modes, but I don’t know how.

In general providing functionality as key-combination would indeed be problematic, as it won’t compose. In this case, I think the public function already exists as include.
Packages do effectively already register a callback to keymap_dict. It does not need to be public, because only the packages need to know about it in the code itself.

This is already the case for all packages which add a REPL shortcut or a REPL mode (see e.g. TerminalPager.jl or Pkg.jl). It might make sense to add a coordination package for REPL shortcuts and for REPL modes similar to FileIO.jl.

Users should at least be able to activate/deactivate this feature. There are probably not too many users which have highly problematic scripts lying around in their working directory and there are are other key combinations which can in the worst case destroy some hours of work (e.g. ctrl+d). But I agree that this feature can be unparalleled in how much you can damage yourself with little effort.

It’d be nice if custom TAB-completion can be isolated to a custom prompt mode instead of changing the builtin prompt modes, but I don’t know how.

This is quite some work, but not too difficult when you use an existing implementation as a template, e.g. the TerminalPager.jl implementation. You create a new mode and either use your own CompletionProvider or add the shortcut to the REPL mode of choice. I used modes[1] above which is the regular REPL mode.

I opted against a REPL mode, because it’s both more work for the initial implementation and for the user. Additionally, I think some features which are currently implemented as a REPL mode are better implemented as a shortcut (see e.g. the help mode versus the inline help feature of TerminalPager.jl). However, the best solution would probably be to offer both and let the user decide. We already see that preferences vary for this include feature.

There are many reasons why this is dirty

I had some additional reasons in my mind, e.g.

  • the way how this patches the REPL.REPLCompletions.completions method, which has a fully specific signature, by using world age is interesting and might be worth its own macro(s) (and possibly package).
    In most of the cases (like this one), I think you should not notice any resulting world age issues, but there are more hideous cases which can hit you hard.
    Additionally, I think this will be slower than it could be (probably does not matter here), but this is the disadvantage of using fully-specific method signatures.
  • I think we do not need to add path completions if the original completions already found some, but I am not sure.
  • Checking only for / to see whether we already have a path is neither sufficient (e.g. division) nor necessary (e.g. Windows) for having a path.
  • It would be cleaner if this last filter! was unnecessary, but I do not know why these entries are generated at all and how to avoid this.
1 Like