If/Then - then executing even when condition is false - powershell

The below is supposed to go through a folder containing jpgs and replace the filenames with nnnZ.jpg, skipping a number if the image is in landscape.
I've seen the examples at https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-if?view=powershell-7.1 and it doesn't look like there's a problem with syntax of the if statement or the condition ($w -gt $h), but the 'then' is being executed every time, even though all but one of the images in the folder are in portrait. The values of $w and $h are correct (per the echo) so I don't understand what is going wrong.
Edit: or are those values actually strings, and is that what the %s mean?
Get-ChildItem | foreach {
$w=$(magick identify -format '%w' $_)
$h=$(magick identify -format '%h' $_)
$flag="false"
if ($w -gt $h) {
$c++
$flag="true"
}
$newname = "$($c)Z.jpg".Padleft(8,'0')
Rename-Item $_ -NewName $newname
$c++
echo $w $h $flag $c
}

In order not to have yet another 'unanswered' question on SO, here my comment as answer:
the code for $w=$(magick identify -format '%w' $_) and $h=$(magick identify -format '%h' $_) produces strings, not integer values.
Cast these to int using
[int]$w = $(magick identify -format '%w' $_)
[int]$h = $(magick identify -format '%h' $_)
so the comparison would do what you expect.
As mklement0 commented, in the above lines, the SubExpression operator $() is not needed, although it doesn't harm in getting the results.
Basically, while a simple ( ) grouping expression means 'execute this part first', a subexpression $( ) means 'execute this first and then treat the result like a variable'. It can contain multiple semicolon ; separated statements.
If the files you need to get the width and height for are all of type BMP, GIF, JPEG, PNG or TIFF, you don't need to use an external application like ImageMagick to get the size, because the .NET System.Drawing.Image class can handle those very well:
# open the image file
$img = [System.Drawing.Image]::FromFile($_.FullName)
$w = $img.Width # these are Int32 values
$h = $img.Height
# dispose of this image object
$img.Dispose()

Related

From output, include line only contain a key word and extract first field from the included lines with powershell

With PowerShell, I am trying to extract the first field from an output that contains multiple lines as below. Along with this, I wanted to exclude if the line doesn't have a key 'web:'
Getting apps in org SomeOrg / space Somespace as x-user...
name requested state processes routes
maskedappname1 started web:1/1 maskedappname1.com
maskedappname2 started web:0/1 maskedappname2.com
maskedappname3 started web:1/1 maskedappname3.com
maskedappname4 started web:1/1 maskedappname4.com
maskedappname5 started web:1/1 maskedappname5.com
maskedappname6 stopped web:0/1 maskedappname6.com
after execution, my final output should be
maskedappname1
maskedappname2
maskedappname3
maskedappname4
maskedappname5
maskedappname6
tried multiple ways didn't help me.
Much appreciate it if I get some help on this.
Thanks.
You can use a switch with the -Regex parameter to match any line having web: and capture the everything from the beginning of the line until the first whitespace.
switch -File path\to\file.ext -Regex {
'(^\S+).+web:' { $Matches[1] }
}
See https://regex101.com/r/fxQtcN/1 for details.
iterate through each line
$array = $textWithMultipleLines.Split(“`n”)
foreach ($line in $array){
# ...
}
take fixed length (if possible) or split on space ant take the first item of the split array
($extract -split " ")[0]
# or the regex way:
$extract -replace '^([^ ]+ ).+$','$1'
all together
$array = $textWithMultipleLines.Split(“`n”)
foreach ($line in $array){
$maskedAppName = ($line -split " ")[0]
Write-Host "maskedAppName: $maskedAppName"
}

Powershell optimizing script wildcard/regex/code golfing

I have the following powershell code I wrote that I am trying to optimize even further. I essentially need to get this code block down to under 259 characters. It's currently at 319, this is a challenge I know.
Using a mix of regex/wildcard matching/code golfing I think it is possible. But this is something I'm still learning.
This function will convert each character in the z file into its capslock and numlock value, then use send keys to in a very simplified way of explaining use the lights as a form of Morse code but in binary format instead.
I need this to run from the run box hence the character limit.
Why am I doing this? I'm passing data through the channel that controls the lock keys on the keyboard.
powershell "foreach($b in $(cat $env:tmp\z -En by)){foreach($a in 0x80,
0x40,0x20,0x10,0x08,0x04,0x02,0x01){if($b-band$a){$o+='%{NUMLOCK}'}else
{$o+='%{CAPSLOCK}'}}};$o+='%{SCROLLLOCK}';echo $o >$env:tmp\z;$f=(cat $env:tmp\z);Add-Type -A System.Windows.Forms;[System.Windows.Forms.SendKeys]::SendWait($f);rm $env:tmp\z"
I'm at work so I haven't gotten to test it yet but I think I got it down to 268 characters. And again it's needs to be at 259 or less.
powershell "$d='$env:tmp\z'%($b in $(cat -En by)){%($a in 0x80,
0x40,0x20,0x10,0x08,0x04,0x02,0x01){if($b-band$a){$o+='%{NUMLOCK}'}else
{$o+='%{CAPSLOCK}'}}};$o+='%{SCROLLLOCK}';echo $o >$d;$o=(cat $d);Add-Type -A *m.W*s.F*s;[*m.W*s.F*s.SendKeys]::SendWait($o);rm $d"
You'll know your solution works if you have a file in the tmp folder called "z" with no extention, and after running your code the lights for your lock keys should look like a rave.
Just to post the entirety of the code in readable format, here's the result with Mclayton's suggestion included:
# Gather the content from the file
# The use of (...) around the variable assignment lets the value pass through evaluating it as an expression.
Get-Content ($d=".\desktop\Abe.txt") -Encoding byte |
ForEach-Object -Process {
# Assign the current object in the pipeline to $b.
# This will allow the use of another Foreach-Object (%) for shorter code.
$b = $_;
128,64,32,16,8,4,2,1 |
Foreach-Object -Process {
# Append the results to $o as a concatenated string.
# Given a hashtable with the wanted values, you can access the value by providing the name in []'s.
# Since the bitwise operator -BAND only operates "properly" on two equal-length binary representations and if statement is needed.
# The if statement will return values 1/0 in accordance with -BAND
$o += "%{$(#{1="NUM";0="CAPS"}[$( if ($_-band$b) { 1 } else { 0 } )])LOCK}%{SCROLLLOCK}"
}
};
# Concatenate again to $o, while assigning to $f then outputting to $d.
($f = $o + "%{SCROLLLOCK}") | Out-File -FilePath $d;
Add-Type -AssemblyName 'System.Windows.Forms';
[System.Windows.Forms.SendKeys]::SendWait($f);
Remove-Item -Path $d
...and in shorter form:
gc ($d="$env:TEMP\z") -en by|%{$b=$_;128,64,32,16,8,4,2,1|%{$o+="%{$(#{1="NUM";0="CAPS"}[$(if($_-band$b){1}else{0})])LOCK}%{SCROLLLOCK}"}};($f=$o+"%{SCROLLLOCK}")>$d;Add-Type -A *m.W*s.F*s;[System.Windows.Forms.SendKeys]::SendWait($f);rm $d
I added comments in the code just for future readers trying to follow along.

How do I change foreach to for in PowerShell?

I want to print the word exist in a text file and print "match" and "not match". My 1st text file is: xxaavv6J, my 2nd file is 6J6SCa.yB.
If it is match, it return like this:
Match found:
Match found:
Match found:
Match found:
Match found:
Match found: 6J
Match found:
Match found:
Match found:
My expectation is just print match and not match.
$X = Get-Content "C:\Users\2.txt"
$Data = Get-Content "C:\Users\d.txt"
$Split = $Data -split '(..)'
$Y = $X.Substring(0, 6)
$Z = $Y -split '(..)'
foreach ($i in $Z) {
foreach ($j in $Split) {
if ($i -like $j) {
Write-Host ("Match found: {0}" -f $i, $j)
}
}
}
The operation -split '(..)' does not produce the result you think it does. If you take a look at the output of the following command you'll see that you're getting a lot of empty results:
PS C:\> 'xxaavv6J' -split '(..)' | % { "-$_-" }
--
-xx-
--
-aa-
--
-vv-
--
-6J-
--
Those empty values are the additional matches you're getting from $i -like $j.
I'm not quite sure why -split '(..)' gives you any non-empty values in the first place, because I would have expected it to produce 5 empty strings for an input string "xxaavv6J". Apparently it has to do with the grouping parentheses, since -split '..' (without the grouping parentheses) actually does behave as expected. Looks like with the capturing group the captured matches are returned on top of the results of the split operation.
Anyway, to get the behavior you want replace
... -split '(..)'
with
... |
Select-String '..' -AllMatches |
Select-Object -Expand Matches |
Select-Object -Expand Value
You can also replace the nested loop with something like this:
foreach ($i in $Z) {
if (if $Split -contains $i) {
Write-Host "Match found: ${i}"
}
}
A slightly different approach using regex '.Match()' should also do it.
I have added a lot of explaining comments for you:
$Test = Get-Content "C:\Users\2.txt" -Raw # Read as single string. Contains "xxaavv6J"
$Data = (Get-Content "C:\Users\d.txt") -join '' # Read as array and join the lines with an empty string.
# This will remove Newlines. Contains "6J6SCa.yB"
# Split the data and make sure every substring has two characters
# In each substring, the regex special characters need to be Escaped.
# When this is done, we join the substrings together using the pipe symbol.
$Data = ($Data -split '(.{2})' | # split on every two characters
Where-Object { $_.Length -eq 2 } | # don't care about any left over character
ForEach-Object { [Regex]::Escape($_) } ) -join '|' # join with the '|' which is an OR in regular expression
# $Data is now a string to use with regular expression: "6J|6S|Ca|\.y"
# Using '.Match()' works Case-Sensitive. To have it compare Case-Insensitive, we do this:
$Data = '(?i)' + $Data
# See if we can find one or more matches
$regex = [regex]$Data
$match = $regex.Match($Test)
# If we have found at least one match:
if ($match.Groups.Count) {
while ($match.Success) {
# matched text: $match.Value
# match start: $match.Index
# match length: $match.Length
Write-Host ("Match found: {0}" -f $match.Value)
$match = $match.NextMatch()
}
}
else {
Write-Host "Not Found"
}
Result:
Match found: 6J
Further to the excellent Ansgar Wiechers' answer: if you are running (above) Windows PowerShell 4.0 then you could apply the .Where() method described in Kirk Munro's exhaustive article ForEach and Where magic methods:
With the release of Windows PowerShell 4.0, two new “magic” methods
were introduced for collection types that provide a new syntax for
accessing ForEach and Where capabilities in Windows PowerShell.
These methods are aptly named ForEach and Where. I call
these methods “magic” because they are quite magical in how they work
in PowerShell. They don’t show up in Get-Member output, even if you
apply -Force and request -MemberType All. If you roll up your
sleeves and dig in with reflection, you can find them; however, it
requires a broad search because they are private extension methods
implemented on a private class. Yet even though they are not
discoverable without peeking under the covers, they are there when you
need them, they are faster than their older counterparts, and they
include functionality that was not available in their older
counterparts, hence the “magic” feeling they leave you with when you
use them in PowerShell. Unfortunately, these methods remain
undocumented even today, almost a year since they were publicly
released, so many people don’t realize the power that is available in
these methods.
…
The Where method
Where is a method that allows you to filter a collection of objects.
This is very much like the Where-Object cmdlet, but the Where
method is also like Select-Object and Group-Object as well,
includes several additional features that the Where-Object cmdlet
does not natively support by itself. This method provides faster
performance than Where-Object in a simple, elegant command. Like
the ForEach method, any objects that are output by this method are
returned in a generic collection of type
System.Collections.ObjectModel.Collection1[psobject].
There is only one version of this method, which can be described as
follows:
Where(scriptblock expression[, WhereOperatorSelectionMode mode[, int numberToReturn]])
As indicated by the square brackets, the expression script block is
required and the mode enumeration and the numberToReturn integer
argument are optional, so you can invoke this method using 1, 2, or 3
arguments. If you want to use a particular argument, you must provide
all arguments to the left of that argument (i.e. if you want to
provide a value for numberToReturn, you must provide values for
mode and expression as well).
Applied to your case (using the simplest variant Where(scriptblock expression) of the .Where() method):
$X = '6J6SCa.yB' # Get-Content "C:\Users\2.txt"
$Data = 'xxaavv6J' # Get-Content "C:\Users\d.txt"
$Split = ($Data -split '(..)').Where({$_ -ne ''})
$Y = $X.Substring(0, 6)
$Z = ($Y -split '(..)').Where{$_ -ne ''} # without parentheses
For instance, Ansgar's example changes as follows:
PS > ('xxaavv6J' -split '(..)').Where{$_ -ne ''} | % { "-$_-" }
-xx-
-aa-
-vv-
-6J-

Properly Trimming and Escaping a String with a Backslash Separator in PowerShell

I've got an array of directories that present as such:
C:\Parent\Child\Child\_Grandchild
Some children are deeper than others. I need to trim off the
C:\Parent\
and the
\_Grandchild
into an array consisting of
Child, Child
but am constantly having PS strip off leading characters. It seems to be mainly 1's, A's, C's, and P's, but could be others (those are the ones at the top of the list so I notice them). Here is the code I am using, I am certain I am using split incorrectly but cannot figure out how to get it to work as needed.
$a # <-- An array item in a foreach loop
$b = $a.TrimStart('C:\Parent\');
$c = $b.TrimEnd('\_Grandchild');
$c_split = $c -split '\\';
This code seems to often produce results like the following
$c_split[0] = 'hild'; # (or 'ild' in some cases, depending on the starting characters)
$c_split[1] = 'Child';
$c_split[2] = 'Child';
and so on. I figured it was something with my initial TrimStart, but viewing $b during the process looks just fine, just as you would expect. I've tried leaving the trailing \ on the first trim but that didn't seem to solve the problem either.
Without some better test data it's hard to determine what you are really going for but I might have something. If you are just wanting to remove C:\Parent and \_GrandChild or at least the last child in the directory chain, the following will work:
# Assumed test data
$Directories = #(
"C:\Parent\Child\Child\_Grandchild",
"C:\Parent\Child\Hulk\_Grandchild",
"C:\Parent\Child\DareDevil\Child\Child\_Grandchild",
"C:\Parent\Child\DoctorWho\Child\_Grandchild",
"C:\Parent\Child\DareDevil\Child\FrenchBread\_Grandchild"
)
$Directories | ForEach-Object {
$Path = $_
$Path = Split-Path -Path $Path -NoQualifier #Remove the C:
$Path = Split-Path -Path $Path # Remove the last portion
# Here you have "\Parent\..." excluding the _Grandchild portion
# Split it and then assign the first value to null to disguard
$Null, $Path = $Path.Split("\")
# Path is your array of items you want
}
This could be a one liner. Maybe a tad messy but works with your sample data.
Using IO.Path you can remove the drive root, use a regex to replace everything up to the first backslash, use io.path again to remove last folder, trimend to remove the last \ and split.
$c_split = ($a.Replace([io.path]::GetPathRoot($a),"") -replace "^[^\\]*\\","").Replace([io.path]::GetFileName($a),"").TrimEnd("\").Split("\")
Something like this maybe:
$path = "C:\Parent\Child\Child\_Grandchild"
$split_path = $path.split("\")
$modified_path = $split_path[2..($split_path.length-2)]
Going on the assumption that you always want to remove "c:\whatever" from the start and the final directory.

Powershell - how can I make

Trying to find the numbers in my file divisible by 3. How can I make my for each loop read each number individually?
this is my file:
6 9 7
-----
5 2 9
3 4 4
1 6 9
This is my code so far:
function number{
param($b)
# Loop through all lines of input
foreach($a in $b){
if ($line = % 3)
{
Write-Output "divisible by 3"
}
else {
Write-Output "number not divisible by 3"
}
}
}
#Variables
#Get input from csv file
$a = Import-Csv "document 2.Dat"
How have you got this far in, without realising that none of that even does anything at all? Whatever development approach you're using, you need to rethink it.
Hints that something is wrong:
How is it printing more dashes than there even are in the file? What is it actually printing? Use useful debugging/testing tool, wrap each thing in dashes so you can see where they start and end:
Oh that's broken.
Inside function number { put write-host 'hello?' and see that it's never printing anything.
Try calling the function by hand to see what it does:
Oh I have no idea what number is not divisible by 3, I'd better fix that so I can see what's going on.
And if you have an eye looking for details
where does $line get assigned? What is = doing in an if test? What is % 3 doing with nothing to the left of the %? Why am I using variable names like $a and $b which don't help me follow what's happening at all?
and, of course, "*why am I not write-host "..." all the way through, and/or stepping through this code in the debugger to see what's happening?
Google(*) "read file powershell"
Try it
That's my file alright. And the limits of the output are ... lines. Cool.
function number I should give it a better name but.
Sigh. alright, alright.
No output, even from a simple 'hi'? Ah, call the function.
Great.
Pass a parameter to it and print it...
No output.
Enough screenshots.
Pass a parameter when calling the function. Get-NumbersWhichDivideEvenlyByThree $FileContent
Iterate over the lines and print them inside the function.
Google "powershell get numbers from string" and stuff
Iteratively develop your code, going from working block to working block. Never end up in a position where you have a dozen lines that all don't work in half a dozen different ways all at once, and nowhere to go from there.
Bit you actually asked
Get numbers out of a string.
Use regex. This is exactly why they exist. But to try and keep it simple - in a way that's actually more complicated but tough - break the lines apart on spaces, and pick out the pieces which are numbers and throw the rest away.
To get this with a reasonably nice answer, you almost need to just magically know about -split, perhaps by stumbling on one of #mklement0's answers here about unary split or split has an unary form or the unary form of the -split operator is key here , or, I guess, have read help about_Split in careful detail.
-split '6 9 7' # this splits the line into parts on *runs* of whitespace
6
9
7 # look like numbers, but are strings really
So you get some text pieces, including the line of ----- in the file, that will be among them. And you need to test which are numbers and keep them, and which are dashes (letters, punctuation, etc) and throw those away.
$thing -as [int] # will try to cast $thing as a (whole) number, and silently fail (no exception) if it cannot.
# Split the line into pieces. Try to convert each piece to a number.
# Filter out the ones which weren't numbers and failed to convert.
$pieces = -split $line
$pieces = $pieces | ForEach-Object { $_ -as [int] }
$numbers = $pieces | Where-Object { $_ -ne $null }
Then you can do the % 3 test. And have code like:
function Get-NumbersWhichDivideEvenlyByThree {
param($lines)
foreach ($line in $lines)
{
$pieces = -split $line
$pieces = $pieces | ForEach-Object { $_ -as [int] }
$numbers = $pieces | Where-Object { $_ -ne $null }
foreach ($number in $numbers)
{
if (0 -eq $number % 3)
{
Write-Output "$number divisible by 3"
}
else
{
Write-Output "$number not divisible by 3"
}
}
}
}
$FileContent = Get-Content 'D:\document 2.dat'
Get-NumbersWhichDivideEvenlyByThree $FileContent
and output like:
(-split(gc D:\test.txt -Raw)-match'\d+')|%{"$_$(('',' not')[$_%3]) divisible by 3"}