Reputation: 37
I'm refactoring a HUGE script I need at work and I want to use splatting in a few utility functions that need to load various CSV files. My problem is getting the target for the datafile. I had previously used REF parameters, but when I do that it takes more overhead because I can't use a nested object inside a hashtable as the REF so it must be wrapped in a function and that's just messy when dealing with this many files. I could use a property listing the name of the final element and then hardcode the beginning of the variable... but that does not seem like an elegant solution.
$m.DataFiles.('LDAP').HashArray = ...
Here is a super abbreviated extract of my script:
################################################################################
############################# SCRIPT SETTINGS #############################
################################################################################
Set-StrictMode -Version latest;
$Global:ErrorActionPreference = 'Stop'; # Options: 'SilentlyContinue','Continue','Stop','Inquire'
$m = @{
DataFiles = @{
LDAP = @{
FilePath = 'C:\Temp\_PPtoO365_LDAP.csv'; ## Full path and file name: 'C:\Temp\log.csv';
Label = 'LdapFile'; ## If present, then I/O functions are written to log;
Append = $False; ## Should append vs re-write the file;
Fatal = $True; ## Should a read/write failure cause immediate script exit;
Dirty = $False; ## Used internally as a save flag;
Target = "`$m.DataFiles.LDAP.HashArray"; ## Name of variable for Load-HashArray();
DelimeterCSV = ','; ## CSV [column] delimeter;
DelimeterArray = ';'; ## Array delimeter (within a [column]);
HashArray = @(); ## Array of HashTable(s);
Template = [Ordered]@{ ## HashTable template for loading/saving;
SamAccountName = '';
MailNickname = '';
Mail = '';
Primaries = @();
Aliases = @();
};
};
};
Settings = @{
SaveExamples = $False;
SaveDebug = $False;
};
};
################################################################################
############################## MAIN ##############################
################################################################################
Function Do-Main() {
Write-Output ([string]::Format("[{0}] 'LDAP' records; Before.",$m.DataFiles.LDAP.HashArray.Length.ToString('#,##0')));
$params = $m.DataFiles.LDAP;
Load-HashArray1 @params;
Write-Output ([string]::Format("[{0}] 'LDAP' records; After Load-HashArray1().",$m.DataFiles.LDAP.HashArray.Length.ToString('#,##0')));
$m.DataFiles.LDAP.HashArray = @(); ## Reset
Write-Output ([string]::Format("[{0}] 'LDAP' records; After Reset.",$m.DataFiles.LDAP.HashArray.Length.ToString('#,##0')));
Load-HashArray2 @params;
Write-Output ([string]::Format("[{0}] 'LDAP' records; After Load-HashArray2().",$m.DataFiles.LDAP.HashArray.Length.ToString('#,##0')));
Set-Close;
Return;
}
Function Set-Close() {
## Script Cleanup - reduce HEAP ##
$m.Clear();
Exit(0);
}
################################################################################
############################## Functions ##############################
################################################################################
################################################################################
############################## Utility Functions ##############################
################################################################################
##### Function: Loads a CSV file #####
## Will return one of four types of collections:
## 1. If Target is @(), and no Template: Object[PSCustomObject] (default)
## 2. If Target is @(), and Template: Object[HashTable]
## 3. If Target is @{}, and no Template: HashTable [Ordered]@{index=PSCustomObject}
## 4. If Target is @{}, and Template: HashTable [Ordered]@{index=HashTable}
Function Load-HashArray1() {
Param (
[parameter(Mandatory=$True)][String] $FilePath, ## Full path and file name: 'C:\Temp\log.csv';
[parameter(Mandatory=$True)][String] $Label, ## If present, then I/O functions are written to log;
[parameter(Mandatory=$False)][Switch] $Append = $False, ## Should append vs re-write the file;
[parameter(Mandatory=$False)][Switch] $Fatal = $False, ## Should a read/write failure cause immediate script exit;
[parameter(Mandatory=$False)][Switch] $Dirty = $False, ## Used internally as a save flag;
[parameter(Mandatory=$True)][String] $Target, ## Name of variable to hold the HashArray;
[parameter(Mandatory=$False)][String] $DelimeterCSV = ',', ## CSV [column] delimeter;
[parameter(Mandatory=$False)][String] $DelimeterArray = ';', ## Array delimeter (within a [column]);
[parameter(Mandatory=$False)][Object[]] $HashArray = @(), ## Not used in this function;
[parameter(Mandatory=$False)][System.Collections.Specialized.OrderedDictionary] $Template = @{} ## HashTable template for loading/saving;
)
Write-Output "<<Debug1>> `$Target [$Target]... Expecting [`$m.DataFiles.LDAP.HashArray]";
$f = @{
Data = @($Template,$Template,$Template);
Target = $m.DataFiles.LDAP.HashArray;
TargetType = '';
};
Write-Output "<<Debug2>> `$f.Target [$($f.Target)]... Expecting []";
$f.TargetType = $f.Target.GetType().Name;
Write-Output "<<Debug3>> `$f.TargetType [$($f.TargetType)]... Expecting [Object[]]";
$Null = ($m.DataFiles.LDAP.HashArray = $f.Data);
Return;
}
Function Load-HashArray2() {
Param (
[parameter(Mandatory=$True)][String] $FilePath, ## Full path and file name: 'C:\Temp\log.csv';
[parameter(Mandatory=$True)][String] $Label, ## If present, then I/O functions are written to log;
[parameter(Mandatory=$False)][Switch] $Append = $False, ## Should append vs re-write the file;
[parameter(Mandatory=$False)][Switch] $Fatal = $False, ## Should a read/write failure cause immediate script exit;
[parameter(Mandatory=$False)][Switch] $Dirty = $False, ## Used internally as a save flag;
[parameter(Mandatory=$True)][String] $Target, ## Name of variable to hold the HashArray;
[parameter(Mandatory=$False)][String] $DelimeterCSV = ',', ## CSV [column] delimeter;
[parameter(Mandatory=$False)][String] $DelimeterArray = ';', ## Array delimeter (within a [column]);
[parameter(Mandatory=$False)][Object[]] $HashArray = @(), ## Not used in this function;
[parameter(Mandatory=$False)][System.Collections.Specialized.OrderedDictionary] $Template = @{} ## HashTable template for loading/saving;
)
Write-Output "<<Debug4>> `$Target [$Target]... Expecting [`$m.DataFiles.LDAP.HashArray]";
$f = @{
Data = @($Template,$Template,$Template);
Target = Invoke-Expression ($Target);
TargetType = '';
};
Write-Output "<<Debug5>> `$f.Target [$($f.Target)]... Expecting []";
$f.TargetType = $f.Target.GetType().Name;
Write-Output "<<Debug6>> `$f.TargetType [$($f.TargetType)]... Expecting [Object[] or HashTable]";
$Null = Invoke-Expression ($Target = $f.Data);
#$f.Target = $f.Data;
Return;
}
################################################################################
############################## Script Entry ##############################
################################################################################
Do-Main;
When executed it returns:
[0] 'LDAP' records; Before.
<<Debug1>> $Target [$m.DataFiles.LDAP.HashArray]... Expecting [$m.DataFiles.LDAP.HashArray]
<<Debug2>> $f.Target []... Expecting []
<<Debug3>> $f.TargetType [Object[]]... Expecting [Object[]]
[3] 'LDAP' records; After Load-HashArray1().
[0] 'LDAP' records; After Reset.
<<Debug4>> $Target [$m.DataFiles.LDAP.HashArray]... Expecting [$m.DataFiles.LDAP.HashArray]
<<Debug5>> $f.Target []... Expecting []
Load-HashArray2 : You cannot call a method on a null-valued expression.
At C:\Temp\__Test.ps1:44 char:9
+ Load-HashArray2 @params;
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [Load-HashArray2], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull,Load-HashArray2
When I comment out the line right after "Debug5", I get this error:
<<Debug5>> $f.Target []... Expecting []
<<Debug6>> $f.TargetType []... Expecting [Object[] or HashTable]
Invoke-Expression : The term 'System.Collections.Specialized.OrderedDictionary' is not recognized as the name of a
cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify
that the path is correct and try again.
At C:\Temp\__Test.ps1:119 char:17
+ $Null = Invoke-Expression ($Target = $f.Data);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (System.Collecti...deredDictionary:String) [Invoke-Expression], CommandN
otFoundException
+ FullyQualifiedErrorId : CommandNotFoundException,Microsoft.PowerShell.Commands.InvokeExpressionCommand
I'm using this blog as inspiration (step 11): Powershell: Dynamically Creating Variable Names; Nested Hashtables
Upvotes: 0
Views: 1733
Reputation: 2629
One thing I would suggest off the bat is to get rid of Invoke-Expression
and use scriptblocks with the call &
operator instead. So this
Target = "`$m.DataFiles.LDAP.HashArray"
becomes this
Target = { $m.DataFiles.LDAP.HashArray } # no quoting issues
and on invocation,
Target = Invoke-Expression ($Target)
simply becomes
Target = & $Target
The reason you're getting the error here
$Null = Invoke-Expression ($Target = $f.Data)
is because $f.Data
is an ordered dictionary. Invoke-Expression
evaluates the ToString()
of its arguments and the ToString()
of the collection is the name of the collections type. Getting rid of Invoke-Expression
should make it easier to debug your code. (NOTE: in general, using Invoke-Expression
is almost always the wrong thing to do and has possible security implications; see Invoke-Expression Considered Harmful)
Plus some minor comments:
First, this
$Null = ($m.DataFiles.LDAP.HashArray = $f.Data);
should simply be
$m.DataFiles.LDAP.HashArray = $f.Data
because assignments as statements don't return values.
Second, you shouldn't use parentheses when invoking commands as in
Target = Invoke-Expression ($Target);
because it can lead to people into thinking that they need to do Copy-Item("from", "to")
which is wrong instead of Copy-Item from to
which is correct.
Upvotes: 2