|
return "This is a demo script file." |
|
|
|
# Find Me: https://jdhitsolutions.github.io |
|
|
|
# Any one can learn syntax and mechanics. |
|
# Let's focus on the squishy bits - what you should write |
|
# and what not to write |
|
|
|
#region Essential rules |
|
|
|
# 1. THE POWERSHELL PARADIGM IS USING OBJECTS IN THE PIPELINE |
|
# 2. DON'T MANIPULATE TEXT TO GET YOUR DESIRED RESULT |
|
# 3. LET POWERSHELL DO THE WORK FOR YOU |
|
# 4. THINK ABOUT WHERE YOUR CODE WILL BE RUN |
|
# 5. THINK ABOUT WHERE YOUR OUTPUT WILL CREATED |
|
|
|
#endregion |
|
|
|
#region Object output |
|
|
|
# don't include blank line to make the output look pretty. Write one type of object |
|
|
|
Function Get-Widget { |
|
[cmdletbinding()] |
|
Param () |
|
|
|
$r = [PSCustomObject]@{ |
|
Name = $env:Username |
|
Computer = $env:COMPUTERNAME |
|
OS = (Get-CimInstance -ClassName Win32_operatingSystem -Property Caption).Caption |
|
LuckyNumber = Get-Random -Minimum 1 -Maximum 20 |
|
} |
|
Write-Output ' ' |
|
Write-Output $r |
|
Write-Output ' ' |
|
} |
|
|
|
Get-Widget |
|
Get-Widget | Measure-Object |
|
|
|
#don't need write-output |
|
Function Get-Widget { |
|
[cmdletbinding()] |
|
Param () |
|
|
|
$r = [PSCustomObject]@{ |
|
Name = $env:Username |
|
Computer = $env:COMPUTERNAME |
|
OS = (Get-CimInstance -ClassName Win32_operatingSystem -Property Caption).Caption |
|
LuckyNumber = Get-Random -Minimum 1 -Maximum 20 |
|
} |
|
#no formatting - let PowerShell handle the output |
|
$r |
|
} |
|
|
|
Get-Widget -ov a | Measure-Object |
|
$a |
|
|
|
#endregion |
|
|
|
#region don't compare boolean strings |
|
|
|
$x = $true |
|
#no |
|
if ($x -eq $True) { |
|
'I am good' |
|
} |
|
|
|
#Think object |
|
#yes |
|
if ($x) { |
|
'I am good' |
|
} |
|
|
|
|
|
#It doesn't work the way you think |
|
$p = Get-Process -id $pid |
|
$p.Responding |
|
$p.Responding.GetType().Name |
|
if ($p.Responding -eq 'true') { "ok" } |
|
if ($p.Responding -eq 'false') { "ok" } |
|
|
|
#better |
|
if ($p.Responding) { "ok" } |
|
if (-Not $p.Responding) { "ok" } else { "not ok"} |
|
|
|
#xml data exceptions |
|
[xml]$data = Get-Content proc.xml |
|
$data.processes.item("Process").IsResponding |
|
if ($data.processes.item("Process").IsResponding) {"ok"} |
|
if ($data.processes.item("Process").IsResponding -eq 'True') {"ok"} |
|
|
|
[xml]$data2 = Get-Content proc2.xml |
|
$data2.processes.item("Process").IsResponding |
|
#Be careful about seeing objects when they don't exist |
|
if ($data2.processes.item("Process").IsResponding) {"ok"} |
|
$data2.processes.item("Process").IsResponding.GetType().Name |
|
if ($data2.processes.item("Process").IsResponding -eq 'True') {"ok"} |
|
|
|
<# |
|
avoid this as well |
|
PS C:\> $x -eq 1 |
|
True |
|
PS C:\> $x -eq 0 |
|
False |
|
#> |
|
|
|
#endregion |
|
|
|
#region write rich objects - use formatting to set defaults |
|
|
|
Function Resolve-WhoIs { |
|
[CmdletBinding()] |
|
Param( |
|
[Parameter(Position = 0, Mandatory, ValueFromPipeline)] |
|
[string]$IPAddress |
|
) |
|
Begin { |
|
Write-Verbose "Starting $($MyInvocation.MyCommand)" |
|
$baseURL = 'http://whois.arin.net/rest' |
|
} |
|
Process { |
|
Write-Verbose "Resolving IP $IPAddress" |
|
|
|
$url = "$baseUrl/ip/$IPAddress" |
|
$r = Invoke-RestMethod $url |
|
#if $r.net exists it is implicitly true |
|
if ($r.net) { |
|
[PSCustomObject]@{ |
|
IP = $IPAddress |
|
Name = $r.net.name |
|
RegisteredOrganization = $r.net.orgRef.name |
|
} |
|
} |
|
} |
|
End { |
|
Write-Verbose "Ending $($MyInvocation.MyCommand)" |
|
} |
|
} |
|
|
|
Resolve-WhoIs 8.8.8.8 |
|
|
|
#what would make this richer or more useful? |
|
Function Resolve-WhoIs { |
|
[CmdletBinding()] |
|
Param( |
|
[Parameter(Position = 0, Mandatory, ValueFromPipeline)] |
|
[string]$IPAddress |
|
) |
|
Begin { |
|
Write-Verbose "Starting $($MyInvocation.MyCommand)" |
|
$baseURL = 'http://whois.arin.net/rest' |
|
} |
|
Process { |
|
Write-Verbose "Resolving IP $IPAddress" |
|
|
|
$url = "$baseUrl/ip/$IPAddress" |
|
$r = Invoke-RestMethod $url |
|
$global:raw = $r |
|
if ($r.net) { |
|
#measure ping latency |
|
$ping = (test-connection $IPAddress -IPv4 -Ping -Count 3 | |
|
Measure-Object -Property Latency -Average).average -as [int] |
|
[PSCustomObject]@{ |
|
PSTypeName = 'PSWhoIs' #<-- this is the key to custom formatting |
|
IP = $IPAddress |
|
Name = $r.net.name |
|
RegisteredOrganization = $r.net.orgRef.Name |
|
OrganizationHandle = $r.net.orgRef.Handle |
|
City = (Invoke-RestMethod $r.net.orgRef.'#text').org.city |
|
StartAddress = $r.net.startAddress |
|
EndAddress = $r.net.endAddress |
|
NetBlocks = $r.net.netBlocks.netBlock | ForEach-Object { "$($_.StartAddress)/$($_.cidrLength)" } |
|
Online = $raw.net.orgref.'#text' |
|
PingLatency = $ping |
|
Updated = $r.net.updateDate -as [DateTime] |
|
} |
|
|
|
} |
|
} |
|
End { |
|
Write-Verbose "Ending $($MyInvocation.MyCommand)" |
|
} |
|
} |
|
|
|
Resolve-WhoIs 8.8.8.8 | Tee-Object -Variable who |
|
$who | Get-Member |
|
|
|
#control the output |
|
|
|
# Install-Module PSScriptTools |
|
# https://github.com/jdhitsolutions/PSScriptTools |
|
# I have already run this |
|
# $who | New-PSFormatXML -Path .\pswhois.format.ps1xml -GroupBy RegisteredOrganization -Properties IP,Name,StartAddress,EndAddress -FormatType Table |
|
#I've tweaked the file |
|
psedit .\pswhois.format.ps1xml |
|
# load the format file. The format file requires a PowerShell 7 session |
|
# that supports PSStyle |
|
Update-FormatData .\pswhois.format.ps1xml |
|
|
|
#You could also create named custom views and type extensions |
|
#add a custom type method |
|
Update-TypeData -TypeName PSWhoIs -MemberType ScriptMethod -MemberName Open -Value {start $this.Online} |
|
Update-TypeData -TypeName PSWhoIs -MemberType AliasProperty -MemberName Locale -Value City |
|
|
|
#Don't do this |
|
Function Resolve-WhoIs2 { |
|
[CmdletBinding()] |
|
Param( |
|
[Parameter(Position = 0, Mandatory, ValueFromPipeline)] |
|
[string]$IPAddress |
|
) |
|
Begin { |
|
Write-Verbose "Starting $($MyInvocation.MyCommand)" |
|
$baseURL = 'http://whois.arin.net/rest' |
|
$out = @() |
|
} |
|
Process { |
|
Write-Verbose "Resolving IP $IPAddress" |
|
|
|
$url = "$baseUrl/ip/$IPAddress" |
|
$r = Invoke-RestMethod $url |
|
if ($r.net) { |
|
$out += [PSCustomObject]@{ |
|
IP = $IPAddress |
|
Name = $r.net.name |
|
RegisteredOrganization = $r.net.orgRef.name |
|
} |
|
} |
|
} |
|
End { |
|
#VERY BAD 😣 🤮 ☠ |
|
$out | Sort-Object -property RegisteredOrganization | |
|
Format-Table -GroupBy RegisteredOrganization -Property Name,IP |
|
|
|
Write-Verbose "Ending $($MyInvocation.MyCommand)" |
|
} |
|
} |
|
|
|
Resolve-WhoIs2 8.8.8.8 |
|
Resolve-WhoIs2 8.8.8.8 | Get-Member |
|
|
|
#you don't know how someone, or you, might want to use the output. |
|
|
|
#endregion |
|
|
|
#region Be careful with Return |
|
#problematic |
|
Function Get-Things { |
|
[cmdletbinding()] |
|
Param( |
|
[int]$Count = 10 |
|
) |
|
|
|
Begin { |
|
Write-Verbose "begin" |
|
} |
|
Process { |
|
do { |
|
$i = Get-Random -Minimum 1 -Maximum 100 |
|
if ($i -ge 50) { |
|
Return $i |
|
} |
|
$count-- |
|
} Until ($count -eq 0) |
|
} |
|
end { |
|
Write-Verbose "end" |
|
} |
|
} |
|
|
|
Get-Things -Verbose |
|
|
|
#better but awkward |
|
Function Get-Things { |
|
[cmdletbinding()] |
|
Param( |
|
[int]$Count = 10 |
|
) |
|
|
|
Begin { |
|
Write-Verbose "begin" |
|
} |
|
Process { |
|
do { |
|
$i = Get-Random -Minimum 1 -Maximum 100 |
|
if ($i -ge 50) { |
|
$i |
|
} |
|
$count-- |
|
} Until ($count -eq 0) |
|
} |
|
end { |
|
Write-Verbose "end" |
|
} |
|
} |
|
|
|
Get-Things -Verbose |
|
|
|
#let the pipeline do its thing |
|
Function Get-Things { |
|
[cmdletbinding()] |
|
Param( |
|
[int]$Count = 10 |
|
) |
|
|
|
Begin { |
|
Write-Verbose "begin" |
|
} |
|
Process { |
|
Write-Verbose "Getting $Count random numbers between 1 and 100" |
|
Get-Random -Minimum 1 -Maximum 100 -Count $Count | Where-Object {$_ -ge 50} |
|
} |
|
end { |
|
Write-Verbose "end" |
|
} |
|
} |
|
|
|
#use when you want to intentionally bail out |
|
|
|
Function Invoke-Weekly { |
|
[cmdletbinding()] |
|
Param( |
|
[string]$Path = "C:\work" |
|
) |
|
|
|
begin { |
|
Write-Verbose "begin" |
|
$now = Get-Date |
|
} |
|
Process { |
|
Write-Verbose "Processing $Path" |
|
Try { |
|
$items = Get-ChildItem -LiteralPath $Path -file -Recurse -ErrorAction stop |
|
} |
|
Catch { |
|
#this is demonstrating the use of RETURN not the best way to validate parameters |
|
Write-Warning "Failed to validate path. Abandoning process." |
|
#this returns from the Process script block |
|
Return |
|
} |
|
|
|
Write-Host "Backing up $($items.count) items" -ForegroundColor Green |
|
Write-Host "Exit Process block" -ForegroundColor Cyan |
|
} |
|
end { |
|
Write-Verbose "end" |
|
} |
|
} |
|
|
|
Invoke-Weekly -Verbose |
|
Invoke-Weekly -path X:\FooBar -Verbose |
|
|
|
# I inserted a Return statement at the beginning of this script file |
|
# so that I wouldn't accidentally run it. |
|
|
|
#endregion |
|
|
|
#region use Switch over multiple If/Else statements |
|
|
|
# 14393 2016 |
|
# 17763 2019 |
|
# 22631 Win11 |
|
# 19044 Win10 |
|
|
|
$os = Get-CimInstance Win32_OperatingSystem |
|
|
|
if ($os.BuildNumber -eq 22631) { |
|
Write-Host "Running Windows 11 code" |
|
} |
|
elseif ($os.BuildNumber -eq 19044) { |
|
Write-Host "Running Windows 10 code" |
|
} |
|
elseif ($os.BuildNumber -eq 14393) { |
|
Write-Host "Running Windows Server 2016 code" |
|
} |
|
elseif ($os.BuildNumber -eq 17763) { |
|
Write-Host "Running Windows Server 2019 code" |
|
} |
|
|
|
#better |
|
Switch ($os.BuildNumber) { |
|
22631 { |
|
Write-Host "Running Windows 11 code" |
|
} |
|
19044 { |
|
Write-Host "Running Windows 10 code" |
|
} |
|
14393 { |
|
Write-Host "Running Windows Server 2016 code" |
|
} |
|
17763 { |
|
Write-Host "Running Windows Server 2019 code" |
|
} |
|
} |
|
|
|
#this also allows you to run code based on multiple matches |
|
|
|
Function Invoke-FileAction { |
|
[CmdletBinding()] |
|
Param( |
|
[Parameter(Position = 0, Mandatory,ValueFromPipeline)] |
|
[string]$Path |
|
) |
|
Begin {} |
|
Process { |
|
Write-Host "Processing $Path" -ForegroundColor green |
|
$file = Get-Item $Path |
|
Switch ($file) { |
|
{$_.length -ge 1000} { Write-Host "large file operation" -ForegroundColor Cyan } |
|
{$_.Extension -match "\.ps*"} { Write-Host "PowerShell file operation" -ForegroundColor DarkBlue } |
|
{$_.Extension -match "\.((json)|(xml)|(csv))"} { Write-Host "Data file operation" -ForegroundColor yellow } |
|
Default { Write-Host "No action taken" -ForegroundColor magenta} |
|
} |
|
} |
|
End {} |
|
} |
|
|
|
Invoke-FileAction -Path .\demo.ps1 |
|
Invoke-FileAction -Path .\proc.xml |
|
|
|
Help about_Switch |
|
|
|
#endregion |
|
|
|
#region think locally act remotely |
|
|
|
psedit .\getstat-local.ps1 |
|
Get-Status |
|
$ps = New-PSSession -computername SRV1 |
|
#use the copy of the function remotely |
|
$sb = (Get-Item Function:\Get-Status).scriptblock |
|
Invoke-Command {New-Item -Path function:\get-status -Value $using:sb} -Session $ps |
|
Invoke-Command {Get-Status -AsInt} -session $ps -HideComputerName | |
|
select * -ExcludeProperty runspaceid |
|
|
|
psedit .\getstat-remote.ps1 |
|
. .\getstat-remote.ps1 |
|
|
|
Get-SystemStatus -Verbose |
|
Get-SystemStatus -Computername dom1 -Verbose |
|
|
|
#this could be rewritten to use an existing PSSession |
|
|
|
#endregion |
|
|
|
#region Consider module purpose and user |
|
|
|
#WHO WILL BE USING YOUR CODE? |
|
#WHAT ARE THEIR EXPECTATIONS? |
|
#HOW WILL THEY USE YOUR COMMANDS |
|
|
|
#endregion |
|
|
|
#region code writing tips |
|
|
|
# FOLLOW COMMUNITY-ACCEPTED BEST PRACTICES |
|
# WRITE VERTICAL CODE |
|
# AVOID REDUNDANT CODE - "COMPONENTIZE" IT |
|
# USE CONSISTENT CASING (CAMELCASE/PASCALCASE) |
|
# STANDARDIZE ON CODE LAYOUT STANDARDS (ESPECIALLY IN A TEAM) |
|
# WRITE RICH OBJECTS TO THE PIPELINE WITH CUSTOM FORMATTING |
|
# USE RETURN KEY WORD INTENTIONALLY |
|
# DON'T ASSUME YOU KNOW HOW YOUR CODE WILL BE USED |
|
# |
|
|
|
#endregion |
|
|