Search code examples
powershellforeach

Stumped on PowerShell ForEach loop not working as expected


I want to traverse all subdirectories and make a file called .hidden containing all the directory's files and folders which have the Windows hidden attribute, so that they are hidden in Linux desktop environments as well (shared directories in dual-boot).

The elements of this script work in the command line, but I cannot work out why the script itself doesn't work. It just results in a single empty .hidden file in the top parent directory.

$param1=$args[0]
Get-ChildItem -Path $param1 -Directory -Recurse | ForEach-Object {
    Get-ChildItem -Path $_.FullName -Hidden -Force -Name | Out-File '.hidden'
}

Solution

  • The problem is that you're using '.hidden' as the output file path, which invariably targets the same output file in the current location (directory).

    Instead, you must specify a path to each directory at hand, which is accessible via $_.FullName:

    Get-ChildItem -LiteralPath $param1 -Force -Directory -Recurse |
      ForEach-Object {  
        if ($hiddenItems = $_ | Get-ChildItem -Hidden -Force -Name) {
          $hiddenItems | 
            Out-File -LiteralPath (Join-Path $_.FullName '.hidden') -WhatIf
        }
      }
    

    Note: The -WhatIf common parameter in the command above previews the operation. Remove -WhatIf and re-execute once you're sure the operation will do what you want.

    Note:

    • You can speed up the file-writing command with
      Set-Content -LiteralPath (Join-Path $_.FullName '.hidden') -Value $hiddenItems, but note that in Windows PowerShell Set-Content uses a different character encoding by default - see the bottom section for more information.

    • With literal paths, it's better to use -LiteralPath rather than -Path, so as to prevent inadvertent interpretation of such paths as wildcard patterns.

    • $_ | Get-ChildItem ... is shorthand for Get-ChildItem -LiteralPath $_.FullName ..., for the reasons explained in this answer.

    • As per your later request, the above command only creates an output file for subdirectories that contain at least one hidden item, hence the if ($hiddenItems = $_ | Get-ChildItem -Hidden -Force -Name) { ... } statement.

      • Note that = in PowerShell is always an assignment statement and that in the context of (...) the assigned value is passed through, which in the context of a conditional such as with if causes that value to be coerced to a [bool]; if the value is the output from a command that happens to produce no output, $false is implied; if there is output, Get-ChildItem outputting at least one object implies $true. In general, however, even non-empty output can result in $false: see the bottom section of this answer for a summary of PowerShell's to-Boolean conversion rules.
    • As an aside:

      • In PowerShell (Core) 7+, $_ rather than $_.FullName would be sufficient in the Join-Path call, because in .NET (Core), System.IO.DirectoryInfo (as output by Get-ChildItem and reflected in their .Parent property) consistently stringify to their full path.

        • For the same reason, Get-ChildItem -LiteralPath $_ would work reliably in PowerShell (Core), but not in Windows PowerShell (see next point);
          $_ | Get-ChildItem works reliably in both PowerShell editions.
      • By contrast, in Windows PowerShell, System.IO.DirectoryInfo instances (as well as System.IO.FileInfo instances) situationally stringify to their name only - see this answer.


    Workaround for Windows PowerShell if creation of BOM-less UTF-8 files is a must / controlling the output encoding:

    • In Windows PowerShell, Out-File as well as > default to UTF-16LE files. While you can create UTF-8 files by adding -Encoding utf8, they will invariably have a BOM; while Set-Content produces ANSI-encoded files by default, it also invariably creates a BOM with -Encoding utf8.

      • As an aside: you wouldn't have that problem in PowerShell (Core) 7+, where BOM-less UTF-8 is the consistent default.
    • The command below creates BOM-less UTF-8 files even in Windows PowerShell, taking advantage of the (surprising) fact that New-Item (invariably) creates such files, as explained in detail in this answer. It also uses Unix-format LF-only newlines ("`n")

    Get-ChildItem -LiteralPath $param1 -Force -Directory -Recurse |
      ForEach-Object {  
        if ($hiddenItems = $_ | Get-ChildItem -Hidden -Force -Name) {
          # Creates BOM-less UTF-8 files.
          New-Item -Force (Join-Path $_.FullName '.hidden') -Value ($hiddenItems -join "`n")
        }
      }
    

    Note: If you need your files to have a trailing newline, replace ($hiddenItems -join "`n") with (($hiddenItems -join "`n") + "`n")