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.
Related
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}
I am trying to pipe the output from a foreach loop into a format command but it does not work. The reason I think is possible is because this works.
$op = foreach ($file in (Get-ChildItem -File)) {
$file |
Get-Member |
Where-Object {$_.MemberType -eq "Method" -and $_.Definition -like "*system*" } |
Select-Object -Property Name, MemberType
}
$op | Format-List
If I can assign the whole output to a variable, and pipe the variable into another command, why does the following NOT work?
(foreach ($file in (Get-ChildItem -File)) {
$file |
Get-Member |
Where-Object {$_.MemberType -eq "Method" -and $_.Definition -like "*system*" } |
Select-Object -Property Name, MemberType
}) | Format-List
Of course I tried without parents, but ultimately I think if anything, the parents make sense. It is like $file in (Get-ChildItem -File) where it evaluates the expression in the parents and uses the result as the actual object
Is there a way to make this work?
please note that the code is not supposed to achieve anything (else) than giving an example of the mechanics
foreach does not have an output you can capture (besides the sugar you've found with variable assignment), but you can gather all the objects returned by wrapping it in a subexpression:
$(foreach ($file in Get-ChildItem -File) {
# ...
}) | Format-List
This same pattern can be used for if and switch statements as well.
Here's another way to do it, without waiting for the whole foreach to finish. It's like defining a function on the fly:
& { foreach ($file in Get-ChildItem -File) {
$file |
Get-Member |
Where-Object {$_.MemberType -eq "Method" -and $_.Definition -like "*system*" } |
Select-Object -Property Name, MemberType
}
} | format-list
By the way, $( ) can go anywhere ( ) can go, but it can enclose multiple statements separated by newlines or semicolons.
Also, you can pipe it directly:
Get-ChildItem -File |
Get-Member |
Where-Object {$_.MemberType -eq "Method" -and $_.Definition -like "*system*" } |
Select-Object -Property Name, MemberType |
Format-List
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
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
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"