Cannot get file last modified time via PowerShell script - windows

What I tried to do is searching a directory to output file path, name, version and last modified time into a txt file.
My code as below:
function Get-Version($filePath)
{
$name = #{Name="Name";Expression= {split-path -leaf $_.FileName}}
$path = #{Name="Path";Expression= {split-path $_.FileName}}
$time = #{Name="Last Modified"; Expression={Get-Date $_.LastWriteTime}}
dir -recurse -path $filePath | % { if ($_.Name -match "(.*exe)$") {$_.VersionInfo} } | select $path, $name,$time, FileVersion
}
Get-Version('E:\PS test') >> "version_info.txt"
However the output txt has name, path and version, but no last modified time.
Any hints?
Thanks!

It's because you're returning the .VersionInfo property from your ForEach-Object (%) call, and .LastWriteTime is a property of the file object, not the version info. Have a look at this:
function Get-Version($filePath)
{
$name = #{Name="Name";Expression= {split-path -leaf $_.VersionInfo.FileName}}
$path = #{Name="Path";Expression= {split-path $_.VersionInfo.FileName}}
$time = #{Name="Last Modified"; Expression={Get-Date $_.LastWriteTime}}
$version = #{Name="FileVersion"; Expression={$_.VersionInfo.FileVersion}}
dir -recurse -path $filePath | ? { $_.Name -match "(.*exe)$" } | select $path, $name,$time, $version
}
By Changing the defintiion of $name and $path to refer directly to the version info, you can operate on the original object. I also had $version to get at the FileVersion you were referring to in the select.
That makes the ForEach-Object redundant, since you'd only be passing along the input. Since you were only checking a condition in it anyway, easier to convert it to Where-Object (?).
Expanding your aliases makes it look like this:
function Get-Version($filePath)
{
$name = #{Name="Name";Expression= {Split-Path -Leaf $_.VersionInfo.FileName}}
$path = #{Name="Path";Expression= {Split-Path $_.VersionInfo.FileName}}
$time = #{Name="Last Modified"; Expression={Get-Date $_.LastWriteTime}}
$version = #{Name="FileVersion"; Expression={$_.VersionInfo.FileVersion}}
Get-ChildItem -Recurse -Path $filePath | Where-Object { $_.Name -match "(.*exe)$" } | Select-Object $path, $name,$time, $version
}
However I should also point out that you can filter the file names directly in dir (Get-ChildItem), making the Where-Object superfluous too:
function Get-Version($filePath)
{
$name = #{Name="Name";Expression= {Split-Path -Leaf $_.VersionInfo.FileName}}
$path = #{Name="Path";Expression= {Split-Path $_.VersionInfo.FileName}}
$time = #{Name="Last Modified"; Expression={Get-Date $_.LastWriteTime}}
$version = #{Name="FileVersion"; Expression={$_.VersionInfo.FileVersion}}
Get-ChildItem -Recurse -Path $filePath -Filter *.exe | Select-Object $path, $name,$time, $version
}
And then based on your comment, I realized it can be simplified even more:
function Get-Version($filePath)
{
$path = #{Name="Path";Expression= {$_.DirectoryName}}
$time = #{Name="Last Modified"; Expression={$_.LastWriteTime}}
$version = #{Name="FileVersion"; Expression={$_.VersionInfo.FileVersion}}
Get-ChildItem -Recurse -Path $filePath -Filter *.exe | Select-Object $path, Name,$time, $version
}
$name is not needed because the file object already has a property called .Name that has the file name.
$path can be simplified because $_.DirectoryName already has the path.
$time can be simplified because the .LastWriteTime property is already a [DateTime] so you don't need Get-Date.
The only reason you still need the name/expression hashes for those is to have the fields be named something other than the underlying property. If you don't care about that, you could do this:
function Get-Version($filePath)
{
$version = #{Name="FileVersion"; Expression={$_.VersionInfo.FileVersion}}
Get-ChildItem -Recurse -Path $filePath -Filter *.exe | Select-Object DirectoryName, Name, LastWriteTime, $version
}

Related

Powershell delete files older than 30 days but exclude certain folders and their contents

Here is my current code but sadly it deletes excluded folder contents. I want to keep folder contents that are inside an excluded folder.
I've been trying for days but can't find a solution. Maybe this is a PowerShell limitation?
$path = "C:\Users\bob\Desktop\testfolder"
$exclude = #('FOLDERNAME', 'filename.txt')
$lastWrite = (Get-Date).AddDays(-30)
Get-ChildItem -Path $path -Recurse -Exclude $exclude | Where-Object {$_.LastWriteTime -le $lastWrite} | Remove-Item
Method 2 (doesn't function) :
$results = Get-ChildItem -Path $path -Recurse | Where-Object {$_.LastWriteTime -le $lastWrite}
$path = "C:\Users\louisp\Desktop\testfolder"
$exclude = #('oldkeep', 'oldkeep2', 'important')
$lastWrite = (Get-Date).AddDays(-30)
foreach ($item in $results) {
$noExeption = $true
foreach($exeption in $exclude){
if($item.name -eq $exeption){
$noExeption = $false
break
}
}
if($noExeption) {
remove-item $item
}
}
There is the code I use. maybe a little rustic, but it works.
$age = (Get-Date).AddDays(-30)
Get-ChildItem C:\Folder -Exclude Z*, FolderName | foreach{
if ($_.LastWriteTime -le $age){
Remove-Item $_.fullname -Recurse -Force -Confirm:$false
}
}
I suggest to following workaround art two:
$results= Get-ChildItem -Path $path -Recurse | Where-Object {$_.LastWriteTime -le $lastWrite}
foreach ($item in $results){
$notExeption=$true
foreach($exeption in $exclude){
if($item.name -eq $exeption){
$noExeption=$false
break
}
}
if($noexeption){
remove-item -LiteralPath $item.name
}
}

Is there a robocopy switch for gathering timestamps longer than 260 characters?

I am attempting to extract the date last modified from the files in a Windows directory. Here is my basic script:
Function Get-FolderItem {
[cmdletbinding(DefaultParameterSetName='Filter')]
Param (
[parameter(Position=0,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
[Alias('FullName')]
[string[]]$Path = $PWD,
[parameter(ParameterSetName='Filter')]
[string[]]$Filter = '*.*',
[parameter(ParameterSetName='Exclude')]
[string[]]$ExcludeFile,
[parameter()]
[int]$MaxAge,
[parameter()]
[int]$MinAge
)
Begin {
$params = New-Object System.Collections.Arraylist
$params.AddRange(#("/L","/E","/NJH","/NDL","/BYTES","/FP","/NC","/XJ","/R:0","/W:0","T:W","/TS","/UNILOG:c:\temp\test.txt"))
#params.AddRange(#("/L","/S","/NJH","/BYTES","/FP","/NC","/NDL","/TS","/XJ","/R:0","/W:0"))
If ($PSBoundParameters['MaxAge']) {
$params.Add("/MaxAge:$MaxAge") | Out-Null
}
If ($PSBoundParameters['MinAge']) {
$params.Add("/MinAge:$MinAge") | Out-Null
}
}
Process {
ForEach ($item in $Path) {
Try {
$item = (Resolve-Path -LiteralPath $item -ErrorAction Stop).ProviderPath
If (-Not (Test-Path -LiteralPath $item -Type Container -ErrorAction Stop)) {
Write-Warning ("{0} is not a directory and will be skipped" -f $item)
Return
}
If ($PSBoundParameters['ExcludeFile']) {
$Script = "robocopy `"$item`" NULL $Filter $params /XF $($ExcludeFile -join ',')"
} Else {
$Script = "robocopy `"$item`" NULL $Filter $params"
}
Write-Verbose ("Scanning {0}" -f $item)
Invoke-Expression $Script | Out-Null
get-content "c:\temp\test.txt" | ForEach {
Try {
If ($_.Trim() -match "^(?<Children>\d+)\s(?<FullName>.*)") {
$object = New-Object PSObject -Property #{
FullName = $matches.FullName
Extension = $matches.fullname -replace '.*\.(.*)','$1'
FullPathLength = [int] $matches.FullName.Length
FileHash = Get-FileHash -LiteralPath "\\?\$($matches.FullName)" |Select -Expand Hash
Created = ([System.IO.FileInfo] $matches.FullName).creationtime
LastWriteTime = ([System.IO.FileInfo] $matches.FullName).LastWriteTime
Characters = (Get-Content -LiteralPath "\\?\$($matches.FullName)" | Measure-Object -ignorewhitespace -Character).Characters
Owner = (Get-ACL $matches.Fullname).Owner
}
$object.pstypenames.insert(0,'System.IO.RobocopyDirectoryInfo')
Write-Output $object
} Else {
Write-Verbose ("Not matched: {0}" -f $_)
}
} Catch {
Write-Warning ("{0}" -f $_.Exception.Message)
Return
}
}
} Catch {
Write-Warning ("{0}" -f $_.Exception.Message)
Return
}
}
}
}
$a = Get-FolderItem "C:\TargetDirectory\Folder" | Export-Csv -Path C:\Temp\output.csv -Encoding Unicode
The script extracts the date last modified of filepaths less than 260 characters. It returns a nonsense date of 1600-12-31 4:00:00 PM for files longer than 260 characters. Here is the line that is not working:
LastWriteTime = ([System.IO.FileInfo] $matches.FullName).LastWriteTime
My first attempt to solve this problem was to find a command that began with Get- because such commands were useful in extracting filehashes, filepaths, character counts and owner names of files longer than 260 characters. For example:
Owner = (Get-ACL $matches.Fullname).Owner
Characters = (Get-Content -LiteralPath "\\?\$($matches.FullName)" | Measure-Object-ignorewhitespace -Character).Characters
FileHash = Get-FileHash -LiteralPath "\\?\$($matches.FullName)" |Select -Expand Hash
Get-Date however seemed to be about getting the current date.
In my second attempt, I went back to Boe Prox's original blogpost on this script and noticed that his script had two components that were missing from mine:
a robocopy switch /TS
Date = [datetime]$matches.Date
I added to my script however doing so return an error: WARNING: Cannot convert null to type "System.DateTime". I rechecked the file in the directory, and it clearly has a date.
I reexamined the documentation on Get-Date and tried
Date = Get-Date -Format o | ForEach-Object { $matches -replace ":", "." }
However, this returned WARNING: Cannot convert value "2018/03/05 18:06:54 C:TargetDirectory\Folder\Temp.csv to type "System.IO.FileInfo". Error: " Illegal characters in path."
(N.B. In other posts, people have suggested changing the server settings to permit the existence of files longer than 260 characters. This is not an option for me because I do not have access to the servers.)
Once you hit 260 characters in the path, you hit the old Windows MAX_PATH limitation. In order to get around that, you have to prepend your path with \\?\.
In your code above, you do that for Characters and FileHash but you don't do that when retrieving LastWriteTime. e.g. Changing the path to this will work:
Created = ([System.IO.FileInfo] "\\?\$($matches.FullName)").creationtime
LastWriteTime = ([System.IO.FileInfo] "\\?\$($matches.FullName)").LastWriteTime
The alternative way is to use the Get-ChildItem cmdlet along with \\?\ prepended to the path to retrieve most of the fields you want without having to query it multiple times:
get-content "c:\temp\test.txt" | ForEach {
Try {
If ($_.Trim() -match "^(?<Children>\d+)\s(?<FullName>.*)") {
$file = Get-ChildItem "\\?\$($matches.FullName)"
$object = New-Object PSObject -Property #{
FullName = $file.FullName
Extension = $file.Extension
FullPathLength = $file.FullName.Length
FileHash = Get-FileHash -LiteralPath "\\?\$($matches.FullName)" |Select -Expand Hash
Created = $file.CreationTime
LastWriteTime = $file.LastWriteTime
Characters = (Get-Content -LiteralPath "\\?\$($matches.FullName)" | Measure-Object -ignorewhitespace -Character).Characters
Owner = (Get-ACL $matches.Fullname).Owner
}
$object.pstypenames.insert(0,'System.IO.RobocopyDirectoryInfo')
Write-Output $object
} Else {
Write-Verbose ("Not matched: {0}" -f $_)
}
} Catch {
Write-Warning ("{0}" -f $_.Exception.Message)
Return
}
}

Powershell - Speeding up writing to files

I wrote this script to find all of the folders in a directory and for each folder, check inside a common file if some strings exist and if not add them. I needed to insert strings in particular places. Not really knowing how to do this, I opted for simpler find and replace where the strings needed to be inserted. Anyway this script takes almost an hour to work through 800 files. I'm hoping some experienced members can point out ways to make my task quicker as I have only been working with Powershell for two days. Many Thanks!!!
# First find and replace items.
$FindOne =
$ReplaceOneA =
$ReplaceOneB =
$ReplaceOneC =
# Second find and replace items.
$FindTwo =
$ReplaceTwo =
# Strings to test if exist.
# To avoid duplicate entries.
$PatternOne =
$PatternTwo =
$PatternThree =
$PatternFour =
# Gets window folder names.
$FilePath = "$ProjectPath\$Station\WINDOW"
$Folders = Get-ChildItem $FilePath | Where-Object {$_.mode -match "d"}
# Adds folder names to an array.
$FolderName = #()
$Folders | ForEach-Object { $FolderName += $_.name }
# Adds code to each builder file.
ForEach ($Name in $FolderName) {
$File = "$FilePath\$Name\main.xaml"
$Test = Test-Path $File
# First tests if file exists. If not, no action.
If ($Test -eq $True) {
$StringOne = Select-String -pattern $PatternOne -path $File
$StringTwo = Select-String -pattern $PatternTwo -path $File
$StringThree = Select-String -pattern $PatternThree -path $File
$StringFour = Select-String -pattern $PatternFour -path $File
$Content = Get-Content $File
# If namespaces or object don't exist, add them.
If ($StringOne -eq $null) {
$Content = $Content -Replace $FindOne, $ReplaceOneA
}
If ($StringTwo -eq $null) {
$Content = $Content -Replace $FindOne, $ReplaceOneB
}
If ($StringThree -eq $null) {
$Content = $Content -Replace $FindOne, $ReplaceOneC
}
If ($StringFour -eq $null) {
$Content = $Content -Replace $FindTwo, $ReplaceTwo
}
$Content | Set-Content $File
}
}
# End of program.
You could try writing to the file with a stream, like this
$stream = [System.IO.StreamWriter] $File
$stream.WriteLine($content)
$stream.close()

How can I use PowerShell to copy the same file to many directories with the same name?

I'm trying to copy one file to any subfolder in a directory that has a specific name. I am part way there, but just can't quite get it to work.
I am able to find all of the subfolders called "help" using:
Get-ChildItem -Path Y:\folder1\subfolder -Directory -Recurse | ? { ($_.PSIsContainer -eq $true) -and ($_.Name -like 'help')}
That will get any folder in Y:\folder1\subfolder named help. So have been trying:
$folder = Get-ChildItem -Path Y:Y:\folder1\subfolder -Directory -Recurse | ? { ($_.PSIsContainer -eq $true) -and ($_.Name -like 'help')}
foreach ($f in $folder){
Copy-Item Y:\Info.html -Destination $folder[$f]
}
and that does not work. Bonus points if you can also tell me how to have it write out to a csv file all of the directories it copies the file to.
Thanks
I wrote this with version 3, but I think it will work with 1 and 2 since I used Set-StrictMode -Version <number> to test them.
The CSV output will look something like this for every line: Y:\Info.html,Y:\folder1\subfolder\help
$logpath = 'C:\log.csv'
$logopts = #{filepath=$logpath; append=$true; encoding='ascii'}
$file = 'Y:\Info.html'
$path = 'Y:\folder1\subfolder'
$search = 'help'
gci $path -d -s `
| ?{ $_.psIsContainer -and $_.name -match $search } `
| %{
cp $file $_.fullName; # copy file
$line = $file, $_.fullName -join ','; # build output
$line | out-file #logopts; # write output
}
Version 1
$folders = #(
(gci Y:\folder1\subfolder -dir -r | ? {$_.Name -like 'help'}).fullname
)
ForEach ($f in $folders) {
Copy-Item Y:\Info.html $f
}
Version 2
(gci Y:\folder1\subfolder -dir -r | ? {$_.Name -like 'help'}).fullname | % {cp Y:\Info.html $_}

How to replace string in files and file and folder names recursively with PowerShell?

With PowerShell (although other suggestions are welcome), how does one recursively loop a directory/folder and
replace text A with B in all files,
rename all files so that A is replaced by B, and last
rename all folders also so that A is replaced by B?
With a few requirements refinements, I ended up with this script:
$match = "MyAssembly"
$replacement = Read-Host "Please enter a solution name"
$files = Get-ChildItem $(get-location) -filter *MyAssembly* -Recurse
$files |
Sort-Object -Descending -Property { $_.FullName } |
Rename-Item -newname { $_.name -replace $match, $replacement } -force
$files = Get-ChildItem $(get-location) -include *.cs, *.csproj, *.sln -Recurse
foreach($file in $files)
{
((Get-Content $file.fullname) -creplace $match, $replacement) | set-content $file.fullname
}
read-host -prompt "Done! Press any key to close."
I would go with something like this:
Get-ChildItem $directory -Recurse |
Sort-Object -Descending -Property { $_.FullName } |
ForEach-Object {
if (!$_.PsIsContainer) {
($_|Get-Content) -replace 'A', 'B' | Set-Content $_.FullName
}
$_
} |
Rename-Item { $_.name -replace 'A', 'B' }
The Sort-Object is there to ensure that first children (subdirs, files) are listed and then directories after them. (12345)
Untested, but should give you a starting point:
$a = 'A';
$b = 'B';
$all = ls -recurse;
$files = = $all | where{ !$_.PSIsContainer );
$files | %{
$c = ( $_ | get-itemcontent -replace $a,$b );
$c | out-file $_;
}
$all | rename-item -newname ( $_.Name -replace $a,$b );
Untested, may be I'm more lucky ;-)
$hello = 'hello'
$world = 'world'
$files = ls -recurse | ? {-not $_.PSIsContainer}
foearch ($file in $files) {
gc -path $file | % {$_ -replace $hello, $world} | Set-Content $file
ri -newname ($file.name -replace $hello, $world)
}
ls -recurse | ? {$_.PSIsContainer} | ri -newname ($_.name -replace $hello, $world)
To use the same recursion:
$hello = 'hello'
$world = 'world'
$everything = ls -recurse
foearch ($thing in $everything) {
if ($thing.PSIsContainer -ne $true) {
gc -path $thing | % {$_ -replace $hello, $world} | Set-Content $thing
}
ri -newname ($thing.name -replace $hello, $world)
}
I needed this for myself and below slightly better version of the script.
I added followings:
Support for verbose parameter so you can actually see what changes script has made.
Ability to specify folders so you can limit changes.
Adding bower.json, txt and md in to include extensions.
Search and replace content first, do rename later.
Do not replace content if search string is not found (this avoids unnecessary change in modified date).
[CmdletBinding(SupportsShouldProcess=$true)]
Param()
$match = "MyProject"
$replacement = Read-Host "Please enter project name"
$searchFolders = #("MyProject.JS", "MyProject.WebApi", ".")
$extensions = #("*.cs", "*.csproj", "*.sln", "bower.json", "*.txt", "*.md")
foreach($searchFolderRelative in $searchFolders)
{
$searchFolder = join-path (get-location) $searchFolderRelative
Write-Verbose "Folder: $searchFolder"
$recurse = $searchFolderRelative -ne "."
if (test-path $searchFolder)
{
$files = Get-ChildItem (join-path $searchFolder "*") -file -include $extensions -Recurse:$recurse |
Where-Object {Select-String -Path $_.FullName $match -SimpleMatch -Quiet}
foreach($file in $files)
{
Write-Verbose "Replaced $match in $file"
((Get-Content $file.fullname) -creplace $match, $replacement) | set-content $file.fullname
}
$files = Get-ChildItem $searchFolder -filter *$match* -Recurse:$recurse
$files |
Sort-Object -Descending -Property { $_.FullName } |
% {
Write-Verbose "Renamed $_"
$newName = $_.name -replace $match, $replacement
Rename-Item $_.FullName -newname $newName -force
}
}
else
{
Write-Warning "Path not found: $searchFolder"
}
}
Note that one change from the answer is that above recurses folder only in specified folders, not in root. If you don't want that then just set $recurse = true.

Resources