Announcing config-shellexpand: Tiny library for configuration placeholders in config-rs

I found myself needing to use placeholders in my configuration file, but config doesn't support them.

I found an open ticket about it and a draft PR, so I decided to write a small library (config-shellexpand) that implements it by combining the file sources from config with shellexpand.

config.toml

value = ${NUMBER_FROM_ENV}
    
[section]
name = "${NAME_FROM_ENV}"

main.rs

use config_shellexpand::TemplatedFile;
use config::Config;
    
let config: Config = Config::builder()
    .add_source(TemplatedFile::with_name(path))
    .build();

When loading, the contents of the files are read into memory, then expanded with shellexpand, and finally loaded using config's FileFormat, like non-expanded files.

You can optionally provide a Context (with_name_and_context) that is passed on to shellexpand for variable lookups if you want to source them from somewhere other than the environment (the tests use this a lot).

It also works with strings if you provide the file format (just like it works in config).

You forgot to include the link to your project :sweat_smile:

Found it anyway.

The project is minutes older than the original post here, and if I understand the comment about "combining the sources" plus CLAUDE.toml right, it's a project where AI copied the source code of two projects together.

Why did you do that instead of contributing upstream?

1 Like

Hah, sorry I missed the most important part.

It's quite obvious that you didn't read the actual code :wink:

It is not a copy of any source code (it depends on both config-rs and shellexpand).

The project wraps FileSource from config-rs and applies shell expansion from shellexpand before handing the contents over to config-rs' FileFormat to extract the value map.

I didn't contribute it back to config-rs because I needed the functionality yesterday and I'm not sure they want it. I've offered to merge it behind a feature if they're interested.

Use of Claude doesn't imply slop or copying. Most of the code is hand-written, but I have a Claude subscription, so I'd be dumb not to use it when it's appropriate.

I don't know where you get your timings from, but the implementation, testing and documentation took me about 3 hours. So it definitely pre-dates the post by more than minutes.

1 Like

Oh absolutely :sweat_smile:

I rechecked them, and I'm guessing I was at a boundary of "5h ago" here and on codeberg was actually separated by 2h, that's def on me, sorry.

The reason I am overly careful about such announcements is that we have around 2 per day that are slop and that unambiguously tick the "code is a few hours old and gets an announcement" box. There's a reason I commented and didn't take any actions, I saw you were an actual human doing work.

1 Like

No worries :slightly_smiling_face:

No go and use it - it's very useful :upside_down_face:

Especially if you happen to have complex configuration and run stuff in containers

I have the following snippet:

   {"path":"/piphp",
   "CGI": true,
   "ext": "php",
   "engine": "php-cgi",
   "headerless": false,
   "options":[{"name":"REDIRECT_STATUS", "value" : "CGI"},
       {"name":"SCRIPT_FILENAME", "value": "$SCRIPT_FILE"},
       {"name":"SERVER_ADDR", "value": "$IP"}],
   "translated": "./../side/Raspberry-Pi-Dashboard"}

Can I use your solution to expand it, or I need to add {} first?

I expect Rust code like:

let config: Config = Config::builder()
    .add_source(TemplatedFile::with_ip("127.0.0.1"))
   .add_source(TemplatedFile::with_script(script))
    .build();

Yes, it should solve that, however, you don't set the placeholders in the code - shellexpand fetches them from the environment automatically - with_name is called with the name of the configuration file (it mimics File::with_name from config-rs).

let config: Config = Config::builder()
    .add_source(TemplatedFile::with_name("path/to/config.json"))
    .build();