gci and gci -recurse have different kind of outputs - powershell

I'm trying to make a filesystem inventory.
This works, it gives me the ower and ACL of each entry
Get-ChildItem \\Server\Share\* | Select-Object #{n='Path';e={ (Get-Item $_.PSPath).FullName }}, PSIsContainer, #{n='Owner';e={ (Get-Acl $_).Owner}}, #{n='Accesstostring';e={ (Get-Acl $_).Accesstostring}}
But using -Recurse gives me empty Owner and Accesstostring
Get-ChildItem \\Server\Share\ -Recurse | Select-Object #{n='Path';e={ (Get-Item $_.PSPath).FullName }}, PSIsContainer, #{n='Owner';e={ (Get-Acl $_).Owner}}, #{n='Accesstostring';e={ (Get-Acl $_).Accesstostring}}
Why does gci is sending something different alon the pipe ?
How can i fix this ?
(I don't want to make an array because that would not fit into memory)

They are different because one array contains a list of files, but in recurse it is an array of directory objects and each of the directory object contains a list of files.
The code below will do what you wanted. Please note that path needs to be in quotes if it contains spaces.
Get-ChildItem \\Server\Share\ -Recurse | Select-Object #{n='Path';e={ $_.FullName }}, PSIsContainer, #{n='Owner';e={ (Get-Acl $_.FullName).Owner}}, #{n='Accesstostring';e={ (Get-Acl $_.FullName).Accesstostring}}

Expanding on #Edjs-perkums answer, this calls Get-Acl once and expands two of its properties in a second Select-Object in the pipe. (Also reformatted into multiple lines for clarity, but it's a single pipeline.)
Get-ChildItem \\Server\Share\ -Recurse `
| Select-Object #{n='Path';e={ $_.FullName }},
#{n='ACL';e={ (Get-Acl $_.Fullname) }},
PSIsContainer `
| Select-Object Path, PSIsContainer,
#{n='Owner';e={ $_.ACL.Owner}},
#{n='Accesstostring';e={ $_.ACL.Accesstostring}}

Related

Powershell: how to loop through folders and execute the same code for each of the folders

Currently I have a script that will sort files in a folder (on their lastwritetime), keep the latest file and move the other files to a different folder. This works correctly:
Get-ChildItem "\\networkfolder\RawData\2_ActionData_Prep\CustomerA\" -Recurse -Filter "*.rpt" -File |
Sort-Object -Property LastWriteTime -Descending |
Select-Object -Skip 1 |
Move-Item -Force -Destination "\\networkfolder\RawData\_Archive\Archive_DataRetrieved\"
The problem is that I have several 'customer' folders and I want to execute the code above in each of those folders.
I tried the following:
$CustomerFolders = Get-ChildItem -Path "\\networkfolder\RawData\2_ActionData_Prep\" -Directory -Recurse
foreach ($folder in $CustomerFolders) {Get-ChildItem -Filter "*.rpt" -File | Sort-Object -Property LastWriteTime -Descending |
Select-Object -Skip 1 |
Move-Item -Force -Destination "\\networkfolder\RawData\_Archive\Archive_DataRetrieved\"}
When I execute this script, nothing happens. Also no error comes up. Hopefully someone could help me on this.
Santiago Squarzon noticed that a $folder was missing, so I added $folder in loop for Get-Childitem:
$CustomerFolders = Get-ChildItem -Path "\\networkfolder\RawData\2_ActionData_Prep\" -Directory -Recurse
foreach ($folder in $CustomerFolders) {Get-ChildItem $folder -Filter "*.rpt" -File | Sort-Object -Property LastWriteTime -Descending |
Select-Object -Skip 1 |
Move-Item -Force -Destination "\\networkfolder\RawData\_Archive\Archive_DataRetrieved\"}
Now I get an error message:
Get-ChildItem : Cannot find path '\networkfolder\CustomerA' because it does not exist.
It somehow misses the part \RawData\2_ActionData_Prep\ in the path, although I defined it in the $CustomerFolders variable?
You could do the process all with pipelines like this:
$base = "\\networkfolder\RawData\2_ActionData_Prep\"
$destination = "\\networkfolder\RawData\_Archive\Archive_DataRetrieved\"
Get-ChildItem -Path $base -Directory -Recurse | ForEach-Object {
$_ | Get-ChildItem -Filter "*.rpt" -File | Sort-Object LastWriteTime -Descending |
Select-Object -Skip 1 | Move-Item -Force -Destination $destination
}
To briefly explain why Get-ChildItem $folder... failed but $folder | Get-ChildItem ... worked, when we do Get-ChildItem $folder, $folder is being passed as argument for the -Path parameter and the parameter type for it is [string[]]. So, in your code when $folder (a DirectoryInfo instance) is passed as argument, it is being converted to a string and, very unfortunately in Windows PowerShell, when we type convert a DirectoryInfo (and a FileInfo too!) object to string what we get as a result is the Directory Name (this is not the case in PowerShell Core, where the string representation of this object becomes the Directory FullName (a.k.a. Absolute Path) so Get-ChildItem thinks it's being fed a relative path and it looking for the folders in your current location.
However when we do $folder | Get-ChildItem ..., $folder gets bound to the -LiteralPath parameter by Property Name on the PSPath ETS property, in other words, the cmdlet receives the object's provider path (you can think of it as the absolute path of the folder) hence why it works fine.

Select folder names with a common word

I am trying to get the profile folders of the user User1 but don't want User10 folders from a directory.
Here is what I have tried:
Get-ChildItem -Path "$Profilesroot" -Recurse -Filter "*User1*"
and the out put shows User1, User1.v1, User10, User10.v1 folders
Same out put for the for the following:
Get-ChildItem "$profilesroot" -Recurse | Select-String -Pattern "User1"
Get-ChildItem "$profilesroot" -Recurse | Where {$_.Name -match 'User1'}
Get-ChildItem "$profilesroot" -Recurse | Where {$_.Name -like '*User1*'}
If I try the following, I am getting the required output but I feel there is a better option:
Get-ChildItem $profiles -Recurse | Where {$_.Name -like 'User1'}
Get-ChildItem $profiles -Recurse | Where {$_.Name -like 'User1.*'}
Apologies if the ask was not clear.
If you use Get-ChildItem's -Include parameter instead of -Filter, you can pass multiple PowerShell wildcard expressions.
Note:
Use of -Include is generally slower than use of -Filter, because the latter filters at the source, whereas -Include collects all items and then filters. Conversely, the -Include uses PowerShell's wildcard expressions, and not the more limited ones supported by -Filter, which are additionally saddled with legacy quirks.
However, a single Get-ChildItem call still outperforms a Get-ChildItem -Recurse ... | Where-Object { ... } pipeline.
That is - generally speaking - use something like
Get-ChildItem -Recurse ... | Where-Object { $_.Name -match '^User1\b' }, as suggested by Mathias R. Jessen, only if you truly need the enhanced matching flexibility that -match, the regular expression-based string-matching operator, provides.
If you're really looking for matching directories at all levels of the $ProfilesRoot subtree (-Recurse), use the following:
Get-ChildItem $ProfilesRoot -Recurse -Directory -Include User1, User1.*
If, by contrast, you're just looking for matching directories located directly in $ProfilesRoot:
Get-ChildItem $ProfilesRoot\* -Directory -Include User1, User1.*
Note the addition of \* to the (positionally implied) -Path argument, because in the absence of -Recurse -Include is unexpectedly only applied to the input path itself, not its children - see this answer for a detailed discussion of this counterintuitive behavior.
To get user's profile path, you should rely on operating system, but not on path concatenation.
$path = #(Get-WmiObject -Class 'Win32_UserProfile' |
Where-Object { $_.Special -eq $false } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_.LocalPath) } |
Where-Object { $_.LocalPath.EndsWith("\User1") } |
Select-Object -ExpandProperty 'LocalPath' )[0]
More accurate way, because
sometimes profile path not matches username
sometimes profile path matches other user's name
sometimes profile path is not in profileroot
sometimes user have more than one profile and some of this profiles are broken and not used by user.
$path = #(Get-WmiObject -Class 'Win32_UserProfile' |
Where-Object { $_.Special -eq $false } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_.LocalPath) } |
Where-Object { $_.SID -eq ([System.Security.Principal.NTAccount]('User1')).Translate([System.Security.Principal.SecurityIdentifier]).ToString() } |
Select-Object -ExpandProperty 'LocalPath' )[0]
Even more accurate way using SID matching and WMI filtering
$userSid = ([System.Security.Principal.NTAccount]('User1')).Translate([System.Security.Principal.SecurityIdentifier]).ToString()
$path = #(Get-WmiObject -Class 'Win32_UserProfile' -Filter "(Special = FALSE) AND (LocalPath LIKE '%\\%') AND (SID = '$($userSid)')" -Property #('LocalPath'))[0].LocalPath

Find the oldest file in each subdirectory with Powershell

My company recently moved to outlook365. We are entirely VDI based so our user profiles are stored on a single server. As a result our users all now have 2+ .ost files taking up storage space on the server. I'd like to write a script to find and delete the extraneous .ost files. In addition I'd like to schedule the script to run on a monthly basis to clean up any orphaned .ost's that occur for any other reason.
I've tried a few different solutions but can't seem to find the right syntax to identify just the oldest/original .ost in each subdirectory, all attempts have identified the oldest file from the whole directory or all .ost files in the directory.
$Path = "<path>"
$SubFolders = dir $Path -Recurse | Where-Object {$_.PSIsContainer} | ForEach-Object -Process {$_.FullName}
ForEach ($Folder in $SubFolders)
{
$FullFileName = dir $Folder | Where-Object {!$_.PSIsContainer} | Sort-Object {$_.LastWriteTime} -Descending | Select-Object -First 1
}
Inside of your loop, you could use the following to list the .ost file that has the oldest LastWriteTime value. Just add the -Descending flag to Sort-Object to list the newest file.
$FullFileName = foreach ($folder in $Subfolders) {
$Get-ChildItem -Path $folder -Recurse -File -Filter "*.ost" |
Sort-Object -Property LastWriteTime |
Select-Object -Property FullName -First 1
}
$FullFileName
If there is only one .ost file found in the $folder path, it will still find that file. So you will need logic to not delete when there is only one file. This does not guarantee it is the oldest file. You probably want a combination of the oldest CreationTime and newest LastWriteTime. The following will list the oldest .ost file based on CreationTime.
$FullFileName = foreach ($folder in $Subfolders) {
Get-ChildItem -Path $folder -Recurse -File -Filter "*.ost" |
Sort-Object -Property CreationTime |
Select-Object -Property FullName -First 1
}
$FullFileName
Another issue is setting the $FullFileName variable inside of the foreach loop. This means it will be overwritten through each loop iteration. Therefore, if you retrieve the value after the loop completes, it will only have the last value found. Setting the variable to be the result of the foreach loop output will create an array with multiple values.
To only output an OST file path when there are multiple OST files, you can do something like the following:
$FullFileName = foreach ($folder in $Subfolders) {
$files = Get-ChildItem -Path $folder -Recurse -File -Filter "*.ost" |
Sort-Object -Property LastWriteTime -Descending
if ($files.count -ge 2) {
$files | Select-Object -Property FullName -First 1
}
$FullFileName
This one liner should do the job, keeping the ost file with the newest LastWriteTime
gci -Path $Path -directory | where {(gci -Path $_\*.ost).count -gt 1}|%{gci -Path $_\*.cmd|Sort-Object LastWriteTime -Descending|Select-Object -Skip 1|Remove-Item -WhatIf}
Longer variant follows.
$Path = '<path>'
$Ext = '*.ost'
Get-ChildItem -Path $Path -directory -Recurse |
Where-Object {(Get-ChildItem -Path "$_\$Ext" -File -EA 0).Count -gt 1} |
ForEach-Object {
Get-ChildItem -Path "$_\$Ext" -File -EA 0| Sort-Object LastWriteTime -Descending |
Select-Object -Skip 1 | Remove-Item -WhatIf
}
The first two lines evaluate folders with more than one .ost file
The next lines iterates those folders and sort them descending by LastWriteTime, skips the first (newest) and pipes the other to Remove-Item with the -WhatIf parameter to only show what would be deleted while testing.
You can of course also move them to a backup location instead.

find and recurse

I have a folder structure d:\domains\<domain>\folder1\folder2\folderx
There are maybe 20 <domain> folders, with differing levels of folders below them.
I want to search all folders for .php files, and just print the unique <domain> folders where they exit.
So for example, if there are files found in
d:\domains\domain1.com\test\test
d:\domains\domain2.com\test\test
d:\domains\domain2.com\test\help
I just want domain1.com,domain2.com to be printed. It needs to work in PowerShell v2.
I have the following, but it only prints the first domain?
Get-ChildItem -Path #(Get-ChildItem -Path D:\domains | Where-Object {$_.PSIsContainer})[1].FullName -Recurse *.php |
Select-Object -ExpandProperty FullName
Enumerate the domain folders, then filter for those of them that contain .php files.
Get-ChildItem 'D:\domains' | Where-Object {
$_.PSIsContainer -and
(Get-ChildItem $_.FullName -Filter '*.php' -Recurse)
}
If you have PowerShell v3 or newer you can use the -Directory switch instead of checking for $_.PSIsContainer:
Get-ChildItem 'D:\domains' -Directory | Where-Object {
Get-ChildItem $_.FullName -Filter '*.php' -Recurse
}
Select the Name property from the output if you want just the folder/domain names:
... | Select-Object -Expand Name

Powershell get second folder in filepath

I am new to powershell, I am trying to get the second folder in a filepath, after searching folders for a specific function. The issue seems to be when using split-item, i have differing levels of folder depths.
Get-ChildItem -Path d:\domains -Recurse *.php | Select-Object -Property FullName
So for example I am outputting:
D:\domains\domain.com\httpdocs\wordpress\wp-content\themes\twentytwelve\single.php
D:\domains\domain.com\httpdocs\wordpress\wp-content\themes\twentytwelve\tag.php
D:\domains\domain.com\httpdocs\wordpress\wp-includes\canonical.php
D:\domains\domain2.com\httpdocs\wordpress\wp-content\themes\twentytwelve\inc\custom-header.php
D:\domains\domain2.com\httpdocs\wordpress\wp-content\themes\twentytwelve\page-templates\front-page.php
D:\domains\domain2.com\httpdocs\wordpress\wp-content\themes\twentytwelve\page-templates\full-width.php
D:\domains\domain2.com\httpdocs\wordpress\wp-includes\canonical.php
I just want to search for the name of the second folder (i.e. domain.com/domain2.com) and just output the unique entries.
Many Thanks
Use Select-Object -First 1 -Skip 1 to grab the second item in a pipeline:
Get-ChildItem -Directory -Path d:\domains |Select -First 1 -Skip 1 |Get-ChildItem -Recurse *.php |Select-Object -ExpandProperty FullName
The -Directory switch for Get-ChildItem on the file system provider and the -First and -Skip parameters for Select-Object were introduced in PowerShell 3.0.
If you're using PowerShell v2, you could do the following:
Get-ChildItem -Path #(Get-ChildItem -Path D:\domains |Where-Object {$_.PSIsContainer})[1].FullName -Recurse *.php |Select-Object -ExpandProperty FullName