Powershell 5 has a nice declarative "using module" statement that can be provided at the top of a file to declare the dependencies of the script. Presumably one should be able to use this programmatically to determine what the dependencies of a given powershell script or module are. But I can't find anything on how to consume that - does only powershell use that internally? Is there no developer-API to read the requirements-list of a .ps1 file?
2 Answers
To complement your own effective solution with some background information:
Preface:
The following finds
usingstatements in a given script file (*.ps1), which not only comprisesusing modulestatements, but alsousing assemblyandusing namespacestatements;using namespacestatements latter do not constitute a dependency per se, as their only purpose is to allow to refer to types by simple name only.usingstatements are not the only way to import modules and load assemblies (e.g, the former can also be imported withImport-Module, and the latter withAdd-Type). Additionally, there are potentially implicit module dependencies that rely on module auto-loading.
In short: Static analysis via using statements isn't guaranteed to find all dependencies.
Ultimately, it is PowerShell's language parser,
[System.Management.Automation.Language.Parser]that provides the AST (Abstract Syntax Tree) that your solution relies on.It has a static
::ParseFile()method that directly accepts script file paths.- However, as with any .NET method call, a full path must be passed, given that .NET's working directory usually differs from PowerShell's.
The
.UsingStatementproperty of the[System.Management.Automation.Language.ScriptBlockAst]instance returned by::ParseFile()(and as contained in a[scriptblock]'s.Astproperty, as shown in your answer) contains[System.Management.Automation.Language.UsingStatementAst]instances, if any, describing theusingstatements.Indeed, their
.Nameproperty is filled in forusing modulestatements with simple module names and paths as well as for the assembly names and paths used inusing assemblystatements..Nameisn't a string, but an instance of[System.Management.Automation.Language.StringConstantExpressionAst]. While such an instance by definition has only verbatim content - variables cannot be used inusingstatements - it may contain incidental quoting or escaping, becauseusing module Foomay also be expressed asusing module 'Foo'orusing module "Foo".Removing the incidental quoting can be as simple as
.Name.ToString.Trim("'`""), though, at least hypothetically, this isn't fully robust, because it would fail with something likeusing module 'Foo''Bar'. Even an unquoted form that uses`-escaping could fail, e.g.using module Foo`'Bar. A pragmatic solution is to useInvoke-Expressionon.Name.ToString()passed toWrite-Output- whileInvoke-Expressionis generally to be avoided, its use is safe here. Note that passing an argument that isn't a string toInvoke-Expressionimplicitly stringifies it.
Only
using modulestatements that use a FQMN (Fully Qualified Module Name, e.g.
using module @{ ModuleName = 'Foo'; ModuleVersion = '2.0.1' })
have the.ModuleSpecificationproperty filled in instead, in the form of a[System.Management.Automation.Language.HashtableAst]instance.- Reconstructing a
[hashtable]from this instance is non-trivial, but, given that its.ToString()representation is the original hash-table literal source code (also composed of literal values only), the simplest approach is the simplest approach is again to pass its string representation toInvoke-Expression.
- Reconstructing a
The following puts it all together:
It extracts all
using moduleandusing assemblystatements from a given script file (*.ps1) ...... and outputs a
[pscustomobject]instance for each with three properties:.Kindis eitherModuleorAssembly(passing the.UsingStatementKindproperty value through).NameOrSpecis:either: the module or assembly name or path, with incidental quoting and escaping removed
- Note: Any relative path is relative to the script's location.
or: a
[hashtable]instance representing the FQMN in the originatingusing modulestatement.
.SourceCodeis the original statement as text ([string]).
$scriptPath = './test.ps1'
[System.Management.Automation.Language.Parser]::ParseFile(
(Convert-Path $scriptPath),
[ref] $null, # `out` parameter that receives the array of tokens; not used here
[ref] $null # `out` parameter that receives an array of errors, if any; not used here.
).UsingStatements |
Where-Object UsingStatementKind -ne Namespace | # Filter out `using namespace` statements
ForEach-Object {
[pscustomobject] @{
Kind = $_.UsingStatementKind
NameOrSpec = if ($_.ModuleSpecification) {
Invoke-Expression $_.ModuleSpecification
} else {
Invoke-Expression ('Write-Output ' + $_.Name)
}
SourceCode = $_.Extent
}
}
If you fill test.ps1 with the following content...
using module PSReadLine
# Variations with quoting
using module 'PSReadLine'
using module "PSReadLine"
# Module with escaped embedded '
using module Foo`'Bar
# FQMN module spec
using module @{ ModuleName = 'Foo'; ModuleVersion = '2.0.0' }
# Reference to built-in assembly.
# Note: Broken in PowerShell (Core) as of v7.3.6 - see https://github.com/PowerShell/PowerShell/issues/11856
using assembly System.Windows.Forms
# Variation with quoting
using assembly 'System.Windows.Forms'
# Reference to assembly relative to the script's location.
using assembly ./path/to/some/assembly.dll
# Variation with quoting
using assembly './path/to/some/assembly.dll'
# ...
... then running the code above yields the following:
Kind NameOrSpec SourceCode
---- ---------- ----------
Module PSReadLine using module PSReadLine
Module PSReadLine using module 'PSReadLine'
Module PSReadLine using module "PSReadLine"
Module Foo'Bar using module Foo`'Bar
Module {[ModuleName, Foo], [ModuleVersion, 2.0.0]} using module @{ ModuleName = 'Foo'; ModuleVersion = '2.0.0' }
Assembly System.Windows.Forms using assembly System.Windows.Forms
Assembly System.Windows.Forms using assembly 'System.Windows.Forms'
Assembly ./path/to/some/assembly.dll using assembly ./path/to/some/assembly.dll
Assembly ./path/to/some/assembly.dll using assembly './path/to/some/assembly.dll'
Comments
Thanks to some help on Mastodon from @nyanhp, I have the answer - the "ScriptBlock" class.
$ScriptBlock = [System.Management.Automation.ScriptBlock]::Create((Get-Content $scriptPath -Raw))
$ScriptBlock.Ast.UsingStatements |
Select-Object Alias, Extent, ModuleSpecification, Name, UsingStatementKind
yields
Alias :
Extent : using module ActiveDirectory
ModuleSpecification :
Name : ActiveDirectory
UsingStatementKind : Module
which is a good place to start for getting more details. I assume that if a full module spec is provided instead of a simple name, it will appear in the ModuleSpecification member instead of the Name member.