Skip to content

Instantly share code, notes, and snippets.

@ngbrown
Created October 10, 2025 16:11
Show Gist options
  • Select an option

  • Save ngbrown/5e005c6fde1d497f71e5efdc17fbf459 to your computer and use it in GitHub Desktop.

Select an option

Save ngbrown/5e005c6fde1d497f71e5efdc17fbf459 to your computer and use it in GitHub Desktop.
Counts the lines of code in the specified directory using cloc. Uses PowerShell and Docker.
<#
.SYNOPSIS
Counts the lines of code in the specified directory using cloc.
.DESCRIPTION
This script utilizes the cloc (Count Lines of Code) tool to analyze and count the lines of code in a given directory. It requires Docker Desktop to be running, as cloc is executed within a Docker container.
To improve performance, the script first compresses the files into a zip archive. This allows the Docker container to analyze the code quickly, avoiding the slow performance that would result from mounting all files as a volume.
Requirements
- Docker Desktop must be installed and running on the host machine.
.PARAMETER AllFiles
If specified, all files tracked by Git will be included, ignoring .gitattributes exclusions.
.INPUTS
None. You can't pipe objects to Count-Lines.ps1.
.EXAMPLE
PS> .\Count-Lines.ps1
.EXAMPLE
PS> .\Count-Lines.ps1 -AllFiles
#>
param (
[switch]$AllFiles
)
function Convert-GitIgnoreToRegex {
<#
Converts a .gitignore pattern into a regular expression.
#>
param (
[Parameter(Mandatory=$true)]
[string]$GitIgnorePattern
)
# Preserve character classes by temporarily replacing them
$tempPattern = $GitIgnorePattern -replace '(\[[^\]]+\])', '___CHARCLASS$1___'
# Escape special regex characters
$RegexPattern = [regex]::Escape($tempPattern)
# Restore character classes (remove escaping and temp marker)
$RegexPattern = $RegexPattern -replace '___CHARCLASS\\\[([^\]]+?)\\?\]___', '[$1]'
# Replace specific gitignore wildcards with regex equivalents
$RegexPattern = $RegexPattern -replace '\\\*\\\*$', '.*' # Replace trailing ** with .* for recursive matching
$RegexPattern = $RegexPattern -replace '\\\*\\\*\/', '(.*/)?' # Replace **/ with (.*?/)? for matching in any directory
$RegexPattern = $RegexPattern -replace '\\\*', '[^/]*' # Replace * with [^/]* to match any non-slash character zero or more times
$regexPattern = $regexPattern -replace '\\\?', '[^/]' # Convert ? to match a single character except a slash
$RegexPattern = $RegexPattern -replace '^\/', '^' # Anchor to the beginning of the string if the pattern starts with /
$RegexPattern = $RegexPattern -replace '\/$', '/.*' # Anchor to the end of the string if the pattern ends with /
$RegexPattern = $RegexPattern + '$'
# Add case-insensitive flag
$RegexPattern = '(?i)' + $RegexPattern
#Write-Host "[DEBUG] Converted pattern: '$GitIgnorePattern' => '$RegexPattern'"
return $RegexPattern
}
function Write-ZipArchive {
<#
Creates a zip archive from the specified files.
#>
param(
[Parameter(Mandatory=$true)]
[string[]]$Files,
[Parameter(Mandatory=$true)]
[string]$ZipPath,
[string]$RootDir = (Get-Location).Path
)
Add-Type -AssemblyName System.IO.Compression.FileSystem
if (Test-Path $ZipPath) { Remove-Item $ZipPath }
$zip = [System.IO.Compression.ZipFile]::Open($ZipPath, [System.IO.Compression.ZipArchiveMode]::Create)
foreach ($file in $Files) {
$fullPath = Join-Path $RootDir $file
if (Test-Path $fullPath) {
[void][System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $fullPath, $file, [System.IO.Compression.CompressionLevel]::Fastest)
}
}
$zip.Dispose()
}
$zipPath = Join-Path (Get-Location) '~$files.zip'
if (Test-Path $zipPath) { Remove-Item $zipPath }
if ($AllFiles) {
git archive HEAD -o $zipPath
} else {
# Get the path to the .gitattributes file
$gitAttributesPath = Join-Path (Get-Location) ".gitattributes"
# Initialize a list to store excluded file patterns
$excludedPatterns = @()
# Read the .gitattributes file if it exists
if (Test-Path $gitAttributesPath) {
Get-Content $gitAttributesPath | ForEach-Object {
if ($_ -match "linguist-generated=true" -or $_ -match "linguist-vendored=true") {
# Extract the file pattern from the line
$pattern = $_.Split(" ")[0]
if ($pattern) {
$excludedPatterns += $pattern
}
}
}
}
# Get all files tracked by Git
$allGitFiles = git ls-files
# Convert excluded patterns to regex patterns before filtering
$excludedRegexPatterns = @()
foreach ($pattern in $excludedPatterns) {
$excludedRegexPatterns += Convert-GitIgnoreToRegex $pattern
}
# Filter out excluded files
$filteredFiles = $allGitFiles | Where-Object {
$filePath = $_
$isExcluded = $false
foreach ($regexPattern in $excludedRegexPatterns) {
if ($filePath -match $regexPattern) {
$isExcluded = $true
break
}
}
-not $isExcluded
}
Write-Host "Filtered files count: $($filteredFiles.Count)"
Write-ZipArchive -Files $filteredFiles -ZipPath $zipPath
}
# Run cloc in a Docker container
docker run --rm -v "${zipPath}:/tmp/files.zip" aldanial/cloc ./files.zip -v
# Clean up
Remove-Item $zipPath
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment