Rumpleteaser
Rumpleteaser

Reputation: 4234

Powershell Recursion with Return

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

Upvotes: 7

Views: 32186

Answers (4)

cloudhal
cloudhal

Reputation: 134

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

Upvotes: 0

Rumpleteaser
Rumpleteaser

Reputation: 4234

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)
}
}

Upvotes: 10

Keith Hill
Keith Hill

Reputation: 201662

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

Upvotes: 3

ravikanth
ravikanth

Reputation: 25810

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.

Upvotes: 2

Related Questions