I'm often frustrated by the lack of ability to obtain simple size metrics about code projects I'm working on, like total lines of code, or character count. To fix this, I wrote the following PowerShell utility:
Function GetProjectMetrics
{
[CmdletBinding()]
Param(
[Parameter(mandatory=$True)]
[string]$projectPath,
[Parameter(mandatory=$True)]
[string]$fileFilter,
[switch]$ignorews
)
Set-Location $projectPath
Switch($ignorews)
{
$True
{
Get-ChildItem -recurse -include $fileFilter `
| Get-Content `
| Measure-Object -Character -Word -Line -IgnoreWhiteSpace
Break
}
$False
{
Get-ChildItem -recurse -include $fileFilter `
| Get-Content `
| Measure-Object -Character -Word -Line
Break
}
Default
{
Write-Host "Error executing 'GetProjectMetrics'."
Break
}
}
}
New-Alias -Name gpm -Value GetProjectMetrics
New-Alias -Name getpm -Value GetProjectMetrics
The above code is placed within my Microsoft.PowerShell_profile.ps1
file (my PowerShell profile), which gives me the ability to use it straight from the PowerShell command line.
Here's some example usage:
PS C:\Users\Ethan> gpm ".\Documents\MyPythonProject" "*.py" -ignorews PS C:\Users\Ethan> getpm ".\Documents\MyPythonProject" "*.py" PS C:\Users\Ethan> GetProjectMetrics ".\OneDrive\CS Projects\CS Tests" "*.cs" -ignorews
What can I improve?
1 Answer 1
My suggestions:
- apply PowerShell scripting best practices; for particulars, see Concept: A list of some Best Practices for Windows PowerShell by powershell-guru;
- get more knowledge from the utility (see
Count
andProperty
output properties; see also-Detailed
input parameter); - allow
-LiteralPath
to take range operator characters in project path literally; - use
Set-Alias -Name ... -Value ... -Force
rather thanNew-Alias
; - (not implemented yet) consider involving
-Encoding
parameter forGet-Content
cmdlet.
The script (rewritten almost completely):
<#
SYNOPSIS: Calculates simple size metrics about code projects.
Combination of `Get-ChildItem`, `Get-Content` and `Measure-Object`
Parameter Meaning (see native cmdlet)
--------- ---------------------------
-Path obvious (Convert-Path, Get-ChildItem, Get-Item, Push-Location)
-LiteralPath obvious (Convert-Path, Get-ChildItem, Get-Item, Push-Location)
-Filter obvious (Get-ChildItem)
-IgnoreWhiteSpace obvious (Measure-Object)
-Detailed returns size metrics about individual files severally
-PushLocation side effect: `Push-Location`
RETURNS: Custom combination of GenericMeasureInfo and TextMeasureInfo:
Name Type Meaning
---- ---- -------
Count [int] number of files of given pattern in given (literal)path
Lines [int] number of lines
Words [int] number of words
Characters [int] number of characters
Property [string] tested path
#>
Set-StrictMode -Version latest
Function Measure-Project
{
[CmdletBinding(DefaultParameterSetName='Items')]
Param(
[Alias('projectPath')]
[Parameter(Mandatory=$false,
Position=0,
ParameterSetName='Items')]
[string]$Path='.\',
[Parameter(Mandatory=$false,
Position=0,
ParameterSetName='LiteralItems')]
[string]$LiteralPath='.\',
[Alias('fileFilter')]
[Parameter(Mandatory=$false,
Position=1)]
[string]$Filter='*.ps1',
[Alias('IgnoreWS')]
[switch]$IgnoreWhiteSpace,
[Alias('PushD')]
[switch]$PushLocation,
[switch]$Detailed
)
Function out ($Cnt, $Prm, $Obj) {
$auxCounter = $Obj | Measure-Object -Character -Word -Line @auxParMO
$auxCounter | Add-Member -NotePropertyName Count -NotePropertyValue $Cnt
$auxCounter.Property = $Prm
return $auxCounter
}
if ( $PSCmdlet.ParameterSetName -eq 'LiteralItems') {
$auxParTP = @{ LiteralPath = $LiteralPath }
} else {
$auxParTP = @{ Path = $Path }
}
if ( Test-Path @auxParTP ) {
$auxPath = Convert-Path @auxParTP
if ( (Get-Item @auxParTP) -is [System.IO.DirectoryInfo]) {
$auxFiles = Get-ChildItem @auxParTP -Filter $Filter -Recurse -File
$auxParMO = @{ IgnoreWhiteSpace = $IgnoreWhiteSpace.IsPresent }
# $PSBoundParameters.ContainsKey('IgnoreWhiteSpace')
if ($Detailed.IsPresent -and $auxFiles) {
$auxFiles | ForEach-Object {
out -Cnt 1 -Prm $_.FullName -Obj ($_ | Get-Content)
}
} else {
$auxPar = Join-Path -Path $auxPath -ChildPath $Filter
if ( $auxFiles ) {
if ( $auxFiles -is [array] ) {
$auxCnt = $auxFiles.Count
} else {
$auxCnt = 1
}
out -Cnt $auxCnt -Prm $auxPar -Obj ($auxFiles | Get-Content)
} else {
Write-Warning "No '$Filter' inside '$Path'"
out -Cnt 0 -Prm $auxPar -Obj ''
}
}
if ( $PushLocation.IsPresent ) {Push-Location @auxParTP}
} else {
$auxMsg = "'$($auxParTP.Values.GetEnumerator())' is not a directory"
Write-Error -Message $auxMsg -Category InvalidArgument
}
}
else
{
$auxMsg = "Cannot find path '$($auxParTP.Values.GetEnumerator())'"
Write-Error -Message $auxMsg -Category ObjectNotFound
}
}
Sample output demonstrates necessity of -Encoding
parameter as both files contain nearly the same text so the utility should show the same count of characters. Of course, I'd expect the same encoding of source files in a particular project so sorry for the following simplified example:
PS D:\PSh> Measure-Project -LiteralPath 'D:\bat\[source]\XX\' -filter * -Detailed | ft Count Lines Words Characters Property ----- ----- ----- ---------- -------- 1 1 5 118 D:\bat\[source]\XX\[sourcefile].log 1 1 5 59 D:\bat\[source]\XX\[sourcefile].txt PS D:\PSh> type -LiteralPath 'D:\bat\[source]\XX\[sourcefile].txt' Toto je obsah souboru `D:\bat\[source]\XX\[sourcefile].txt` PS D:\PSh> type -LiteralPath 'D:\bat\[source]\XX\[sourcefile].log' -Encoding Unicode Toto je obsah souboru `D:\bat\[source]\XX\[sourcefile].log`