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
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.

- 880
- 8
- 18
To complement your own effective solution with some background information:
Preface:
The following finds
using
statements in a given script file (*.ps1
), which not only comprisesusing module
statements, but alsousing assembly
andusing namespace
statements;using namespace
statements latter do not constitute a dependency per se, as their only purpose is to allow to refer to types by simple name only.using
statements 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
.UsingStatement
property of the[System.Management.Automation.Language.ScriptBlockAst]
instance returned by::ParseFile()
(and as contained in a[scriptblock]
's.Ast
property, as shown in your answer) contains[System.Management.Automation.Language.UsingStatementAst]
instances, if any, describing theusing
statements.Indeed, their
.Name
property is filled in forusing module
statements with simple module names and paths as well as for the assembly names and paths used inusing assembly
statements..Name
isn'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 inusing
statements - it may contain incidental quoting or escaping, becauseusing module Foo
may 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-Expression
on.Name.ToString()
passed toWrite-Output
- whileInvoke-Expression
is generally to be avoided, its use is safe here. Note that passing an argument that isn't a string toInvoke-Expression
implicitly stringifies it.
Only
using module
statements that use a FQMN (Fully Qualified Module Name, e.g.
using module @{ ModuleName = 'Foo'; ModuleVersion = '2.0.1' }
)
have the.ModuleSpecification
property 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 module
andusing assembly
statements from a given script file (*.ps1
) ...... and outputs a
[pscustomobject]
instance for each with three properties:.Kind
is eitherModule
orAssembly
(passing the.UsingStatementKind
property value through).NameOrSpec
is: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 module
statement.
.SourceCode
is 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'

- 382,024
- 64
- 607
- 775