5
\$\begingroup\$

A lot of the time when working in DevOps, documentation and code snippets from colleagues will be based on bash. Most of those same commands work seamlessly in PowerShell; but setting environment variables doesn't; e.g. export DEMO_CLIENT_ID="ff3b7b84-e041-4e76-993e-239930808aa7".

I've written the below function so I can more easily just copy-paste these snippets and have them work without needing a rewrite:

Function Export {
    $regex = [System.Text.RegularExpressions.Regex]::new('^\s*([^=]+?)\s*=(.*)$')
    $result = $regex.Match(($args -join ' '))
    if (!$result.Success) {
        throw "Could not set an environment variable based on [$($args -join ' ')]"
    }
    [Environment]::SetEnvironmentVariable($result.Groups[1].Value, $result.Groups[2].Value)
}

This works well for most real world examples I've hit so far... but I'm sure there are gotchas lurking (e.g. I know that multiple spaces will be squashed to a single space when the split arguments are re-joined... which may be an issue in some scenarios).

Is there a better approach to this, or are there improvements I can make to avoid this and similar/unforeseen pitfalls?

Note: I know the function name doesn't meet best practices (i.e. verb-noun naming)... My only use case for this function is to simulate the export command; so I figured giving it a verb noun name and then aliasing that was just adding complexity with no benefit.

\$\endgroup\$
3
  • \$\begingroup\$ I've also submitted a suggestion to provide native support for export in pwsh: github.com/PowerShell/PowerShell/issues/20475 \$\endgroup\$
    – JohnLBevan
    Commented Oct 11, 2023 at 18:25
  • \$\begingroup\$ Where does this DEMO_CLIENT_ID="ff3b7b84-e041-4e76-993e-239930808aa7" string come from? What I mean: why not using the native hash table syntax: Export @{ DEMO_CLIENT_ID="ff3b7b84-e041-4e76-993e-239930808aa7"; Name="Value" }? \$\endgroup\$
    – iRon
    Commented Oct 12, 2023 at 8:37
  • 1
    \$\begingroup\$ @iRon That's just an example of how one would assign a value to an environment variable in shell script. I agree that a hash table would be a good approach if defining a new solution from scratch; but my aim is to make something that accepts the sorts of commands you see all over the web; e.g. developer.hashicorp.com/terraform/cli/config/… \$\endgroup\$
    – JohnLBevan
    Commented Oct 12, 2023 at 9:42

2 Answers 2

4
\$\begingroup\$
  • The PowerShell-idiomatic approach is to:

    • Define a command (function) that adheres to PowerShell's <verb>-<noun> naming convention, using one of the approved verbs.

    • Use an alias to provide another, typically shorter, name for the command, that isn't bound by this convention.

  • Using -split, the string splitting operator, limiting the number of resulting tokens to 2, allows you to split each name-value pair into its constituent parts, and the Env: drive can be used to define a process-scoped environment variable for each pair.

  • Given that the export builtin in POSIX-compatible shells such as Bash accepts multiple <name>=<value> pairs (e.g. export FOO='bar none' BAZ=2), so does the function below.

    • Caveat: POSIX-compatible shells allow defining environment variables without a value (e.g. export FOO), which PowerShell and .NET do not support: assigning $null or '' invariably undefines (removes) an environment variable there.
      Therefore, the function below only accepts <name>=<value> pairs, and reports a non-terminating error if a given argument lacks an = or a value thereafter.
Set-Alias export Set-EnvironmentVariableByNameValuePair

function Set-EnvironmentVariableByNameValuePair {
  param(
    [Parameter(Mandatory, ValueFromPipeline, ValueFromRemainingArguments)]
    [string[]] $NameValuePair
  )
  process {
    foreach ($pair in $NameValuePair) {
       $name, $value = $pair -split '=', 2
       if (-not $name -or -not $value) { Write-Error "Not in <name>=<value> format: `"$pair`". For technical reasons, you must supply a value."; continue }
       Set-Content Env:$name $value
    }
  }
}

Note:

  • The above is more permissive than POSIX-compatible shells with respect to the names of environment variables - even though names that do not adhere to the constraints spelled out below are supported by the underlying platforms themselves.

  • If you want to enforce the same name constraints that Bash does, insert the following before the Set-Content call:

    if ($name -notmatch '^[a-z_]\w*$') { Write-Error "Invalid name: $name"; continue } 
    
  • See also:

    • GitHub issue #3316, which asks for PowerShell to support the / something like the concise syntax that POSIX-compatible shells offer for defining child-process-scoped environment variables (e.g. FOO='bar none' BAZ=2 some_utility ...)
\$\endgroup\$
1
  • 1
    \$\begingroup\$ This is fantastic -- thank-you for improving it so much. I wasn't aware of the multiple assignment option; great catch there. \$\endgroup\$
    – JohnLBevan
    Commented Oct 12, 2023 at 9:40
1
\$\begingroup\$

Yes, you're quite right, the "export" name is just fine.

You started with an anchoring ^ caret -- kudos for that!

non-greedy

([^=]+?)\s*=(.*)

We have a "lazy" +? in there. I don't understand why.

Now, I will grant you that export EQUATION=y-x=0 potentially poses an issue, since second capturing group needs to be y-x=0. But you already dealt with that, very nicely, using the "all but = equal-sign" character class. There's no danger of first group capturing EQUATION=y-x. So the lazy ? quantifier seems redundant.

This leaves the Gentle Reader speculating that maybe the \s* optional whitespace match is relevant? But with no visible suite of test strings it's hard to see what Author anticipated. Please add a test suite.

Recommend you delete the lazy ? quantifier.

That \s* optional whitespace match makes perfect sense. Perhaps you would also like to strip leading blanks that come after the = equal-sign?

It is possible for bash to define an empty env var, e.g. $ export FOO=. But for your human-oriented use case, maybe you'd prefer .+ over .* ? So we will call out a truncated copy-n-paste with a fatal diagnostic message?


quoting, interpolation

Sometimes documentation will say things like

$ export FOO="bar"
$ export FOO='baz'
$ export FOO=$PATH:/my/directory

I feel that stripping balanced double-quotes, or single-quotes, would be a win.

Emulating $ dollar-sign interpolation might pose some challenges for you. Consider raising fatal error when you see one. The flip side is I have occasionally encountered credentials like this:

$ export VENDOR_API_SECRET='123speciaLcharacter$dollar*star'

(And then it's common enough to see wrong quoting produce 123speciaLcharacter*star on some production linux server.)

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for your response. Good point on the non-greedy piece already being taken care of by [^=]; my mistake. Great point also on quotes and dollars... I'll have a think on how those work, as they've special meanings in both languages, so could be complicated... \$\endgroup\$
    – JohnLBevan
    Commented Oct 11, 2023 at 19:54
  • \$\begingroup\$ I'll also add some examples for testing soon... great point. Getting late here now so probably won't be until tomorrow evening. \$\endgroup\$
    – JohnLBevan
    Commented Oct 11, 2023 at 19:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.