Environment variables behave much more like dynamic variables (AKA dynamically scoped/bound variables), rather than global variables https://en.wikipedia.org/wiki/Scope_(computer_science)#Dynam...
In particular, we can override env vars for a particular call, without affecting anything else. For example, consider the following script:
echo "BEFORE $FOO"
echo "AFTER $FOO"
Env vars are mutable, and mutating an env var acts differently to mutating a dynamic variable: dynamic scope looks up variables on the call stack, so mutations high up the stack will be visible after returning. Instead, env vars are copied from parent to child at each new scope (process), so mutations are only visible to that process and any subsequent subprocesses. In fact, that makes env vars even less 'globalish' and 'mutableish' than ordinary dynamic variables!
For example: if 'printFoo' finished by mutating 'FOO' to equal 'baz', it wouldn't affect the above script at all. Even line 2, which "inherits" the script's FOO, would only be mutating its own copy of 'FOO', which doesn't affect the script's variable.
In any case I highly recommend to avoid mutating env vars, for the same reason I avoid mutating any variables, regardless of language; unless there's a specific reason to. If we treat env vars in an immutable way, then they act exactly like dynamic variables.
As an example of dynamic variables, consider the following Lisp code:
(write-line (concatenate "BEFORE " FOO))
(let ((FOO "bar"))
(write-line (concatenate "AFTER " FOO)
Not only do I find env vars very useful for config, I also find dynamic scope is very under-utilised in "proper" (non-shell) languages. For example, dynamic scope is a great way to do dependency injection: rather than passing around extra arguments, or adding a bunch of private fields to objects, etc. we can just reference the dependency with a dynamic variable, and open a new scope whenever we want to set its value (at an application's entry point, or in a test, etc.)