I have some code which is behaving strangely and I am not sure why. I am attempting to validate the user input is a number and that it is less than 255. Pretty easy.
Problem is that numbers from 26 to 99 are not valid for me in my testing. 1-25 are fine, and 100+ seem fine too.. but for some reason 26-99 keep me in the loop.
DO
{
$ip_addr_first = Read-Host 'enter a number less than 255'
} while ($ip_addr_first -notmatch '\p{Nd}+' -or $ip_addr_first -gt 255)
write-host 'You entered' $ip_addr_first
suggestions welcome on where the problem is, as I'm at a loss here.
Try
do
{
$ip_addr_first = Read-Host 'Enter an integer between 0 and 255'
} while ($ip_addr_first -notmatch '^\p{Nd}+$' -or [int] $ip_addr_first -gt 255)
write-host 'You entered:' $ip_addr_first
Note the [int] cast, which ensures that $ip_addr_first is compared as a number, which makes all the difference here.
Without it, given that $ip_addr_first is a string (the type returned by Read-Host), -gt performs lexical comparison, and 26 is lexically greater than 255, for instance (the same applies to any number (string) starting with 3 or greater).
Also note that I've anchored your regular expression p{Nd}+ with ^ and $ to ensure that the entire input is matched against it (you could make this more permissive by allowing leading and trailing whitespace).
Alternative approach that uses number parsing only:
[int] $ip_addr_first = -1
do
{
$userInput = Read-Host 'Enter an integer between 0 and 255'
} while (-not ([int]::TryParse($userInput, [ref] $ip_addr_first) -and
$ip_addr_first -ge 0 -and $ip_addr_first -le 255))
write-host 'You entered:' $ip_addr_first
Related
I am trying to understand the behavior of the #() array constructor, and I came across this very strange test.
It seems that the value of an empty pipeline is "not quite" the same as $null, even though it is -eq $null
The output of each statement is shown after the ###
$y = 1,2,3,4 | ? { $_ -ge 5 }
$z = $null
if ($y -eq $null) {'y is null'} else {'y NOT null'} ### y is null
if ($z -eq $null) {'z is null'} else {'z NOT null'} ### z is null
$ay = #($y)
$az = #($z)
"ay.length = " + $ay.length ### ay.length = 0
"az.length = " + $az.length ### az.length = 1
$az[0].GetType() ### throws exception because $az[0] is null
So the $az array has length one, and $az[0] is $null.
But the real question is: how is it possible that both $y and $z are both -eq $null, and yet when I construct arrays with #(...) then one array is empty, and the other contains a single $null element?
Expanding on Frode F.'s answer, "nothing" is a mostly magical value in PowerShell - it's called [System.Management.Automation.Internal.AutomationNull]::Value. The following will work similarly:
$y = 1,2,3,4 | ? { $_ -ge 5 }
$y = [System.Management.Automation.Internal.AutomationNull]::Value
PowerShell treats the value AutomationNull.Value like $null in most places, but not everywhere. One notable example is in a pipeline:
$null | % { 'saw $null' }
[System.Management.Automation.Internal.AutomationNull]::Value | % { 'saw AutomationNull.Value' }
This will only print:
saw $null
Note that expressions are themselves pipelines even if you don't have a pipeline character, so the following are roughly equivalent:
#($y)
#($y | Write-Output)
Understanding this, it should be clear that if $y holds the value AutomationNull.Value, nothing is written to the pipeline, and hence the array is empty.
One might ask why $null is written to the pipeline. It's a reasonable question. There are some situations where scripts/cmdlets need to indicate "failed" without using exceptions - so "no result" must be different, $null is the obvious value to use for such situations.
I've never run across a scenario where one needs to know if you have "no value" or $null, but if you did, you could use something like this:
function Test-IsAutomationNull
{
param(
[Parameter(ValueFromPipeline)]
$InputObject)
begin
{
if ($PSBoundParameters.ContainsKey('InputObject'))
{
throw "Test-IsAutomationNull only works with piped input"
}
$isAutomationNull = $true
}
process
{
$isAutomationNull = $false
}
end
{
return $isAutomationNull
}
}
dir nosuchfile* | Test-IsAutomationNull
$null | Test-IsAutomationNull
The reason you're experiencing this behaviour is becuase $null is a value. It's a "nothing value", but it's still a value.
PS P:\> $y = 1,2,3,4 | ? { $_ -ge 5 }
PS P:\> Get-Variable y | fl *
#No value survived the where-test, so y was never saved as a variable, just as a "reference"
Name : y
Description :
Value :
Visibility : Public
Module :
ModuleName :
Options : None
Attributes : {}
PS P:\> $z = $null
PS P:\> Get-Variable z | fl *
#Our $null variable is saved as a variable, with a $null value.
PSPath : Microsoft.PowerShell.Core\Variable::z
PSDrive : Variable
PSProvider : Microsoft.PowerShell.Core\Variable
PSIsContainer : False
Name : z
Description :
Value :
Visibility : Public
Module :
ModuleName :
Options : None
Attributes : {}
The way #() works, is that it guarantees that the result is delievered inside a wrapper(an array). This means that as long as you have one or more objects, it will wrap it inside an array(if it's not already in an array like multiple objects would be).
$y is nothing, it's a reference, but no variable data was stored. So there is nothing to create an array with. $z however, IS a stored variable, with nothing(null-object) as the value. Since this object exists, the array constructor can create an array with that one item.
I'm looking for a way to accelerate the (Windows 10) PowerShell command Where-Object for a sorted array.
In the end the array will contain thousands of lines from a log file. All lines in the log file start with date and time and are sorted by date/time (new lines will always be appended).
The following command would work but is extremely slow and ineffective with a sorted array:
$arrFileContent | where {($_ -ge $Start) -and ($_ -le $End)}
Here is a (strongly simplified) example:
$arrFileContent = #("Bernie", "Emily", "Fred", "Jake", "Keith", "Maria", "Paul", "Richard", "Sally", "Tim", "Victor")
$Start = "E"
$End = "P"
Expected result: "Emily", "Fred", "Jake", "Keith", "Maria", "Paul".
I guess, using "nested intervals" it should be much faster, like "find the first entry starting with "E" or above and the first starting with "P" or below and return all entries in between.
I suppose there must be a simple PowerShell or .NET solution for this, so I won't have to code it myself, correct?
Edit 31.08.19: Not sure if "nested intervals" (German "Intervallschachtelung") is the right term.
What I mean is the "telephone book principle": Open the book in the middle, check if the wanted name is listed before or after, open the book in the middle of the first (or last) half, and so on.
In this case (checking 100.000 lines of a log file for a given date range):
- check line no. 50.000
- if after given start date check line no. 75.000 else check no. 25.000
- check line no. 75.000 (or 25.000)
- if after given start date check line no. 87.500 (or ...) else check no. 62.500 (or ...)
and so on ...
The log file contains lines like this:
2018-01-17 14:28:19 Installation xxx started
(only with a lot more text)
Let's measure all ways mentioned in comments. Let's mimic thousands of lines from a log file using Get-ChildItem:
$arrFileContent = (
Get-ChildItem d:\bat\* -File -Recurse -ErrorAction SilentlyContinue
).Name | Sort-Object -Unique
$Start = "E"
$End = "P"
$arrFileContent.Count
('Where-Object', $(Measure-Command {
$arrFileNarrowed = $arrFileContent | Where-Object {
($_ -ge $Start) -and ($_ -le $End)
}
}).TotalMilliseconds, $arrFileNarrowed.Count) -join "`t"
('Where method', $(Measure-Command {
$arrFileNarrowed = $arrFileContent.Where( {
($_ -ge $Start) -and ($_ -le $End)
})
}).TotalMilliseconds, $arrFileNarrowed.Count) -join "`t"
('foreach + if', $(Measure-Command {
$arrFileNarrowed = foreach ($OneName in $arrFileContent) {
if ( ($OneName -ge $Start) -and ($OneName -le $End) ) {
$OneName
}
}
}).TotalMilliseconds, $arrFileNarrowed.Count) -join "`t"
Output using Get-ChildItem d:\bat\*:
D:\PShell\SO\56993333.ps1
2777
Where-Object 111,5433 535
Where method 56,8577 535
foreach + if 6,542 535
Output using Get-ChildItem d:\* (much more names):
D:\PShell\SO\56993333.ps1
89570
Where-Object 4056,604 34087
Where method 1636,9539 34087
foreach + if 422,8259 34087
"Nested intervals", to me, means "intervals within intervals." I think I'd describe what you're looking to do is select a range. We can exploit the fact that the data is sorted to stop enumerating as soon as the end of the range is found.
.NET's LINQ queries allow us to do this easily. Assuming this content for Names.txt...
Bernie
Emily
Fred
Jake
Keith
Maria
Paul
Richard
Sally
Tim
Victor
...in C# the filtering would be as simple as...
IEnumerable<string> filteredNames = System.IO.File.ReadLines("Names.txt")
.Where(name => name[0] >= 'E')
.TakeWhile(name => name[0] <= 'P');
ReadLines() enumerates the lines of the file, Where() filters the output of ReadLines() (setting a lower bound on the range), and TakeWhile() stops enumerating Where() (and, therefore, ReadLines()) once its condition is no longer true (setting an upper bound on the range). This is all very efficient because A) the file is enumerated rather than read entirely into memory and B) enumeration stops as soon as the end of the desired range is reached.
We can invoke LINQ methods from PowerShell, too, but since PowerShell supports neither extension methods nor lamba expressions the equivalent code is a little more verbose...
$source = [System.IO.File]::ReadLines($inputFilePath)
$rangeStartPredicate = [Func[String, Boolean]] {
$name = $args[0]
return $name[0] -ge [Char] 'E'
}
$rangeEndPredicate = [Func[String, Boolean]] {
$name = $args[0]
return $name[0] -le [Char] 'P'
}
$filteredNames = [System.Linq.Enumerable]::TakeWhile(
[System.Linq.Enumerable]::Where($source, $rangeStartPredicate),
$rangeEndPredicate
)
In order for this to work you have to invoke the static LINQ methods directly and get all of the types correct. Thus, the first parameter of Where() is an System.Collections.Generic.IEnumerable[String], which is what ReadLines() returns (that's why I used a file for this). The predicate parameters of Where() and TakeWhile() are of type [Func[String, Boolean]] (a function that takes a String and returns a Boolean), which is why the ScriptBlocks must be explicitly cast to that type.
After this code executes $filteredNames will contain a query object; that is, it doesn't contain the results but rather a blueprint for how to get the results...
PS> $filteredNames.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
False False <TakeWhileIterator>d__27`1 System.Object
Only when the query is executed/evaluated does file enumeration and filtering actually occur...
PS> $filteredNames
Emily
Fred
Jake
Keith
Maria
Paul
If you are going to access the results multiple times you should store them in an array to avoid reading the file multiple times...
PS> $filteredNames = [System.Linq.Enumerable]::ToArray($filteredNames)
PS> $filteredNames.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String[] System.Array
PS> $filteredNames
Emily
Fred
Jake
Keith
Maria
Paul
I tried a variation on #josefz's answer. I didn't get amazing results breaking when I was past the last line I wanted. Actually, if it was just 'a' to 'b', I save a minute. Unless the slowness is due to get-content? "Get-content log" will be slower than "get-content -readcount -1 log".
$arrFileContent = Get-ChildItem -name -File -Recurse | select -first 89570 | sort -u
$start = 'e'
$end = 'p'
measure-command {
$arrFileNarrowed = foreach ($OneName in $arrFileContent) {
if ($OneName -ge $Start) {
if ($OneName -le $End ) {
$OneName
}
}
}
} | fl seconds, milliseconds
# break early
measure-command {
$arrFileNarrowed = foreach ($OneName in $arrFileContent) {
if ($OneName -ge $Start) {
if ($OneName -le $End ) {
$OneName
} else {
break
}
}
}
} | fl seconds, milliseconds
Output:
Seconds : 1
Milliseconds : 207
Seconds : 1
Milliseconds : 174
Trying out get-content vs switch -file:
$start = 'e'
$end = 'p'
# uses more memory
measure-command {
$result1 = get-content -readcount -1 log | foreach { $_ |
where { $_ -ge $start -and $_ -le $end } }
} | fl seconds,milliseconds
measure-command {
$result2 = switch -file log {
{ $_ -ge $start -and $_ -le $end } { $_ } }
} | fl seconds,milliseconds
Output:
Seconds : 4
Milliseconds : 491
Seconds : 2
Milliseconds : 747
It is also effective to simply replace as follows
$arr | where { <expression> }
↓
$arr | & { process { if (<expression>) { $_ } } }
$arrFileContent | & { process { if ($_ -ge $Start -and $_ -lt $End) { $_ } } }
So let me tell you what I'm trying to do here. Our SolarWinds alerts report on disk capacity as read by Windows, not the Virtual Machine vDisk size setting. What I'm trying to do is match the size so that I can find the correct vDisk and report on its datastore free space to determine whether or not we can add more.
Here's the problem, the GB number never matches between Windows and VMWare. Say the disk has a 149.67 capacity as reported by Windows, well the VMWare setting is 150, or 150.18854, or anything of that sort. I cannot find the vdisk without knowing the exact number, but theoretically I could find it if I could just say, have a comparison operator that had some breathing room, like plus or minus 1 or even 0.5. So for example:
Get-HardDisk -Vm SERVERNAME | Where-Object {
$_.CapacityGB -lt $size + 0.5 -and
$_.CapacityGB -gt $size - 0.5
}
This doesn't work though, for whatever reason. I need something similar to this. Any ideas?
UPDATE: Turns out to be user error, I was experimenting with the wrong number when testing the command. I thought it was the syntax, it was the number I was using itself.
So because I managed to answer my own question I thought I'd post a script for achieving this here. Note that you will need to have a txt file with a comma separated servername and capacity. You could probably modify this to do many other things with VMWare data gathering if you wanted. In the end you'll need to know which columns are which and import to Excel as comma delimited.
Most the variables are decimal values.
Also note that I have no yet figured out a way to programatically deal with the discovery of multiple matching disks.
$serverlist = Get-Content "./ServerList.txt"
$logfile = "./Stores.txt"
remove-item "./Stores.txt"
Function LogWrite {
Param (
[string]$srv,
[string]$disk,
[string]$store
)
Add-Content $logfile -value $srv","$disk","$store
}
foreach ($item in $serverlist){
$store = "Blank"
$disk = "Blank"
try {
$server,$arg = $item.split(',')
$round = [math]::Round($arg,0)
$disk = get-harddisk -vm $server | where-object{$_.CapacityGB -lt ($round + 2) -and $_.CapacityGB -gt ($round - 2) }
if ([string]::IsNullOrEmpty($disk)){
$disk = "Problem locating disk."
$store = "N/A"
continue
}
if ($disk.count -gt 1) {
$disk = "More than one matching disk."
$store = "N/A"
} else {
$store = get-harddisk -vm $server | where-object{$_.CapacityGB -lt ($round + 2) -and $_.CapacityGB -gt ($round - 2) } | Get-Datastore | %{ "{0},{1},{2}" -f $_.Name,[math]::Round($_.FreeSpaceGB,1),[math]::Round($_.CapacityGB,1) }
}
}
catch {
$disk = "Physical"
$store = "N/A"
}
LogWrite $server $disk $store
}
I am running PS cmdlet get-customcmdlet which is generating following output
Name FreeSpaceGB
---- -----------
ABC-vol001 1,474.201
I have another variable $var=vol
Now, I want to strip out just 001 and want to check if it is an integer.
I am using but getting null value
$vdetails = get-customcmdlet | split($var)[1]
$vnum = $vdetails -replace '.*?(\d+)$','$1'
My result should be integer 001
Assumption: get-customcmdlet is returning a pscustomobject object with a property Name that is of type string.
$var = 'vol'
$null -ne ((get-customcmdlet).Name -split $var)[1] -as [int]
This expression will return $true or $false based on whether the cast is successful.
If your goal is to pad zeroes, you need to do that after-the-fact (in this case, I just captured the original string):
$var = 'vol'
$out = ((get-customcmdlet).Name -split $var)[1]
if ($null -ne $out -as [int])
{
$out
}
else
{
throw 'Failed to find appended numbers!'
}
I'm comparing 2 strings stored in a variable and validating them against a couple of conditions. See Code Below:
do
{
Write-Host -ForegroundColor 'Yellow' "Enter Users Password `nMust be 8 or more characters long and contain a UPPER case character and a Digit."
$Password = Read-Host "First time" -AsSecureString
$Passcheck = Read-Host "And again" -AsSecureString
$PassConvertFirst = [System.Runtime.InteropServices.Marshal]::SecureStringtoBSTR($Password) ; $PlainPass1 = [system.Runtime.InteropServices.Marshal]::PtrToStringAuto($passconvertfirst)
$PassConvertSecond = [System.Runtime.InteropServices.Marshal]::SecureStringtoBSTR($Passcheck) ; $PlainPass2 = [system.Runtime.InteropServices.Marshal]::PtrToStringAuto($Passconvertsecond)
if ($PlainPass1 -Contains $PlainPass2 -and $PlainPass1.Length -ge 8 -or $PlainPass2.Length -ge 8)
{
$PassCheckBreak = $false
$PlainPassword = $PlainPass2
}
else
{
Write-Warning "Passwords don't match, Try again.."
$PassCheckBreak = $true
}
}
while ($PassCheckBreak)
While this works 99.99% of the time i'm coming across a weird error when I try and compare this:
P4ssw0rd with P4ssword
Using the code above this gets validated as true! which makes no sense as if I try validating
W0rd with word
It fails validation.
I've tried changing to a comparative operator and I'm still getting the same issue.
Any thoughts or ideas?
Many Thanks,
Nigel Tatschner
The problem is within the if statement.
Let's have a closer look:
$PlainPass1 -Contains $PlainPass2 -and $PlainPass1.Length -ge 8 -or $PlainPass2.Length -ge 8
Evaluate each comparision:
$PlainPass1 -Contains $PlainPass2 # False
$PlainPass1.Length -ge 8 # True
$PlainPass2.Length -ge 8 #true
Thus the outcome is:
$false -and $true -or $true
Anything -or $true is true. Lesson: mind the binding order. When in doubt, use parenthesis.