When using the basic-cli platform, the main! args start with the program name followed by the command-line arguments:
% roc args-basic-cli.roc foo bar
[dbg] ["args-basic-cli.roc", "foo", "bar"]
But in the echo platform, we don't have the program name:
% roc args-echo.roc foo bar
[dbg] ["foo", "bar"]
Here's args-basic-cli.roc
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.22.0/F1JVZPYfWP71s8vk6tHcV1Qx1Ef6CZkwswGoCn8VHZmL.tar.zst" }
main! = |args| {
dbg args
Ok({})
}
And here's args-echo.roc:
main! = |args| {
dbg args
Ok({})
}
Tbh I've never liked the fact that a program's arguments list starts with the program name. I feel like this mixes apples and oranges and often causes confusion. Many languages keep these two things separate, for example Haskell, Java, C#, Elixir, Julia, or PowerShell.
I propose that basic-cli be modified to only include the actual command line arguments. Users can already access the executable's full path using Env.exe_path!(), so I think it would make sense to simply add another function in Env to give access to what's currently the first main! arg (e.g., Env.exe_name()).
We obviously chose args for historical reasons... but if we are deviating from that should we take the opportunity to provide more than just arguments?
I think I looked into this and was disappointed to discover it was nonstandard or something like that
In roc-ray for example I like to provide environmental type information that is already available and it saves a round trip
as in, it's not a guarantee across all shells that the first arg will be that
Things that come to mind, Env vars, Locale, Arguments, Cpu arch etc
having said that, I could be misremembering - but we should be confident it's safe to assume that about the first arg before committing the API to assume it :smile:
I just looked into this, and it seems that the non-standardized part is the content of the first argument, not its presence. All shells pass the program name as the first argument, and the actual command line arguments after that. However, the program name is not something you can really rely on. Indeed, you can just set it to whatever you want using exec -a.
For example, consider this little program that prints the Env.exe_path! and all main! args:
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.22.0/F1JVZPYfWP71s8vk6tHcV1Qx1Ef6CZkwswGoCn8VHZmL.tar.zst" }
import cli.Env
main! = |args| {
dbg Env.exe_path!()
dbg args
Ok({})
}
Now let's run it in different ways:
% roc test.roc foo bar
[ROC DBG] Ok(Path.unix("/var/folders/[...]/test.roc"))
[ROC DBG] [OsStr.unix("test.roc"), OsStr.unix("foo"), OsStr.unix("bar")]
% (exec -a 'Hello, World!' roc test.roc foo bar)
[ROC DBG] Ok(Path.unix("/var/folders/[...]/test.roc"))
[ROC DBG] [OsStr.unix("test.roc"), OsStr.unix("foo"), OsStr.unix("bar")]
% roc build test.roc
[...]
% ./test foo bar
[ROC DBG] Ok(Path.unix("/Users/ageron/test"))
[ROC DBG] [OsStr.unix("./test"), OsStr.unix("foo"), OsStr.unix("bar")]
% (exec -a 'Hello, World!' ./test foo bar)
[ROC DBG] Ok(Path.unix("/Users/ageron/test"))
[ROC DBG] [OsStr.unix("Hello, World!"), OsStr.unix("foo"), OsStr.unix("bar")]
The last example seems pretty dangerous to me: an attacker could pretend that the executable sits in an arbitrary directory, even one they can't access. If the program relies on the program name to decide what to give access to, then all hell could break loose.
I think that's another good reason to separate the program name from the args.
Just submitted PR #494 to remove the program name from the main! args and make it available via Env.program_name!.
Last updated: Sep 24 2026 at 15:59 UTC