1

I have a .properties file with the following properties in them:

repository.host=hostname.com/nexus
repository.api.url=https://${repository.host}/service/rest/v1
repository.url=https://${repository.host}/repository

I am able to return the values using the following powershell function:

    static [string] getProperty( [string] $property ){

        $properties = "../resources/vars/$([jenkins]::PROPERTIES_FILE)"
        $properties = get-content $properties | convertfrom-stringdata

        return $properties.$property

    }

When attempting to return the property repository.url powershell return this string: https://${repository.host}/repository/

My question is: Is it possible through features that already exist in powershell for the returned string to be https://hostname.com/nexus/repository/?

2 Answers2

0

By design, for security reasons, ConvertFrom-StringData does not perform string expansion (interpolation) on its input.

Assuming you trust the input string[1], you can perform the expansion on demand, after having read the values from the file.

Note that use of ConvertFrom-StringData is problematic, as you've discovered, because the hashtable it creates invariably has unordered keys; that is, the order of the entries does not reflect the order in which the properties are defined in the file. Therefore, processing the hashtable entries can make the on-demand expansion fail, if an out-of-order entry is processed before another entry whose value it needs for the expansion.

The solution is to roll your own ConvertFrom-StringData variant[2] that reads the properties into an ordered hashtable. This additionally allows you to combine the read-from-file and expansion-on-demand tasks:

# Create a sample properties file.
@'
repository.host=hostname.com/nexus
repository.api.url=https://${repository.host}/service/rest/v1
repository.url=https://${repository.host}/repository
'@ > sample.properties

# Parse the file and build an *ordered* hashtable from it.
$orderedHash = [ordered] @{}
switch -Regex -File sample.properties {
  '^\s*#|^\s*$' { continue } # skip comment and blank lines.
  default {
    # Determine the key and value...
    $key, $value = $_ -split '=', 2
    # ... and create the entry while expanding ${...} references to previous
    #     entries.
    $orderedHash[$key.Trim()] = $ExecutionContext.InvokeCommand.ExpandString((
      $value.Trim() -replace '\$\{([^}]+)\}', '$$($$orderedHash[''$1''])'
    ))  
  }
}

# Output the result.
$orderedHash
  • Note the use of method $ExecutionContext.InvokeCommand.ExpandString to perform on-demand string expansion (interpolation); since this method isn't easy to discover, GitHub issue #11693 proposes that this functionality be surfaced as a proper, easily discoverable cmdlet named something like Expand-String or Expand-Template.

    • Note: In order to be able to use $ExecutionContext from the method of a PS custom class, you must explicitly reference it in the global scope via $global:ExecutionContext.
  • For more information about the regex-based -replace operator, see this answer.


The above yields (note that the input order was maintained):

Name                           Value
----                           -----
repository.host                hostname.com/nexus
repository.api.url             https://hostname.com/nexus/service/rest/v1
repository.url                 https://hostname.com/nexus/repository

[1] Via $(), the subexpression operator, it is possible to embed arbitrary commands in the input strings.

[2] The code below does not replicate all features of ConvertFrom-String data, but it works with the sample input. While it does support skipping comment lines (those whose first non-whitespace character is a #) and blank lines, treating \ as escape characters and supporting escape sequences such as \n for a newline is not implemented.

mklement0
  • 382,024
  • 64
  • 607
  • 775
0

The original solution provided by @mklement0 was very useful, and has guided me towards a more complete solution. This solution accomplishes/corrects a couple of things:

  • The ability to create the hashtable from a file source.
  • The ability to access the $ExecutionContext variable from within a class method, using the $global: scope.
  • The ability to thoroughly parse all keys within the hashtable.
    static [string] getProperties ( [string] $file, [string] $property ){

        $properties = get-content $file -raw | convertfrom-stringdata

        while ( $properties.values -match '\$\{([^}]+)\}' ){
            foreach ($key in @($properties.Keys)) {
                $properties[$key] = $global:ExecutionContext.InvokeCommand.ExpandString( ($properties[$key] -replace '\$\{([^}]+)\}', '$$($$properties[''$1''])') )
            }
        }

        return $properties[$property]
    }

Note: When the while loop is not present and searching matches of ${*}, any given returned value may not be completely interpolated or expanded. As an example without the while loop present output from a file may look like this:

/nexus
${nexus.protocol}://${nexus.hostname}:${nexus.port}${nexus.context}
${nexus.protocol}://${nexus.hostname}:${nexus.port}${nexus.context}/repository/installers/com/amazon/java/8.0.252/java-1.8.0-amazon-corretto-devel-1.8.0_252.b09-1.x86_64.rpm
${nexus.protocol}://${nexus.hostname}:${nexus.port}${nexus.context}
${nexus.protocol}://${nexus.hostname}:${nexus.port}${nexus.context}/repository/installers/com/oracle/tuxedo/12.1.3.0.0/p30596495_121300_Linux-x86-64.zip
443
https://hostname.com:443/nexus
https://hostname.com:443/nexus/repository/installers/com/oracle/java/jdk/8u251/jdk-8u251-linux-x64.rpm
https://hostname.com:443/nexus/repository/installers/com/oracle/weblogic/12.2.1.3.0/p30965714_122130_Generic.zip
hostname.com
https
https://hostname.com:443/nexus/repository/installers/com/oracle/weblogic/12.2.1.3.0/p30965714_122130_Generic.zip

And if you were to run the same script again (still without the while loop) would look like this:

hostname.com
https://hostname.com:443/nexus
/nexus
https://hostname.com:443/nexus
https://hostname.com:443/nexus
https://hostname.com:443/nexus/repository/installers/com/oracle/weblogic/12.2.1.3.0/p30965714_122130_Generic.zip
https://${nexus.hostname}:443/nexus/repository/installers/com/oracle/java/jdk/8u251/jdk-8u251-linux-x64.rpm
https://hostname.com:443/nexus/repository/installers/com/oracle/tuxedo/12.1.3.0.0/p30596495_121300_Linux-x86-64.zip
https://hostname.com:443/nexus/repository/installers/com/amazon/java/8.0.252/java-1.8.0-amazon-corretto-devel-1.8.0_252.b09-1.x86_64.rpm
443
https
https://${nexus.hostname}:443/nexus/repository/installers/com/oracle/weblogic/12.2.1.3.0/p30965714_122130_Generic.zip

The reason for the sometimes incompletely interpolated/expanded strings is because hashtables are naturally unordered. With the introduction of the while loop, results will not be returned until all interpolated/expanded strings are resolved.

The official output would look as such:

hostname.com
https://hostname.com:443/nexus
/nexus
https://hostname.com:443/nexus
https://hostname.com:443/nexus
https://hostname.com:443/nexus/repository/installers/com/oracle/weblogic/12.2.1.3.0/p30965714_122130_Generic.zip
https://hostname.com:443/nexus/repository/installers/com/oracle/java/jdk/8u251/jdk-8u251-linux-x64.rpm
https://hostname.com:443/nexus/repository/installers/com/oracle/tuxedo/12.1.3.0.0/p30596495_121300_Linux-x86-64.zip
https://hostname.com:443/nexus/repository/installers/com/amazon/java/8.0.252/java-1.8.0-amazon-corretto-devel-1.8.0_252.b09-1.x86_64.rpm
443
https
https://hostname.com:443/nexus/repository/installers/com/oracle/weblogic/12.2.1.3.0/p30965714_122130_Generic.zip