diff --git a/Readme.adoc b/Readme.adoc index ac5b7dc..20da532 100644 --- a/Readme.adoc +++ b/Readme.adoc @@ -1667,6 +1667,19 @@ val sha = os.spawn(cmd = ("shasum", "-a", "256"), stdin = gzip.stdout) sha.stdout.trim ==> "acc142175fa520a1cb2be5b97cbbe9bea092e8bba3fe2e95afa645615908229e -" ---- +==== Customizing the default environment + +Client-server CLI applications sometimes want to run subprocesses on the server based on the environment of the client. +It is possible to customize the default environment passed to subprocesses by setting the `os.SubProcess.env` threadlocal: + +[source,scala] +---- +val clientEnvironment: Map[String, String] = ??? +os.SubProcess.env.withValue(clientEnvironment) { + os.call(command) // clientEnvironment is passed by default instead of the system environment +} +---- + == Spawning Pipelines of Subprocesses After constructing a subprocess with `os.proc`, you can use the `pipeTo` method diff --git a/build.sc b/build.sc index 8ef32ea..de9903f 100644 --- a/build.sc +++ b/build.sc @@ -51,7 +51,8 @@ trait SafeDeps extends ScalaModule { } trait MiMaChecks extends Mima { - def mimaPreviousVersions = Seq("0.9.0", "0.9.1", "0.9.2", "0.9.3", "0.10.0") + def mimaPreviousVersions = + Seq("0.9.0", "0.9.1", "0.9.2", "0.9.3", "0.10.0", "0.10.1", "0.10.2", "0.10.3", "0.10.4") override def mimaBinaryIssueFilters: T[Seq[ProblemFilter]] = Seq( ProblemFilter.exclude[ReversedMissingMethodProblem]("os.PathConvertible.isCustomFs"), // this is fine, because ProcessLike is sealed (and its subclasses should be final) diff --git a/os/src/ProcessOps.scala b/os/src/ProcessOps.scala index 389b34f..267a67e 100644 --- a/os/src/ProcessOps.scala +++ b/os/src/ProcessOps.scala @@ -486,17 +486,28 @@ private[os] object ProcessOps { val environment = builder.environment() - if (!propagateEnv) { - environment.clear() - } - - if (env != null) { - for ((k, v) <- env) { - if (v != null) builder.environment().put(k, v) - else builder.environment().remove(k) + def addToProcessEnv(env: Map[String, String]) = + if (env != null) { + for ((k, v) <- env) { + if (v != null) environment.put(k, v) + else environment.remove(k) + } } + + os.SubProcess.env.value match { + case null => + if (!propagateEnv) { + environment.clear() + } + case subProcessEnvValue => + environment.clear() + if (propagateEnv) { + addToProcessEnv(subProcessEnvValue) + } } + addToProcessEnv(env) + builder.directory(Option(cwd).getOrElse(os.pwd).toIO) builder diff --git a/os/src/SubProcess.scala b/os/src/SubProcess.scala index 075b2a8..120ab3f 100644 --- a/os/src/SubProcess.scala +++ b/os/src/SubProcess.scala @@ -162,6 +162,12 @@ class SubProcess( object SubProcess { + /** + * The env passed by default to child processes. + * When `null`, the system environment is used. + */ + val env = new scala.util.DynamicVariable[Map[String, String]](null) + /** * A [[BufferedWriter]] with the underlying [[java.io.OutputStream]] exposed * diff --git a/os/test/src/SubprocessTests.scala b/os/test/src/SubprocessTests.scala index 16eefe9..ac01bd4 100644 --- a/os/test/src/SubprocessTests.scala +++ b/os/test/src/SubprocessTests.scala @@ -146,6 +146,28 @@ object SubprocessTests extends TestSuite { } } } + test("envWithValue") { + if (Unix()) { + val variableName = "TEST_ENV_FOO" + val variableValue = "bar" + def envValue() = os.proc( + "bash", + "-c", + s"""if [ -z $${$variableName+x} ]; then echo "unset"; else echo "$$$variableName"; fi""" + ).call().out.lines().head + + val before = envValue() + assert(before == "unset") + + os.SubProcess.env.withValue(Map(variableName -> variableValue)) { + val res = envValue() + assert(res == variableValue) + } + + val after = envValue() + assert(after == "unset") + } + } test("multiChunk") { // Make sure that in the case where multiple chunks are being read from // the subprocess in quick succession, we ensure that the output handler