I am new to PowerShell, but i am slowly getting the hang of it.
I was wondering if there is a better way to write this?
In a single directory I have monthly reports for 14 names as text files. The below looks at the directory, searches for the NAME1 and for any files containing Jan, Feb, Mar and combines them into a single file and exports the combined file to another location with a specific name:
Get-childitem -Path 'C:\Powershell\Attempt\*.txt' | where-object {$_.name -like "*NAME1**JAN*" -or $_.name -like "*NAME1**FEB*" -or $_.name -like "*NAME1**MAR*"} | get-content | Out-File C:\Powershell\Attempt\test\NAME1_Q1_.txt -Encoding ascii
Get-childitem -Path 'C:\Powershell\Attempt\*.txt' | where-object {$_.name -like "*NAME1**APR*" -or $_.name -like "*NAME1**MAY*" -or $_.name -like "*NAME1**JUN*"} | get-content | Out-File C:\Powershell\Attempt\test\NAME1_Q2_.txt -Encoding ascii
Get-childitem -Path 'C:\Powershell\Attempt\*.txt' | where-object {$_.name -like "*NAME1**JUL*" -or $_.name -like "*NAME1**AUG*" -or $_.name -like "*NAME1**SEP*"} | get-content | Out-File C:\Powershell\Attempt\test\NAME1_Q3_.txt -Encoding ascii
Get-childitem -Path 'C:\Powershell\Attempt\*.txt' | where-object {$_.name -like "*NAME1**OCT*" -or $_.name -like "*NAME1**NOV*" -or $_.name -like "*NAME1**DEC*"} | get-content | Out-File C:\Powershell\Attempt\test\NAME1_Q4_.txt -Encoding ascii
Is this possible with a loop? or is it better to just write out all 14 names and quarterly combinations?
The above also creates blank Qx.txt files if the source files don't exist (does that make any sense) so I have also written this to remove those that are blank
get-childitem C:\powershell\attempt\test -Recurse | foreach {
if($_.Length -eq 0){
Write-Output "Removing Empty File $($_.FullName)"
$_.FullName | Remove-Item -Force
}
}
if( $_.psiscontainer -eq $true){
if((gci $_.FullName) -eq $null){
Write-Output "Removing Empty folder $($_.FullName)"
$_.FullName | Remove-Item -Force
}
}
Is there a way to incorporate this into the main script, or is it better to keep this as i "tidy up" at the end?
I do have another query, but I'm not sure if it's better being a separate post (I don't want to put too much in this one if it's not the way) It is about how to rename the files from different variables. I can get the different name variables, but not working harmoniously within the above script - this comes down to my lack of knowledge
Many thanks in advance,
Kind Regards
Before trying to create a loop, let's do something about those clunky -like clauses. We can make them go away with a single -match clause.
The expression
$_.name -like "*NAME1**JAN*" -or $_.name -like "*NAME1**FEB*" -or $_.name -like "*NAME1**MAR*"
is equivalent to
$_.name -match "NAME1.*(JAN|FEB|MAR)"
The -like operator uses Wildcards. Wildcards are nice, but the -match operator works with full fledged Regular Expressions which are much more versatile.
These are not compatible with each other - some Wildcard expressions are valid Regular Expressions and vice versa, but match different strings.
Don't forget that -match and -like are not case-sensitive. For case-sensitive comparisons use -cmatch and -clike
Now we can solve the rest of your problem using a pair of loops and arrays.
You'll need to create a $names array by typing out all 14 names.
$names = #("NAME1", "NAME2", "NAME3", ...)
Luckily, your example names have a nice pattern, so we can use that
$ctr = 0
$names = #("NAME") * 14 | ForEach-Object {$_ + ++$ctr}
We'll need another array containing the months in our Regular Expressions
$quarts = #("JAN|FEB|MAR", "APR|MAY|JUN", "JUL|AUG|SEP", "OCT|NOV|DEC")
And now we shall loop
$ctr = 0
$names = #("NAME") * 14 | Foreach-Object {$_ + ++$ctr}
$quarts = #("JAN|FEB|MAR", "APR|MAY|JUN", "JUL|AUG|SEP", "OCT|NOV|DEC")
$container = "C:\Powershell\Attempt"
$files = Get-ChildItem -Path $container -Filter *.txt
foreach ($name in $names)
{
$quart_num = 1
foreach ($quart in $quarts)
{
$files |
Where-Object {$_.name -match "${name}\D.*(${quart})"} |
Get-Content |
Out-File "${container}\test\${name}_Q${quart_num}.txt" -Encoding ascii
$quart_num += 1
}
}
# Remove any empty files
Get-ChildItem -Path "${container}\test" | Where-Object {$_.Length -eq 0} | Remove-Item
Note that the script will error out if the path ${container}\test doesn't exist.
Note that I've slightly changed the Regular expression in the script used - the regular expressions look like NAME1\D.*(JAN|FEB|MAR) instead of NAME1.*(JAN|FEB|MAR). This is so that a file named NAME14_JAN.txt doesn't match the regular expression corresponding to NAME1 as well as NAME14
The following solution uses the Group-Object cmdlet; it may not be the easiest to understand, but it is concise and doesn't require looping over the input files multiple times:
$sourceDir = 'C:\Powershell\Attempt'
$outDir = 'C:\Powershell\Attempt\test' # Make sure this dir. exists.
$months = 'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'
# The regex to match file names against.
$regex = '\b(NAME\d+).*[^a-z]({0})[^a-z]' -f ($months -join '|')
Get-ChildItem -File -Path "$sourceDir\*.txt" |
Group-Object { # Group files by shared name and quarter
if ($_.Name -match $regex) {
$qIndex = 1 + [math]::Floor([Array]::IndexOf($months, $Matches[2].ToUpper()) / 3)
'{0}\{1}_Q{2}.txt' -f $outDir, $Matches[1], $qIndex # full output path
} else {
'N/A'
}
} |
Where-Object Name -ne 'N/A' | # Weed out non-matching files
ForEach-Object {
Set-Content -Encoding Ascii -LiteralPath $_.Name -Value ($_.Group | Get-Content)
}
If there are only a few files, what you have looks pretty good to me. But if there are a large amount of files or the files are large, you have a couple places where you can gain some performance.
First is by using the -Filter parameter, like this:
Get-childitem -Path 'C:\Powershell\Attempt\*' -Filter '*NAME1*.txt'
[NOTE: The -Filter parameter usually only works well on one filter, so you'll still want to use the where-object {$_.name -like... for the different months]
The second place you can gain some performance is by setting the 'C:\Powershell\Attempt\*' -Filter '*NAME1*.txt' command equal to a variable. Setting the results to a variable allows you to make the search once and then reuse the results:
$name1 = Get-childitem -Path 'C:\Powershell\Attempt\*' -Filter '*NAME1*.txt'
You can put this into a loop if desired but in my opinion it isn't worth the effort. You can however get rid of the blank text files with some sort of existence check. Here is how the entire thing could look:
$name1 = Get-childitem -Path 'C:\Powershell\Attempt\*' -Filter '*NAME1*.txt'
if (1 -eq ($name1 | where-object {$_.name -like "*JAN*" -or $_.name -like "*FEB*" -or $_.name -like "*MAR*"}).Count){
get-content | Out-File C:\Powershell\Attempt\test\NAME1_Q1_.txt -Encoding ascii}
if (1 -eq ($name1 | where-object {$_.name -like "*APR*" -or $_.name -like "*MAY*" -or $_.name -like "*JUN*"}).Count){
get-content | Out-File C:\Powershell\Attempt\test\NAME1_Q2_.txt -Encoding ascii}
if (1 -eq ($name1 | where-object {$_.name -like "*JUL*" -or $_.name -like "*AUG*" -or $_.name -like "*SEP*"}).Count){
get-content | Out-File C:\Powershell\Attempt\test\NAME1_Q3_.txt -Encoding ascii}
if (1 -eq ($name1 | where-object {$_.name -like "*OCT*" -or $_.name -like "*NOV*" -or $_.name -like "*DEC*"}).Count){
get-content | Out-File C:\Powershell\Attempt\test\NAME1_Q4_.txt -Encoding ascii}
Related
I am writing a PowerShell script to sort the directory and return folders with the names that start with the number sequence that is greater than specified sequence, but -gt operator acts as a -ge operator.
Here is the code I'm running:
Get-ChildItem C:\Users\USER\Testing -recurse |
Where-Object { $_.PSIsContainer -ge $true -and $_.Name -gt "003" -and $_.Name -match '^\d+.*$' } |
Select-Object Name
The response I get is:
005-folder
003-folder
004-folder
There seems to be the similar but opposite pattern for -le and -lt operators. Both of them do NOT include the equal item. So when I run
Get-ChildItem C:\Users\USER\Testing -recurse |
Where-Object {$_.PSIsContainer -ge $true -and $_.Name -le "003" -and $_.Name -match '^\d+.*$' } |
Select-Object Name
The response I get is:
001-folder
002-folder
I can't seem to find anything on the internet that solves the issue I am having, so I assume something in my scripts breaks the -gt and -le operators?
I think I figured out the issue few minutes after I posted it. '003-folder' will be greater than '003' cause I'm comparing strings. Need to truncate first.
Get-ChildItem C:\Users\USER\Testing -recurse -Directory |
Where-Object {$_.Name.subString(0, 3) -gt '003' -and $_.Name -match '^\d+.*$' } |
Select-Object Name
I'm fairly new to Powershell and programming in general. I want to search files using Powershell having multiple conditions. I have managed to write this code
$Drives = Get-PSDrive -PSProvider 'FileSystem'
$Filename= 'Result'
$IncludeExt= '*csv,*docx'
$StartDate= '11/1/20'
$EndDate= '1/26/21'
Get-ChildItem -Path $Drives.Root -Recurse |Where-Object {$IncludeExt -match $_.Extension} | Where-Object { $_.BaseName -match $Filename} | Where-Object {$_.lastwritetime -ge $StartDate -AND $_.lastwritetime -le $EndDate} |
foreach{
$Item = $_.Basename
$Path = $_.FullName
$Type = $_.Extension
$Age = $_.CreationTime
$Path | Select-Object `
#{n="Name";e={$Item}},`
#{n="Created";e={$Age}},`
#{n="filePath";e={$Path}},`
#{n="Folder/File";e={if($Folder){"Folder"}else{$Type}}}`
}| Export-Csv D:\FFNew.csv -NoTypeInformation
This works well when the all variables are mentioned. But how do I get this to work when
Case1: If $Filename is empty then it gives all the files with the mentioned extensions and files modified in Range of dates
Case2: If $IncludeExt is left empty then it gives all files with the $Filename mentioned, currently it gives only the folders and files modified in Range of dates
Case 3: If $Filename and $IncludeExt is left empty it gives all the files modified between the $StartDate and $EndDate
Pranay,
[EDITED]
Ok, here's the revised (exact) script with notes and sample output. Note: you'll have to change the items that are specific to my machine!
$Drives = Get-PSDrive -PSProvider 'FileSystem'
$Filename = "*" #for all or "*partial name*"
$IncludeExt = $Null #for no ext. or "*.csv","*.docx",etc...
$StartDate = '01/1/2020' #to ignore this use 1/1/1920
#For latest date use below otherwise specify date.
$EndDate = (Get-Date).ToShortDateString()
#Note: below uses only 3rd drive in the array remove [2] for all.
$GCIArgs = #{Path = $Drives[2].Root
Recurse = $True
}
If ($Null -ne $IncludeExt) {
$GCIArgs.Add("Include",$IncludeExt)
}
Get-ChildItem #GCIArgs |
Where-Object {($_.BaseName -Like $Filename) -and
($_.lastwritetime -ge $StartDate) -and
($_.lastwritetime -le $EndDate) } |
foreach{
$Item = $_.Basename
$Path = $_.FullName
$Type = $_.Extension
$Type = & {if($_.PSIsContainer){"Folder"}else{$_.Extension}}
$Age = $_.CreationTime
$Path | Select-Object #{n="Name" ;e={$Item}},
#{n="Created" ;e={$Age}} ,
#{n="filePath" ;e={$Path}},
#{n="Folder/File";e={$Type}}
} | Export-Csv -LiteralPath 'G:\BEKDocs\FFNew.csv' -NoTypeInformation
Notes:
$IncludeExt is specified as $Null if it is not used and if used the list is like this ".csv",".docx"
$Filename is specified as "*" for all filenames. Also changed the test from -match to -like so partial filenames should include *, e.g. "partial name".
Notice I changed the location of the check for Extensions to use the -Include parameter of the Get-ChildItem vs checking in the Where-Object.
Changed the piping of data to successive Where-Object clauses and replaced with -and operator, same effect and more efficient.
Changed the test for Directories to use the PSIsContainer property, couldn't see where you were getting the value for $Folder.
Removed the continuation characters from the Select-Object as the comma serves that purpose and is cleaner.
Sample output on Single drive (per code shown above) with some lines hidden for space considerations but notice the last line number.
Sample output on all drives (code edited as per comment in code), again lines hidden for space but showing multiple drives and final line number.
HTH
I have written a script that will recurse a specified folder and do some analysis on the files within it. I need to exclude specified sub-folders in the analysis. This list of exclusions changes dependent on the base folder being analysed. I have the script working using a long pattern like this:
Get-ChildItem -File -Recurse $source_folder |
Where-Object {
$_.FullName -notlike "*\folder_name0\*" -and
$_.FullName -notlike "*\folder_name1\*" -and
$_.FullName -notlike "*\folder_name2\*" -and
$_.FullName -notlike "*\folder_name3\*" -and
$_.FullName -notlike "*\folder_name4\*"
}
but this is not very reusable. I would like to be able to store exception lists in .CSVs and call the exception list I need based on the folder set I am analyzing. What I would like to do is something like:
$exception_list = Import-CSV .\exception_list
Get-ChildItem -File -Recurse $source_folder |
Where-Object {$_.FullName -notlike $exception_list}
but this does not work. I suspect because I can't specify and 'and' or an 'or' between the elements in the array. I did briefly consider trying to create the whole argument on the fly using a foreach($exception in $exception_list){$argument += "$_.FullName -notlike $exception -and"}, but that got silly and complex pretty quickly since you still have to remove the last 'and'.
Is there an efficient way to do this?
this builds an array of partial names to be excluded, and uses that array to build a regex OR for use in a -notmatch test.
$ExcludedDirList = #(
'PSES-'
'vscode'
'Test_'
)
# regex uses the pipe symbol as the logical "OR"
$RegexExcludedDirList = $ExcludedDirList -join '|'
$Results = Get-ChildItem -Path $env:TEMP -File -Recurse |
Where-Object {
$_.DirectoryName -notmatch $RegexExcludedDirList
}
I really like #lee_dailey's pattern of creating the regex. An alternative method could be to use -in or -notin to compare collections.
Using Pester:
It 'Filters correctly' {
$list = #('fileA', 'file1', 'file2', 'file32')
$filter = #('file1', 'file3')
$expected = #('fileA', 'file2', 'file32')
$list | Where-Object { $_ -notin $filter} | should -be $expected
}
Or just plain comparison operators:
$list = #('fileA', 'file1', 'file2', 'file32')
$filter = #('file1', 'file3')
$expected = #('fileA', 'file2', 'file32')
$newlist = $list | Where-Object { $_ -notin $filter}
(Compare-Object $newlist $expected).length -eq 0
> True
my purpose is to output the files with the extensions that match the ones I store on a .txt file but I don't know how to make it work. The way I am trying to do now does not generate any output or output the files of the extensions that are not on the text file (Extension.txt) I indicate. How am I supposed to fix this?
The content in my .txt file is:
*.xlsx,*.xlsm,*.xlsb,*.xltx,*.xltm,*.xls,*.xml
My current code is as followed:
$fileHeaders = #('country','cDrive','dDrive')
$extensions = ${C:temp:Extension.txt}
$LocContent = Import-Csv "C:\temp\Location.txt" -Header $fileHeaders
$NumberOfDays = Read-Host 'Within how many days the files created would you like to output?'
$SizeOfFile = Read-Host 'Above what size of the files would you like to output (in kb or mb)?'
$Output = ForEach($Row in $LocContent){
if (($Row.country -ne $null) -and ($Row.cDrive -ne $null) -and ($Row.dDrive -ne $null)){
Get-ChildItem $Row.cDrive,$Row.dDrive -Force -Include -Recurse |
$extensions Where-Object LastWriteTime -gt (Get-Date).AddDays(-$NumberOfDays) |
Where-Object {$_.length/$SizeOfFile -gt 1} |
Select-Object -Property #{N='File Basename';E={$_.BaseName}},
#{N='File Extension';E={$_.Extension}},
#{N='size in MB';E={$_.Length/1024kb}},
Directory,
CreationTime,
LastWriteTime,
#{N="Location";E={$Row.country}}
}
$Output | Format-Table -Auto
$Output | Out-Gridview
$Output | Export-Csv '\NewData.csv' -NoTypeInformation
This both generated the files, outputs the files and makes sure they are in your .txt file.
I am not surprised your code wont work. You were doing it fairly poorly. Look forward to seeing you improve.
$extensions = ((Get-Content C:\temp\Extensions.txt) -join ',') -split ',' -replace '\*',''
Foreach($ext in $extensions){
Get-ChildItem "C:\temp" -Recurse | select Name, FullName, CreationTime, Extension | Where-Object {$_.Extension -like $ext} | export-csv C:\Files.csv -NoTypeInformation -append
}
I just cleaned up your code.
Without knowing the content of Location.txt,
if there are repetitions in the drives, wouldn't Output contain dublettes of the files with different country?
Without your environment untested.
## Q:\Test\2018\07\05\SO_51183354.ps1
$fileHeaders = #('country','cDrive','dDrive')
$Extensions = (Get-Content 'C:\temp\Extension.txt') -replace '\*' -split ','
$LocContent = Import-Csv "C:\temp\Location.txt" -Header $fileHeaders |
Where-Object {($_.country -ne $null) -and
($_.cDrive -ne $null) -and
($_.dDrive -ne $null) }
$NumberOfDays = Read-Host 'Max file age in days?'
$SizeOfFile = Read-Host 'Min file size (in kb or mb)?'
$FileAge = (Get-Date).AddDays(-$NumberOfDays)
$Output = ForEach($Row in $LocContent){
Get-ChildItem $Row.cDrive,$Row.dDrive -Force -Recurse |
Where-Object {($_.Extension -in $Extensions) -and
($_.LastWriteTime -gt $FileAge) -and
($_.Length -gt $SizeOfFile) } |
Select-Object -Property `
#{N='File Basename' ;E={$_.BaseName}},
#{N='File Extension';E={$_.Extension}},
#{N='size in MB' ;E={$_.Length/1MB}},
Directory,
CreationTime,
LastWriteTime,
#{N="Location" ;E={$Row.country}}
}
$Output | Format-Table -Auto
$Output | Out-Gridview
$Output | Export-Csv '\NewData.csv' -NoTypeInformation
Is there a way when you use Get-ChildItem with a Where-Object clause to have it produce the results in a text file only if there are results?
Get-ChildItem -path \\$server\e$ -Recurse | Where-Object {$_.name -eq help.txt} | `
out-file "c:\temp\$server.txt"
The above will produce a file regardless if there are results. I'm having trouble telling implementing the logic to only create when results are available.
You can't do it that way. You'll have to do it in 2 parts:
$results = Get-ChildItem -path \\$server\e$ -Recurse | Where-Object {$_.name -eq help.txt}
if ($results) {
$results | out-file "c:\temp\$server.txt"
}
Seems to work how you want if you use Set-Content instead of Out-File.
Get-ChildItem -path \\$server\e$ -Recurse | Where-Object {$_.name -eq help.txt} |
Set-Content "c:\temp\$server.txt"
#or
gci -R \\$server\e$ |? Name -eq "help.txt" | sc "c:\temp\$server.txt"