Best practice for script with ARGS that can also be run with `include`

I’m trying to figure out a good pattern for writing scripts that take command line arguments, and that are easy to run both from the shell (julia script.jl), and from inside the REPL/another Julia script (via include). The latter option is primarily to reduce the compilation overhead from running multiple scripts, or the same script with different arguments.

The best I’ve come up with so far is to make script.jl look like this:

module TestProg

function main(args=ARGS)
    @show args
end

end

_progmod = TestProg
if abspath(PROGRAM_FILE) == @__FILE__
    _progmod.main()
end

In order to run this programmatically from another script driver.jl, I’d use something like

function run(script, args...; mod=:_progmod, main=:main)
    include(script)
    @eval $mod.$main($args)
end

run("script.jl", "A", "B")

The _progmod is just to establish a standard across all my scripts, so I don’t have to look at the source code of each script for the module name (run("script.jl", "A", "B", mod=:TestProg) would also work, if _progmod wasn’t defined).

In the REPL, I would do

julia> include("script.jl")
julia> TestProg.main(["A", "B"])

Does this look like a good pattern? Does anybody have any idea on how to improve on this?

The one small drawback is that running the run helper function multiple times from within the same julia session shows a WARNING: replacing module TestProg. I don’t think there’s any actual problem with the reloading, though.

Why not just manipulate ARGS?

Something like script.jl:

module TestProg

function main(args=ARGS)
    @show args
end

end

TestProg.main()

and driver.jl

setargs!(ARGS, args...) = (empty!(ARGS); append!(ARGS, args))
function run(script, args...)
    setargs!(ARGS, args...)
    include(script)
end

run("script.jl", "A", "B")

In the long run, it’s far better to write functions with arguments as parameters than scripts.with global arguments for any code you want to re-use. (You can still use scripts — just put the script in a short file that uses/includes your other code and passes ARGS as function parameters.)

See also Best practise: organising code in Julia - #2 by stevengj and Best practise: organising code in Julia - #4 by stevengj

Oh, I agree 100%! This is for those short top-level scripts, e.g., this one. A more fleshed-out “driver” to run that script to programmatically is here, as part of a make-like system to programmatically run all the scripts in the project, cf. Julia-based Makefile replacement for research workflows. Of course, I still want to be able to run the script either from the command line with normal arguments, or from the REPL or a notebook, for playing around with different parameters.