Access JumpStart 2.0 | Blog

A Rapid Development Framework for Microsoft Access

In Part 3 (Refactoring the function PS_GetFileHashes (Part 3) | Access JumpStart) we arrived at this code with helper functions and a helper class:

Function PS_GetFileHashes(aFiles As Variant, sHashAlgorithm As String) As Scripting.Dictionary
    Dim retVal As New Scripting.Dictionary
    Dim LenCmd As Long, aIdx As Long
    Dim sPSCmd As String, itm As Variant, sPathsPart As String, aOutput As Variant, itmParts As Variant
    
    On Error GoTo Error_Handler
    If AlgorithimIsUnknown(sHashAlgorithm) Then GoTo Error_Handler
    
    Dim PScmd As New PS_FileHashesCommand
    LenCmd = PScmd.LenCmdWithoutPaths(sHashAlgorithm)
    aIdx = LBound(aFiles)
    Do While aIdx <= UBound(aFiles)
        sPathsPart = GetCmdFilesArgument(aIdx, aFiles, LenCmd)
        sPSCmd = PScmd.BuildFileHashesCommand(sPathsPart, sHashAlgorithm)
        aOutput = Split(CStr(PS_GetOutput(sPSCmd)), vbCrLf)
        ' Add the output for filenames / hashes to the dictionary to return
        For Each itm In aOutput
            itmParts = Split(itm, "|")
            If UBound(itmParts) > 0 Then retVal.Add itmParts(0), itmParts(1)
        Next itm
    Loop
    Set PS_GetFileHashes = retVal

Error_Handler_Exit:
    On Error Resume Next
    Exit Function
'Get-ChildItem -Path '.\*.xls','.\*.csv' -Recurse -File | Get-FileHash -Algorithm SHA256
Error_Handler:
    MsgBox "The following error has occured" & vbCrLf & vbCrLf & _
           "Error Number: " & Err.Number & vbCrLf & _
           "Error Source: PS_GetFileHash" & vbCrLf & _
           "Error Description: " & Err.Description & _
           Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
           , vbOKOnly + vbCritical, "An Error has Occured!"
           'Resume Next
    Resume Error_Handler_Exit
End Function

Private Function AlgorithimIsUnknown(sHashAlgorithm As String) As Boolean
    Dim retVal As Boolean: retVal = False
    Select Case sHashAlgorithm
        Case "MACTripleDES", "MD5", "RIPEMD160", "SHA1", "SHA256", "SHA384", "SHA512"
        Case Else
            MsgBox "Unknown Algorithm", vbCritical Or vbOKOnly, "Operation Aborted"
            retVal = True
    End Select
    AlgorithimIsUnknown = retVal
End Function

Private Function GetNextStringSegment(ByRef aIdx As Long, ByRef aFiles As Variant, LenCmd As Long) As String
    Dim sPathsPart As String: sPathsPart = ""
    Dim sPreviousPart As String
    
    Do
        sPreviousPart = sPathsPart
        sPathsPart = sPathsPart & HALutil.WrapText(aFiles(aIdx), "'") & ","
        aIdx = aIdx + 1
        If aIdx >= UBound(aFiles) Then Exit Do
    Loop Until Len(sPathsPart) - 1 + LenCmd > 32656
    
    ' We left because either we reached the end of the array or we exceeded the limit.
    ' If we exceeded the limit then we need to back one element off of sPathsPart and the aIdx
    If Len(sPathsPart) - 1 + LenCmd > 32656 Then
        aIdx = aIdx - 1
        sPathsPart = sPreviousPart
    End If
    'Remove the comma from the end if there, if empty, use *.*
    If Len(sPathsPart) > 0 Then sPathsPart = Left(sPathsPart, Len(sPathsPart) - 1) Else sPathsPart = "'*.*'"
    GetNextStringSegment = sPathsPart
End Function
' Class PS_FileHashesCommand
Option Compare Database
Option Explicit

Const PS_Cmd_Beg As String = "Get-ChildItem -Path "
Const PS_Cmd_Mid As String = " -File | Get-FileHash -Algorithm "
Const PS_Cmd_End As String = " | ForEach { $_.Path + '|' + $_.Hash }"

Public Function BuildFileHashesCommand(sPathsPart As String, sHashAlgorithm As String) As String
    Dim retVal As String: retVal = ""
    retVal = PS_Cmd_Beg & sPathsPart & PS_Cmd_Mid & sHashAlgorithm & PS_Cmd_End
    
    BuildFileHashesCommand = retVal
End Function

Public Function LenCmdWithoutPaths(sHashAlgorithm) As Long
    Dim retVal As Long: retVal = 0
    retVal = Len(PS_Cmd_Beg & PS_Cmd_Mid & sHashAlgorithm & PS_Cmd_End)
    LenCmdWithoutPaths = retVal
End Function

So, I am focusing on that very first function PS_GetFileHashes and refactoring that down to something more understandable and compact. I want users to be able to read the function and be able to tell what it is doing and follow along. I’ve refactored a lot of the code out to a class and some helper functions. Looking at the function, I’m going to remove some of the comments since the following line explains what’s happening in place of the comment now.

Function PS_GetFileHashes(aFiles As Variant, sHashAlgorithm As String) As Scripting.Dictionary
    '...
    Do While aIdx <= UBound(aFiles)
        sPathsPart = GetNextStringSegment(aIdx, aFiles, LenCmd)
        ' Getting rid of this comment: Build the PowerShell command string
        sPSCmd = PScmd.BuildFileHashesCommand(sPathsPart, sHashAlgorithm)
        aOutput = Split(CStr(PS_GetOutput(sPSCmd)), vbCrLf)
        ' Going to refactor: Add the output for filenames / hashes to the dictionary to return
        For Each itm In aOutput
            itmParts = Split(itm, "|")
            If UBound(itmParts) > 0 Then retVal.Add itmParts(0), itmParts(1)
        Next itm
    Loop
    '...
End Function

So the question becomes, how do I refactor the second portion. I’m thinking first that I don’t really like the way aOutput is loaded with the crazy nested functions. I think I’ll move that to our PScmd class in a method that will execute the command and return the array output.

Function PS_GetFileHashes(aFiles As Variant, sHashAlgorithm As String) As Scripting.Dictionary
    '...
    Do While aIdx <= UBound(aFiles)
        sPathsPart = GetNextStringSegment(aIdx, aFiles, LenCmd)
        sPSCmd = PScmd.BuildFileHashesCommand(sPathsPart, sHashAlgorithm)
        aOutput = PScmd.RunCmdAndReturnOutputAsArray(sPSCmd)
        ' Going to refactor: Add the output for filenames / hashes to the dictionary to return
        For Each itm In aOutput
            itmParts = Split(itm, "|")
            If UBound(itmParts) > 0 Then retVal.Add itmParts(0), itmParts(1)
        Next itm
    Loop
    '...
End Function

And actually, the part I want to refactor, the loop, isn’t easily lending itself to refactoring. But what if I were to return a dictionary to aOutput instead of an array. It might be more intuitive to loop over each key. Or, I guess I could pass the retVal dictionary by reference. Although I don’t really like that idea because I don’t like passing arguments to functions and have them modified. Although it’s more concise that way AND I am already doing that with aIdx. I guess I’ll try it and see what I think. In fact, I’ll just add it to the RunCmd function and not return anything. So here is the original function portion after all that:

Function PS_GetFileHashes(aFiles As Variant, sHashAlgorithm As String) As Scripting.Dictionary
    '...
    Do While aIdx <= UBound(aFiles)
        sPathsPart = GetNextStringSegment(aIdx, aFiles, LenCmd)
        sPSCmd = PScmd.BuildFileHashesCommand(sPathsPart, sHashAlgorithm)
        PScmd.RunCmdAndAddOutputToDictionary sPSCmd, retVal
    Loop
    '...
End Function

Ok, then in the next segment, I’ll remove my unneeded Dim statements from the beginning of the function, and refactor the Error display code to be briefer in the function itself, and we’ll be done. Whew!