Powershell - Use two hashtable in function - powershell

I would like to create groups out of two hash tables
$var_computer = #{
"Notebooks" = "NB";
"PCs" = "PC";
}
$var_os = #{
"Windows10" = "Windows 10*";
"Windows8.1" = "Windows 8.1*";
}
function create_groups ($var_computer, $var_os, $path){
ForEach ($key in ($var_computer).Keys) {
$new_group_name = "_DEFAULT_" + $key
if ((Get-ADGroup -Filter {Name -eq $new_group_name} -SearchBase $path) -eq $null) {
New-ADGroup -name $new_group_name -GroupScope Global -Path $path
}
So that i get
_DEFAULT_Notebooks_Windows10
_DEFAULT_Notebooks_Windows8.1
_DEFAULT_PCs_Windows10
...
How could i add another key in my $new_group_name so that it doesn't start at the first key in my second hash table.
Or would it be easier to have nested hash tables?

Is this what you need? Foreach OS foreach computer-type
$var_computer = #{
"Notebooks" = "NB";
"PCs" = "PC";
}
$var_os = #{
"Windows10" = "Windows 10*";
"Windows8.1" = "Windows 8.1*";
}
foreach ($type in $var_computer.Keys) {
foreach ($os in $var_os.Keys) {
$new_group_name = "_DEFAULT_$($type)_$($os)"
$new_group_name
#Create group
}
}
Output:
_DEFAULT_Notebooks_Windows8.1
_DEFAULT_Notebooks_Windows10
_DEFAULT_PCs_Windows8.1
_DEFAULT_PCs_Windows10

Related

Find duplicates in array of hashtables

I have an array of hashtables and I need to find if there are elements who has the same Name.
I have this HasDuplicate function which return True or False if the array contains duplicate or not.
What I am doing here is that I am iterating through each element and add Name of it to another array, and then check it if it exists. But this code does not looks good, and I was thinking if there is another way of achieving this
# object looks like this
$array = #(
#{ Name = 'First', Passed = $True }
#{ Name = 'First', Passed = $False }
)
Function HasDuplicate
{
param($array)
$all = #()
foreach($item in $array)
{
$item_name = $item.Name
if($all -contains $item_name)
{
Write-Error "Duplicate name ""$item_name"""
return $True
}
else
{
$all += $item_name
}
}
return $False
}
Group-Object is probably the easiet, something like this:
$array = #(
#{ Name = 'First'; Passed = $True }
#{ Name = 'First'; Passed = $False }
)
$array.Name | Group-Object | Where-Object Count -GT 1
Another way you could do it using an hash table:
$array = #(
#{ Name = 'First'; Passed = $True }
#{ Name = 'First'; Passed = $False }
)
$h = #{}
$array | % {$h[$_.Name] += 1 }
$h.GetEnumerator() | Where value -GT 1
This might not be very good looking compared to the other answers, but you could just count your names in another hashtable then output the duplicates afterwards.
$array = #(
#{ Name = 'First'; Passed = $True }
#{ Name = 'First'; Passed = $False }
);
# Count names in array
$counts = #{}
foreach ($object in $array) {
$name = $object.Name
if (-not $counts.ContainsKey($name)) {
$counts[$name] = 0
}
$counts[$name] += 1
}
# Output duplicates
foreach ($name in $counts.Keys) {
if ($counts[$name] -gt 1) {
Write-Output ("Duplicate Name: " + $name)
}
}
Output:
Duplicate Name: First

Is it possible to autocomplete a Read-Host prompt with previous Get command output. Without saving an output file

I am writing a script to deploy VM hosts and I want to run a Get command to show them available options and then use their input with autocomplete from previous GET command. I do this because I want to avoid any typos that can be made during manual input.
I have tried using Select-string but I think it saves to .txt file and I don't want this to be saved in a txt file. I would rather have it saved in variable.
Get-VMHost | Select-Object -Property Name | Format-Table -Property Name
$VMHost = Read-Host -Prompt 'Please select the host for your VM
I expect the user to be able to autocomplete string with output from previously executed GET command. Please help if you can
Here is a New-ChoicePrompt function I use for similar purposes.
function New-ChoicePrompt { [cmdletBinding()]
param(
[parameter(mandatory=$true)]$Choices,
$Property,
$ReadProperty,
$ExprLabel,
[switch]$AllowManualInput,
[Scriptblock]$ReadPropertyExpr,
$ManualInputLabel = "Type my own"
)
if ( $choices[0] -isnot [string] -and !$property ) {"Please include New-ChoicePrompt -Property unless -Choices is an array of strings."; break}
if ( $choices[0] -is [string] -and ($property -or $ReadProperty) ) {"When New-ChoicePrompt -Choices is an array of strings, please omit -Property and -ReadProperty."; break}
#if ( $choices[0] -isnot [string] -and $allowManualInput ) {"When New-ChoicePrompt -Choices is a PSobject, please omit -AllowManualInput"; break}
$x = 0; $script:options = #()
$script:propty = $property
$script:choices = $choices
$manualInputLabel = "<" + $manualInputLabel + ">"
foreach ($item in $choices) { $value = $null
$x += 1
if ($property) { $value = $item | select -expand $property } `
else {$value = $item}
if ($readProperty) {
$readVal = $item | select -expand $readProperty
$row = new-object -type psObject -property #{Press = $x; 'to select' = $value; $readproperty = $readVal}
} ` #close if readProperty
elseif ($readPropertyExpr) `
{
$readVal = & $ReadPropertyExpr
$row = new-object -type psObject -property #{Press = $x; 'to select' = $value; $ExprLabel = $readVal}
}` #close if readPropertyExpr
else { $row = new-object -type psObject -property #{'to select' = $value; Press = $x} }
$script:options += $row
} #close foreach
if ($AllowManualInput) {
$row = new-object -type psObject -property #{'to select' = $manualInputLabel; Press = ($x + 1) }
$script:options += $row
} #close if allowManualInput
if ($ReadProperty) { $script:options | Select Press, "to select", $readproperty | ft -auto }
elseif ($ReadPropertyExpr) { $script:options | Select Press, "to select", $ExprLabel | ft -auto }
else { $script:options | Select Press, "to select" | ft -auto }
} #end function new-choicePrompt
Here is a usage example.
$vmhosts = Get-VMHost | sort Name
if ($vmhosts.count -gt 1) {
do {
new-choicePrompt -choices $vmhosts -property name
$in = read-host -prompt 'Please select a target host'
$range = $options | select -expand press
} #close do
until ($range -contains $in)
$selection = $options | where {$_.press -eq $in} | select -expand 'To select'
$choice = $choices | where {$_.#($propty) -eq $selection}
$vmHost = $choice
} else {$vmhost = $vmhosts} #close if multiple hosts
"Target host: " + $vmhost.name
If it was a function parameter, you could limit the values with [ValidateSet] like from here: https://www.mssqltips.com/sqlservertip/4205/powershell-parameters-part-ii--validateset-and-validatepattern/ It ends up supporting tab completion as well. If you set it to mandatory, it will prompt for it as well, if it's not given.
Function Pass-Set {
Param(
[ValidateSet("oro","plata")][string]$specificstring
)
Process
{
Write-Host "It must be one of two words; in this case $specificstring."
}
}
pass-set -specificstring oro
It must be one of two words; in this case oro.
pass-set -specificstring plata
It must be one of two words; in this case plata.
pass-set -specificstring plata2
Pass-Set : Cannot validate argument on parameter 'specificstring'. The argument "plata2" does not belong to the set "oro,plata" specified by the
ValidateSet attribute. Supply an argument that is in the set and then try the command again.
At line:1 char:26
+ pass-set -specificstring plata2
+ ~~~~~~
+ CategoryInfo : InvalidData: (:) [Pass-Set], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Pass-Set

Query and exclude softwares installed on remote computers

I'm using the following powershell function to list the software installed on remote computers and export the data into various formats such as CSV, GridView or console :
Function Get-InstalledApplication
{
Param(
[Parameter(Mandatory=$true)]
[string[]]$Computername,
[String[]]$OutputType,
[string[]]$outpath
)
#Registry Hives
$Object =#()
$excludeArray = ("Security Update for Windows",
"Update for Windows",
"Update for Microsoft .NET",
"Security Update for Microsoft",
"Hotfix for Windows",
"Hotfix for Microsoft .NET Framework",
"Hotfix for Microsoft Visual Studio 2007 Tools",
"Microsoft Visual C++ 2010",
"cwbin64a",
"Hotfix")
[long]$HIVE_HKROOT = 2147483648
[long]$HIVE_HKCU = 2147483649
[long]$HIVE_HKLM = 2147483650
[long]$HIVE_HKU = 2147483651
[long]$HIVE_HKCC = 2147483653
[long]$HIVE_HKDD = 2147483654
Foreach($EachServer in $Computername){
$Query = Get-WmiObject -ComputerName $Computername -query "Select AddressWidth, DataWidth,Architecture from Win32_Processor"
foreach ($i in $Query)
{
If($i.AddressWidth -eq 64){
$OSArch='64-bit'
}
Else{
$OSArch='32-bit'
}
}
Switch ($OSArch)
{
"64-bit"{
$RegProv = GWMI -Namespace "root\Default" -list -computername $EachServer| where{$_.Name -eq "StdRegProv"}
$Hive = $HIVE_HKLM
$RegKey_64BitApps_64BitOS = "Software\Microsoft\Windows\CurrentVersion\Uninstall"
$RegKey_32BitApps_64BitOS = "Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
$RegKey_32BitApps_32BitOS = "Software\Microsoft\Windows\CurrentVersion\Uninstall"
#############################################################################
# Get SubKey names
$SubKeys = $RegProv.EnumKey($HIVE, $RegKey_64BitApps_64BitOS)
# Make Sure No Error when Reading Registry
if ($SubKeys.ReturnValue -eq 0)
{ # Loop through all returned subkeys
ForEach ($Name in $SubKeys.sNames)
{
$SubKey = "$RegKey_64BitApps_64BitOS\$Name"
$ValueName = "DisplayName"
$ValuesReturned = $RegProv.GetStringValue($Hive, $SubKey, $ValueName)
$AppName = $ValuesReturned.sValue
$Version = ($RegProv.GetStringValue($Hive, $SubKey, "DisplayVersion")).sValue
$Publisher = ($RegProv.GetStringValue($Hive, $SubKey, "Publisher")).sValue
$donotwrite = $false
if($AppName.length -gt "0"){
Foreach($exclude in $excludeArray)
{
if($AppName.StartsWith($exclude) -eq $TRUE)
{
$donotwrite = $true
break
}
}
if ($donotwrite -eq $false)
{
$Object += New-Object PSObject -Property #{
Application = $AppName;
Architecture = "64-BIT";
ServerName = $EachServer;
Version = $Version;
Publisher= $Publisher;
}
}
}
}}
#############################################################################
$SubKeys = $RegProv.EnumKey($HIVE, $RegKey_32BitApps_64BitOS)
# Make Sure No Error when Reading Registry
if ($SubKeys.ReturnValue -eq 0)
{
# Loop Through All Returned SubKEys
ForEach ($Name in $SubKeys.sNames)
{
$SubKey = "$RegKey_32BitApps_64BitOS\$Name"
$ValueName = "DisplayName"
$ValuesReturned = $RegProv.GetStringValue($Hive, $SubKey, $ValueName)
$AppName = $ValuesReturned.sValue
$Version = ($RegProv.GetStringValue($Hive, $SubKey, "DisplayVersion")).sValue
$Publisher = ($RegProv.GetStringValue($Hive, $SubKey, "Publisher")).sValue
$donotwrite = $false
if($AppName.length -gt "0"){
Foreach($exclude in $excludeArray)
{
if($AppName.StartsWith($exclude) -eq $TRUE)
{
$donotwrite = $true
break
}
}
if ($donotwrite -eq $false)
{
$Object += New-Object PSObject -Property #{
Application = $AppName;
Architecture = "32-BIT";
ServerName = $EachServer;
Version = $Version;
Publisher= $Publisher;
}
}
}
}
}
} #End of 64 Bit
######################################################################################
###########################################################################################
"32-bit"{
$RegProv = GWMI -Namespace "root\Default" -list -computername $EachServer| where{$_.Name -eq "StdRegProv"}
$Hive = $HIVE_HKLM
$RegKey_32BitApps_32BitOS = "Software\Microsoft\Windows\CurrentVersion\Uninstall"
#############################################################################
# Get SubKey names
$SubKeys = $RegProv.EnumKey($HIVE, $RegKey_32BitApps_32BitOS)
# Make Sure No Error when Reading Registry
if ($SubKeys.ReturnValue -eq 0)
{ # Loop Through All Returned SubKEys
ForEach ($Name in $SubKeys.sNames)
{
$SubKey = "$RegKey_32BitApps_32BitOS\$Name"
$ValueName = "DisplayName"
$ValuesReturned = $RegProv.GetStringValue($Hive, $SubKey, $ValueName)
$AppName = $ValuesReturned.sValue
$Version = ($RegProv.GetStringValue($Hive, $SubKey, "DisplayVersion")).sValue
$Publisher = ($RegProv.GetStringValue($Hive, $SubKey, "Publisher")).sValue
if($AppName.length -gt "0"){
$Object += New-Object PSObject -Property #{
Application = $AppName;
Architecture = "32-BIT";
ServerName = $EachServer;
Version = $Version;
Publisher= $Publisher;
}
}
}}
}#End of 32 bit
} # End of Switch
}
#$AppsReport
$column1 = #{expression="ServerName"; width=15; label="Name"; alignment="left"}
$column2 = #{expression="Architecture"; width=10; label="32/64 Bit"; alignment="left"}
$column3 = #{expression="Application"; width=80; label="Application"; alignment="left"}
$column4 = #{expression="Version"; width=15; label="Version"; alignment="left"}
$column5 = #{expression="Publisher"; width=30; label="Publisher"; alignment="left"}
if ($outputType -eq "Console")
{
"#"*80
"Installed Software Application Report"
"Number of Installed Application count : $($object.count)"
"Generated $(get-date)"
"Generated from $(gc env:computername)"
"#"*80
$object |Format-Table $column1, $column2, $column3 ,$column4, $column5
}
elseif ($OutputType -eq "GridView")
{
$object|Out-GridView
}
elseif ($OutputType -eq "CSV")
{
[string]$FileDS = Get-Date -Format "yyyyMMdd"
[string]$outFile = $outpath+'\'+$computername+'_'+$FileDS+'.csv'
New-Item -ItemType file $outfile -Force
$object | export-csv -path $outfile
}
else
{
write-host " Invalid Output Type $OutputType"
}
}
I'm also using the code to exclude some application from a list. I have a problem on PCs running a 32-bit version of Windows (tested on a 32-bit Windows 7) : applications excluded from this list still appear in the output. No problem on 64-bit systems however.
Ideas of what's wrong ?
Thanks

How to track path/key during recursion through hash of hashes in PowerShell?

I've been trying to compare two hashes of hashes against each other. Unfortunately even with the great help from here I struggled to pull together what I was trying to do.
So, I've resorted to searching the internet again and I was lucky enough to find Edxi's code (full credit to him and Dbroeglin who looks to have written it originally)https://gist.github.com/edxi/87cb8a550b43ec90e4a89d2e69324806 His compare-hash function does exactly what I needed in terms of its comparisons. However, I'd like to report of the full path of the hash in the final output. So I've tried to update the code to allow for this. My thinking being that I should be able to take the Key (aka path) while the code loops over itself but I'm clearly going about it in the wrong manner.
Am I going about this with the right approach, and if not, would someone suggest another method please? The biggest problem I've found so far is that if I change the hash, my code changes don't work e.g. adding 'More' = 'stuff' So it becomes $sailings.Arrivals.PDH083.More breaks the path in the way I've set it.
My version of the code:
$Sailings = #{
'Arrivals' = #{
'PDH083' = #{
'GoingTo' = 'Port1'
'Scheduled' = '09:05'
'Expected' = '10:11'
'Status' = 'Delayed'
}
}
'Departures' = #{
'PDH083' = #{
'ArrivingFrom' = 'Port1'
'Scheduled' = '09:05'
'Expected' = '09:05'
'Status' = 'OnTime'
'Other' = #{'Data' = 'Test'}
}
}
}
$Flights = #{
'Arrivals' = #{
'PDH083' = #{
'GoingTo' = 'Port'
'Scheduled' = '09:05'
'Expected' = '10:20'
'Status' = 'Delayed'
}
}
'Departures' = #{
'PDH083' = #{
'ArrivingFrom' = 'Port11'
'Scheduled' = '09:05'
'Expected' = '09:05'
'Status' = 'NotOnTime'
'Other' = #{'Data' = 'Test_Diff'}
}
}
}
function Compare-Hashtable {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[Hashtable]$Left,
[Parameter(Mandatory = $true)]
[Hashtable]$Right,
[string] $path,
[boolean] $trackpath = $True
)
write-host "PAth received as: $path"
function New-Result($Key, $LValue, $Side, $RValue) {
New-Object -Type PSObject -Property #{
key = $Key
lvalue = $LValue
rvalue = $RValue
side = $Side
}
}
[Object[]]$Results = $Left.Keys | ForEach-Object {
if ($trackpath ) {
write-host "Working on Path: " $path + $_
}
if ($Left.ContainsKey($_) -and !$Right.ContainsKey($_)) {
write-host "Right key not matched. Report path as: $path"
New-Result $path $Left[$_] "<=" $Null
}
else {
if ($Left[$_] -is [hashtable] -and $Right[$_] -is [hashtable] ) {
if ($path -ne $null -or $path -ne "/") {
$path = $path + "/" + $_
write-host "Sending Path to function as: $path"
}
Compare-Hashtable $Left[$_] $Right[$_] $path
}
else {
$LValue, $RValue = $Left[$_], $Right[$_]
if ($LValue -ne $RValue) {
$path = $path + "/" + $_
write-host "Not a hash so must be a value at path:$path"
New-Result $path $LValue "!=" $RValue
}
else {
Write-Host "Before changing path: $path "
if (($path.Substring(0, $path.lastIndexOf('/'))).length >0) {
$path = $path.Substring(0, $path.lastIndexOf('/'))
Write-Host "After changing path: $path "
}
}
}
}
# if (($path.Substring(0, $path.lastIndexOf('/'))).length >0) {
# Tried to use this to stop error on substring being less than zero
# but clearly doesnt work when you add more levels to the hash
$path = $path.Substring(0, $path.lastIndexOf('/'))
# } else { $path = $path.Substring(0, $path.lastIndexOf('/')) }
}
$Results += $Right.Keys | ForEach-Object {
if (!$Left.ContainsKey($_) -and $Right.ContainsKey($_)) {
New-Result $_ $Null "=>" $Right[$_]
}
}
if ($Results -ne $null) { $Results }
}
cls
Compare-Hashtable $Sailings $Flights
outputs
key lvalue side rvalue
--- ------ ---- ------
/Arrivals/PDH083/Expected 10:11 != 10:20
/Arrivals/GoingTo Port1 != Port
/Departures/PDH083/ArrivingFrom Port1 != Port11
/Departures/PDH083/Status OnTime != NotOnTime
/Departures/PDH083/Other/Data Test != Test_Diff
I won't do that much string manipulation on the $Path but threat $Path as an array and join it to a string at the moment you assign it as a property (key = $Path -Join "/") to the object:
function Compare-Hashtable {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[Hashtable]$Left,
[Parameter(Mandatory = $true)]
[Hashtable]$Right,
[string[]] $path = #(),
[boolean] $trackpath = $True
)
write-host "Path received as: $path"
function New-Result($Path, $LValue, $Side, $RValue) {
New-Object -Type PSObject -Property #{
key = $Path -Join "/"
lvalue = $LValue
rvalue = $RValue
side = $Side
}
}
$Left.Keys | ForEach-Object {
$NewPath = $Path + $_
if ($trackpath ) {
write-host "Working on Path: " $NewPath
}
if ($Left.ContainsKey($_) -and !$Right.ContainsKey($_)) {
write-host "Right key not matched. Report path as: $NewPath"
New-Result $NewPath $Left[$_] "<=" $Null
}
else {
if ($Left[$_] -is [hashtable] -and $Right[$_] -is [hashtable] ) {
Compare-Hashtable $Left[$_] $Right[$_] $NewPath
}
else {
$LValue, $RValue = $Left[$_], $Right[$_]
if ($LValue -ne $RValue) {
New-Result $NewPath $LValue "!=" $RValue
}
}
}
}
$Right.Keys | ForEach-Object {
$NewPath = $Path + $_
if (!$Left.ContainsKey($_) -and $Right.ContainsKey($_)) {
New-Result $NewPath $Null "=>" $Right[$_]
}
}
}
cls
Compare-Hashtable $Sailings $Flights
side-note: Write Single Records to the Pipeline (SC03), see: Strongly Encouraged Development Guidelines. In other words, don't do the $Results += ... but intermediately put any completed result in the pipeline for the next cmdlet to be picked up (besides, it unnecessarily code)...

Unable to trim/trimend "$"

I am trying to modify a PS script from online resource:
Trap {"Error: $_"; Break;}
$D = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$Domain = [ADSI]"LDAP://$D"
$Searcher = New-Object System.DirectoryServices.DirectorySearcher
$Searcher.PageSize = 200
$Searcher.SearchScope = "subtree"
$Searcher.Filter = "(objectCategory=computer)"
$Searcher.PropertiesToLoad.Add("samAccountName") > $Null
$Searcher.PropertiesToLoad.Add("lastLogon") > $Null
# Create hash table of users and their last logon dates.
$arrComp = #{}
# Enumerate all Domain Controllers.
ForEach ($DC In $D.DomainControllers)
{
$Server = $DC.Name
$Searcher.SearchRoot = "LDAP://$Server/" + $Domain.distinguishedName
$Results = $Searcher.FindAll()
ForEach ($Result In $Results)
{
$DN = $Result.Properties.Item("samAccountName")
$LL = $Result.Properties.Item("lastLogon")
If ($LL.Count -eq 0)
{
$Last = [DateTime]0
}
Else
{
$Last = [DateTime]$LL.Item(0)
}
If ($Last -eq 0)
{
$LastLogon = $Last.AddYears(1600)
}
Else
{
$LastLogon = $Last.AddYears(1600).ToLocalTime()
}
If ($arrComp.ContainsKey("$DN"))
{
If ($LastLogon -gt $arrComp["$DN"])
{
$arrComp["$DN"] = $LastLogon
}
}
Else
{
$arrComp.Add("$DN", $LastLogon)
}
}
}
Script above give me the computername & its' last logon date, however the computernames are having "$" at the end. I would like to trim the "$" in order for me to use it remove the computer from AD later. However my script is not working.
$Compdollar = $arrComp.getEnumerator() | Select-Object Key | out-string
$AllComp = #()
Foreach ($inactD in $Compdollar) {
$AllComp += $inactD.Trim("$")
}
$Allcomp
The output is still computer name with "$", can anyone tells me why it wasn't trimmed?
Don't use double quotes with a $ as it is treated like a variable. Use single quotes instead.
$AllComp += $inactD.Trim('$')
Or use the backtick to escape the dollar sign.
$AllComp += $inactD.Trim("`$")