0

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

1
  • Don't forget to ask your question. Commented Apr 25, 2018 at 15:21

1 Answer 1

2

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.

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

1 Comment

Thank you, I ended up going a different route and going ahead and embracing classes and using a Static GetInstance(); but your detailed explanation helps a lot. in other regards.

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.