Skip to content

Commit 640cbac

Browse files
committed
refactor: restructure files and scripts, improve tests to cover all platforms, ps editions
1 parent 138074d commit 640cbac

File tree

6 files changed

+279
-201
lines changed

6 files changed

+279
-201
lines changed

.github/workflows/test.yml

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,34 @@ on:
1515

1616
jobs:
1717
test:
18-
name: Test ${{ matrix.style }} on ${{ matrix.os }}
18+
name: Test ${{ matrix.style }} on ${{ matrix.os }} (${{ matrix.shell }})
1919
runs-on: ${{ matrix.os }}
20+
defaults:
21+
run:
22+
shell: ${{ matrix.shell }}
2023
strategy:
2124
fail-fast: false
2225
matrix:
2326
style: [mustache, envsubst, make]
2427
os: [ubuntu-latest, windows-latest, macos-latest]
28+
shell: [pwsh]
29+
include:
30+
# Add PowerShell 5.1 tests on Windows for each style
31+
- os: windows-latest
32+
style: mustache
33+
shell: powershell
34+
- os: windows-latest
35+
style: envsubst
36+
shell: powershell
37+
- os: windows-latest
38+
style: make
39+
shell: powershell
2540
steps:
2641
- name: Checkout repository for ${{ matrix.style }} test on ${{ matrix.os }}
2742
uses: actions/checkout@main
2843

2944
- name: Setup ${{ matrix.style }} test on ${{ matrix.os }}
3045
id: test-setup
31-
shell: pwsh
3246
run: |
3347
$expected = (Join-Path $pwd test stubs expected replaced.tpl)
3448
$subject = (Join-Path $pwd test subject-${{ matrix.style }}.tpl)
@@ -74,7 +88,6 @@ jobs:
7488

7589
- name: Assert ${{ matrix.style }} replacement test on ${{ matrix.os }}
7690
id: assert-replacement
77-
shell: pwsh
7891
run: |
7992
$expected = "${{ steps.test-setup.outputs.expected-path }}"
8093
$subject = "${{ steps.test-setup.outputs.subject-path }}"
@@ -94,7 +107,6 @@ jobs:
94107

95108
- name: Assert ${{ matrix.style }} untouched test on ${{ matrix.os }}
96109
id: assert-untouched
97-
shell: pwsh
98110
run: |
99111
$expected = "${{ steps.test-setup.outputs.untouched-hash }}"
100112
$actual = (Get-FileHash -Path "${{ steps.test-setup.outputs.untouched-path }}" -Algorithm SHA256).Hash
@@ -106,13 +118,5 @@ jobs:
106118
echo "✓ Token untouched test passed"
107119
}
108120
109-
pester:
110-
runs-on: ubuntu-latest
111-
name: Pester tests
112-
steps:
113-
- name: Checkout repository
114-
uses: actions/checkout@main
115-
116121
- name: Run Pester Tests
117-
shell: pwsh
118-
run: ./test/ReplaceTokens.Tests.ps1
122+
run: Invoke-Pester -Path ./test/ReplaceTokens.Tests.ps1 -Output Detailed

Expand-TemplateFile.ps1

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
function Expand-TemplateFile
2+
{
3+
<#
4+
.SYNOPSIS
5+
Replaces tokens in template files with environment variable values.
6+
7+
.DESCRIPTION
8+
Expands template files by replacing tokens with values from environment variables.
9+
Supports multiple token styles: mustache ({{VAR}}), envsubst (${VAR}), and make ($(VAR)).
10+
11+
.PARAMETER Path
12+
Specify the path(s) to process. Can be files or directories.
13+
14+
.PARAMETER Style
15+
Specify the token style to use. Valid values: mustache, handlebars, envsubst, make.
16+
Default: mustache
17+
18+
.PARAMETER Filter
19+
Specify a filter for the files to process (e.g., *.txt, *.config).
20+
21+
.PARAMETER Recurse
22+
Recurse into subdirectories when processing paths.
23+
24+
.PARAMETER Depth
25+
Specify the depth of recursion. Only valid when Recurse is specified.
26+
27+
.PARAMETER FollowSymlinks
28+
Follow symbolic links when traversing directories.
29+
30+
.PARAMETER Encoding
31+
Specify the file encoding. Default: utf8
32+
33+
.PARAMETER NoNewline
34+
Do not add a newline at the end of the file.
35+
36+
.PARAMETER Exclude
37+
Specify files or directories to exclude from processing.
38+
39+
.PARAMETER DryRun
40+
Run in dry-run mode (do not modify files). Shows what would be changed.
41+
42+
.EXAMPLE
43+
Expand-TemplateFile -Path ./config.template -Style mustache
44+
45+
.EXAMPLE
46+
Expand-TemplateFile -Path ./templates -Recurse -Filter *.tpl -Style envsubst
47+
48+
.OUTPUTS
49+
System.Collections.Generic.HashSet[string]
50+
Returns a collection of file paths that were modified.
51+
#>
52+
[CmdletBinding()]
53+
[OutputType([System.Collections.Generic.HashSet[string]])]
54+
param (
55+
[Parameter(Mandatory = $true, HelpMessage = 'Specify the path(s) to process')]
56+
[ValidateNotNullOrEmpty()]
57+
[string[]]
58+
$Path,
59+
60+
[Parameter(HelpMessage = 'Specify the token style to use')]
61+
[ValidateSet('mustache', 'handlebars', 'envsubst', 'make', ErrorMessage = 'Unknown token style', IgnoreCase = $true)]
62+
[string]
63+
$Style = 'mustache',
64+
65+
[Parameter(HelpMessage = 'Specify a filter for the files to process')]
66+
[string]
67+
$Filter,
68+
69+
[Parameter(HelpMessage = 'Recurse into subdirectories')]
70+
[switch]
71+
$Recurse,
72+
73+
[Parameter(HelpMessage = 'Specify the depth of recursion')]
74+
[int]
75+
$Depth,
76+
77+
[Parameter(HelpMessage = 'Follow symbolic links')]
78+
[switch]
79+
$FollowSymlinks,
80+
81+
[Parameter(HelpMessage = 'Specify the file encoding')]
82+
[ValidateSet('utf8', 'utf-8', 'utf8NoBOM', 'utf8BOM', 'ascii', 'ansi', 'bigendianunicode', 'bigendianutf32', 'oem', 'unicode', 'utf32', ErrorMessage = 'Unknown encoding', IgnoreCase = $true)]
83+
[string]
84+
$Encoding = 'utf8',
85+
86+
[Parameter(HelpMessage = 'Do not add a newline at the end of the file')]
87+
[switch]
88+
$NoNewline,
89+
90+
[Parameter(HelpMessage = 'Specify files or directories to exclude')]
91+
[string[]]
92+
$Exclude,
93+
94+
[Parameter(HelpMessage = 'Run in dry-run mode (do not modify files)')]
95+
[switch]
96+
$DryRun
97+
)
98+
99+
begin
100+
{
101+
# Initialize tracking variables
102+
$script:filesReplaced = New-Object System.Collections.Generic.HashSet[string]
103+
$script:tokensReplaced = 0
104+
$script:tokensSkipped = 0
105+
106+
# Define token patterns with validation for environment variable names
107+
$mustachePattern = '\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}' # handlebars/mustache pattern, e.g. {{VARIABLE}}
108+
$envsubstPattern = '\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}' # envsubst template pattern, e.g. ${VARIABLE}
109+
$makePattern = '\$\(([a-zA-Z_][a-zA-Z0-9_]*)\)' # make pattern, e.g. $(VARIABLE)
110+
111+
# Determine the token pattern based on the style
112+
$tokenPattern = switch ($Style.ToLower())
113+
{
114+
'mustache' { $mustachePattern }
115+
'handlebars' { $mustachePattern }
116+
'envsubst' { $envsubstPattern }
117+
'make' { $makePattern }
118+
default { throw "Unknown token style: $Style" }
119+
}
120+
121+
# Normalize utf8 (no bom) encoding
122+
$fileEncoding = switch ($Encoding.ToLower())
123+
{
124+
'utf8' { 'utf8NoBOM' }
125+
'utf-8' { 'utf8NoBOM' }
126+
'utf8nobom' { 'utf8NoBOM' }
127+
default { $Encoding }
128+
}
129+
130+
# Function to replace tokens in a file
131+
function ReplaceTokens([string] $File, [string] $Pattern, [string] $FileEncoding, [bool] $NoNewline)
132+
{
133+
try
134+
{
135+
$content = Get-Content -Path $File -Raw -Encoding $FileEncoding -ErrorAction Stop
136+
$originalContent = $content
137+
$tokensInFile = 0
138+
$skippedInFile = 0
139+
140+
# Replace tokens using a regex evaluator
141+
$content = [Regex]::Replace($content, $Pattern, {
142+
param ($match)
143+
$varName = $match.Groups[1].Value
144+
145+
if (-not (Test-Path -LiteralPath "Env:$varName"))
146+
{
147+
Write-Warning "[$File] Environment variable '$varName' not found - token will not be replaced"
148+
$script:tokensSkipped++
149+
$skippedInFile++
150+
return $match.Value
151+
}
152+
153+
$replacement = (Get-Item -LiteralPath "Env:$varName" -ErrorAction Continue).Value
154+
if ([string]::IsNullOrWhiteSpace($replacement))
155+
{
156+
Write-Warning "[$File] Environment variable '$varName' exists but has empty value - token will not be replaced"
157+
$script:tokensSkipped++
158+
$skippedInFile++
159+
return $match.Value
160+
}
161+
162+
$script:tokensReplaced++
163+
$tokensInFile++
164+
165+
return $replacement
166+
})
167+
168+
if ($content -ne $originalContent)
169+
{
170+
if (-not $DryRun)
171+
{
172+
Set-Content -Path $File -Value $content -Encoding $FileEncoding -NoNewline:$NoNewline -Force -ErrorAction Stop
173+
}
174+
175+
$script:filesReplaced.Add($File) | Out-Null
176+
177+
Write-Verbose "[$File] Replaced $tokensInFile token(s) (skipped $skippedInFile)"
178+
}
179+
}
180+
catch
181+
{
182+
Write-Error "Failed to process file ${File}: $_"
183+
}
184+
}
185+
}
186+
187+
process
188+
{
189+
# Build parameters for Get-ChildItem
190+
$params = @{
191+
Path = $Path
192+
File = $true
193+
ErrorAction = 'Continue'
194+
}
195+
196+
if (-not [string]::IsNullOrWhiteSpace($Filter)) { $params.Add('Filter', $Filter) }
197+
if ($Recurse) { $params.Add('Recurse', $true) }
198+
if ($Depth -gt 0) { $params.Add('Depth', $Depth) }
199+
if ($FollowSymlinks) { $params.Add('FollowSymlink', $true) }
200+
if ($Exclude) { $params.Add('Exclude', $Exclude) }
201+
202+
# Get files to process
203+
$files = Get-ChildItem @params | Where-Object { -not $_.PSIsContainer }
204+
205+
# Process each file
206+
foreach ($file in $files)
207+
{
208+
ReplaceTokens -File $file.FullName -Pattern $tokenPattern -FileEncoding $fileEncoding -NoNewline $NoNewline
209+
}
210+
}
211+
212+
end
213+
{
214+
# Output results
215+
if ($DryRun)
216+
{
217+
Write-Information "DRY RUN: Would replace $($script:tokensReplaced) token(s) in $($script:filesReplaced.Count) file(s)" -InformationAction Continue
218+
}
219+
else
220+
{
221+
Write-Verbose "Replaced $($script:tokensReplaced) token(s) in $($script:filesReplaced.Count) file(s)"
222+
}
223+
224+
Write-Output $script:filesReplaced
225+
}
226+
}

PSScriptAnalyzerSettings.psd1

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,27 @@
66
# Example usage:
77
# Invoke-ScriptAnalyzer -Settings PSScriptAnalyzerSettings.psd1 -Path . -Recurse
88
@{
9+
# Exclude specific rules globally
10+
ExcludeRules = @(
11+
'PSAvoidUsingWriteHost'
12+
)
13+
914
Rules = @{
1015
PSUseCompatibleSyntax = @{
16+
1117
# This turns the rule on (setting it to false will turn it off)
1218
Enable = $true
1319

14-
# Simply list the targeted versions of PowerShell here
20+
# The targeted versions of PowerShell
1521
TargetVersions = @(
16-
'5.1',
17-
'6.2',
18-
'7.0'
22+
'5.1', # Windows PowerShell (legacy)
23+
'6.1', # PowerShell Core (first stable)
24+
'6.2', # PowerShell Core LTS
25+
'7.0', # PowerShell 7 initial release
26+
'7.1', # First PowerShell 7 LTS
27+
'7.2', # PowerShell 7 LTS
28+
'7.4', # PowerShell 7 LTS (current)
29+
'7.5' # Latest stable
1930
)
2031
}
2132
}

0 commit comments

Comments
 (0)