I thought at first that this was a general way to write an application that would be precompiled and would therefore load fast. However, I think I see a problem. There seems to be no way to pass arguments to a module’s __init__
function. Consequently, __init__()
can’t be controlled by command-line arguments. Is there a way around this problem?
Why do you need to pass arguments to __init__
? (As opposed to passing arguments at runtime to a function in the module from a script that calls the module?)
(You can modify the behavior of __init__
at runtime with environment variables, for example. But probably you just want to make __init__
do less and put the runtime functionality into ordinary functions.)
The goal is to get the code to precompile so it will load quickly. In an answer to my original question, user “mkitti” showed how to implement “Hello world” within __init__
in order to take advantage of the automatic precompilation of modules. I’m trying to figure out whether this approach can be generalized. For example, could one use this approach to rewrite the Unix “echo” command?
What you would normally do would be to put most of your code into a precompiled module/package, but then to write a short script that merely loads and calls your module. This short script is what parses e.g. command-line arguments (or runs interactively, as in a Jupyter or Pluto notebook).
On reflection, I think you’re right. The obvious approach would be to write an “echo” function within the module, which would take arguments.
No further questions. Thanks for the help.
Let me start with a minimal example that emulates echo
. The ARGS
global constant contains command line arguments. It is a Vector{String}
.
Here is Echo.jl.
module Echo
__init__() = println(join(ARGS, " "))
end
Here is a demo.
$ julia Echo.jl This is a test
This is a test
$ julia Echo.jl "This is another test." "Hello world"
This is another test. Hello world
stevengj, I usually write all my scripts as modules so their accessory functions can used elsewhere.
Here is an expanded Echo.jl.
module Echo
function __init__()
@debug "Globals" PROGRAM_FILE @__FILE__() ARGS
if !Base.isinteractive() && @__FILE__() == abspath(PROGRAM_FILE)
# We are directly executing this file from the command line
main()
end
end
main() = echo(ARGS)
echo(args) = println(join(args, " "))
end
We see that the program still works. Additionally, we can now print some debugging information.
$ julia Echo.jl Hello alan and stevengj
Hello alan and stevengj
$ JULIA_DEBUG=Echo julia Echo.jl Hello alan and stevengj
┌ Debug: Globals
│ PROGRAM_FILE = "Echo.jl"
│ #= /home/mkitti/Echo.jl:3 =# @__FILE__ = "/home/mkitti/Echo.jl"
│ ARGS =
│ 4-element Vector{String}:
│ "Hello"
│ ⋮
└ @ Main.Echo ~/Echo.jl:3
Hello alan and stevengj
I can also interact with this file from the Julia REPL.
$ julia --banner=no -i Echo.jl This is a test
julia> Echo.main()
This is a test
julia> Echo.echo(["I am calling this from", "the Julia REPL"])
I am calling this from the Julia REPL
The above is a fancy script that I can run, include, and debug. While there is some compilation happening in memory, it is not being cached to disk.
s ~/.julia/compiled/v1.9/Echo
ls: cannot access '/home/mkitti/.julia/compiled/v1.9/Echo': No such file or directory
Let’s restructure the program so that it is precompiled.
We organize a project simliar to before.
$ tree -p src/Echo.jl
[drwxrwxr-x] src/Echo.jl
├── [-rw-rw-r--] Project.toml
├── [drwxrwxr-x] scripts
│ └── [-rwxrwxr-x] echo.jl
└── [drwxrwxr-x] src
└── [-rw-rw-r--] Echo.jl
$ cat src/Echo.jl/Project.toml
name = "Echo"
uuid = "594283f3-b8da-40eb-8bd5-2e27980f0e39"
$ cat src/Echo.jl/scripts/echo.jl
#!/bin/env julia
using Pkg
if length(ARGS) > 0 && ARGS[1] == "-q"
io = devnull
popfirst!(ARGS)
else
io = stdout
end
Pkg.activate(
@__FILE__() |>
x->(islink(x) ? readlink(x) : x) |>
dirname |>
dirname
; io
)
using Echo
$ cat src/Echo.jl/src/Echo.jl
module Echo
const EXE = abspath(joinpath(@__FILE__, "..", "..", "scripts", "echo.jl"))
function __init__()
@debug "Globals" PROGRAM_FILE EXE ARGS ismain()
# Run main if we are executing $EXE from the command line
ismain() && main()
end
function ismain(executable=EXE)
Base.isinteractive() && return false
program_file = abspath(PROGRAM_FILE)
while islink(program_file)
program_file = readlink(program_file)
end
@debug "ismain" executable program_file
return executable == program_file
end
main() = echo(ARGS)
echo(args) = println(join(args, " "))
end
Now we can try installing the script in ~/bin
and running it.
$ ln -s ~/src/Echo.jl/scripts/echo.jl ~/bin/
$ echo.jl This is a test. Hello world!
Activating project at `~/src/Echo.jl`
This is a test. Hello world!
$ echo.jl -q Quieter please.
Quieter please.
We also see that the code has been precompiled.
$ ls ~/.julia/compiled/v1.9/Echo/
5n5AW_0ml5H.ji 5n5AW_0ml5H.so
To be honest, for a simple script like this, I might not bother with all of this machinery. Frankly, I mostly do this while deploying this code for other people.
That was very helpful. Thank you.