1

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?

0

2 Answers 2

1

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 comprises using module statements, but also using assembly and using 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 with Import-Module, and the latter with Add-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 the using statements.

    • Indeed, their .Name property is filled in for using module statements with simple module names and paths as well as for the assembly names and paths used in using 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 in using statements - it may contain incidental quoting or escaping, because using module Foo may also be expressed as using module 'Foo' or using 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 like using module 'Foo''Bar'. Even an unquoted form that uses `-escaping could fail, e.g. using module Foo`'Bar. A pragmatic solution is to use Invoke-Expression on .Name.ToString() passed to Write-Output - while Invoke-Expression is generally to be avoided, its use is safe here. Note that passing an argument that isn't a string to Invoke-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 to Invoke-Expression.

The following puts it all together:

  • It extracts all using module and using assembly statements from a given script file (*.ps1) ...

  • ... and outputs a [pscustomobject] instance for each with three properties:

    • .Kind is either Module or Assembly (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 originating using 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'
Sign up to request clarification or add additional context in comments.

Comments

1

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.