Runtime arguments for __init__?

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?

1 Like

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.

2 Likes

That was very helpful. Thank you.