Process.run load environnement by sourcing a file

Hi guys, how are you ?

I have a question . Is there a way to load an environment but from a file by sourcing it ?

Because in a case, I need to run a process but by sourcing first a file

That’s a feature in POSIX shell. You can spawn a shell, execute the source command (.) in it to evaluate the env file and then run your process.

Process.run(". env.sh; my-process", shell: true)

This only works on POSIX systems, of course.
If you want a cross-platform solution, you’ll need to parse the environment file in Crystal and populate an env object which you can pass to Process.run. For simple key=value assignments this should be trivial. But more powerful features are difficult because you have no shell available.

1 Like

Mmmh okay, so it was the way I was thinking to do. Thank you man :slightly_smiling_face:

There are also shards that can do this if you need to be shell-agnostic. The one I use the most is gdotdesign/cr-dotenv and loads from the file .env by default.

require "dotenv"

# raises if the `.env` file doesn't exist
Dotenv.load

# ignores when the `.env` file doesn't exist
Dotenv.load?

# can also specify a different filename
Dotenv.load? ".env.test"

There’s also an Athena component for this that comes with some additional features: Dotenv. Might have to steal that .load? method tho :stuck_out_tongue:.

IIUC both cr-dotenv and Athena Dotenv load the environment variables into the parent process and thus pollutes its environment.
This may be unintended and is generally intended for a different use case, loading enviornment configuration for the Crystal app itself, not any process it spawns.
And these shards have the same limitation that they do not actually source the file like a shell would. They just read key-value pairs written in a subset of shell syntax.

You could use env: setting of Process.run. That is expecting a Hash(String, String) in order to set that to the child process.

You could be reading the file that contains your variables, build that Hash and then pass that to Process.run:

env_hash = {
  "FOO" => "1",
  "BAR" => "20",
}

Process.run("env", shell: true, env: env_hash, output: :inherit, error: :inherit)
$ crystal run script.cr
HOSTNAME=d6ef2b43ef02
SHLVL=3
HOME=/root
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
BAR=20
FOO=1
PWD=/app

I hope that helps.

Cheers.

2 Likes

sh -c 'env' and shell: true is a tautology. It spawns a shell inside a shell with no apparent benefit. You can drop either of the two. The end result would be identical.

:person_facepalming:

Updating my code example! Thank you!

There is https://athenaframework.org/Dotenv/top_level/#Athena::Dotenv#parse(data,path) that can parse a String (from reading a .env file) into a Hash(String, String) that could then be passed to Process.run. That should do the trick, while still supporting the same features as if you were using .load.

Could potentially add some additional overloads to allow parsing it from a Path, or IO if that would be helpful tho.

This doesn’t appear to be happening for me with cr-dotenv. When I terminate my program that loads env vars from .env, those env vars do not exist in the parent shell.

I mean the Crystal process from which you spawn the other process.

For example, Dotenv.load; Process.run("foobar") makes the environment configuration available in the sub process (foobar) but also in the Crystal program itself (which seems to be an unintended side-effect).

1 Like

I will stick on that, thanks a lot