Using PowerShell 'If' statement to compare function (Windows version) - powershell

I'm currently creating an AD script that can get the AD groups of one machine and transfer them to a new machine (in case of system failure).
I've managed to get the script to go out and find the versions of Windows that the two machines are running via their hostname, however I'm having a problem creating an 'if' statement to compare the two versions of Windows.
The idea is that if the are the same version (and thus the same package version) the groups will be copied automatically, but I can't for the life of me figure out how to do it.
Please consider this code:
function W_version_current
{
$current = Get-WmiObject Win32_OperatingSystem -computer $current_hostname.text | select buildnumber
if ($current -match '7601')
{
"Windows 7"
}
elseif($current -match '2600')
{
"Windows XP"
}
elseif($current -eq $null)
{
"The box is empty"
}
else
{
"Function not supported"
}
}
function W_version_target
{
$target = Get-WmiObject Win32_OperatingSystem -computer $target_hostname.text | select buildnumber
if ($var -match '7601')
{
"Windows 7"
}
elseif($target -match '2600')
{
"Windows XP"
}
elseif($target -eq $null)
{
"The box is empty"
}
else
{
"Function not supported"
}
}
function compare_current_target
{
if(W_version_current -eq W_version_target)
{
"Matching version of Windows detected"
}
else
{
"Versions of Windows do not match"
}
}
Now is it true that all variables cannot be accessed outside of functions?
If so, what else can I do?

Probably what you're missing is that with PowerShell order of operations, you often have to put function calls in parentheses.
Try this instead:
if ((W_version_current) -eq (W_version_target))
{
"Matching version of Windows detected"
}
else
{
"Versions of Windows do not match"
}
To answer your question, scope in PowerShell works pretty much like most other scripting languages, e.g. variables declared in functions cannot be used outside of the functions they were declared, unless you declare them as global, which you can do like so:
$global:x = "hi"
You can then use the variable $x anywhere, or if you like, $global:x, and it will have the value of "hi".

Related

ShouldProcess failing in PowerShell7

Environment: Windows Server 2022 21H2, Powershell 7.2 (running as administrator)
I have a script that implements ShouldProcess, which works fine in Windows PowerShell 5. However, in PowerShell 7, the script invariably throws the error Cannot find an overload for "ShouldProcess" and the argument count: "1". ShouldProcess at MSDoc says that the one-argument overload for $PSCmdlet.ShouldProcess() exists and should work.
It's failing, as above. Why?
The script in question is pasted below; it's in a script module:
function Remove-DomainUserProfile {
<#
#Comment-based help removed for space considerations
#>
[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact="High")]
param(
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[Parameter(ParameterSetName='SpecificProfile')]
[Parameter(ParameterSetName='ByAge')]
[Parameter(ParameterSetName='AllProfiles')]
[String[]]$ComputerName = $env:ComputerName,
[Parameter(Mandatory=$true,ParameterSetName='SpecificProfile')]
[Parameter(ParameterSetName='ByAge')]
[Alias("UserName","sAMAccountName")]
[String]$Identity,
[Parameter(ParameterSetName='ByAge')]
[Parameter(ParameterSetName='AllProfiles')]
[Switch]$DomainOnly,
[Parameter(ParameterSetName='SpecificProfile')]
[Parameter(ParameterSetName='ByAge')]
[Int]$Age,
[Parameter(Mandatory=$true,ParameterSetName='AllProfiles')]
[Switch]$All
)
BEGIN {
if (-NOT (Test-IsAdmin)) {
Write-Output "This function requires being run in an Administrator session! Please start a PowerShell
session with Run As Administrator and try running this command again."
return
}
$NoSystemAccounts = "SID!='S-1-5-18' AND SID!='S-1-5-19' AND SID!='S-1-5-20' AND NOT SID LIKE 'S-1-5-%-500' "
# Don't even bother with the system or administrator accounts.
if ($DomainOnly) {
$SIDQuery = "SID LIKE '$((Get-ADDomain).DomainSID)%' " # All domain account SIDs begin
with the domain SID
} elseif ($Identity.Length -ne 0) {
$SIDQuery = "SID LIKE '$(Get-UserSID -AccountName $Identity)' "
}
$CutoffDate = (Get-Date).AddDays(-$Age)
$Query = "SELECT * FROM Win32_UserProfile "
}
PROCESS{
ForEach ($Computer in $ComputerName) {
Write-Verbose "Processing Computer $Computer..."
if ($SIDQuery) {
$Query += "WHERE " + $SIDQuery
} else {
$Query += "WHERE " + $NoSystemAccounts
}
if ($All) {
Write-Verbose "Querying WMI using '$Query'"
$UserProfiles = Get-WMIObject -ComputerName $Computer -Query $Query
} else {
Write-Verbose "Querying WMI using '$Query' and filtering for profiles last used before $CutoffDate ..."
$UserProfiles = Get-WMIObject -ComputerName $Computer -Query $Query | Where-Object {
[Management.ManagementDateTimeConverter]::ToDateTime($_.LastUseTime) -lt $CutoffDate }
}
ForEach ($UserProfile in $UserProfiles) {
if ($PSCmdlet.ShouldProcess($UserProfile)) {
Write-Verbose "Deleting profile object $UserProfile ($(Get-SIDUser $UserProfile.SID))..."
$UserProfile.Delete()
}
}
}
}
END {}
}
To complement Santiago Squarzon's excellent analysis:
The behavior, present up to at least PowerShell 7.2.1, should be considered a bug, because any object should be auto-convertible to a string in a .NET method call.
There is no reason for [pscustomobject] a.k.a [psobject] instances to act differently than instances of any other type (irrespective of whether implicit stringification makes sense in a given situation); to give a simple example:
If (42).ToString((Get-Item /)) works, ...
... there's no reason why (42).ToString(([pscustomobject] #{ foo=1 })) shouldn't.
Note that implicit stringification in the context of cmdlets / functions / script is not affected; e.g., Get-Date -Format ([pscustomobject] #{ foo=1 }) doesn't cause an error.
See GitHub issue #16988.
The reason that the serialization infrastructure is involved at all is that the obsolete WMI cmdlets such as Get-WmiObject aren't natively available in PowerShell (Core) v6+ anymore, and using them implicitly makes use of the Windows PowerShell Compatibility feature:
This entails using a hidden powershell.exe child process, communication with which requires use of serialization, during which most non-primitive types lose their type identity and are emulated with method-less [psobject] instances that contain copies of the original object's properties.
In PowerShell v3 and above, and especially in PowerShell (Core) v6+, use the CIM cmdlets instead, such as Get-CimInstance, instead:
While similar to the WMI cmdlets in many respects, an important difference is that objects returned from CIM cmdlets have no methods; instead, methods must be called via Invoke-CimMethod.
See this answer for more information.
For reference, this error can be reproduced on both PowerShell versions 5.1 and Core. The steps to reproduce is passing a System.Management.Automation.PSObject as argument to the .ShouldProcess(String) overload. It makes sense, by looking at your comment mentioning a serialized object. In below example, if the System.Diagnostics.Process object is not serialized it works properly on both versions.
function Test-ShouldProcess {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = "High")]
param()
$obj = [System.Management.Automation.PSSerializer]::Deserialize(
[System.Management.Automation.PSSerializer]::Serialize((Get-Process)[0])
)
# will throw
if ($PSCmdlet.ShouldProcess($obj)) { 'hello' }
}
Test-ShouldProcess

According to the condition, don't proceed with the remaining script

I'm trying to validate the Win10 upgrade result by powershell script remotely.
I built this code, and run Invoke-Command -FilePath C:\validate.ps1 -ComputerName remotePC
my question is, if OS edition is win10 ,proceed with the remaining validation.
if OS is win7, don't proceed with the remaining validation. Any help please? thanks a lot.
#Auto-validate the Win10 upgrade result
# Get PC name
$PCname = $env:computername
# Check OS Edition
$OSEdition = (Get-WmiObject Win32_OperatingSystem).name
if ($OSEdition -match '10') {
Write-Host "$PCname Win10" -ForegroundColor Green
}
else {
Write-Host "$PCname Win7" -ForegroundColor Red
}
# System Locale
$locale = (Get-WinSystemLocale).Name
if ($locale -eq 'US') {
Write-Host "$PCname locale is correct" -ForegroundColor Green
}
else {
Write-Host "$PCname locale is wrong" -ForegroundColor Red
}
# Check printer status
Get-Printer | Format-Table ComputerName,Name,DriverName,PrinterStatus
If you want to end execution in the current scope (the current function or script), use the return keyword, causing PowerShell to return control to the caller:
if($OSEdition -notmatch '10'){
# OS Name doesn't contain '10', let's return!
return
}
For version detection, I'd suggest looking at the Version property of Win32_OperatingSystem instead of the OS Name:
$OSVersion = (Get-WmiObject Win32_OperatingSystem).Version
if($OSVersion -notlike '10.*'){
return
}
To learn more about the return keyword, check out the about_Return help file!

Filter processes by module's FileName

If I need to filter processes by a module's FileName, the following code does the job:
Get-Process | where { $_.Modules.FileName -eq "xxx\yyy.dll”) }
But if I need to filter modules by FileName starting with a string, the following code doesn't seem to work:
Get-Process | where { $_.Modules.FileName.StartsWith("xxx\yyy.dll”)) }
As result, I see all the processes in the output. I'm very confused why filtering doesn't seem to work in case of StartsWith
The member modules might be a collection. Thus it needs to be iterated too. Like so,
(get-process) | % {
if($_.modules -ne $null) { # No modules, no action
$_.modules | ? { $_.filename.tolower().startswith("c:\program") }
}
}
As for the question, there are actually two iterations. Let's use explicit variables instead of pipelining and printing the acutal module files. Passing multiple $_s around is not easy to read syntax anyway. Like so,
foreach ($p in get-process) {
if ($p.modules -ne $null){
write-host $p.id $p.ProcessName
foreach($m in $p.modules){
if ($m.filename.tolower().startswith("c:\program") ) {
write-host `t $m.moduleName $m.FileName # ` markdown bug
}
}
write-host
}
}

Powershell Switch Condition

I'm having a little trouble with a PS script right now.
What I am trying to do:
Depending on which OU a computer belongs to, it should get a different printer mapped.
I am trying this with a switch condition, but regardless of what I try the condition seems to be always TRUE (although I know it isn't)
When I type in the condition into PowerShell manually, I get the correct values if the condition is TRUE or FALSE. But as soon as I use it in the switch, the condition seems to be always TRUE.
What I have so far:
With dsquery I check if a computer belongs to a specific OU.
If a value is returned, which only happens if the query succeeds, I put it into my $SwitchDump variable (Condition TRUE).
From my understanding, if a device is not found in the OU, there is no value that will be passed to my $SwitchDump variable and hence should be $null right?
But it keeps mapping the printer.
Switch ($SwitchDump = dsquery computer $OU_TO_SEARCH_IN|findstr $env:COMPUTERNAME | Out-String)
{
$SwitchDump -ne $null {Add-Printer -ConnectionName \\$PrintServer\$DesiredPrinter}
}
Or am I just barking up the wrong tree?
Any ideas would be greatly appreciated.
$SwitchDump = dsquery computer $OU_TO_SEARCH_IN | findstr $env:COMPUTERNAME | Out-String
Switch ($SwitchDump)
{
{$_ -ne $null} {Add-Printer -ConnectionName \\$PrintServer\$DesiredPrinter}
}
You need to use $_ to represent the variable being tested by the Switch if you want to do anything beyond simple comparisons for values. You also need to make those comparisons a scriptblock by using { }.
You're assigning the dsquery to $SwitchDump... which will [almost] always return true ;-)
You probably want to perform an equality check i.e.
Switch ($SwitchDump -eq dsquery computer $OU...
Also it looks like you have your switch syntax slightly off: https://ss64.com/ps/switch.html
$SwitchDump = dsquery computer $OU_TO_SEARCH_IN|findstr $env:COMPUTERNAME
if (-not $SwitchDump) {
Write-Output "SwitchDump is empty"
}
switch ($SwitchDump) {
$null { Write-Host "SwitchDump = NULL" ; break; }
"value1" { Write-Host "SwitchDump = value1"; break; }
"value2" { Write-Host "SwitchDump = value2"; break; }
"value3" { Write-Host "SwitchDump = value3"; break; }
default { Write-Host "None of the above" ; break; }
}

Checking several Hyper-V Hosts for a specific VM in Powershell

I am writing a script to administer Hyper-VMs using the PowerShell Management Library for Hyper-V.
Since we are using several Hyper-V Hosts and our VMs can change their host for performance reasons or other reasons I need a script that finds out which Host a VM runs on for the following functions.
This was my try at accomplishing this:
function IdentifyHost
{
param
(
[parameter(Position=0, Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$VM
)
[Array]$hosts=Get-VMHost
if ($hosts.count -eq 0)
{
Write-Warning "No valid hosts found."
}
for ([int]$i=0; $i -lt $hosts.count; $i++ )
{
try
{
$out = Get-VM -Name $VM -Server $hosts[$i] -ErrorAction Stop
}
catch [UnauthorizedAccessException]
{
Write-Warning "Access to $hosts[$i] denied."
}
if ($VM -is [String])
{
if ($out.VMElementName -eq $VM )
{
return $out.__SERVER
}
}
elseif ($VM.ElementName -ne $null)
{
if ($out.VMElementName -eq $VM.VMElementName)
{
return $out.__SERVER
}
}
}
Write-Warning "No Host found for $VM"
}
Get-VMHost returns an array of all available Hyper-V hosts in the local area network.
My problem is that my function always returns the first element of the $hosts array whenever there is an UnauthorizedAccessException for the first element.
The plan is as following:
If the VM exists on the Host he will return a WMI Object representing that VM whose VMElementName property is equal to the VMs name given as parameter.
If the VM is given a WMI Object representing a VM the VMElementName properties of the two objects are equal.
If the VM does not exist on the Host he returns nothing.
If there's an access issue it should be catched.
But somehow it doesn't work out.
My question is this: What am I doing wrong in the code? And how can I fix it?
EDIT: The output of the function is the access problem warning for the first element of the $hosts array and then the first element of $hosts itself.
EDIT2: I fixed this myself by changing the return from the fragile $hosts[$i] to $out.__Server
Okay so I found a possible way of solving this issue:
Instead of returning the $hosts[$i] which yields unfavorable results I return the __Server property of $out, assuming there is a valid $out that matches the conditions.
If any of you guys knows a better or cleaner way of doing this, please by my guest.