Skip to content

Commit 8c32254

Browse files
authored
Add support for Set/New-Alias
Merge pull request #116 from PoshCode/feature/aliases
2 parents 774977e + c470b2d commit 8c32254

16 files changed

+433
-394
lines changed

GitVersion.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
mode: Mainline
2-
commit-message-incrementing: MergeMessageOnly
32
assembly-versioning-format: '{Major}.{Minor}.{Patch}.{env:BUILDCOUNT ?? 0}'
43
assembly-informational-format: '{NuGetVersionV2}+Build.{env:BUILDCOUNT ?? 0}.Date.{CommitDate}.Branch.{env:SAFEBRANCHNAME ?? unknown}.Sha.{Sha}'
5-
next-version: 1.0.0
4+
major-version-bump-message: '\+?semver:\s?(breaking|major)'
5+
minor-version-bump-message: '\+?semver:\s?(feature|minor)'
6+
patch-version-bump-message: '\+?semver:\s?(fix|patch)'
7+
no-bump-message: '\+?semver:\s?(none|skip)'
68
branches:
79
master:
810
increment: Patch
911
pull-request:
1012
tag: rc
1113
increment: Patch
12-
features:
14+
feature:
1315
increment: Patch
1416
regex: .*?/
1517
source-branches:
1618
- master
17-
- features
19+
- feature

Source/Classes/AliasVisitor.ps1

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using namespace System.Management.Automation.Language
2+
using namespace System.Collections.Generic
3+
4+
# This is used only to parse the parameters to New|Set|Remove-Alias
5+
# NOTE: this is _part of_ the implementation of AliasVisitor, but ...
6+
# PowerShell can't handle nested classes so I left it outside,
7+
# but I kept it here in this file.
8+
class AliasParameterVisitor : AstVisitor {
9+
[string]$Parameter = $null
10+
[string]$Command = $null
11+
[string]$Name = $null
12+
[string]$Value = $null
13+
[string]$Scope = $null
14+
15+
# Parameter Names
16+
[AstVisitAction] VisitCommandParameter([CommandParameterAst]$ast) {
17+
$this.Parameter = $ast.ParameterName
18+
return [AstVisitAction]::Continue
19+
}
20+
21+
# Parameter Values
22+
[AstVisitAction] VisitStringConstantExpression([StringConstantExpressionAst]$ast) {
23+
# The FIRST command element is always the command name
24+
if (!$this.Command) {
25+
$this.Command = $ast.Value
26+
return [AstVisitAction]::Continue
27+
} else {
28+
# Nobody should use minimal parameters like -N for -Name ...
29+
# But if they do, our parser works anyway!
30+
switch -Wildcard ($this.Parameter) {
31+
"S*" {
32+
$this.Scope = $ast.Value
33+
}
34+
"N*" {
35+
$this.Name = $ast.Value
36+
}
37+
"Va*" {
38+
$this.Value = $ast.Value
39+
}
40+
"F*" {
41+
if ($ast.Value) {
42+
# Force parameter was passed as named parameter with a positional parameter after it which is alias name
43+
$this.Name = $ast.Value
44+
}
45+
}
46+
default {
47+
if (!$this.Parameter) {
48+
# For bare arguments, the order is Name, Value:
49+
if (!$this.Name) {
50+
$this.Name = $ast.Value
51+
} else {
52+
$this.Value = $ast.Value
53+
}
54+
}
55+
}
56+
}
57+
58+
$this.Parameter = $null
59+
60+
# If we have enough information, stop the visit
61+
# For -Scope global or Remove-Alias, we don't want to export these
62+
if ($this.Name -and $this.Command -eq "Remove-Alias") {
63+
$this.Command = "Remove-Alias"
64+
return [AstVisitAction]::StopVisit
65+
} elseif ($this.Name -and $this.Scope -eq "Global") {
66+
return [AstVisitAction]::StopVisit
67+
}
68+
return [AstVisitAction]::Continue
69+
}
70+
}
71+
72+
[AliasParameterVisitor] Clear() {
73+
$this.Command = $null
74+
$this.Parameter = $null
75+
$this.Name = $null
76+
$this.Value = $null
77+
$this.Scope = $null
78+
return $this
79+
}
80+
}
81+
82+
# This visits everything at the top level of the script
83+
class AliasVisitor : AstVisitor {
84+
[HashSet[String]]$Aliases = @()
85+
[AliasParameterVisitor]$Parameters = @{}
86+
87+
# The [Alias(...)] attribute on functions matters, but we can't export aliases that are defined inside a function
88+
[AstVisitAction] VisitFunctionDefinition([FunctionDefinitionAst]$ast) {
89+
@($ast.Body.ParamBlock.Attributes.Where{
90+
$_.TypeName.Name -eq "Alias"
91+
}.PositionalArguments.Value).ForEach{
92+
if ($_) {
93+
$this.Aliases.Add($_)
94+
}
95+
}
96+
97+
return [AstVisitAction]::SkipChildren
98+
}
99+
100+
# Top-level commands matter, but only if they're alias commands
101+
[AstVisitAction] VisitCommand([CommandAst]$ast) {
102+
if ($ast.CommandElements[0].Value -imatch "(New|Set|Remove)-Alias") {
103+
$ast.Visit($this.Parameters.Clear())
104+
105+
# We COULD just remove it (even if we didn't add it) ...
106+
if ($this.Parameters.Command -ieq "Remove-Alias") {
107+
# But Write-Verbose for logging purposes
108+
if ($this.Aliases.Contains($this.Parameters.Name)) {
109+
Write-Verbose -Message "Alias '$($this.Parameters.Name)' is removed by line $($ast.Extent.StartLineNumber): $($ast.Extent.Text)"
110+
$this.Aliases.Remove($this.Parameters.Name)
111+
}
112+
# We don't need to export global aliases, because they broke out already
113+
} elseif ($this.Parameters.Name -and $this.Parameters.Scope -ine 'Global') {
114+
$this.Aliases.Add($this.Parameters.Name)
115+
}
116+
}
117+
return [AstVisitAction]::SkipChildren
118+
}
119+
}

Source/Private/GetBuildInfo.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ function GetBuildInfo {
8383
}
8484

8585
if ((-not $BuildInfo.SourcePath) -and $ParameterValues["SourcePath"] -notmatch '\.psd1') {
86-
Write-Debug " Searching: SourcePath"
86+
Write-Debug " Searching: SourcePath ($BuildManifestParent\**\*.psd1)"
8787
# Find a module manifest (or maybe several)
8888
$ModuleInfo = Get-ChildItem $BuildManifestParent -Recurse -Filter *.psd1 -ErrorAction SilentlyContinue |
8989
ImportModuleManifest -ErrorAction SilentlyContinue

Source/Private/GetCommandAlias.ps1

Lines changed: 1 addition & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,11 @@
1-
# This is used only to parse the parameters to New|Set|Remove-Alias
2-
class AliasParameterVisitor : System.Management.Automation.Language.AstVisitor {
3-
[string]$Parameter = $null
4-
[string]$Command = $null
5-
[string]$Name = $null
6-
[string]$Value = $null
7-
[string]$Scope = $null
8-
9-
# Parameter Names
10-
[System.Management.Automation.Language.AstVisitAction] VisitCommandParameter([System.Management.Automation.Language.CommandParameterAst]$ast) {
11-
$this.Parameter = $ast.ParameterName
12-
return [System.Management.Automation.Language.AstVisitAction]::Continue
13-
}
14-
15-
# Parameter Values
16-
[System.Management.Automation.Language.AstVisitAction] VisitStringConstantExpression([System.Management.Automation.Language.StringConstantExpressionAst]$ast) {
17-
# The FIRST command element is always the command name
18-
if (!$this.Command) {
19-
$this.Command = $ast.Value
20-
return [System.Management.Automation.Language.AstVisitAction]::Continue
21-
} else {
22-
switch ($this.Parameter) {
23-
"Scope" {
24-
$this.Scope = $ast.Value
25-
}
26-
"Name" {
27-
$this.Name = $ast.Value
28-
}
29-
"Value" {
30-
$this.Value = $ast.Value
31-
}
32-
"Force" {
33-
if ($ast.Value) {
34-
# Force parameter was passed as named parameter with a positional parameter after it which is alias name
35-
$this.Name = $ast.Value
36-
}
37-
}
38-
default {
39-
if (!$this.Parameter) {
40-
# For bare arguments, the order is Name, Value:
41-
if (!$this.Name) {
42-
$this.Name = $ast.Value
43-
} else {
44-
$this.Value = $ast.Value
45-
}
46-
}
47-
}
48-
}
49-
50-
$this.Parameter = $null
51-
52-
# If we have enough information, stop the visit
53-
# For -Scope global or Remove-Alias, we don't want to export these
54-
if ($this.Name -and $this.Command -eq "Remove-Alias") {
55-
$this.Command = "Remove-Alias"
56-
return [System.Management.Automation.Language.AstVisitAction]::StopVisit
57-
} elseif ($this.Name -and $this.Scope -eq "Global") {
58-
return [System.Management.Automation.Language.AstVisitAction]::StopVisit
59-
}
60-
return [System.Management.Automation.Language.AstVisitAction]::Continue
61-
}
62-
}
63-
64-
[AliasParameterVisitor] Clear() {
65-
$this.Command = $null
66-
$this.Parameter = $null
67-
$this.Name = $null
68-
$this.Value = $null
69-
$this.Scope = $null
70-
return $this
71-
}
72-
}
73-
74-
# This visits everything at the top level of the script
75-
class AliasVisitor : System.Management.Automation.Language.AstVisitor {
76-
[System.Collections.Hashtable]$Aliases = @{}
77-
[AliasParameterVisitor]$Parameters = @{}
78-
79-
# The [Alias(...)] attribute on functions matters, but we can't export aliases that are defined inside a function
80-
[System.Management.Automation.Language.AstVisitAction] VisitFunctionDefinition([System.Management.Automation.Language.FunctionDefinitionAst]$ast) {
81-
$this.Aliases[$ast.Name] = @($ast.Body.ParamBlock.Attributes.Where{ $_.TypeName.Name -eq "Alias" }.PositionalArguments.Value)
82-
return [System.Management.Automation.Language.AstVisitAction]::SkipChildren
83-
}
84-
85-
# Top-level commands matter, but only if they're alias commands
86-
[System.Management.Automation.Language.AstVisitAction] VisitCommand([System.Management.Automation.Language.CommandAst]$ast) {
87-
if ($ast.CommandElements[0].Value -imatch "(New|Set|Remove)-Alias") {
88-
$ast.Visit($this.Parameters.Clear())
89-
if ($this.Parameters.Command -ieq "Remove-Alias") {
90-
Write-Warning -Message "Found an alias '$($this.Parameters.Name)' that is removed using $($this.Parameters.Command), assuming the alias should not be exported."
91-
92-
$this.Aliases.Remove($this.Parameters.Name)
93-
} elseif ($this.Parameters.Scope -ine 'Global') {
94-
if ($this.Parameters.Name -notin $this.Aliases.Keys)
95-
{
96-
$this.Aliases[$this.Parameters.Name] = $this.Parameters.Name
97-
}
98-
}
99-
}
100-
return [System.Management.Automation.Language.AstVisitAction]::SkipChildren
101-
}
102-
}
1031

1042
function GetCommandAlias {
1053
<#
1064
.SYNOPSIS
1075
Parses one or more files for aliases and returns a list of alias names.
1086
#>
1097
[CmdletBinding()]
110-
[OutputType([System.Collections.Hashtable])]
8+
[OutputType([System.Collections.Generic.Hashset[string]])]
1119
param(
11210
# The AST to find aliases in
11311
[Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)]

Source/Public/Build-Module.ps1

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -213,20 +213,20 @@ function Build-Module {
213213
$ParseResult = ConvertToAst $RootModule
214214
$ParseResult | MoveUsingStatements -Encoding "$($ModuleInfo.Encoding)"
215215

216-
if (-not $ModuleInfo.IgnoreAlias) {
217-
$AliasesToExport = $ParseResult | GetCommandAlias
218-
}
219-
220216
# If there is a PublicFilter, update ExportedFunctions
221217
if ($ModuleInfo.PublicFilter) {
222218
# SilentlyContinue because there don't *HAVE* to be public functions
223-
if (($PublicFunctions = Get-ChildItem $ModuleInfo.PublicFilter -Recurse -ErrorAction SilentlyContinue | Where-Object BaseName -in $AllScripts.BaseName | Select-Object -ExpandProperty BaseName)) {
224-
Update-Metadata -Path $OutputManifest -PropertyName FunctionsToExport -Value ($PublicFunctions | Where-Object {$_ -notin $AliasesToExport.Values})
219+
if (($PublicFunctions = Get-ChildItem $ModuleInfo.PublicFilter -Recurse -ErrorAction SilentlyContinue |
220+
Where-Object BaseName -in $AllScripts.BaseName |
221+
Select-Object -ExpandProperty BaseName)) {
222+
223+
Update-Metadata -Path $OutputManifest -PropertyName FunctionsToExport -Value $PublicFunctions
225224
}
226225
}
227226

228-
if ($PublicFunctions -and -not $ModuleInfo.IgnoreAlias) {
229-
if (($AliasesToExport = $AliasesToExport[$PublicFunctions] | ForEach-Object { $_ } | Select-Object -Unique)) {
227+
# In order to support aliases to files, such as required by Invoke-Build, always export aliases
228+
if (-not $ModuleInfo.IgnoreAlias) {
229+
if (($AliasesToExport = $ParseResult | GetCommandAlias)) {
230230
Update-Metadata -Path $OutputManifest -PropertyName AliasesToExport -Value $AliasesToExport
231231
}
232232
}

Source/Public/ConvertFrom-SourceLineNumber.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function ConvertFrom-SourceLineNumber {
2929
$Command = [IO.Path]::GetFileNameWithoutExtension($SourceFile)
3030
$Module = (Get-Command $Command -ErrorAction SilentlyContinue).Source
3131
if (!$Module) {
32-
Write-Warning "Please specify -Module for breakpoint ${SourceFile}: $SourceLineNumber"
32+
Write-Warning "Please specify -Module for ${SourceFile}: $SourceLineNumber"
3333
return
3434
}
3535
}

Tests/Integration/Source1.Tests.ps1

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,21 +52,22 @@ Describe "Regression test for #55: I can pass SourceDirectories" -Tag Integratio
5252
}
5353

5454
Describe "Regression test for #55: I can pass SourceDirectories and PublicFilter" -Tag Integration, Regression {
55-
$Output = Build-Module $PSScriptRoot\Source1\build.psd1 -SourceDirectories "Private" -PublicFilter "P*\*" -Passthru
56-
$Module = [IO.Path]::ChangeExtension($Output.Path, "psm1")
55+
BeforeAll {
56+
$Output = Build-Module $PSScriptRoot\Source1\build.psd1 -SourceDirectories "Private" -PublicFilter "Pub*\*" -Passthru
57+
$Module = [IO.Path]::ChangeExtension($Output.Path, "psm1")
58+
$Metadata = Import-Metadata $Output.Path
59+
}
5760

5861
It "Should not put the module's DefaultCommandPrefix into the psm1 as code. Duh!" {
5962
$Module | Should -Not -FileContentMatch '^Source$'
6063
}
6164

62-
$Metadata = Import-Metadata $Output.Path
63-
6465
It "Should not have any FunctionsToExport if SourceDirectories don't match the PublicFilter" {
65-
$Metadata.FunctionsToExport | Should -Be @("Get-TestNotExportedAliases", "GetFinale", "GetPreview")
66+
$Metadata.FunctionsToExport | Should -BeNullOrEmpty
6667
}
6768

6869
It "Should update AliasesToExport in the manifest" {
69-
$Metadata.AliasesToExport | Should -Be @("GF", "GP")
70+
$Metadata.AliasesToExport | Should -Be @("Get-MyAlias")
7071
}
7172

7273
It "Should de-dupe and move using statements to the top of the file" {
@@ -111,6 +112,36 @@ Describe "Supports building without a build.psd1" -Tag Integration {
111112
$Build.Output = Build-Module @BuildParameters -Passthru
112113
}
113114

115+
It "Works even based on current path" {
116+
$BuildParameters = @{
117+
OutputDirectory = "TestDrive:\Result1"
118+
VersionedOutputDirectory = $true
119+
}
120+
Push-Location TestDrive:\Source1
121+
try {
122+
$Build.Output = Build-Module @BuildParameters -Passthru
123+
} finally {
124+
Pop-Location
125+
}
126+
}
127+
128+
# This test case for coverage of "If we found more than one module info"
129+
It "Ignores extra manifest files" {
130+
$BuildParameters = @{
131+
OutputDirectory = "TestDrive:\Result1"
132+
VersionedOutputDirectory = $true
133+
}
134+
Push-Location TestDrive:\Source1
135+
New-Item SubModule -ItemType Directory
136+
Copy-Item Source1.psd1 .\SubModule\SubModule.psd1
137+
138+
try {
139+
$Build.Output = Build-Module @BuildParameters -Passthru
140+
} finally {
141+
Pop-Location
142+
}
143+
}
144+
114145
It "Creates the same module as with a build.psd1" {
115146
$Build.Metadata = Import-Metadata $Build.Output.Path
116147
Get-Content $Build.Output.Path | Should -Be $ManifestContent
@@ -126,7 +157,6 @@ Describe "Supports building without a build.psd1" -Tag Integration {
126157
}
127158
}
128159

129-
130160
Describe "Defaults to VersionedOutputDirectory" -Tag Integration {
131161
Copy-Item $PSScriptRoot\Source1 TestDrive:\Source1 -Recurse
132162
# This is the old build, with a build.psd1
@@ -176,8 +206,6 @@ Describe "Defaults to VersionedOutputDirectory" -Tag Integration {
176206
}
177207
}
178208

179-
180-
181209
Describe "Supports building discovering the module without a build.psd1" -Tag Integration {
182210
Copy-Item $PSScriptRoot\Source1 TestDrive:\source -Recurse
183211

Tests/Integration/Source1/Private/GetFinale.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ using module ModuleBuilder
22

33
function GetFinale {
44
[CmdletBinding()]
5-
[Alias("gf")]
5+
# [Alias("gf")]
66
param()
77
}

0 commit comments

Comments
 (0)