7

I am trying to write a recursive function that will return information in an array, however when I put a return statement into the function it misses certain entries.

I am trying to recursively look through a specified depth of folders getting the acl's associated with the folder. I know getChildItem has a recurse option, but I only want to step through 3 levels of folders.

The excerpt of code below is what I have been using for testing. When getACLS is called without a return statement (commented out below) the results are:

Folder 1

Folder 12

Folder 13

Folder 2

When the return statement is used I get the following output:

Folder 1

Folder 12

So it looks like the return statement is exiting out from the recursive loop?

The idea is that I want to return a multidimensional array like [folder name, [acls], [[subfolder, [permissions],[[...]]]]] etc.

cls

function getACLS ([string]$path, [int]$max, [int]$current) {

    $dirs = Get-ChildItem -Path $path | Where { $_.psIsContainer }
    $acls = Get-Acl -Path $path
    $security = @()

    foreach ($acl in $acls.Access) {
        $security += ($acl.IdentityReference, $acl.FileSystemRights)
    }   

    if ($current -le $max) {
        if ($dirs) {
            foreach ($dir in $dirs) {
                $newPath = $path + '\' + $dir.Name
                Write-Host $dir.Name
   #            return ($newPath, $security, getACLS $newPath $max ($current+1))
   #            getACLS $newPath $max ($current+1)
                return getACLS $newPath $max ($current+1)
            }   
        }
    } elseif ($current -eq $max ) {
        Write-Host max
        return ($path, $security)
    }
}

$results = getACLS "PATH\Testing" 2 0

4 Answers 4

10

The problem was the location of the return. I had it inside the foreach loop, meaning it was trying to return multiple times in the one function. I moved it outside the foreach, into the if statement instead.

function getACLS ([string]$path, [int]$max, [int]$current) {

$dirs = Get-ChildItem -Path $path | Where { $_.psIsContainer }
$acls = Get-Acl -Path $path
$security = @()
$results = @()

foreach ($acl in $acls.Access) {
    $security += ($acl.IdentityReference, $acl.FileSystemRights)
}   

if ($current -lt $max) {
    if ($dirs) {
        foreach ($dir in $dirs) {
            $newPath = $path + '\' + $dir.Name
            $next = $current + 1
            $results += (getACLS $newPath $max $next)
        }   
    } else {
        $results = ($path, $security)
    }
    return ($path, $security, $results)
} elseif ($current -eq $max ) {
    return ($path, $security)
}
}
Sign up to request clarification or add additional context in comments.

Comments

3

In recursion, I would only use a return statement where I needed to end the recursion - just for clarity. I've done a good bit of recursion in PowerShell and it works well. However you need to remember that PowerShell functions do behave differently. The following:

return 1,2

is equivalent to:

1,2
return

In other words, anything you don't capture in a variable or redirect to a file (or $null) is automatically considered output of the function. Here's a simple example of a working, recursive function:

function recursive($path, $max, $level = 1)
{
    $path = (Resolve-Path $path).ProviderPath
    Write-Host "$path - $max - $level"
    foreach ($item in @(Get-ChildItem $path))
    {
        if ($level -eq $max) { return }

        recursive $item.PSPath $max ($level + 1) 
    }
}

recursive ~ 3

Comments

2

Update: I am leaving the first answer as is and adding the new code here. I see that there are multiple issues with your code. here is the updated one.

cls

function getACLS ([string]$path, [int]$max, [int]$current) {

    $dirs = Get-ChildItem -Path $path | Where { $_.psIsContainer }
    $acls = Get-Acl -Path $path
    $security = @()

    foreach ($acl in $acls.Access) {
        $security += ($acl.IdentityReference, $acl.FileSystemRights)
    }   

    if ($current -lt $max) {
        if ($dirs) {
            foreach ($dir in $dirs) {
                $newPath = $dir.FullName
                $security
                getACLS $newPath $max ($current+1)
            }   
        }
    } elseif ($current -eq $max ) {
        Write-Host max
        return $security
    }
}

$results = getACLS "C:\Scripts" 2 0

If you see above, I am not using return. I just throw the object from the GetACLs function. Also, I modified it to return on $security for testing purpose. I can see the all ACLs in $results. I changed the first if condition to if ($current -lt $max). It should not be if ($current -le $max).

Let me know if this what you are looking for. I can continue to optimize this.

==========================================OLD============================================= Return will exit the function.

I am not providing the complete solution here but want to give you an idea about how this can be changed.

You can use PS Custom object to capture the information you need. For example,

function GetItem {
$itemsArray = @()
Get-ChildItem C:\Scripts | ForEach-Object {
    $itemsObject = New-Object PSObject
    Add-Member -InputObject $itemsObject -MemberType NoteProperty -Name "FullName" -Value $_.FullName
    Add-Member -InputObject $itemsObject -MemberType NoteProperty -Name "Name" -Value $_.Name
    $itemsArray += $itemsObject
}
return $itemsArray
}

This way you can return the object once it is completely built with the information you need.

3 Comments

Not sure that would work because I would need to nest the objects.
A recursive function needs to be able to return different options depending on whether it is a base case of a recursive case. So if a return statement breaks recursion in powershell, is recursion simply not possible? or is there a different type of output?
I might have misunderstood your requirements. Let me get back to the script and take a look at it again. I missed the recursion part and gave you a general tip. Recursion can be done in PowerShell.
0

I found none of these solutions did what I need to, which was to have an array with the results of the whole recursive function. Since we can't initialise the array inside the function, otherwise it is re-initialised every time recursion is used, we have to define it outside and use global:

# Get folder contents recursively and add to an array
function recursive($path, $max, $level = 1)
{
    Write-Host "$path" -ForegroundColor Red
    $global:arr += $path
    foreach ($item in @(Get-ChildItem $path))
    {
        if ($level -eq $max) { return }
        if ($item.Length -eq "1") # if it is a folder
        {
            $newpath = "$path\$($item.Name)"
            recursive $newpath $max ($level + 1)
        }
        else { # else it is a file
            Write-Host "$path\$($item.Name)" -ForegroundColor Blue
            $global:arr += "$path\$($item.Name)"
            
        }
    }
}
$global:arr = @() # have to define this outside the function and make it global
recursive C:\temp\test2 4
write-host $global:arr.count

2 Comments

Hyper-V Server 2019: displays folder paths in red; file paths in blue onscreen - but $arr.count is always zero
Fixed it! The global specifier is required on all instances related to that scope to ensure it's using the specified scope. The original code had 2 instances of $arr, which I changed to $global:arr (as I think was intended), and which made the code work great! Additionally, if the variable was only needed inside the script, the modifier could've been $script:arr instead. Examples at about Scopes - Powershell | Microsoft Learn -- tested in PS 5 and 7

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.