Using an array of wildcards in a Powershell WHERE clause - powershell

I'm trying to make the following PowerShell script more generic. I want to pass in an array of excludes rather than a fixed list. I can't figure out how to do this except my partial solution below:
ORIGINAL
This gets all the files in a path except a list of wildcard files or folders:
Get-ChildItem -Path "$sitePath" -Recurse | `
where {!$_.PSIsContainer } | `
Select -ExpandProperty FullName | `
Where {$_ -notlike "$sitePath\Custom\*"} | `
Where {$_ -notlike "$sitePath\Download\*"} | `
Where {$_ -notlike "$sitePath\Temp\*"} | `
Where {$_ -notlike "$sitePath\Portal\*"} | `
Where {$_ -notlike "$sitePath\web.config*"} | `
SELECT $_
PARTIAL SOLUTION
This is the best I've come up with. It allows me to create an array of wildcards called $excludeList, but is limited and is slightly slower:
$excludeList = #("$sitePath\Custom\*",
"$sitePath\Download\*",
"$sitePath\Portal\*",
"$sitePath\web.config*")
Get-ChildItem -Path "$sitePath" -Recurse | `
where {!$_.PSIsContainer } | `
Select -ExpandProperty FullName | `
Where {$_ -notlike $excludeList[0]} | `
Where {$_ -notlike $excludeList[1]} | `
Where {$_ -notlike $excludeList[2]} | `
Where {$_ -notlike $excludeList[3]} | `
Where {$_ -notlike $excludeList[4]} | `
Where {$_ -notlike $excludeList[5]} | `
Where {$_ -notlike $excludeList[6]} | `
Where {$_ -notlike $excludeList[7]} | `
Where {$_ -notlike $excludeList[8]} | `
Where {$_ -notlike $excludeList[9]} | `
Where {$_ -notlike $excludeList[10]} | `
SELECT $_
Is there a better way to pass an array in to the where clause? All the solutions I've found only allow non-wildcard matches.
Hope someone can help!

One approach would be to iterate over the items in the exclude list, and only include a path if it does not match any of the exclusions:
$excludeList = #("$sitePath\Custom\*",
"$sitePath\Download\*",
"$sitePath\Portal\*",
"$sitePath\web.config*")
Get-ChildItem -Path "$sitePath" -Recurse |
where { !$_.PSIsContainer } |
select -ExpandProperty FullName |
where { $path = $_; -not #($excludeList | ? { $path -like $_ }) }
If all of your excluded items follow the same pattern, you can also simplify the exclude list by moving the common pattern to the like call:
$excludeList = #('Custom','Download','Portal','web.config')
Get-ChildItem -Path "$sitePath" -Recurse |
where { !$_.PSIsContainer } |
select -ExpandProperty FullName |
where { $path = $_; -not #($excludeList | ? { $path -like "$sitePath\$_*" }) }

If you're willing to go with a regex instead, you can simplify this a lot:
$excludeList = [regex]::Escape("$sitePath\Custom\"),
[regex]::Escape("$sitePath\Download\"),
[regex]::Escape("$sitePath\Temp\") -join "|"
Get-ChildItem $sitePath -Recurse | `
where {!$_.PSIsContainer } | `
Select -ExpandProperty FullName | `
Where {$_ -notmatch $excludeList}
Not sure why you have the trailing Select $_, it is unnecessary AFAICT.

You should try -contains and -notcontains operators if you are trying to compare against an array.
$array = "name1","name2","name3"
#This will return false
$array -contains "name"
#this will return true
$array -notcontains "name"

Related

How replace a character by $_ with PowerShell

I have a problem with my PowerShell script and I don't find the answer to my question.
I try to replace:
get-vm | Where-Object {$_.IsClustered -eq $False} | ForEach-Object {
With:
get-vm | Where-Object {$_.IsClustered -eq $False} | Where-Object {$_.State -eq 'Running'} | ForEach-Object {
My code:
$old = [regex]::Escape(' get-vm | Where-Object {$_.IsClustered -eq $False} | ForEach-Object {')
$new = " get-vm | Where-Object {$_.IsClustered -eq `$False} | Where-Object {$_.State -eq 'Running'} | ForEach-Object {"
(Get-Content -Path c:\test\test.txt) | ForEach {$_ -Replace "$old","$new"} | Set-Content -Path c:\test\testsuccess
Before:
After:
I don't understand why the $_ is replaced by nothing. I would like to keep $_ in the output file.
UPDATE 1
I have tried to do
$old = [regex]::Escape(' get-vm | Where-Object {$_.IsClustered -eq $False} | ForEach-Object {')
$new = " get-vm | Where-Object {`$_.IsClustered -eq `$False} | Where-Object {`$_.State -eq 'Running'} | ForEach-Object {"
(Get-Content -Path c:\test\test.txt) | ForEach {$_ -Replace "$old","$new"} | Set-Content -Path c:\test\testsuccess
But it doesn't work. The output in the testsuccess file is:
get-vm | Where-Object { get-vm | Where-Object {$_.IsClustered -eq $False} | ForEach-Object { .IsClustered -eq $False} | Where-Object { get-vm | Where-Object {$_.IsClustered -eq $False} | ForEach-Object { .State -eq 'Running'} | ForEach-Object {
You can use the PowerShell parser to do this along with here-strings and the .NET Replace method.
$parsedcode = [System.Management.Automation.Language.Parser]::ParseFile('c:\test\test.txt',[ref]$null,[ref]$null)
$old = #'
get-vm | Where-Object {$_.IsClustered -eq $False} | ForEach-Object {
'#
$new = #'
get-vm | Where-Object {$_.IsClustered -eq $False} | Where-Object {$_.State -eq 'Running'} | ForEach-Object {
'#
$parsedcode.ToString().Replace($old,$new) | Out-File c:\test\testsuccess -Force
Since we are doing static string replacements, I believe regex is just making this more complicated. The PowerShell language parser can read PowerShell code from a string or file and output it to the console or store it in memory as is. Other capabilities open up using the parser as well.
Using the here-strings (#''#) allows for special symbols in the strings to be treated literally.
The .Replace() method is a case-sensitive string replacement. It does not use regex.
Try this one:
$regex = '\sget-vm\s\|\sWhere-Object\s\{\$_\.IsClustered\s-eq\s\$False\}\s\|\sForEach-Object\s\{'
$new = " get-vm | Where-Object {$_.IsClustered -eq $False} | Where-Object {$_.State -eq 'Running'} | ForEach-Object {"
$testString = ' get-vm | Where-Object {$_.IsClustered -eq $False} | ForEach-Object {'
$testString -replace $regex, $new
An online version of the above example can be found under tio.run.
You've to be aware to escape special characters like $ in your regex correctly ( use \).
Also, be aware to escape the simple quotes in $new correctly. I used the PowerShell quoting rules and defined $new as "formattable" string to escape ``'Running' correctly.
Update1 (based on #Ansgars comments):
Change quotation of $new to:
$new = ' get-vm | Where-Object {$_.IsClustered -eq $False} | Where-Object {$_.State -eq "Running"} | ForEach-Object {'
When enclosing the string of $new in single quotes no substitution actions are performed. In this case, you've to change the single quotes at 'Running' with double-quotes. An online example can be found here.
UPDATE 2:
Hopefully, this will be the last update. Your problem is related to the .NET defined substitution elements (which PowerShell seems to use behind the scenes):
$_ Includes the entire input string in the replacement string.
In addition PowerShell's quoting rules come into play. Let's check this example. We want to replace the word car with $_:
"car" -replace "car", "$_"
The output is empty. Why? Because of using double quotes PowerShell substitutes $_ with its actual value. Well since nothing is stored in $_ ( because it was not defined and it normally holds the actual pipeline value, but there is no pipeline included), car is replaced to an empty string.
Ok, next try, lets put $_ into single quotes:
"car" -replace "car", '$_'
The output is car. Why? Well using single quotes deactivates PowerShell's variable substitutions, and $_ is forwarded as literal to the replace operator. Now the .Net regex substitutions come into play. As stated in substitution elements $_ includes the entire input string, in our case car. So the -replace operator replaces car with car. We can see the entire input is included in this example:
"car" -replace "car", '$_ $_ $_'
Output:
car car car
So, that's the reason why we see this result in the OP:
get-vm | Where-Object { get-vm | Where-Object {$_.IsClustered -eq $False} | ForEach-Object { .IsClustered -eq $False} | Where-Object { get-vm | Where-Object {$_.IsClustered -eq $False} | ForEach-Object { .State -eq 'Running'} | ForEach-Object {
Resulting from that it seems that $_ can't be escaped. The only way I could achieve the request solution for the OP is performing a double substitution (which is not the best way when thinking about performance, maybe there is a better way).
TL;DR
Solution:
$old = [regex]::Escape('get-vm | Where-Object {$_.IsClustered -eq $False} | ForEach-Object {')
$new = ' get-vm | Where-Object {#_.IsClustered -eq $False} | Where-Object {#_.State -eq "Running"} | ForEach-Object {'
$testString = 'get-vm | Where-Object {$_.IsClustered -eq $False} | ForEach-Object {'
($testString -replace $old, $new) -replace '#', '$'
Output:
get-vm | Where-Object {$_.IsClustered -eq $False} | Where-Object {$_.State -eq "Running"} | ForEach-Object {
So I used #_ instead of $_ to deactivate substitution algorithms in general, and perform the first substitution with #_ in $new, which result in:
get-vm | Where-Object {#_.IsClustered -eq $False} | Where-Object {#_.State -eq "Running"} | ForEach-Object {
Now it is easy to substitute # with $, since $ alone is not defined in substitution elements.
Hope that solve the problem.

Can't remove all the other files but those in one variable in powershell

I have a script that filters my logs, but the problem is that when I would like to delete everything else but certain files I get errors of Unrecognized escape sequence. I've been trying to split the values but it seems that nothing works. I also tried -exclude before, but didn't get it to work. It's supposed to remove all the other files but $result and $clr.
$files = #()
[xml]$photonconfig = Get-Content C:\Users\Administrator\Desktop\PhotonServer.config
$photonconfig.SelectNodes("Configuration/*") | Select-Object -Expand Name | % {
$_.Replace("xxx","")
} | ForEach {
$files+= Get-ChildItem C:\Users\Administrator\Desktop\log\log/*$_*.log |
sort -Property LastWriteTime -Descending |
Select-Object -First 3
}
$result = $files | Sort-Object LastAccessTime -Descending |
Select-Object -First 3
$clr = "PhotonCLR.log"
$all = Get-ChildItem C:\Users\Administrator\Desktop\log\log/* |
Where-Object { $_.Name -notmatch $result } |
Remove-Item
The second operand of the -match and -notmatch operators is a regular expression, not an array of file names. Use the -contains operator instead:
... | Where-Object { $result -notcontains $_.Name } | ...
On PowerShell v3 and newer you can also use the -notin operator, which feels a little more "natural" to most people:
... | Where-Object { $_.Name -notin $result } | ...
Note that for this to work you also need to expand the Name property when building $result:
$result = $files | Sort-Object LastAccessTime -Descending |
Select-Object -First 3 -Expand Name

Using Get-childitem to get list of files -like $pattern or newer than any file -like $pattern

as for now I have script to find files -like '*ver1.0*', and it's working fine.
$files = Get-ChildItem "D:\path\" | where {($_ -like "*$version*.sql")}
List of files:
file_ver1.0_xx.sql
file_ver1.0_xy.sql
But now I need to find files which look like before OR it's newer than it.
For example, I need to find files with pattern: *ver1.0* or *ver0.9* whose LastWriteTime is newer than any *ver1.0* file.
For performance reasons I'd enumerate the files just once, determine the most recent modification date of the files with $version, then filter for files with $version and files with a different version but newer date:
$allFiles = Get-ChildItem 'D:\path' -Filter '*.sql' |
Where-Object { -not $_.PSIsContainer }
$refDate = $allFiles | Where-Object {
$_.BaseName -like "*$version*"
} | Sort-Object LastWriteTime | Select-Object -Last 1 -Expand LastWriteTime
$files = $allFiles | Where-Object {
$_.BaseName -like "*$version*" -or
($_.BaseName -notlike "*$version*" -and $_.LastWriteTime -gt $refDate)
}
If you need to actually compare version numbers you probably need a somewhat more elaborate approach, though, e.g. like this:
$pattern = 'ver(\d\.\d)'
$refVersion = [version]'1.0'
$allFiles = Get-ChildItem 'D:\path' -Filter '*.sql' |
Where-Object { -not $_.PSIsContainer }
$refDate = $allFiles | Where-Object {
$_.BaseName -match $pattern -and
[version]$matches[1] -eq $refVersion
} | Sort-Object LastWriteTime | Select-Object -Last 1 -Expand LastWriteTime
$files = $allFiles | Where-Object {
$_.BaseName -match $pattern -and
([version]$matches[1] -eq $refVersion -or
([version]$matches[1] -lt $refVersion -and $_.LastWriteTime -gt $refDate))
}
I know that's ugly, but it works.
$files = Get-ChildItem "D:\path" | where {($_ -like "*$version*.sql")}
$files += Get-ChildItem "D:\path" | where {($_ -like "*.sql" -notlike "*$version*.sql" -and $_.LastWriteTime -gt $files[0].LastWriteTime)}

Exclude A server from list of Output

I have a script that reports NTDS service status from entire forest there are few 2003 servers which I want exclude below is the script.
$getForest = [system.directoryservices.activedirectory.Forest]::GetCurrentForest()
$getForest.domains | ForEach-Object {$_.DomainControllers} | ForEach-Object {$_.Name}
I was trying something like
$getForest = [system.directoryservices.activedirectory.Forest]::GetCurrentForest()
$getForest.domains | ForEach-Object {$_.DomainControllers} | ForEach-Object {$_.Name} | where-object {$_.name -notlike "server2003.domain.local"}
No luck
Your second ForEach-Object isn't needed, so I've consolidated it into the first.
For this kind of thing I would recommend creating an array of names you want excluded, which I called $exclude.
Then I'm using the -notcontains operator to check the name against the list from within Where-Object.
$exclude = #(
'server2003.domain.local'
'other2003.domain.local'
)
$getForest = [system.directoryservices.activedirectory.Forest]::GetCurrentForest()
$getForest.domains |
ForEach-Object {$_.DomainControllers.Name} |
Where-Object {$exclude -notcontains $_.name}
I got it working
$exclude = #(
'dc2.local'
'dc3.local'
'dc4.local'
'dc5.local'
)
(Get-ADForest).Domains | %{ Get-ADDomainController -Filter * -Server $_ } | Where-Object {$exclude -notcontains $_.hostname} | select Hostname

Converting list of strings to numbers

I have a directory whose subdirectories are all numbers:
./2856
./2357
./10198
and so on.
I'm trying to write a Powershell script that would return the largest subdirectory name smaller than X.
So in this example, for the input 3000 it should return 2856.
However what I've written so far looks very cumbersome to me, and I'm wondering how it can be shortened:
Get-ChildItem "$path" `
| ?{ $_.PSIsContainer } `
| Select-Object #{Name="AsInt"; Expression={[int]$_.Name}} `
| Select-Object -expand AsInt `
| ?{$_ -lt [int]$lessThanNumber} `
| measure-object -max `
| Select-Object -expand Maximum
I tried this with PowerShell v3:
$max = 3000
$cur = 0
ls -d | %{
# Potential for issues if the directory name cannot be cast to [int]
$name = ([int]$_.Name)
if (($name -gt $cur) -and ($name -le $max)) {
$cur = $name
}
}
($cur = 2856 at the end)
With PowerShell v3:
Get-ChildItem $path -dir | Select *,#{n='Number';e={[int]$_.Name}} |
Where Number -lt $lessThanNumber | Sort Number | Select -Last 1
If you have V3:
#(Get-ChildItem -Path $Path -Directory -Name |
ForEach-Object {$_ -as [int]}) -lt $LessThanNumber |
sort | select -last 1
You can try :
Get-ChildItem "$path" | Where-Object {$_.PSIsContainer -and [int]$_.name -le 3000} `
| Sort-Object -Property #{exp={[int]$_.name}} `
| Select-Object -Last 1
You can write it :
Get-ChildItem "$path" | ? {$_.PSIsContainer -and [int]$_.name -le 3000} `
| Sort -Property #{exp={[int]$_.name}} `
| Select -Last 1
If you want to avoid errors due to these directory names which are not integers :
Get-ChildItem "$path" | ? {$_.PSIsContainer -and ($_.name -as [int]) -le 3000} `
| Sort -Property #{exp={$_.name -as [int]}} `
| Select -Last 1
Yet another (v3) example. Only directory names that contain numbers are passed on and the Invoke-Expression cmdlet is used to evaluate the name into a number (no explicit cast is needed)
$x = 3000
Get-ChildItem -Directory | Where-Object {
$_.Name -notmatch '\D' -and (Invoke-Expression $_.Name) -lt $x
} | Sort-Object | Select-Object -Last 1