PSFunctionExplorer.psm1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
using namespace System.Management.Automation.Language

class FUFunction {
    $Name
    [System.Collections.ArrayList]$Commands = @()
    $Path
    hidden $RawFunctionAST

    FUFunction ([System.Management.Automation.Language.FunctionDefinitionAST]$Raw,$Path,[Bool]$TitleCase) {
        $this.RawFunctionAST = $Raw
        $this.Path = $path
        $this.GetCommands($TitleCase)

        If ( $TitleCase ) {
            $this.name = [FUUtility]::ToTitleCase($this.RawFunctionAST.name)    
        } Else {
            $this.name = $this.RawFunctionAST.name
        }
    }

    FUFunction ([System.Management.Automation.Language.FunctionDefinitionAST]$Raw,$ExclusionList,$Path,[Bool]$TitleCase) {
        $this.RawFunctionAST = $Raw
        $this.Path = $path
        $this.GetCommands($ExclusionList,$TitleCase)

        If ( $TitleCase ) {
            $this.name = [FUUtility]::ToTitleCase($this.RawFunctionAST.name)    
        } Else {
            $this.name = $this.RawFunctionAST.name
        }
    }

    hidden GetCommands ([Bool]$TitleCase) {

        $t = $this.RawFunctionAST.findall({$args[0] -is [System.Management.Automation.Language.CommandAst]},$true)
        If ( $t.Count -gt 0 ) {
            ## si elle existe deja, on ajotue juste à ces commands
            ($t.GetCommandName() | Select-Object -Unique).Foreach({
                
                If ( $TitleCase ) {
                    $Command = [FUUtility]::ToTitleCase($_)
                } Else {
                    $Command = $_
                }
                
                $this.Commands.Add($Command)
            })
        }
    }

    ## Overload
    hidden GetCommands ($ExclusionList,[Bool]$TitleCase) {

        $t = $this.RawFunctionAST.findall({$args[0] -is [System.Management.Automation.Language.CommandAst]},$true)
        If ( $t.Count -gt 0 ) {
            ($t.GetCommandName() | Select-Object -Unique).Foreach({
                $Command = [FUUtility]::ToTitleCase($_)
                If ( $ExclusionList -notcontains $Command) {
                    If ( $TitleCase ) {
                        $Command = [FUUtility]::ToTitleCase($_)
                    } Else {
                        $Command = $_
                    }
                    $this.Commands.Add($Command)
                }
            })
        }
    }
}

Class FUUtility {

    ## Static Method to TitleCase
    Static [String]ToTitleCase ([string]$String){
        return (Get-Culture).TextInfo.ToTitleCase($String.ToLower())
    }

    ## Static Method to return Function in AST Form, exclude classes
    [Object[]] static GetRawASTFunction($Path) {

        $RawFunctions   = $null
        $ParsedFile     = [System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$null, [ref]$Null)
        $RawAstDocument = $ParsedFile.FindAll({$args[0] -is [System.Management.Automation.Language.Ast]}, $true)

        If ( $RawASTDocument.Count -gt 0 ) {
            ## source: https://stackoverflow.com/questions/45929043/get-all-functions-in-a-powershell-script/45929412
            $RawFunctions = $RawASTDocument.FindAll({$args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $($args[0].parent) -isnot [System.Management.Automation.Language.FunctionMemberAst] })
        }

        return $RawFunctions
    }

    ## GetFunction, return [FuFunction]
    [FUFunction] Static GetFunction($RawASTFunction,$path,$TitleCase){
        return [FUFunction]::New($RawASTFunction,$path,$TitleCase)
    }

    ## GetFunctions Overload, with ExclustionList, return [FuFunction]
    [FUFunction] Static GetFunction($RawASTFunction,$Exculde,$path,$TitleCase){
        return [FUFunction]::New($RawASTFunction,$Exculde,$path,$TitleCase)
    }

    ## SaveTofile in current path
    [System.IO.FileSystemInfo] static SaveToFile ([FuFunction]$Function) {
        return New-Item -Name $([FUUtility]::FileName($Function.name)) -value $Function.RawFunctionAST.Extent.Text -ItemType File
    }

    ## SaveTofile Overload, with Specific path for export
    [System.IO.FileSystemInfo] static SaveToFile ([FuFunction]$Function,$Path) {
        return New-Item -Path $Path -Name $([FUUtility]::FileName($Function.name)) -value $Function.RawFunctionAST.Extent.Text -ItemType File
    }

    ## Construct filename for export
    [string] hidden static FileName ($a) {
        return "$a.ps1"
    }

}

Function Expand-FUFile {
    <#
    .SYNOPSIS
        Export a FUFunction to a ps1 file. It's like a reverse build process.
    .DESCRIPTION
        Export a FUFunction to a ps1 file. It's like a reverse build process.
    .EXAMPLE
        PS C:\> Find-FUFunction -Path .\PSFunctionExplorer.psm1 | Expand-FUFile
            Répertoire : C:\
 
 
        Mode LastWriteTime Length Name
        ---- ------------- ------ ----
        -a---- 30/04/2019 23:24 658 Expand-FUFile.ps1
        -a---- 30/04/2019 23:24 3322 Find-Fufunction.ps1
        -a---- 30/04/2019 23:24 2925 Write-Fufunctiongraph.ps1
 
        Find all functions definitions inside PSFunctionExplorer.psm1 and save each function inside it's own ps1 file.
    .EXAMPLE
        PS C:\> Find-FUFunction -Path .\PSFunctionExplorer.psm1 | Expand-FUFile -Path C:\Temp
            Répertoire : C:\Temp
 
 
        Mode LastWriteTime Length Name
        ---- ------------- ------ ----
        -a---- 30/04/2019 23:24 658 Expand-FUFile.ps1
        -a---- 30/04/2019 23:24 3322 Find-Fufunction.ps1
        -a---- 30/04/2019 23:24 2925 Write-Fufunctiongraph.ps1
 
        Find all functions definitions inside PSFunctionExplorer.psm1 and save each function inside it's own ps1 file, inside the C:\Temp directory.
    .INPUTS
        [FuFunction]
    .OUTPUTS
        [System.IO.FileSystemInfo]
    .NOTES
    #>


    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        [Object[]]$FUFunction,

        [String]$Path
    )

    begin {
        If ( $PSBoundParameters['Path']) {
            $item = Get-Item (Resolve-Path -Path $path).Path
        }
    }

    process {
        ForEach( $Function in $FUFunction) {

            If ( $PSBoundParameters['Path']) {
                [FUUtility]::SaveToFile($Function,$Item.FullName)
            } Else {
                [FUUtility]::SaveToFile($Function)
            }

        }
    }

    end {
    }
}

Function Find-FUFunction {
    <#
    .SYNOPSIS
        Find All Functions declaration inside a ps1/psm1 file and their inner commands.
    .DESCRIPTION
        Find All Functions declaration inside a ps1/psm1 file.
        Return an object describing a powershell function. Output a custom type: FUFunction.
    .EXAMPLE
        PS C:\> Find-FUFunction .\PSFunctionExplorer.psm1
 
        Name Commands Path
        ---- -------- ----
        Find-Fufunction {Get-Command, Get-Alias, Select-Object, Get-Item...} C:\PSFunctionExplorer.psm1
        Write-Fufunctiongraph {Get-Item, Resolve-Path, Find-Fufunction, Graph...} C:\PSFunctionExplorer.psm1
 
        return all function present in the PSFunctionExplorer.psm1 and every commands present in it.
    .EXAMPLE
        PS C:\> Find-FUFunction .\PSFunctionExplorer.psm1 -ExcludePSCmdlets
        Name Commands Path
        ---- -------- ----
        Find-Fufunction {} C:\Users\Lx\GitPerso\PSFunctionUtils\PSFunctionExplorer\PSFunctionExplorer.psm1
        Write-Fufunctiongraph {Find-Fufunction, Graph, Node, Edge...} C:\Users\Lx\GitPerso\PSFunctionUtils\PSFunctionExplorer\PSFunctionExplorer.psm1
 
        Return all function present in the PSFunctionExplorer.psm1 and every commands present in it, but exclude default ps cmdlets.
    .INPUTS
        Path. Accepts pipeline inputs
    .OUTPUTS
        A FUFunction custom object
    .NOTES
        General notes
    #>

    [CmdletBinding()]
    param (
        [Alias("FullName")]
        [Parameter(ValueFromPipeline=$True,Position=1,ValueFromPipelineByPropertyName=$True)]
        [string[]]$Path,
        [Switch]$ExcludePSCmdlets,
        [Switch]$NoTitleCase
    )
    
    begin {

        If ( ! $PSBoundParameters['NoTitleCase'] ) {
            $NoTitleCase = $True
        } else {
            $NoTitleCase = $False
        } 

        If ( $PSBoundParameters['ExcludePSCmdlets'] ) {
            $ToExclude = (Get-Command -Module "Microsoft.PowerShell.Archive","Microsoft.PowerShell.Utility","Microsoft.PowerShell.ODataUtils","Microsoft.PowerShell.Operation.Validation","Microsoft.PowerShell.Management","Microsoft.PowerShell.Core","Microsoft.PowerShell.LocalAccounts","Microsoft.WSMan.Management","Microsoft.PowerShell.Security","Microsoft.PowerShell.Diagnostics","Microsoft.PowerShell.Host").Name
            $ToExclude += (Get-Alias | Select-Object -Property Name).name
        }
    }
    
    process {
        ForEach( $p in $Path) {
            $item = get-item (resolve-path -path $p).path
            If ( $item -is [system.io.FileInfo] -and $item.Extension -in @('.ps1','.psm1') ) {
                Write-Verbose ("[FUFunction]Analyzing {0} ..." -f $item.FullName)
                $t = [FUUtility]::GetRawASTFunction($item.FullName)
                Foreach ( $RawASTFunction in $t ) {
                    If ( $PSBoundParameters['ExcludePSCmdlets'] ) {
                        [FUUtility]::GetFunction($RawASTFunction,$ToExclude,$item.FullName,$NoTitleCase)
                    } Else {
                        [FUUtility]::GetFunction($RawASTFunction,$item.FullName,$NoTitleCase)
                    }
                }
            }
        }
    }
    
    end {
    }
}

Function Write-FUGraph {
    <#
    .SYNOPSIS
        Generate dependecy graph for a function or a set of functions found in a ps1/psm1 file.
    .DESCRIPTION
        Generate dependecy graph for a function or a set of functions found in a ps1/psm1 file.
    .EXAMPLE
        PS C:\> $x = Find-FUFunction .\PSFunctionExplorer.psm1
        PS C:\> Write-FUGraph -InputObject $x -ExportPath c:\temp\fufuncion.png -outputformat png -ShowGraph
 
        Répertoire : C:\temp
 
        Mode LastWriteTime Length Name
        ---- ------------- ------ ----
        -a---- 08/09/2019 15:08 71598 fufunction.png
 
        Will Find all function(s) declarations in the psfunctionexplorer.psm1 file, and create a graph name fufunction.png. Then display it.
    .EXAMPLE
        PS C:\> Find-FUFunction .\PSFunctionExplorer.psm1 | Write-FUGraph -ExportPath c:\temp\fufuncion.png -outputformat png -ShowGraph
 
        Will Find all function(s) declarations in the psfunctionexplorer.psm1 file, and create a graph name fufunction.png. Then display it.
    .INPUTS
        FullName Path. Accepts pipeline inputs.
    .OUTPUTS
        Outputs Graph, thanks to psgraph module.
    .NOTES
        First Draft. For the moment the function only output graphviz datas. Soon you ll be able to generate a nice graph as a png, pdf ...
    #>

    [CmdletBinding()]
    param (
        [Alias("FullName")]
        [Parameter(ValueFromPipeline=$True)]
        [FUFunction[]]$InputObject,
        [System.IO.FileInfo]$ExportPath,
        [Parameter(ParameterSetName='Graph')]
        [ValidateSet('pdf',"png")]
        [String]$OutPutFormat,
        [Parameter(ParameterSetName='Graph')]
        [ValidateSet('dot','circo','hierarchical')]
        [String]$LayoutEngine,
        [Parameter(ParameterSetName='Graph')]
        [Switch]$ShowGraph,
        [Parameter(ParameterSetName='Dot')]
        [Switch]$AsDot
    )
    
    begin {
        $Results = @()
    }
    
    process {

        Foreach ( $Function in $InputObject ) {
            $Results += $Function
        }
        
    }
    
    end {

        $ExportAttrib = @{
            DestinationPath = If ( $null -eq $PSBoundParameters['ExportPath']) {$pwd.Path+'\'+[system.io.path]::GetRandomFileName().split('.')[0]+'.png'} Else {$PSBoundParameters['ExportPath']}
            OutPutFormat    = If ( $null -eq $PSBoundParameters['OutPutFormat']) {'png'} Else { $PSBoundParameters['OutPutFormat'] }
            LayoutEngine    = If ( $null -eq $PSBoundParameters['LayoutEngine']) {'dot'} Else { $PSBoundParameters['LayoutEngine'] }
            ShowGraph    = If ( $null -eq $PSBoundParameters['ShowGraph']) {$False} Else { $True }
        }

        $graph = graph depencies @{rankdir='LR'}{
            Foreach ( $t in $Results ) {
                If ( $t.commands.count -gt 0 ) {
                        node -Name $t.name -Attributes @{Color='red'}
                } Else {
                    node -Name $t.name -Attributes @{Color='green'}
                }
            
                If ( $null -ne $t.commands) {
                    Foreach($cmdlet in $t.commands ) {
                        edge -from $t.name -to $cmdlet
                    }
                }
            }
        }

        Switch ( $PSCmdlet.ParameterSetName ) {
            
            "Graph" {
                $graph | export-PSGraph @ExportAttrib
            }

            "Dot" {
                If ( $PSBoundParameters['ExportPath'] ) {
                    Out-File -InputObject $graph -FilePath $PSBoundParameters['ExportPath']
                } Else {
                    $graph
                }
            }
        }
        
    }
}