Referencing key in value when declaring a hash table in Powershell

Viewed 326

If I have a hash table where the key is used in the value, e.g. 'hello' in this:

$ht = @{ 'hello' = 'hello world' }

Is it possible to reference the actual key in the value, something like this:

$ht = @{ 'hello' = "$key world" }

The real world example is a hash table where the keys are field names, and the values are script blocks that define how to compare the field between two objects, something like this:

$ht = @{
  'thisField' = { Param($tp, $dp) $tp.thisField -eq $db.thisField }
  'thatField' = { Param($tp, $dp) $tp.thatField -eq $db.thatField }
}

The comparison is often more complex than shown, and varies between fields, but each script block just compares the field that is the key for the block. I want to do something like this, if possible:

$ht = @{
  'thisField' = { Param($tp, $dp) $tp.$key -eq $db.$key }
  'thatField' = { Param($tp, $dp) $tp.$key -eq $db.$key }
}

Edit: Response to the question from @Mathias R. Jessen:

The real world hash table is used in test scripts. Testing database records is done by using a hash table where the keys are column names, and the script block defines how to verify the actual column value against the expected value. The comparison isn't always just '-eq'.

When testing a table, the hash table is used to verify that the expected records match the actual records, by comparing each field according to the script block, similar to this:

function compare($rules, $expected, $actual)
{
  $rules.Keys | Foreach-Object {
    $key = $_            # e.g. 'thisField'
    $rule = $rules[$key] # e.g. { Param($tp, $dp) $tp.thisField -eq $db.thisField }
    if (-not (& $rule $expected $actual))
    {
      throw "Comparison failed for $key"
    }
}

Using hash tables with script blocks is a convenient way to define the rules for each table in a block of "data". It works well, I just wondered if there was a way to avoid repeating the column name.

2 Answers

If you populate your hashtable sequentially, programmatically - rather than with a declarative hashtable literal - a solution is possible, by way of calling the .GetNewClosure() method on the script blocks that are used as the values:

# Initialize the hashtable.
# Note: Use `$ht = [ordered] @{}` if you want to preserve the insertion order.
$ht = @{}

# Populate the key and value arrays:
# Keys:
$keys = 'thisField', 
        'thatField'
# Corresponding values (script blocks):
$values = { Param($tp, $dp) $tp.$key -eq $db.$key },
          { Param($tp, $dp) $tp.$key -eq $db.$key } # potentially different

# Populate the hashtable entry by entry.
$i = 0
$keys.ForEach({

  # Store the key in an aux. variable.
  $key = $_

  # Create the entry, calling .GetNewClosure() on the script block,
  # which captures the then-current value of $key.
  $ht[$key] = $values[$i++].GetNewClosure()

})

Your compare function should then work as intended when $ht is passed as the $rules argument.

Since you invoke the scriptblock directly, you could just add a $key parameter to the comparer scriptblock and then pass the current key as an argument:

function compare($rules, $expected, $actual)
{
  $rules.Keys | Foreach-Object {
    $key = $_            # e.g. 'thisField'
    $rule = $rules[$key] # e.g. { Param($tp, $dp) $tp.thisField -eq $db.thisField }
    if (-not (& $rule $expected $actual $key))
    {
      throw "Comparison failed for $key"
    }
}

$ht = @{
  'thisField' = { Param($tp, $dp, $key) $tp.$key -eq $db.$key }
  'thatField' = { Param($tp, $dp, $key) $tp.$key -eq $db.$key }
}

# ...

compare $ht $expected $actual
Related