Safely access environment variables and system properties
There are two common methods of passing textual key/value data into a JVM when
it starts: environment variables and system properties. Environment variables
are taken from the shell environment within which the JVM is started, and
usually have uppercase names like PATH
or XDG_CONFIG_DIR
, while system
properties are specified as parameters to the java
command, and have
lowercase or camel-case, period-separated names like default.log.directory
.
The format of the values of each is dependent on the variable or property, but
a program running on the JVM will have expectations about the format of the
variables and properties it accesses, and it is desirable to parse them into
structured types as early as possible. This is the role of Ambience.
- typesafe access of environment variables and system properties
- types are inferred for known variables and properties, avoiding strings wherever possible
- concise syntax with idiomatic renaming of environment variables
- complete flexibility to use substitute environments
All Ambience terms and types are in the ambience
package.
import ambience.*
All other entities are imported explicitly.
An environment variable, such as XDG_DATA_DIRS
, can be accessed by applying
it as a Text
value, to the Environment
object, like so:
import gossamer.t
import environments.jvm
import contingency.strategies.throwUnsafely
val xdgDataDirs = Environment(t"XDG_DATA_DIRS")
Note that we import environments.jvm
because we want to use the environment
from the running JVM. It's possible to use alternative environments, for
example when testing, but normally we want environments.jvm
.
Environment variables typically use a naming scheme that is incongruous with
other identifiers in Scala. Environment variable names are typically in
uppercase, with multiple words separated by underscores. So an identifier which
would be written moduleDataHome
in Scala would be written MODULE_DATA_HOME
as an environment variable. Ambience can perform this translation
automatically, just by accessing the variable name as a dynamic member of the
Environment
object, for example,
val dirs = Environment.moduleDataHome
will access the environment variable MODULE_DATA_HOME
.
If the variable is not defined in the environment, an EnvironmentError
will
be raised.
It might be reasonably presumed that in the examples above, string values (as
Text
s) would be returned, and in general this is true, unless the variable is
known in which case, the variable will be parsed into a more appropriate
representation.
For a variable to be known, a contextual EnvironmentVariable
instance,
parameterized on the singleton type of the environment variable's name (in its
camel-case variant) and a result type, must be in scope. For example,
Environment.columns
(referring to the COLUMNS
environment variable) will
return an Int
thanks to the presence of an
EnvironmentVariable["columns", Int]
typeclass instance, which is provided by
Ambience since COLUMNS
is a standard POSIX environment variable name.
However, the moduleDataHome
example above will default to returning a Text
value, since no EnvironmentVariable
instance for the "moduleDataHome"
singleton literal type is provided by Ambience.
We can, of course, provide one. The EnvironmentVariable
trait defines two methods:
read
, which takes aText
value and returns a parsed value, andname
, which allows custom renamings from "Scala style" names to a particular environment variable, with a default implementation that can be overridden
Here is an example implementation for a process ID:
import anticipation.Text
import rudiments.Pid
import spectacular.decodeAs
given EnvironmentVariable["child", Pid] with
def read(value: Text): Pid = Pid(value.decodeAs[Int])
override def name: Text = t"CHILD_PID"
Now, accessing Environment.child
will read the CHILD_PID
environment
variable, parse it as an integer and construct a new Pid
instance. We could
also omit the override of name
and just access the value as
Environment.childPid
.
Note that value.decodeAs[Int]
, which is provided by
Spectacular, and uses a
Decoder[Int]
instance may, given the definition of Decoder[Int]
, fail with
a NumberError
if the CHILD_PID
variable contains a value which isn't an
integer. The capability to raise this error will be aggregated into the
Environment.child
call, ensuring that it will be handled.
Sometimes it is useful to specify a different source of environment variables than the JVM. This may be because we need to test a method which has been written to get a value from the environment, but we don't want to introduce the nondeterminism that arises when running the tests on different machines with different environments.
Another example presents itself if we want a method call (which takes an
Environment
) to see different values for certain environment variables, for
example, to ensure that the method accesses an SSH agent with a particular PID,
rather than the SSH agent specified in the SSH_AGENT_PID
environment
variable, captured when the JVM started.
The Environment
typeclass defines just a single apply
method for accessing
environment variables, taking a Text
value of the environment variable name,
and returning a Maybe[Text]
, with Unset
indicating that the environment
variable is not specified. It is distinct from the empty string.
For example, given a Map[Text, Text]
of environment variables, vars
, we
could create a new Environment
with the following given
definition:
import vacuous.Unset
val vars = Map(t"HOME" -> t"/home/root")
given Environment = vars.getOrElse(_, Unset)
Together, these facilities provide access to environment variables which is idiomatic (with Scala-style identifiers), typesafe (using checked errors if parsing fails), concise (types can be inferred) and flexible (allowing substitution of entire environments).
Support for system properties is provided in much the same way as for environment variables:
- access is provided through the
Properties
object, byText
value or dynamic member access systemProperties.jvm
provides the standard system properties from the JVM- alternative instances of
SystemProperties
can be defined to provide substitute values SystemProperty
provides the same functionality asEnvironmentVariable
- a
SystemPropertyError
will be raised instead of anEnvironmentError
There is no need to rename system properties, since they already follow the
familiar Scala identifier style. Access through the Properties
object is
slightly different from Environment
, though: since property names use a
"dotted" format, they can be accessed as dynamic members of the Properties
object, for example,
import systemProperties.jvm
val home = Properties.user.home()
or,
val dir = Properties.db.user.cache.dir()
where the empty parentheses are necessary to signal that the path representing the property name has been specified, and its value should be retrieved. The retrieval itself works in much the same way as for environment variables.
Ambience is classified as fledgling. For reference, Soundness projects are categorized into one of the following five stability levels:
- embryonic: for experimental or demonstrative purposes only, without any guarantees of longevity
- fledgling: of proven utility, seeking contributions, but liable to significant redesigns
- maturescent: major design decisions broady settled, seeking probatory adoption and refinement
- dependable: production-ready, subject to controlled ongoing maintenance and enhancement; tagged as version
1.0.0
or later - adamantine: proven, reliable and production-ready, with no further breaking changes ever anticipated
Projects at any stability level, even embryonic projects, can still be used, as long as caution is taken to avoid a mismatch between the project's stability level and the required stability and maintainability of your own project.
Ambience is designed to be small. Its entire source code currently consists of 265 lines of code.
Ambience will ultimately be built by Fury, when it is published. In the meantime, two possibilities are offered, however they are acknowledged to be fragile, inadequately tested, and unsuitable for anything more than experimentation. They are provided only for the necessity of providing some answer to the question, "how can I try Ambience?".
-
Copy the sources into your own project
Read the
fury
file in the repository root to understand Ambience's build structure, dependencies and source location; the file format should be short and quite intuitive. Copy the sources into a source directory in your own project, then repeat (recursively) for each of the dependencies.The sources are compiled against the latest nightly release of Scala 3. There should be no problem to compile the project together with all of its dependencies in a single compilation.
-
Build with Wrath
Wrath is a bootstrapping script for building Ambience and other projects in the absence of a fully-featured build tool. It is designed to read the
fury
file in the project directory, and produce a collection of JAR files which can be added to a classpath, by compiling the project and all of its dependencies, including the Scala compiler itself.Download the latest version of
wrath
, make it executable, and add it to your path, for example by copying it to/usr/local/bin/
.Clone this repository inside an empty directory, so that the build can safely make clones of repositories it depends on as peers of
ambience
. Runwrath -F
in the repository root. This will download and compile the latest version of Scala, as well as all of Ambience's dependencies.If the build was successful, the compiled JAR files can be found in the
.wrath/dist
directory.
Contributors to Ambience are welcome and encouraged. New contributors may like to look for issues marked beginner.
We suggest that all contributors read the Contributing Guide to make the process of contributing to Ambience easier.
Please do not contact project maintainers privately with questions unless there is a good reason to keep them private. While it can be tempting to repsond to such questions, private answers cannot be shared with a wider audience, and it can result in duplication of effort.
Ambience was designed and developed by Jon Pretty, and commercial support and training on all aspects of Scala 3 is available from Propensive OÜ.
An ambience is a sense derived from the surrounding environment, and Ambience provides sensible access to the "surrounding environment" of a Scala program.
In general, Soundness project names are always chosen with some rationale, however it is usually frivolous. Each name is chosen for more for its uniqueness and intrigue than its concision or catchiness, and there is no bias towards names with positive or "nice" meanings—since many of the libraries perform some quite unpleasant tasks.
Names should be English words, though many are obscure or archaic, and it should be noted how willingly English adopts foreign words. Names are generally of Greek or Latin origin, and have often arrived in English via a romance language.
The logo depicts the upper atmosphere of an imagined planet, alluding to the synonymous meaning of "ambience".
Ambience is copyright © 2024 Jon Pretty & Propensive OÜ, and is made available under the Apache 2.0 License.