Repository: mkdocs/mkdocs

Issue:
Environment variables in config file
Different environments may have different configurations such `site_url`.

It would be great to be able to set any attribute via env var. Some plugins implement this on their own but OTB solution would be very needed.

This is more general then #1827

Current solution looks funky:

```
site_name: !!python/object/apply:os.getenv ["CI_PROJECT_PATH"]  
repo_url: !!python/object/apply:os.getenv ["CI_PROJECT_URL"]
repo_name: !!python/object/apply:os.getenv ["CI_PROJECT_PATH"]
```

Something like following is more readable 

`site_name: ${CI_PROJECT_PATH}`


Discussion:
I am having similar issues when dealing with different OSes on same sources. 

My path used for a shared theme is different whether I am on Linux (origin) or Windows (dev machine with nfs mapped disks).

Different targets/environments might work. Maybe having multiple `mkdocs.yml` files? 
BTW, #1827 doesnt work always, just for strings, which makes this proposal even more needed.

I asked for help about this to no avail:
https://stackoverflow.com/questions/59680815/setting-non-string-variable-using-pyyaml
> Maybe having multiple `mkdocs.yml` files?

This is how MkDocs was built to handle this sort of thing. Using the `-f/--config-file` flag you can pass in a different config file. Note that the file does not *need* to be named `mkdocs.yml`. That is simply the default value for the `--config-file` flag. You could have any number of different config files. Then you can set a environment variable which points to the appropriate config file and pass that env variable to the flag.

```
mkdocs build --config-file "$CONFIG_FILE"
```
That is OK, but not DRY, and will **significantly** slow you down (i.e. adding nav section on 4 environments ? right). There need to either be base environment that can be overriden or env vars for meaningfull and efficent work on it.
> Using the `-f/--config-file` flag you can pass in a different config file

Oops, I omit the _nested_ command helps, I was checking only the top level `-h`. Thanks, that helps!

I came across this: 
https://stackoverflow.com/questions/528281/how-can-i-include-a-yaml-file-inside-another
which mentions: https://github.com/tanbro/pyyaml-include

It might be worth throwing some custom Yaml loader commands here. With such inheritance one could use _base_ file and target wise _prod_/_dev_ which would include the first one. One command for include, one for env variables (with .env support?). This would be really handy (having both).


>  https://github.com/tanbro/pyyaml-include

I wasn't aware that that lib existed. We get requests to add support for "include" to our config files with some regularity. Our answer has always been that we don't support it because YAML doesn't support it. And we have no interest in building and maintaining a one-off solution. However, if a third party lib exists which we simply need to call, then we may be more open to adding support. A PR would be given serious consideration.
Config path makes sense if it can be overriden. Take docker-compose for example, you can add multiple config files and each next may contain overrides (and has env vars nevertheless). Things mentioned by @maciejmatczak are also great!

I think env var is probably trivial to support and will be most flexible but having both is the best.

> I wasn't aware that that lib existed. We get requests to add support for "include" to our config files with some regularity. 

This always comes up. Gitlab recently added it in CI/CD yaml config as it was coming up for years.
Guys docs are meant to be built, it means, multi site, multi context, multi environments, please make it dynamic to be more DevOps friendly. I would add that a DotEnv support is also a very big improvement. just like docker-compose do.
Exactly. 

Multi context would require some more work besides env vars and I do happen to have that on almost 100% of the cases - different subset of NAV section per environment.

Example:

1. _Staging_ environment contains all types of docs - functional, technical, user manual etc. It is installed internally on stakeholder infrastructure.
2. _Production_ env has only user manual, which is only 1 part of the complete docs project. It is publically available which means that end users shouldn't see internal IT stuff of the project. It is good to keep them together as stuff between different consumer groups are constantly reused. 

Currently I mark with comment lines sections that should be added to specific environments and then build servers removes stuff and constructs new mkdocs.yml that differs only in NAV section.

While that is defnitelly part of different issue, I think that NAV section should be moved out of the config and if there is motivation for this, I can do detailed spec with examples and benefits versus current approach.
FYI I recently posted a proposal at #2218 which discusses various options for addressing complex configs. That discussion does not specifically address environment variables (which is the subject of this issue), but some of the other things mentioned in comments here are directly addressed there. Please leave any feedback at that issue.

Regarding environment variables, I expect we will be adding something for that separately from #2218. Look for a PR in the near future.
I recently created [waylan/pyyaml-env-tag][1], which provides for a nice and clean way to include the values of environment variables in YAML files with simple types being preserved. It should be relatively simple to incorporate that into MkDocs' Yaml loader. However, before I do, will this sufficiently address the concerns raised here?

[1]: https://github.com/waylan/pyyaml-env-tag
Look OK!

Resolving patch:
diff --git a/mkdocs/utils/__init__.py b/mkdocs/utils/__init__.py
--- a/mkdocs/utils/__init__.py
+++ b/mkdocs/utils/__init__.py
@@ -17,6 +17,7 @@
 import functools
 from datetime import datetime, timezone
 from urllib.parse import urlparse
+from yaml_env_tag import construct_env_tag
 
 from mkdocs import exceptions
 
@@ -56,6 +57,10 @@ class Loader(loader):
     # will be unicode on translation.
     Loader.add_constructor('tag:yaml.org,2002:str', construct_yaml_str)
 
+    # Attach Environment Variable constructor.
+    # See https://github.com/waylan/pyyaml-env-tag
+    Loader.add_constructor('!ENV', construct_env_tag)
+
     try:
         return yaml.load(source, Loader)
     finally:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -61,7 +61,8 @@ def get_packages(package):
         'Markdown>=3.2.1',
         'PyYAML>=3.10',
         'tornado>=5.0',
-        'ghp-import>=1.0'
+        'ghp-import>=1.0',
+        'pyyaml_env_tag>=0.1'
     ],
     python_requires='>=3.6',
     entry_points={


Test patch:
diff --git a/mkdocs/tests/utils/utils_tests.py b/mkdocs/tests/utils/utils_tests.py
--- a/mkdocs/tests/utils/utils_tests.py
+++ b/mkdocs/tests/utils/utils_tests.py
@@ -305,6 +305,28 @@ def test_unicode_yaml(self):
         self.assertTrue(isinstance(config['key'], str))
         self.assertTrue(isinstance(config['key2'][0], str))
 
+    @mock.patch.dict(os.environ, {'VARNAME': 'Hello, World!', 'BOOLVAR': 'false'})
+    def test_env_var_in_yaml(self):
+
+        yaml_src = dedent(
+            '''
+            key1: !ENV VARNAME
+            key2: !ENV UNDEFINED
+            key3: !ENV [UNDEFINED, default]
+            key4: !ENV [UNDEFINED, VARNAME, default]
+            key5: !ENV BOOLVAR
+            '''
+        )
+        config = utils.yaml_load(yaml_src)
+        self.assertIsInstance(config['key1'], str)
+        self.assertEqual(config['key1'], 'Hello, World!')
+        self.assertIsNone(config['key2'])
+        self.assertIsInstance(config['key3'], str)
+        self.assertEqual(config['key3'], 'default')
+        self.assertIsInstance(config['key4'], str)
+        self.assertEqual(config['key4'], 'Hello, World!')
+        self.assertIs(config['key5'], False)
+
     def test_copy_files(self):
         src_paths = [
             'foo.txt',