1

Is it possible to assign a string value to a variable of a different type given that the data type is not known in advance? For example, in the sample below, how do I update the values of the $values hash without changing their data types:

$values = @{
    "Boolean" = $true
    "Int"     = 5
    "DateTime"= (Get-Date)
    "Array"   = @("A", "B", "C")
}

$stringValues = @{
    "Boolean" = 'false'
    "Int"     = '10'
    "DateTime"= '2019-01-02 14:45:59.146'
    "Array"   = '@("X", "Y", "Z")'
}

"INITIAL VALUES:"
foreach ($key in $values.Keys) {
    ($key + " = " + $values[$key] + " (" + $values[$key].GetType().FullName + ")")
}

"`nUPDATING..."
foreach ($key in $stringValues.Keys) {
    $values[$key] = $stringValues[$key]
}

"`nUPDATED VALUES:"
foreach ($key in $values.Keys) {
    ($key + " = " + $values[$key] + " (" + $values[$key].GetType().FullName + ")")
}

OUTPUT:

INITIAL VALUES:
DateTime = 04/23/2019 16:54:13 (System.DateTime)
Array = A B C (System.Object[])
Boolean = True (System.Boolean)
Int = 5 (System.Int32)

UPDATING...

UPDATED VALUES:
DateTime = 2019-01-02 14:45:59.146 (System.String)
Array = @("X", "Y", "Z") (System.String)
Boolean = false (System.String)
Int = 10 (System.String)

I need the updated values to match the original data types and not just get converted to System.String.

I am flexible on the contents of the strings. E.g. a string holding a boolean false value may be $false/false/[boolean]false/[boolean]$false/etc or a string holding an array may use a different formatting (basically, whatever is easier to convert the string to a proper data type).

In essence, I want to simulate whatever the ConvertFrom-Json cmdlet does when it sets the object property from a JSON string, only in my case, I do not have a JSON structure.

(In case someone wonders what I'm trying to do: I am trying to add an INI file parser to my ConfigFile module, and no, I cannot just use a hash to return the INI settings; I need to load the values into the corresponding PSVariables and for this to work, I need to convert strings to proper data types.)

0

4 Answers 4

2

So you want to cast/convert the new value to the type of the old value.

The idea needs to cast from a variable,
here is a related question powershell-type-cast-using-type-stored-in-variable

The answer suggest:

You can roughly emulate a cast using the following method: [System.Management.Automation.LanguagePrimitives]::ConvertTo($Value, $TargetType)

The following changed routine shows: it isn't that simple, especially when the new data needs overloads/other parameters in the conversion.

"UPDATING..."
foreach ($key in $stringValues.Keys) {
    $values[$key] = [System.Management.Automation.LanguagePrimitives]::ConvertTo(
                    $stringValues[$key], $values[$key].gettype())
}

My German locale error message:

Ausnahme beim Aufrufen von "ConvertTo" mit 2 Argument(en): "Der Wert "2019-01-02 14:45.59.146" kann nicht in den Typ "System.DateTime" konvertiert werden. Fehler: "Die Zeichenfolge wurde nicht als gültiges DateTime erkannt."" In Zeile:2 Zeichen:5 + $values[$key] = [System.Management.Automation.LanguagePrimitives] ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [], MethodInvocationException + FullyQualifiedErrorId : PSInvalidCastException

And the unsufficient result:

DateTime = 04/24/2019 09:49:19 (System.DateTime)
Array = @("X", "Y", "Z") (System.Object[])
Boolean = True (System.Boolean)
Int = 10 (System.Int32)

You may elaborate yourself on this idea, handling old types/new data more individually.

Sign up to request clarification or add additional context in comments.

4 Comments

Nice approach; the only reason the ConvertTo() fails here is that there's a presumed typo in the date-time string: 14:45.59.146 should be 14:45:59.146, in which case it would work. Note that conversion is always based on the invariant culture and therefore not sensitive to what culture is in effect.
The issue, though, is that [System.Management.Automation.LanguagePrimitives]::ConvertTo() doesn't work with arrays: you'll get a single-element array whose element is the entire input string, @("X", "Y", "Z"), which is not the intent.
I like this approach, but why does the boolean value gets set to 'True' from the string 'false'? I tried '$false' and it still gets set to true.
As @mklement0 already stated in the comment to his answer, coercing a string to boolean (what allows if ($var){...} returns $true if nonempty.
2

You can use a custom class in lieu of a hashtable; unlike hashtable keys, the properties of custom classes can be specifically typed.

  • With scalar values, you can then simply let PowerShell perform the from-string conversion for you - except that Boolean strings need special treatment (see comments in source code below).

  • With arrays, things are trickier. The solution below uses [System.Management.Automation.PSParser]::Tokenize() to parse the string, but is currently limited to recognizing string and number literals.

    • Note: It is tempting to use Invoke-Expression on the entire array, but that would be a security risk, because it opens to the door to arbitrary code execution. While there are legitimate uses - such as on a string known to represent a number below - Invoke-Expression should generally be avoided.

(If you don't want to define classes, you can derive the types from the values of hashtable $values and use [System.Management.Automation.LanguagePrimitives]::ConvertTo() to convert the strings to those types, as shown in LotPings' answer, though note that arrays and Booleans still need special treatment as shown below.)

# Define a custom [Values] class
# with specifically typed properties.
class Values {
  [bool]     $Boolean
  [int]      $Int
  [datetime] $DateTime
  [Array]    $Array
}

# Instantiate a [Values] instance
$values = [Values] @{
  Boolean = $true
  Int     = 5
  DateTime= (Get-Date)
  Array   = @("A", "B", "C")
}

$stringValues = @{
  Boolean = 'false'
  Int     = '10'
  DateTime = '2019-01-02 14:45:59.146'
  Array   = '@("X", "Y", "Z")'
}

"INITIAL VALUES:"
foreach ($key in $values.psobject.properties.Name) {
  ($key + " = " + $values.$key + " (" + $values.$key.GetType().FullName + ")")
}

""
"UPDATING..."

foreach ($key in $stringValues.Keys) {
  switch ($key) {
    'Array' {
      # Parse the string representation.
      # Assumptions and limitations:
      #  The array is flat.
      #  It is sufficient to only support string and numeric constants.
      #  No true syntax validation is needed.
      $values.$key = switch ([System.Management.Automation.PSParser]::Tokenize($stringValues[$key], [ref] $null).Where( { $_.Type -in 'String', 'Number' })) {
        { $_.Type -eq 'String' } { $_.Content; continue }
        { $_.Type -eq 'Number' } { Invoke-Expression $_Content; continue }
      }
      continue
    }
    'Boolean' {  # Boolean scalar
      # Boolean strings need special treatment, because PowerShell considers
      # any nonempty string $true
      $values.$key = $stringValues[$key] -notin 'false', '$false'
      continue
    }
    default { # Non-Boolean scalar
      # Let PowerShell perform automatic from-string conversion
      # based on the type of the [Values] class' target property.
      $values.$key = $stringValues[$key]
    }
  }
}

""
"UPDATED VALUES:"
foreach ($key in $values.psobject.properties.Name) {
  ($key + " = " + $values.$key + " (" + $values.$key.GetType().FullName + ")")
}

This yields:

INITIAL VALUES:
Boolean = True (System.Boolean)
Int = 5 (System.Int32)
DateTime = 04/24/2019 14:45:29 (System.DateTime)
Array = A B C (System.Object[])

UPDATING...

UPDATED VALUES:
Boolean = True (System.Boolean)
Int = 10 (System.Int32)
DateTime = 01/02/2019 14:45:59 (System.DateTime)
Array = X Y Z (System.Object[])

6 Comments

Unfortunately, I cannot use a custom class (this is just a sample, but in the actual code, I need to assign the values to the script's PSVariables). Great idea on handling the array. Thanks.
@AlekDavis. I see - so where does the type information come from?
I can get it from the original PSVariable object (such as parameter or script variable).
Btw, why does the boolean value show True after the update?
@AlekDavis: Good point. The problem is that PowerShell considers any nonempty string $true when coerced to a Boolean, so Booleans need special-casing - please see my update. As stated in the answer, you can combine my array-parsing approach with the [System.Management.Automation.LanguagePrimitives]::ConvertTo() approach from LotPings' answer.
|
1

Agreed on the Write-Host thing. It should really only be used to leverage color output and some specific format cases. Output to the screen is the default as you'll see in my response.

You could do the below, but that date string is a bit odd, well, for me, well, I've not seen anyone use that format. So, I modified it for US style, but change as needed for your language.

$values = @{
    'Boolean' = $true
    'Int'     = 5
    'DateTime'= (Get-Date)
    'Array'   = @('A', 'B', 'C')
}

$stringValues = @{
    'Boolean' = 'false'
    'Int'     = '10'
    'DateTime'= '2019-01-02 14:45:59'
    'Array'   = "@('X', 'Y', 'Z')"
}

'INITIAL VALUES:'
foreach ($key in $values.Keys) 
{
    "$key = $($values[$key]) $($values[$key].GetType())"
}


"`nUPDATING..."
foreach ($key in $stringValues.Keys) 
{
    switch ($key) 
    { 
        Boolean  {[Boolean]$values[$key] = $stringValues['$'+$key]} 
        Int      {[Int]$values[$key] = $stringValues[$key]} 
        DateTime {[DateTime]$values[$key] = $stringValues[$key]} 
        Array    {[Array]$values[$key] = $stringValues[$key]} 
        default {'The value could not be determined.'}
    }
}


"`nUPDATED VALUES:"
foreach ($key in $values.Keys) 
{
    "$key = $($values[$key]) $($values[$key].GetType())"
}

# Results

INITIAL VALUES:
DateTime = 04/24/2019 01:44:17 datetime
Array = A B C System.Object[]
Boolean = True bool
Int = 5 int

UPDATING...

UPDATED VALUES:
DateTime = 01/02/2019 14:45:59 datetime
Array = @("X", "Y", "Z") System.Object[]
Boolean = False bool
Int = 10 int

1 Comment

This works for scalar types, but casting '@("X", "Y", "Z")' to [Array] creates a single-element array containing the entire input string, which is not the intent. The date format in the OP had a (since corrected) typo, but otherwise it works with a [datetime] cast.
0

Thanks to @LotPings, @mklement0, and @postanote for giving me a few ideas, so here is the solution I will go with:

$values = @{
    "Boolean" = $true
    "Int"     = 5
    "DateTime"= (Get-Date)
    "Array"   = @("A", "B", "C")
}

$stringValues = @{
    "Boolean" = 'false'
    "Int"     = '10'
    "DateTime"= '2019-01-31 14:45:59.005'
    "Array"   = 'X,Y,Z'
}

"INITIAL VALUES:"
foreach ($key in $values.Keys) {
    ($key + " = " + $values[$key] + " (" + $values[$key].GetType().FullName + ")")
}

"`nUPDATING..."
foreach ($key in $stringValues.Keys) {
    $value = $stringValues[$key]

    if ($values[$key] -is [Array]) {
        $values[$key] = $value -split ','
    }
    elseif (($values[$key] -is [Boolean]) -or ($values[$key] -is [Switch])) {
        $values[$key] = $value -notin 'false', '$false', '0', ''
    }
    else {
        $values[$key] = [System.Management.Automation.LanguagePrimitives]::ConvertTo($value, $values[$key].GetType())
    }
}

"`nUPDATED VALUES:"
foreach ($key in $values.Keys) {
    ($key + " = " + $values[$key] + " (" + $values[$key].GetType().FullName + ")")
}

OUTPUT:

INITIAL VALUES:
DateTime = 04/25/2019 09:32:31 (System.DateTime)
Array = A B C (System.Object[])
Boolean = True (System.Boolean)
Int = 5 (System.Int32)

UPDATING...

UPDATED VALUES:
DateTime = 01/31/2019 14:45:59 (System.DateTime)
Array = X Y Z (System.String[])
Boolean = False (System.Boolean)
Int = 10 (System.Int32)

I adjusted the format of the array in the string value (which, as I mentioned in the question, was an option). The actual code that will use this will be a bit different, but the basic idea is here. The only caveat that I noticed is that the array data type gets changed from object[] to string[]. Ideally, I'd like to keep it as-is, but it would not change the functionality of the code, so it is fine. Thanks again to all for the ideas and corrections, and if you come up with better alternatives, feel free to post.

2 Comments

-split invariably creates [string[]] arrays, which points to the limitation of your 'X,Y,Z' notation: you won't be able to represent quoted strings (e.g., to represent values with embedded spaces), and you won't be able to distinguish between strings and numbers.
I do not really need to care for quoted strings. The goal is to allow passing arrays via text notation (from an INI file). And since I can customize the delimiter, the values can have embedded spaces or whatever. I really like your array parser idea, but while it worked fine in the sample, I could not figure out how to port it into the module: github.com/alekdavis/ConfigFile/blob/master/ConfigFile/…

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.