show-syntax.psm1
|
#Requires -Version 5.1 $ESC = [char]27 $script:LanguageHighlightRules = @{ ps1 = @( @{ Pattern = '\b(function|param|begin|process|end|return|if|elseif|else|switch|foreach|for|while|do|until|break|continue|try|catch|finally|throw|exit|filter|workflow|configuration|class|enum|using|namespace|module|requires|define|from|in|inlinescript|parallel|sequence|dynamicparam)\b'; Color = "$ESC[95m" } @{ Pattern = '\b(Write-Host|Write-Output|Write-Error|Write-Warning|Write-Verbose|Write-Debug|Write-Information|Get-Content|Set-Content|Add-Content|Remove-Item|Get-Item|Set-Item|Copy-Item|Move-Item|Get-ChildItem|Test-Path|Import-Module|Export-ModuleMember|New-Object|Select-Object|Where-Object|ForEach-Object|Sort-Object|Group-Object|Measure-Object|Compare-Object|Invoke-Command|Start-Process|Stop-Process|Get-Process|Get-Service|Start-Service|Stop-Service)\b'; Color = "$ESC[96m" } @{ Pattern = '(\$[\w:]+)'; Color = "$ESC[33m" } @{ Pattern = '(#.*)$'; Color = "$ESC[90m" } @{ Pattern = '("[^"]*"|''[^'']*'')'; Color = "$ESC[32m" } @{ Pattern = '\b(\d+(\.\d+)?)\b'; Color = "$ESC[36m" } ) psm1 = $null psd1 = $null json = @( @{ Pattern = '("(?:[^"\\]|\\.)*")(\s*:)'; Color = "$ESC[96m" } @{ Pattern = ':\s*("(?:[^"\\]|\\.)*")'; Color = "$ESC[32m" } @{ Pattern = '\b(true|false|null)\b'; Color = "$ESC[33m" } @{ Pattern = '\b(\d+(\.\d+)?([eE][+-]?\d+)?)\b'; Color = "$ESC[36m" } @{ Pattern = '([{}\[\]])'; Color = "$ESC[93m" } ) xml = @( @{ Pattern = '(<!--.*?-->)'; Color = "$ESC[90m" } @{ Pattern = '(<[/?!]?[\w:.-]+)'; Color = "$ESC[96m" } @{ Pattern = '([\w:.-]+=)'; Color = "$ESC[33m" } @{ Pattern = '("(?:[^"\\]|\\.)*"|''[^'']*'')'; Color = "$ESC[32m" } @{ Pattern = '(/?>)'; Color = "$ESC[96m" } ) md = @( @{ Pattern = '^(#{1,6}\s+.*)$'; Color = "$ESC[93m" } @{ Pattern = '(\*\*[^*]+\*\*|__[^_]+__)'; Color = "$ESC[1m" } @{ Pattern = '(\*[^*]+\*|_[^_]+_)'; Color = "$ESC[3m" } @{ Pattern = '(`[^`]+`)'; Color = "$ESC[32m" } @{ Pattern = '^(```.*|```)$'; Color = "$ESC[35m" } @{ Pattern = '(\[.+?\]\(.+?\))'; Color = "$ESC[36m" } @{ Pattern = '^(\s*[-*+]\s+.*)$'; Color = "$ESC[37m" } ) py = @( @{ Pattern = '\b(def|class|import|from|return|if|elif|else|for|while|in|not|and|or|is|None|True|False|try|except|finally|raise|with|as|pass|break|continue|lambda|yield|global|nonlocal|del|assert|async|await)\b'; Color = "$ESC[95m" } @{ Pattern = '(#.*)$'; Color = "$ESC[90m" } @{ Pattern = '("(?:[^"\\]|\\.)*"|''[^'']*'')'; Color = "$ESC[32m" } @{ Pattern = '\b(\d+(\.\d+)?)\b'; Color = "$ESC[36m" } @{ Pattern = '\b(print|len|range|type|str|int|float|list|dict|set|tuple|bool|open|input|enumerate|zip|map|filter|sorted|sum|max|min)\b'; Color = "$ESC[96m" } ) js = @( @{ Pattern = '\b(var|let|const|function|return|if|else|switch|case|default|for|while|do|break|continue|new|delete|typeof|instanceof|in|of|class|extends|import|export|from|try|catch|finally|throw|async|await|yield|this|super|null|undefined|true|false)\b'; Color = "$ESC[95m" } @{ Pattern = '(//[^\n]*)'; Color = "$ESC[90m" } @{ Pattern = '("(?:[^"\\]|\\.)*"|''[^'']*'')'; Color = "$ESC[32m" } @{ Pattern = '\b(\d+(\.\d+)?)\b'; Color = "$ESC[36m" } @{ Pattern = '\b(console|Math|JSON|Object|Array|String|Number|Boolean|Promise|setTimeout|setInterval|require|module|exports)\b'; Color = "$ESC[96m" } ) ts = $null yaml = @( @{ Pattern = '^(---|\.\.\.)$'; Color = "$ESC[93m" } @{ Pattern = '^([\w.-]+)\s*:'; Color = "$ESC[96m" } @{ Pattern = ':\s*(.+)$'; Color = "$ESC[32m" } @{ Pattern = '(#.*)$'; Color = "$ESC[90m" } @{ Pattern = '\b(true|false|null|yes|no)\b'; Color = "$ESC[33m" } ) sh = @( @{ Pattern = '\b(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|exit|in|break|continue|local|export|readonly|source|unset|shift|set)\b'; Color = "$ESC[95m" } @{ Pattern = '(#.*)$'; Color = "$ESC[90m" } @{ Pattern = '("(?:[^"\\]|\\.)*"|''[^'']*'')'; Color = "$ESC[32m" } @{ Pattern = '(\$[\w{}\(\)]+)'; Color = "$ESC[33m" } @{ Pattern = '\b(\d+)\b'; Color = "$ESC[36m" } ) css = @( @{ Pattern = '(/\*[\s\S]*?\*/)'; Color = "$ESC[90m" } @{ Pattern = '([.#]?[\w-]+)\s*\{'; Color = "$ESC[96m" } @{ Pattern = '([\w-]+)\s*:'; Color = "$ESC[33m" } @{ Pattern = ':\s*([^;{]+)'; Color = "$ESC[32m" } ) html = @( @{ Pattern = '(<!--[\s\S]*?-->)'; Color = "$ESC[90m" } @{ Pattern = '(<[/?!]?[\w.-]+)'; Color = "$ESC[96m" } @{ Pattern = '([\w-]+=)'; Color = "$ESC[33m" } @{ Pattern = '("(?:[^"\\]|\\.)*"|''[^'']*'')'; Color = "$ESC[32m" } @{ Pattern = '(/?>)'; Color = "$ESC[96m" } ) csv = @( @{ Pattern = '^([^,\n]+)'; Color = "$ESC[96m" } @{ Pattern = ',"([^"]*)"'; Color = "$ESC[32m" } ) txt = @() log = @( @{ Pattern = '\b(ERROR|FATAL|CRITICAL)\b'; Color = "$ESC[91m" } @{ Pattern = '\b(WARN|WARNING)\b'; Color = "$ESC[93m" } @{ Pattern = '\b(INFO|INFORMATION)\b'; Color = "$ESC[96m" } @{ Pattern = '\b(DEBUG|TRACE|VERBOSE)\b'; Color = "$ESC[90m" } ) } $script:ExtensionToLanguage = @{ '.ps1' = 'ps1' '.psm1' = 'ps1' '.psd1' = 'ps1' '.json' = 'json' '.xml' = 'xml' '.md' = 'md' '.markdown' = 'md' '.py' = 'py' '.js' = 'js' '.mjs' = 'js' '.cjs' = 'js' '.ts' = 'js' '.tsx' = 'js' '.jsx' = 'js' '.yaml' = 'yaml' '.yml' = 'yaml' '.sh' = 'sh' '.bash' = 'sh' '.zsh' = 'sh' '.css' = 'css' '.scss' = 'css' '.less' = 'css' '.html' = 'html' '.htm' = 'html' '.csv' = 'csv' '.txt' = 'txt' '.log' = 'log' } function Get-LanguageFromExtension { [CmdletBinding()] param ( [string]$Extension ) $ext = $Extension.ToLowerInvariant() if ($script:ExtensionToLanguage.ContainsKey($ext)) { $lang = $script:ExtensionToLanguage[$ext] if ($null -ne $lang -and $script:LanguageHighlightRules.ContainsKey($lang)) { return $lang } } return $null } function Get-HighlightRules { [CmdletBinding()] param ( [string]$Language ) if ([string]::IsNullOrEmpty($Language)) { return $null } $lang = $Language.ToLowerInvariant() # Aliases if ($lang -eq 'powershell') { $lang = 'ps1' } if ($lang -eq 'python') { $lang = 'py' } if ($lang -eq 'javascript') { $lang = 'js' } if ($lang -eq 'typescript') { $lang = 'js' } if ($lang -eq 'bash') { $lang = 'sh' } if ($lang -eq 'markdown') { $lang = 'md' } # psm1/psd1 fall back to ps1 rules if ($lang -in 'psm1', 'psd1') { $lang = 'ps1' } if ($lang -eq 'ts') { $lang = 'js' } if ($script:LanguageHighlightRules.ContainsKey($lang)) { $rules = $script:LanguageHighlightRules[$lang] if ($null -eq $rules) { # Null value means it's an alias resolved above, re-lookup return $null } return $rules } return $null } function Apply-SyntaxHighlighting { [CmdletBinding()] param ( [string]$Line, [hashtable[]]$Rules ) if ($null -eq $Rules -or $Rules.Count -eq 0) { return $Line } $reset = "$ESC[0m" $result = $Line foreach ($rule in $Rules) { $replacement = $rule.Color + '$0' + $reset $result = [regex]::Replace($result, $rule.Pattern, $replacement, [System.Text.RegularExpressions.RegexOptions]::Multiline) } return $result } function Show-Syntax { <# .SYNOPSIS Displays file content with ANSI syntax highlighting, inspired by the Rust bat tool. .DESCRIPTION Show-Syntax reads one or more files and outputs their content to the console with basic ANSI escape-code-based syntax highlighting derived from the file extension. It supports optional line numbers and manual language override. .PARAMETER Path One or more paths to the files to display. Accepts pipeline input. .PARAMETER ShowLineNumbers When specified, prepends each output line with its line number. .PARAMETER Language Overrides automatic language detection based on file extension. Supported values: ps1, json, xml, md, py, js, ts, yaml, sh, css, html, csv, txt, log, and their common aliases (e.g. powershell, python, javascript, bash, markdown). .EXAMPLE Show-Syntax -Path .\script.ps1 Displays script.ps1 with PowerShell syntax highlighting. .EXAMPLE Show-Syntax -Path .\data.json -ShowLineNumbers Displays data.json with JSON highlighting and line numbers. .EXAMPLE Get-ChildItem *.ps1 | Show-Syntax -ShowLineNumbers Pipes multiple .ps1 files into Show-Syntax with line numbers. .EXAMPLE Show-Syntax -Path .\notes.txt -Language md Forces Markdown highlighting on a .txt file. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('FullName', 'FilePath')] [string[]]$Path, [switch]$ShowLineNumbers, [string]$Language ) process { foreach ($filePath in $Path) { $resolved = $null try { $resolved = Resolve-Path -LiteralPath $filePath -ErrorAction Stop } catch { Write-Error "Cannot find file: '$filePath'. $_" continue } if (-not (Test-Path -LiteralPath $resolved -PathType Leaf)) { Write-Error "Path is not a file or does not exist: '$filePath'" continue } # Determine highlighting language $effectiveLang = $null if (-not [string]::IsNullOrWhiteSpace($Language)) { $effectiveLang = $Language } else { $ext = [System.IO.Path]::GetExtension($resolved.Path) $effectiveLang = Get-LanguageFromExtension -Extension $ext } $rules = Get-HighlightRules -Language $effectiveLang # Print a header similar to bat $header = "$ESC[2m$ESC[36m$('─' * 4) $($resolved.Path) $('─' * 4)$ESC[0m" Write-Host $header $lineNumber = 1 $reader = [System.IO.StreamReader]::new($resolved.Path) try { while ($null -ne ($rawLine = $reader.ReadLine())) { $displayLine = Apply-SyntaxHighlighting -Line $rawLine -Rules $rules if ($ShowLineNumbers) { $linePrefix = "$ESC[2m{0,4}$ESC[0m " -f $lineNumber Write-Host "$linePrefix$displayLine" } else { Write-Host $displayLine } $lineNumber++ } } finally { $reader.Dispose() } Write-Host "$ESC[2m$ESC[36m$('─' * 30)$ESC[0m" } } } Export-ModuleMember -Function Show-Syntax |