Nested if and for loop errors - powershell

would any of you be able to help me with the below code
$pcname = "jwang"
#We set $test as a string to be called later
$test = get-adcomputer -filter "name -like '$pcname'" | select -ExpandProperty name
#We define a null string to test our IF statement
$nothing = $null
$number = 1
if ($test -ne $null) {
Write-Host "$pcname is currently in use"
for($number = 1; ($test = get-adcomputer -filter "name -like '$pcname + $number'" | select -ExpandProperty name) -ne $null; $number++ ){
Write-Host $pcname + $number
}
}
else {
Write-host "AD lookup complete, new PC name will be $pcname"
}
The IF statement works correctly, but the trouble starts when I add the nested FOR loop.
The end goal is to have AD tell me if the name is available or not.
Right now in AD there is
JWANG
JWANG1
JWANG2
JWANG3
JWANG4
I want the for loop to eventuelly tell me that "JWANG5" is available, any help is appreciated, thank you

The immediate problem with your code is that $pcname + $number - an expression - isn't enclosed in $(...), the subexpression operator inside your expandable (double-quoted) string ("...").
Therefore, use $($pcname + $number) or, relying on mere string interpolation, $pcname$number
An aside re testing for $null:
It is advisable to make $null the LHS for reliably null testing (if ($null -ne $test)), because on the RHS it acts as a filter if the LHS happens to be an array - see about_Comparison_Operators.
However, often - such as when objects are being returned from a command, you can simply use implicit to-Boolean conversion (if ($test)) - see the bottom section of this answer for the complete rules.
Taking a step back:
Querying AD multiple times is inefficient, and since you're already using the -like operator, you can use it with a wildcard pattern to find all matching names at once:
$pcname = 'jwang'
# Note the use of "*" to match all names that *start with* $pcname
$names = get-adcomputer -filter "name -like '$pcname*'" | select -ExpandProperty name
if ($names) { # at least one name found
Write-Host "$pcname is currently in use"
# Find the highest number embedded in the matching names.
$seqNums = [int[]] ($names -replace '\D')
# The new sequence number is the next number following the currently highest.
$nextSeqNum = 1 + [Linq.Enumerable]::Max($seqNums)
# Form the new name and output it.
$pcname + $nextSeqNum
}
else {
Write-host "AD lookup complete, new PC name will be $pcname"
}

Related

PowerShell If statement not equating properly

What am I doing wrong here?
Why do the 2 variables not equal each other?
When I run this script
$temp1 = "#{Dhcp=Disabled}"
$temp2 = Get-NetIPInterface My_Ethernet | select Dhcp
write-host ""
write-host "1" $temp1
write-host "2" $temp2
write-host ""
if ($temp2 -eq $temp1){
write-host "IP address is Static "
}
Else {
write-host "IP address is Not Static"
}
I get this result
1 #{Dhcp=Disabled}
2 #{Dhcp=Disabled}
IP address is Not Static
With the helpful suggestion from Mathias this is now working as expected
$temp1 = "Disabled"
$temp2 = Get-NetIPInterface My_Ethernet | select Dhcp
write-host ""
write-host ""
write-host "1" $temp1
write-host "2" $temp2.dhcp
write-host ""
write-host ""
if ($temp2.dhcp -eq $temp1){
write-host "IP address is Static "
}
Else {
write-host "IP address is Not Static"
}
Just to complement your own effective solution:
Since your intent was to compare a property's value to another value, select -ExpandProperty Dhcp would have returned that value directly (see the docs for Select-Object):
if ((Get-NetIPInterface My_Ethernet | select -ExpandProperty Dhcp) -eq $temp1) { # ...
However, it would be much simpler to use direct property access, using ., the member-access operator:
if ((Get-NetIPInterface My_Ethernet).Dhcp -eq $temp1) { # ...
Note that .Dhcp would work even if your Get-NetIPInterface call returned multiple objects, in which case an array of values is returned, courtesy of PowerShell's member-access enumeration feature.[1]
Finally, note that Write-Host is typically the wrong tool to use, unless the intent is to write to the display only, bypassing the success output stream and with it the ability to send output to other commands, capture it in a variable, or redirect it to a file. To output a value, use it by itself; e.g. $value, instead of Write-Host $value (or use Write-Output $value); see this answer.
To explicitly print only to the display but with rich formatting, use Out-Host, given that the .ToString() stringification that Write-Host uses on its input is often unhelpful - see this post.
[1] Note that PowerShell's comparison operators such as -eq exhibit filtering behavior when their LHS is an array (collection); that is, instead of returning $true or $false, they then return the sub-array of matching elements - see about_Comparison_Operators.

PowerShell Isn't Parsing Data as Expected // Basic PowerShell Operator Question

I have a pretty nooby question regarding the proper usage of certain operators.
I am writing a script that pulls data out of a CSV. The script asks the user to enter a location # and then is supposed to output the IP of their server.
The script works as expected except on rows where the location number also has other text in it like numbers or special characters.
Here is an example CSV to illustrate my problem:
Loc#,State,Server IP
1,NY,10.0.0.1
2,CA,10.0.0.2
3,WA,10.0.0.3
4 (inp),KY,10.0.0.4
My script looks something like this:
$CSV = import-csv C:\users\Self\MyProject.csv
$location = read-host "enter the location #"
foreach( $row in $CSV){
if($row.loc# -eq $location)
{
write-host $row.'Server IP'
}
Now, this script works as expected unless the user chooses location 4. If the users chooses location 4, then the $location variable is left blank.
Ok, this makes a little bit of sense since I'm using the -eq operator. But even if I use the -contains operator I get the same results.
Here is another way of showing my problem:
$number = "10 ten"
if ($number -contains "10"){ (Write-Host "true")} else{ write-host "false"}
false
Now, why isn't the output showing as "true" since that $number variable does indeed contain "10"?
Any help is much appreciated,
Thanks
As it seems, Location can have a random value and not only digits, normally you could use -match as Mahmoud Moawad pointed out in his comment, however this could also bring you problems since there is no specific pattern you can follow there is also no clear way of how you can filter the specific value. What I would personally do is give the user a list where he can choose the Location by Index:
$csv = Import-Csv C:\users\Self\MyProject.csv
$csv.foreach({
begin
{
'- Choose a location:'
$i = 0
$map = #{}
}
process
{
$map[(++$i)] = $_
"[$i] - {0}" -f $_.'Loc#'
}
})
$question = { Read-Host 'Index' }
while($true)
{
$index = (& $question) -as [int]
if($index -ge 1 -and $index -le $csv.Count)
{
break
}
'Input must be between 1 and {0}!!' -f $csv.Count
}
$map[$index].'Server IP'
As for why -contains is not showing you $true on your condition, the Containment Operators will look for an exact match of an element:
'10 ten' -contains '10' # => False
'10 ten' -contains '10 ten' # => True
'9 nine', '10 ten' -contains '10 ten' # => True

Comparing multiple email address using powershell match

I have a CSV file of 2000 email addresses. I am using PowerShell to check if the user is active in AD. Another developer wrote a PowerShell script for me to do this but he only used the main domain for the email format to match, he didn't add the subdomian that it could have. Some of our email addresses have a 3 part email address after the # symbol.
For example, his code:
foreach ($user in $users) {
Write-Host $user.email
if ($user.email -match "\#mycompany\.com$") {
$status = "NOT FOUND"
# loop through possible AD domains until there is a hit
foreach ($domain in "na","au","eu","as") {
if ($status -eq "NOT FOUND") {
Write-Host " $($domain)" -NoNewline
$status = Get-UserFromEmail -EMail $user.email -ADDomain $domain
Write-Host $status
}
else {
break
}
}
Write-Host
Add-Content -Path $outcsv -Value "$($user.email),$($user.type),`"$($status)`""
}
else {
Add-Content -Path $outcsv -Value "$($user.email),$($user.type),NOT MYCOMPANY"
}
What I need to be able to do is get the match to check if it is a two or three part email address.
#consultant.mycompany.com or #mycompany.com.
Any insight for this PowerShell newbie would be appreciated.
here is one way to test for membership in more than one email domain. all of the domains are all in the same example.com, but they could easily be in testing.com or wizbang.org.
this demos the idea, i presume you can insert it into your script as needed. [grin]
what it does ...
builds a list of email addresses to test
you will get that from your source ... just be sure they are plain strings, not a string inside a property of an object.
builds a domain list
uses the built in regex escape method to escape things like dots as they are needed
adds a $ to the end of each escaped string to anchor the pattern to the end of the email address
uses the escaped strings to build a regex OR of that list
iterates thru the email address list and gets the ones that match one of the domain list items
saves the matches to a $Var
displays the content of that $Var on screen
the code ...
$EmailList = #(
'ABravo#example.com'
'BCharlie#more.example.com'
'CDelta#example.com'
'DEcho#zigzag.papers.com'
'EFoxtrot#even.more.example.com'
)
$DomainList = #(
'#example.com'
'#more.example.com'
'#even.more.example.com'
)
$Regex_DL = $DomainList.ForEach({
[regex]::Escape($_) + '$'
}) -join '|'
$ValidEmailAddressList = $EmailList -match $Regex_DL
$ValidEmailAddressList
output ...
ABravo#example.com
BCharlie#more.example.com
CDelta#example.com
EFoxtrot#even.more.example.com
You can always use the -or operator to chain multiple expressions inside the if condition:
if ($user.email -match "\#mycompany\.com$" -or $user.email -match '#consultant\.mycompany\.com$'){
# ...
}
Alternatively, you can construct a regex pattern that'll match both:
if($user.email -match '#(?:consultant\.)?mycompany\.com$'){
# ...
}
If you're ever unsure about how to escape a literal string in a reguar expression, use [regex]::Escape():
PS C:\> [regex]::Escape('#consultant.mycompany.com')
#consultant\.mycompany\.com

How can I use wildcards with IF -contains on an array?

I'm working on a script backing up the schema for a database to a version control system. I just realized that replicated objects are being scripted out as well; these objects start with sp_MSupd_, sp_MSdel_, sp_MSins_, and syncobj_0x. I would like to skip these items.
I know I can do something like this:
$ExcludeObjectNames = #("sp_MSupd_", "sp_MSdel_","sp_MSins_","syncobj_0x")
If ($ExcludeObjectNames -contains "sp_MSdel_SampleObject")
{
Write-Host "replicated item found"
}
But this only works for exact matches. I tried adding * to the elements in the array but that did not work. And I know I can use -like, but that seems to only be for single values, not arrays.
I know I can do this
$ObjectNames = "sp_MSdel_SampleObject"
If ($ObjectNames -like "sp_MSupd_*" -OR $ObjectNames -like "sp_MSdel_*" -OR $ObjectNames -like "sp_MSins_*" -OR $ObjectNames -like "syncobj_0x*")
{
Write-Host "replicated item found"
}
This is well and fine, BUT if I find more and more things to omit, this becomes more and more ugly.
Is there a way to use wildcards with -contains on an array?
$ExcludeObjectNames = #("sp_MSupd_", "sp_MSdel_","sp_MSins_","syncobj_0x")
Foreach ($ExcludeObjectName in $ExcludeObjectNames) {
If ("sp_MSdel_SampleObject" -match $ExcludeObjectName)
{
Write-Host "replicated item found"
}
}
Or if you only want to edit variables in the script, you can do:
$ExcludeObjectNames = #("sp_MSupd_", "sp_MSdel_","sp_MSins_","syncobj_0x")
$ObjectToCheck = "sp_MSdel_SampleObject" # This variable contains your original contains target
Foreach ($ExcludeObjectName in $ExcludeObjectNames) {
If ($ObjectToCheck -match $ExcludeObjectName)
{
Write-Host "replicated item found"
}
}
If you don't like looping, you can just build a regex filter from your original array:
$ExcludeObjectNames = #("sp_MSupd_", "sp_MSdel_","sp_MSins_","syncobj_0x")
$ObjectToCheck = "sp_MSdel_SampleObject" # This variable contains your original contains target
$excludeFilter = '(^' + ($excludeobjectnames -join "|^") + ')' # Regex filter that you never have to manually update
If ($ObjectToCheck -match $ExcludeFilter)
{
Write-Host "replicated item found"
}

Pass a single space-delimited string as multiple arguments

I have a Powershell function in which I am trying to allow the user to add or remove items from a list by typing the word "add" or "remove" followed by a space-delimited list of items. I have an example below (slightly edited, so you can just drop the code into a powershell prompt to test it "live").
$Script:ServerList = #("Server01","Server02","Server03")
Function EditServerList (){
$Script:ServerList = $Script:ServerList |Sort -Unique
Write-host -ForegroundColor Green $Script:ServerList
$Inputs = $args
If ($Inputs[0] -eq "start"){
$Edits = Read-Host "Enter `"add`" or `"remove`" followed by a space-delimited list of server names"
#"# EditServerList $Edits
# EditServerList $Edits.split(' ')
EditServerList ($Edits.split(' ') |Where {$_ -NotLike "add","remove"})
EditServerList start
} Elseif ($Inputs[0] -eq "add"){
$Script:ServerList += $Inputs |where {$_ -NotLike $Inputs[0]}
EditServerList start
} Elseif ($Inputs[0] -eq "remove"){
$Script:ServerList = $Script:ServerList |Where {$_ -NotLike ($Inputs |Where {$_ -Notlike $Inputs[0]})}
EditServerList start
} Else {
Write-Host -ForegroundColor Red "ERROR!"
EditServerList start
}
}
EditServerList start
As you can see, the function takes in a list of arguments. The first argument is evaluated in the If/Then statements and then the rest of the arguments are treated as items to add or remove from the list.
I have tried a few different approaches to this, which you can see commented out in the first IF evaluation.
I have two problems.
When I put in something like "add Server05 Server06" (without quotes) it works, but it also drops in the word "add".
When I put in "remove Server02 Server03" (without quotes) it does not edit the array at all.
Can anybody point out where I'm going wrong, or suggest a better approach to this?
To address the title's generic question up front:
When you pass an array to a function (and nothing else), $Args receives a single argument containing the whole array, so you must use $Args[0] to access it.
There is a way to pass an array as individual arguments using splatting, but it requires an intermediate variable - see bottom.
To avoid confusion around such issues, formally declare your parameters.
Try the following:
$Script:ServerList = #("Server01", "Server02", "Server03")
Function EditServerList () {
# Split the arguments, which are all contained in $Args[0],
# into the command (1st token) and the remaining
# elements (as an array).
$Cmd, $Servers = $Args[0]
If ($Cmd -eq "start"){
While ($true) {
Write-host -ForegroundColor Green $Script:ServerList
$Edits = Read-Host "Enter `"add`" or `"remove`" followed by a space-delimited list of server names"
#"# Pass the array of whitespace-separated tokens to the recursive
# invocation to perform the requested edit operation.
EditServerList (-split $Edits)
}
} ElseIf ($Cmd -eq "add") {
# Append the $Servers array to the list, weeding out duplicates and
# keeping the list sorted.
$Script:ServerList = $Script:ServerList + $Servers | Sort-Object -Unique
} ElseIf ($Cmd -eq "remove") {
# Remove all specified $Servers from the list.
# Note that servers that don't exist in the list are quietly ignored.
$Script:ServerList = $Script:ServerList | Where-Object { $_ -notin $Servers }
} Else {
Write-Host -ForegroundColor Red "ERROR!"
}
}
EditServerList start
Note how a loop is used inside the "start" branch to avoid running out of stack space, which could happen if you keep recursing.
$Cmd, $Servers = $Args[0] destructures the array of arguments (contained in the one and only argument that was passed - see below) into the 1st token - (command string add or remove) and the array of the remaining arguments (server names).
Separating the arguments into command and server-name array up front simplifies the remaining code.
The $var1, $var2 = <array> technique to split the RHS into its first element - assigned as a scalar to $var1 - and the remaining elements - assigned as an array to $var2, is commonly called destructuring or unpacking; it is documented in Get-Help about_Assignment Operators, albeit without giving it such a name.
-split $Edits uses the convenient unary form of the -split operator to break the user input into an array of whitespace-separated token and passes that array to the recursive invocation.
Note that EditServerList (-split $Edits) passes a single argument that is an array - which is why $Args[0] must be used to access it.
Using PowerShell's -split operator (as opposed to .Split(' ')) has the added advantage of ignoring leading and trailing whitespace and ignoring multiple spaces between entries.
In general, operator -split is preferable to the [string] type's .Split() method - see this answer of mine.
Not how containment operator -notin, which accepts an array as the RHS, is used in Where-Object { $_ -notin $Servers } in order to filter out values from the server list contained in $Servers.
As for what you tried:
EditServerList ($Edits.split(' ') |Where {$_ -NotLike "add","remove"}) (a) mistakenly attempts to remove the command name from the argument array, even though the recursive invocations require it, but (b) actually fails to do so, because the RHS of -like doesn't support arrays. (As an aside: since you're looking for exact strings, -eq would have been the better choice.)
Since you're passing the arguments as an array as the first and only argument, $Inputs[0] actually refers to the entire array (command name + server names), not just to its first element (the command name).
You got away with ($Inputs[0] -eq "add") - even though the entire array was compared - because the -eq operator performs array filtering if its LHS is an array, returning a sub-array of matching elements. Since add was among the elements, a 1-element sub-array was returned, which, in a Boolean context, is "truthy".
However, your attempt to weed out the command name with where {$_ -NotLike $Inputs[0]} then failed, and add was not removed - you'd actually have to compare to $Inputs[0][0] (sic).
Where {$_ -NotLike ($Inputs |Where {$_ -Notlike $Inputs[0]})} doesn't filter anything out for the following reasons:
($Inputs |Where {$_ -Notlike $Inputs[0]}) always returns an empty array, because, the RHS of -Notlike is an array, which, as stated, doesn't work.
Therefore, the command is the equivalent of Where {$_ -NotLike #() } which returns $True for any scalar on the LHS.
Passing an array as individual arguments using splatting
Argument splatting (see Get-Help about_Splatting) works with arrays, too:
> function foo { $Args.Count } # function that outputs the argument count.
> foo #(1, 2) # pass array
1 # single parameter, containing array
> $arr = #(1, 2); foo #arr # splatting: array elements are passed as indiv. args.
2
Note how an intermediate variable is required, and how it must be prefixed with # rather than $ to perform the splatting.
I'd use parameters to modify the ServerList, this way you can use a single line to both add and remove:
Function EditServerList {
param(
[Parameter(Mandatory=$true)]
[string]$ServerList,
[array]$add,
[array]$remove
)
Write-Host -ForegroundColor Green "ServerList Contains: $ServerList"
$Servers = $ServerList.split(' ')
if ($add) {
$Servers += $add.split(' ')
}
if ($remove) {
$Servers = $Servers | Where-Object { $remove.split(' ') -notcontains $_ }
}
return $Servers
}
Then you can call the function like this:
EditServerList -ServerList "Server01 Server02 Server03" -remove "Server02 Server03" -add "Server09 Server10"
Which will return:
Server01
Server09
Server10