Referencing targeted object in ForEach-Object in Powershell

I am fairly new to Powershell. I am attempting to re-write the names of GPO backup folders to use their friendly name rather than their GUID by referencing the name in each GPO backup's 'gpresult.xml' file that is created as part of the backup. However, I do not understand how I can reference the specific object (in this case, the folder name) that is being read into the ForEach-Object loop in order to read into the file beneath this folder.

function Backup_GPO {

    $stamp = Get-Date -UFormat "%m%d" 
    New-Item -ItemType "directory" -Name $stamp -Path \\dc-16\share\GPO_Backups -Force | out-null # create new folder to specify day backup is made
    Backup-GPO -All -Path ("\\dc-16\share\GPO_Backups\" + "$stamp\" )
    Get-ChildItem -Path \\dc-16\share\GPO_Backups\$stamp | ForEach-Object {
        # I want to reference the current folder here
        [xml]$file = Get-Content -Path (folder that is being referenced in for loop)\gpresult.xml 
        $name = $file.GPO.Name
    }

I'm coming from Python, where if I want to reference the object I'm currently iterating on, I can do so very simply -

for object in list:
    print(object)

How do you reference the currently in-use object in Powershell's ForEach-Object command?

Upvotes: 1

Views: 3587

Answers (4)

js2010
js2010

Reputation: 27428

Here's my solution. It's annoying that $_ doesn't have the full path. $gpath is easier to work with than $_.fullname for joining the two strings together on the next line with get-content. I get a gpreport.xml file when I try backup-gpo. Apparently you can't use relative paths like .\gpo_backups\ with backup-gpo.

mkdir c:\users\js\gpo_backups\
get-gpo -all | where displayname -like '*mygpo*' | 
  backup-gpo -path c:\users\js\gpo_backups\

Get-ChildItem -Path .\GPO_Backups\ | ForEach-Object {
  $gpath = $_.fullname
  [xml]$file = Get-Content -Path "$gpath\gpreport.xml"
  $file.GPO.Name   
}

Upvotes: 0

lit
lit

Reputation: 16236

Another way...

$dlist = Get-ChildItem -Path "\\dc-16\share\GPO_Backups\$stamp"
foreach ($dir in $dlist) {
    # I want to reference the current folder here
    [xml]$file = Get-Content -Path (Join-Path -Path $_.FullName -ChildPath 'gpresult.xml')
    $name = $file.GPO.Name
}

Upvotes: 0

mklement0
mklement0

Reputation: 437197

I'm coming from Python, where if I want to reference the object I'm currently iterating on, I can do so very simply -

for object in list: print(object)

The direct equivalent of that in PowerShell is the foreach statement (loop):

$list = 1..3  # create a 3-element array with elements 1, 2, 3
foreach ($object in $list) {
  $object  # expression output is *implicitly* output
}

Note that you cannot directly use a foreach statement in a PowerShell pipeline.

In a pipeline, you must use the ForEach-Object cmdlet instead, which - somewhat confusingly - can also be referred to as foreach, via an alias - it it is only the parsing mode that distinguishes between the statement and the cmdlet's alias.


You're using the ForEach-Object cmdlet in the pipeline, where different rules apply.

Script blocks ({ ... }) passed to pipeline-processing cmdlets such as ForEach-Object and Where-Object do not have an explicit iteration variable the way that the foreach statement provides.

Instead, by convention, such script blocks see the current pipeline input object as automatic variable $_ - or, more verbosely, as $PSItem.


While the foreach statement and the ForEach-Object cmdlet operate the same on a certain level of abstraction, there's a fundamental difference:

  • The foreach statement operates on collections collected up front in memory, in full.

  • The ForEach-Object cmdlet operates on streaming input, object by object, as each object is being received via the pipeline.

This difference amounts to the following trade-off:

  • Use the foreach statement for better performance, at the expense of memory usage.

  • Use the ForEach-Object cmdlet for constant memory use and possibly also for the syntactic elegance of a single pipeline, at the expense of performance - however, for very large input sets, this may be the only option (assuming you don't also collect a very large dataset in memory on output).

Upvotes: 3

Mathias R. Jessen
Mathias R. Jessen

Reputation: 174465

Inside the ForEach-Object scriptblock, the current item being iterated over is copied to $_:

Get-ChildItem -Filter gpresult.xml |ForEach-Object {
    # `$_` is a FileInfo object, `$_.FullName` holds the absolute file system path
    [xml]$file = Get-Content -LiteralPath $_.FullName
}

If you want to specify a custom name, you can either specify a -PipelineVariable name:

Get-ChildItem -Filter gpresult.xml -PipelineVariable fileinfo |ForEach-Object {
    # `$fileinfo` is now a FileInfo object, `$fileinfo.FullName` holds the absolute file system path
    [xml]$file = Get-Content -LiteralPath $fileinfo.FullName
}

or use a foreach loop statement, much like for object in list in python:

foreach($object in Get-ChildItem -Filter gpresult.xml)
{
    [xml]$file = Get-Content -LiteralPath $object.FullName
}

Upvotes: 2

Related Questions