Skip to content

Instantly share code, notes, and snippets.

@thedavecarroll
Last active June 24, 2020 14:35
Show Gist options
  • Save thedavecarroll/bdb519bf474739851ca1e7d2d3faeee6 to your computer and use it in GitHub Desktop.
Save thedavecarroll/bdb519bf474739851ca1e7d2d3faeee6 to your computer and use it in GitHub Desktop.
IronScripter Challenge - June 9, 2020 - Building a PowerShell Command Inventory
function Measure-PSCodeLine {
[CmdLetBinding()]
param(
[Parameter(Mandatory,ValueFromPipeline)]
[ValidateScript({Test-Path -Path $_})]
[string]$Path
)
$TotalLines = $CodeLines = 0
$Files = Get-ChildItem -Path $Path -Recurse -Include *.ps1,*.psm1 -File
$Files | Foreach-Object {
$Content = Get-Content -Path $_.FullName
$CodeComments = $Content | Where-Object { $_ -match '\S' }
$TotalLines += $Content.Count
$CodeLines += $CodeComments.Count
}
[PSCustomObject]@{
ParentPath = Split-Path -Path $Path
TotalFiles = $Files.Count
TotalCodeLines = $CodeLines
TotalLines = $TotalLines
Date = Get-Date
}
}
function Get-PSCodeStructure {
[CmdletBinding()]
param(
[Parameter(Mandatory,ValueFromPipeline)]
[ValidateScript({Test-Path -Path $_})]
[string]$Path,
[switch]$Recurse
)
begin {
$Timer = [System.Diagnostics.Stopwatch]::StartNew()
$PSCodeExtensions = '.ps1','.psm1'
$PSPatterns = [ordered]@{
PSVerbNoun = '\s+(?<PSVerbNoun>\w+\-\w+)\s+'
DotNetObjects = '(?<DotNet>\[\S+\.{1}\S+\])'
FOperator = '\s+(?<FOperator>\-f)\s'
FunctionDefinition = '(?<FunctionDefinition>function\s+\w+\-\w+)'
ClassDefinition = '(?<ClassDefinition>class\s+\w+)\s'
VariableDeclaration = '((?<VariableDeclaration>\$\w+)\s+\=)'
CmdletBindingAttribute = '(\[(?<CmdletBindingAttribute>CmdletBinding)(\s*\())'
ParameterAttribute = '(\[(?<ParameterAttribute>Parameter)(\s*\())'
ParamDeclaration = '((?<ParamDeclaration>param)(\s*\())'
DynamicParamDeclaration = '((?<DynamicParamDeclaration>dynamicparam)(\s*\())'
TryCatchFinally = '((?<TryCatchFinally>try)(\s*\{))'
Trap = '((?<TrapStatement>trap)(\s*\{))'
EnumDefinition = '((?<EnumDefinition>enum)(\s*\{))'
LoopStatementForEachDo = '((?<LoopStatement>for|foreach|do)(\s*\{|\s*\())'
LoopStatementWhile = '((?<!\})\s)(?<LoopStatement>while)(\s*\()'
SwitchStatement = '((?<SwitchStatement>switch)(\s*\(|\s(\S+\s)+)\{)'
}
$RegExPattern = $PSPatterns.Values -join '|'
$RegexOptions = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase, [System.Text.RegularExpressions.RegexOptions]::CultureInvariant
$Regex = [regex]::new($RegExPattern,$RegexOptions)
$Aliases = Get-Alias
$Verbs = (Get-Verb).Verb
function GetPowerShellCode {
param($Path)
$FileInfo = Get-ChildItem -Path $Path
$FileContent = Get-Content -Path $Path
for ($LineCount = 0; $LineCount -lt $FileContent.Count; $LineCount++) {
$Regex.Matches($FileContent[$LineCount]).Groups.Where{$_.Success -and $_.Name -notmatch '\d+' } |
Select-Object @{l='FileName';e={$FileInfo.Name}},@{l='FileFullName';e={$FileInfo.FullName}},
@{l='Line';e={$LineCount+1}},Index,@{l='Type';e={$_.Name}},AliasName,@{l='Command';e={$_.Value}} |
Where-Object { if ($_.Type -ne 'PSVerbNoun') { $_ } else {
if ($Verbs -contains $_.Command.Split('-')[0]) {
$_
}
}}
try {
$Words = $FileContent[$LineCount].Split(' ')
}
catch {
continue
}
for ($WordCount = 0; $WordCount -lt $Words.Count; $WordCount++) {
$AliasMatch = [PSCustomObject]@{
FileName = $FileInfo.Name
FileFullName = $FileInfo.FullName
Line = $LineCount+1
Index = $FileContent[$LineCount].IndexOf($Words[$WordCount])
Type = 'Alias'
AliasName = $null
Command = $null
}
if ($Words[$WordCount] -eq 'foreach' -and $Words[$WordCount+1] -notmatch '\(') {
$AliasMatch.AliasName = $Words[$WordCount]
$AliasMatch.Command = 'Foreach-Object'
$AliasMatch
continue
}
if ($Words[$WordCount] -eq 'select') {
$AliasMatch.AliasName = $Words[$WordCount]
$AliasMatch.Command = 'Select-Object'
$AliasMatch
continue
}
try {
$CheckAlias = '^{0} ->' -f $Words[$WordCount]
if ($Aliases.Name -match $CheckAlias -and $Words[$WordCount] -match '^[A-Za-z]+') {
$MatchedAlias = $Aliases.Where{$_.Name -match $CheckAlias }
$AliasMatch.AliasName = $Words[$WordCount]
$AliasMatch.Command = $MatchedAlias.Definition
$AliasMatch
continue
}
} catch {}
}
}
}
}
process {
$PathType = (Get-Item -Path $Path).GetType().Name
if ($PathType -eq 'DirectoryInfo') {
if ($PSBoundParameters.ContainsKey('Recurse')) {
$Files = Get-ChildItem -Path $Path -Recurse -File | Where-Object { $_.Extension -in $PSCodeExtensions }
} else {
$Files = Get-ChildItem -Path $Path -File | Where-Object { $_.Extension -in $PSCodeExtensions }
}
} elseif ($PathType -eq 'FileInfo') {
$Files = Get-ChildItem -Path $Path
}
foreach ($File in $Files) {
GetPowerShellCode -Path $File.FullName
}
}
end {
$Timer.Stop()
$Time = 'Elapsed Time : {0}h {1}m {2}.{3}s' -f $Timer.Elapsed.Hours,$Timer.Elapsed.Minutes,$Timer.Elapsed.Seconds,$Timer.Elapsed.Milliseconds
$FileCount = 'FileCount : {0}' -f $Files.Count
$FileCount,$Time -join [System.Environment]::NewLine | Write-Information -InformationAction Continue
}
}
#Requires -Version 7
function Measure-PSCommand {
[CmdletBinding()]
param(
[Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ByFile')]
[ValidateScript({Test-Path -Path $_})]
[string]$Path,
[Parameter(ParameterSetName='ByFile')]
[switch]$Recurse,
[Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ByScriptblock')]
[scriptblock]$Scriptblock,
[switch]$Raw,
[int]$First,
[int]$Last
)
begin {
$Timer = [System.Diagnostics.Stopwatch]::StartNew()
$PSCommandsFromTokens = [hashtable]::new()
function Get-ElapsedTimeText {
param([timespan]$TimeSpan)
'{0}h {1}m {2}.{3}s' -f $TimeSpan.Hours,$TimeSpan.Minutes,$TimeSpan.Seconds,$TimeSpan.Milliseconds
}
function Get-CommandsFromAstTokens {
[CmdLetBinding()]
param(
[System.Management.Automation.Language.Token[]]$Tokens
)
$CommandNames = $Tokens.Where{ $_.TokenFlags -eq 'CommandName'}
foreach ($AstToken in $CommandNames) {
$Value = $AstToken.Kind -eq 'Generic' ? $AstToken.Value : $AstToken.Text
$File = $AstToken.Extent.File ? $AstToken.Extent.File : 'Scriptblock'
if (!$PSCommandsFromTokens.ContainsKey($Value)) {
try {
if ($Value -match '\w+\-\w+') {
# Generic term cmdlet, from PowerShell team twitter thread
$PSCommandsFromTokens.Add($Value,@{CommandName = $Value;CommandType = 'Cmdlet'})
} elseif ($Value -in '?','??') {
if ($Value -eq '?') {
$PSCommandsFromTokens.Add($Value,@{CommandName = '? -> Where-Object';CommandType = 'Alias'})
} else {
# ?? is null coalescing operator, first available in PowerShell 7
break
}
} else {
$Command = Get-Command $Value -ErrorAction SilentlyContinue
if ($Command) {
# We can filter Application out of the output
if ($Command.CommandType -in 'Function','Cmdlet','Filter','Application') {
$PSCommandsFromTokens.Add($Value,@{CommandName = $Command.Name;CommandType = $Command.CommandType})
# I want the DisplayName for the alias so we know what is in the code and what it expands to
} elseif ($Command.CommandType -eq 'Alias') {
$PSCommandsFromTokens.Add($Value,@{CommandName = $Command.DisplayName;CommandType = $Command.CommandType})
}
} else {
$PSCommandsFromTokens.Add($Value,@{CommandName = $Value;CommandType = 'Unknown'})
}
}
}
catch {
$PSCommandsFromTokens.Add($Value,@{CommandName = $Value;CommandType = 'UnknownCatch'})
}
}
[PsCustomObject]@{
CommandName = $PSCommandsFromTokens[$Value].CommandName
CommandType = $PSCommandsFromTokens[$Value].CommandType
LineNumber = $AstToken.Extent.StartLineNumber
ColumnNumber = $AstToken.Extent.StartColumnNumber
File = $File
}
}
}
}
process {
switch ($PSCmdlet.ParameterSetName) {
'ByFile' {
$PathType = (Get-Item -Path $Path).GetType().Name
if ($PathType -eq 'DirectoryInfo') {
if ($PSBoundParameters.ContainsKey('Recurse')) {
$Files = Get-ChildItem -Path $Path -Recurse -File | Where-Object { $_.Extension -in '.ps1','.psm1' }
} else {
$Files = Get-ChildItem -Path $Path -File | Where-Object { $_.Extension -in '.ps1','.psm1' }
}
} elseif ($PathType -eq 'FileInfo') {
$Files = Get-ChildItem -Path $Path
}
$AllCommands = foreach ($File in $Files) {
$Tokens = $ParseErrors = $null
$null = [System.Management.Automation.Language.Parser]::ParseFile($File.FullName,[ref]$Tokens,[ref]$ParseErrors)
Get-CommandsFromAstTokens -Tokens $Tokens
}
break
}
'ByScriptblock' {
$Tokens = $ParseErrors = $null
$null = [System.Management.Automation.Language.Parser]::ParseInput($Scriptblock.ToString(),[ref]$Tokens,[ref]$ParseErrors)
$AllCommands = Get-CommandsFromAstTokens -Tokens $Tokens
}
}
$Select = @{}
if ($PSBoundParameters.ContainsKey('First')) {
$Select.Add('First',$First)
}
if ($PSBoundParameters.ContainsKey('Last')) {
$Select.Add('Last',$Last)
}
if ($PSBoundParameters.ContainsKey('Raw')) {
$AllCommands
} else {
$AllCommands | Group-Object CommandName |
Sort-Object Count -Descending |
Select-Object @Select -Property Count,
@{l='CommandName';e={$_.Name}},
@{l='CommandType';e={($_.Group.CommandType | Sort-Object -Unique)}},
@{l='FileCount';e={($_.Group.File | Sort-Object -Unique).Count}},
@{l='Files';e={($_.Group.File | Sort-Object -Unique)}}
}
}
end {
$Timer.Stop()
$Time = 'Elapsed Time : {0}' -f (Get-ElapsedTimeText -Timespan $Timer.Elapsed)
$FileCount = 'FileCount : {0}' -f $Files.Count
$FilesPerSecond = 'Files/Second : {0:N2}' -f ($Files.Count/$Timer.Elapsed.TotalSeconds)
$FileCount,$Time,$FilesPerSecond,'' -join [System.Environment]::NewLine | Write-Information -InformationAction Continue
}
}
function Measure-FileLine {
[CmdLetBinding()]
param(
[Parameter(Mandatory,ValueFromPipeline)]
[ValidateScript({Test-Path -Path $_})]
[string]$Path,
[string[]]$Extensions = @('ps1','psm1')
)
$Timer = [System.Diagnostics.Stopwatch]::StartNew()
$Files = Get-ChildItem -Path $Path -Recurse -Include $Extensions.ForEach{ '*.{0}' -f $_} -File
$Files | Get-Content | Measure-Object -Line -IgnoreWhiteSpace |
Select-Object @{l='ComputerName';e={[System.Environment]::MachineName}},
@{l='Path';e={$Path}},
@{l='TotalFiles';e={$Files.count}},
@{l='TotalLines';e={$_.Lines}},
@{l='Date';e={Get-Date}},
@{l='ElapsedTime';e={$Timer.Elapsed}}
$Timer.Stop()
}
@thedavecarroll
Copy link
Author

Limitations of Get-PSCodeStructure

PSVerbNoun only returns a command if the verb matches approved verbs derived from Get-Verb.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment