Skip to content

Commit d603e1f

Browse files
author
James Brundage
committed
feat: Namespaced Objects ( Fixes #797 )
1 parent 47f0ba6 commit d603e1f

File tree

1 file changed

+144
-0
lines changed

1 file changed

+144
-0
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<#
2+
.SYNOPSIS
3+
Namespaced functions
4+
.DESCRIPTION
5+
Allows the declaration of a object or singleton in a namespace.
6+
7+
Namespaces are used to logically group functionality and imply standardized behavior.
8+
.EXAMPLE
9+
Invoke-PipeScript {
10+
My Object Precious { $IsARing = $true; $BindsThemAll = $true }
11+
My.Precious
12+
}
13+
#>
14+
[Reflection.AssemblyMetaData('Order', -10)]
15+
[ValidateScript({
16+
# This only applies to a command AST
17+
$cmdAst = $_ -as [Management.Automation.Language.CommandAst]
18+
if (-not $cmdAst) { return $false }
19+
# It must have at 4-5 elements.
20+
if ($cmdAst.CommandElements.Count -lt 4 -or $cmdAst.CommandElements.Count -gt 5) {
21+
return $false
22+
}
23+
# The second element must be a function or filter.
24+
if ($cmdAst.CommandElements[1].Value -notin 'object', 'singleton') {
25+
return $false
26+
}
27+
# The third element must be a bareword
28+
if ($cmdAst.CommandElements[1].StringConstantType -ne 'Bareword') {
29+
return $false
30+
}
31+
32+
# The last element must be a ScriptBlock or HashtableAst
33+
if (
34+
$cmdAst.CommandElements[-1] -isnot [Management.Automation.Language.ScriptBlockExpressionAst] -and
35+
$cmdAst.CommandElements[-1] -isnot [Management.Automation.Language.HashtableAst]
36+
) {
37+
return $false
38+
}
39+
40+
# Attempt to resolve the command
41+
if (-not $global:AllCommands) {
42+
$global:AllCommands = $executionContext.SessionState.InvokeCommand.GetCommands('*','Alias,Function,Cmdlet', $true)
43+
}
44+
$potentialCmdName = "$($cmdAst.CommandElements[0])"
45+
return -not ($global:AllCommands.Name -eq $potentialCmdName)
46+
})]
47+
param(
48+
# The CommandAST that will be transformed.
49+
[Parameter(Mandatory,ValueFromPipeline)]
50+
[Management.Automation.Language.CommandAst]
51+
$CommandAst
52+
)
53+
54+
process {
55+
# Namespaced functions are really simple:
56+
57+
# We use multiple assignment to pick out the parts of the function
58+
$namespace, $objectType, $functionName, $objectDefinition = $CommandAst.CommandElements
59+
60+
# Then, we determine the last punctuation.
61+
$namespaceSeparatorPattern = [Regex]::new('[\p{P}<>]{1,}','RightToLeft')
62+
$namespaceSeparator = $namespaceSeparatorPattern.Match($namespace).Value
63+
# If there was no punctuation, the namespace separator will be a '.'
64+
if (-not $namespaceSeparator) {$namespaceSeparator = '.'}
65+
# If the pattern was empty brackets `[]`, make the separator `[`.
66+
elseif ($namespaceSeparator -eq '[]') { $namespaceSeparator = '[' }
67+
# If the pattern was `<>`, make the separator `<`.
68+
elseif ($namespaceSeparator -eq '<>') { $namespaceSeparator = '<' }
69+
70+
# Replace any trailing separators from the namespace.
71+
$namespace = $namespace -replace "$namespaceSeparatorPattern$"
72+
73+
$blockComments = ''
74+
75+
$defineInstance =
76+
if ($objectDefinition -is [Management.Automation.Language.HashtableAst]) {
77+
"[PSCustomObject][Ordered]$($objectDefinition)"
78+
}
79+
elseif ($objectDefinition -is [Management.Automation.Language.ScriptBlockExpressionAst]) {
80+
$findBlockComments = [Regex]::New("
81+
\<\# # The opening tag
82+
(?<Block>
83+
(?:.|\s)+?(?=\z|\#>) # anything until the closing tag
84+
)
85+
\#\> # the closing tag
86+
", 'IgnoreCase,IgnorePatternWhitespace', '00:00:01')
87+
$foundBlockComments = $objectDefinition -match $findBlockComments
88+
if ($foundBlockComments -and $matches.Block) {
89+
$blockComments = $null,"<#",$($matches.Block),"#>",$null -join [Environment]::Newline
90+
}
91+
"New-Module -ArgumentList @(@(`$input) + @(`$args)) -AsCustomObject $objectDefinition"
92+
}
93+
94+
95+
# Join the parts back together to get the new function name.
96+
$NewFunctionName = $namespace,$namespaceSeparator,$functionName,$(
97+
# If the namespace separator ends with `[` or `<`, try to close it
98+
if ($namespaceSeparator -match '[\[\<]$') {
99+
if ($matches.0 -eq '[') { ']' }
100+
elseif ($matches.0 -eq '<') { '>' }
101+
}
102+
) -ne '' -join ''
103+
104+
$objectDefinition = "{
105+
$($objectDefinition -replace '^\{' -replace '\}$')
106+
Export-ModuleMember -Function * -Alias * -Cmdlet * -Variable *
107+
}"
108+
109+
$objectDefinition =
110+
if ($objectType -eq "singleton") {
111+
"{$(if ($blockComments) {$blockComments})
112+
$(
113+
@('$this = $myInvocation.MyCommand'
114+
'if (-not $this.Instance) {'
115+
"`$singletonInstance = $defineInstance"
116+
'$singletonInstance.pstypenames.clear()'
117+
"`$singletonInstance.pstypenames.add('$($NewFunctionName -replace "'","''")')"
118+
"`$singletonInstance.pstypenames.add('$($namespace -replace $namespaceSeparatorPattern -replace "'","''")')"
119+
'Add-Member -InputObject `$this -MemberType NoteProperty -Name Instance -Value $singletonInstance -Force'
120+
'}'
121+
'$this.Instance'
122+
) -join [Environment]::newLine
123+
)
124+
125+
}"
126+
} else {
127+
"{
128+
$(if ($blockComments) {$blockComments})
129+
`$Instance = $defineInstance
130+
`$Instance.pstypenames.clear()
131+
`$Instance.pstypenames.add('$($NewFunctionName -replace "'","''")')
132+
`$Instance.pstypenames.add('$($namespace -replace $namespaceSeparatorPattern -replace "'","''")')
133+
`$Instance
134+
}"
135+
}
136+
137+
138+
# Redefine the function
139+
$redefined = [ScriptBlock]::Create("
140+
function $NewFunctionName $objectDefinition
141+
")
142+
# Return the transpiled redefinition.
143+
$redefined | .>Pipescript
144+
}

0 commit comments

Comments
 (0)