1

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'
}
paradroid
  • 219
  • 3
  • 12
  • 2
    You need to provide a full path for our `Out-File`. Try `ForEach($Directory in (gci $param1 -dir -rec)){GCI $Directory.FullName -hidden -force -name | Out-File "$($Directory.FullName)\.hidden"}` so that way you can reference the folder after the pipe inside the loop. – TheMadTechnician Aug 02 '23 at 20:55
  • @TheMadTechnician, that's an effective solution, but note that using `$_` isn't a problem in a nested pipeline as long as you don't try to use it in a _script block_, which isn't necessary here. – mklement0 Aug 02 '23 at 21:55

1 Answers1

1

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

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Looks like it worked, thanks for accepting, @paradroid; however, it occurred to me that it should be `$_.FullName`, not `$_.FullName` to construct the output file with, right? I've updated the answer accordingly, but tell me if I got something wrong. – mklement0 Aug 03 '23 at 12:44
  • Hmmn,I am finding a couple of problems. The `.hidden` files aren't being created in all directories that contain hidden files, but I cannot see a pattern so far. Also, the `.hidden` files are encoded with UTF-16, where Linux expects UTF-8. – paradroid Aug 03 '23 at 14:58
  • @paradroid, I have no explanation for missing directories: `-Force` tells `Get-ChildItem` to also consider hidden items; if there are no permissions issue and it still misses directories, it would be a bug. – mklement0 Aug 03 '23 at 15:07
  • 1
    @paradroid, in _Windows PowerShell_, `Out-File` (as well as `>`) defaults to UTF-16LE files. You can create UTF-8 files by adding `-Encoding utf8`, but note that the file will invariably have a _BOM_. See [this answer](https://stackoverflow.com/a/66553739/45375) for workarounds. As an aside: you wouldn't have that problem in [_PowerShell (Core) 7+_](https://github.com/PowerShell/PowerShell/blob/master/README.md), where BOM-less UTF-8 is the consistent default. – mklement0 Aug 03 '23 at 15:09
  • When I upgrade to PowerShell Core, would there be any reason to still keep a tab configuration for Windows PowerShell in Windows Terminal settings? I am also unsure that PowerShell Core would be used when the script is called for through the bash script on WSL (which is how it is to triggered). – paradroid Aug 03 '23 at 15:18
  • 1
    @paradroid, since PowerShell (Core) isn't fully backward compatible with Windows PowerShell and since there are modules that only work in the latter, you may still have a need to use Windows PowerShell. PowerShell (Core)'s CLI is `pwsh.exe`, which is distinct from Windows PowerShell's CLI, `powershell.exe`. Note that you can also install PowerShell (Core) natively inside WSL. – mklement0 Aug 03 '23 at 15:59
  • I think the most portable way of doing this would be to use Windows PowerShell, but I'm having difficulty getting the utf8 no-BOM workaround to work. Could you add it to your answer? Thanks for your help. – paradroid Aug 03 '23 at 16:54
  • 1
    @paradroid, please see the new bottom section I've added to the answer. – mklement0 Aug 03 '23 at 17:41
  • The `.hidden` files don't work with CRLF line endings either. I have tried the solution I have found with `-join "`n"` but my experiments aren't working. Sorry about the hassle. – paradroid Aug 04 '23 at 20:24
  • 1
    @paradroid, ``-join "`n"`` should work - please see my update. – mklement0 Aug 04 '23 at 20:31
  • 1
    Thanks for your help. This is the completed script, made with help from ChatGPT: http://ix.io/4Crs Cheers. – paradroid Aug 05 '23 at 11:23