# Automate PowerShell module creation the smart way

> 
I will start with little warning. This is my first blog post ever and my english is far from perfect, so please be nice :D

In this post I will show you, how to easily generate PowerShell modules from ps1 scripts (that contains PowerShell functions) using my PowerShell function [Export-ScriptsToModule](https://github.com/ztrhgf/useful_powershell_functions/blob/master/Export-ScriptsToModule.ps1).

### Why to store functions in ps1 script files instead of placing them directly into psm1 module, you can ask?
For me its about **simplicity**
 - working with separate ps1 is much easier in Visual Studio Code IDE thanks to shortcut *CTRL + P*, that allows you to easily switch between files just by typing their name
 - it is much easier to create and debug new functions
 - one ps1 per functions is much easier to read than one big module with several hundreds or even thousands of lines
 - if you have (same as me) some pre-commit checks, it is much easier to work with functions in separate files

---

## TL;DR
Use my PowerShell function 
 [Export-ScriptsToModule](https://github.com/ztrhgf/useful_powershell_functions/blob/master/Export-ScriptsToModule.ps1) like this:
 - download the  [Export-ScriptsToModule.ps1](https://github.com/ztrhgf/useful_powershell_functions/blob/master/Export-ScriptsToModule.ps1) script
 - open PowerShell console and:
  - dot source downloaded script (or just copy paste its content into the console)
  - run function **Export-ScriptsToModule** like
  ```
Export-ScriptsToModule @{"<pathToFolderWithps1ScriptsContainingFunctions>" = "<pathToModuleFolderThatWillBeGenerated>"}
```
 - check the generated module at *&lt;pathToModuleFolderThatWillBeGenerated&gt;*
 - **Requirements:** there have to be some ps1 scripts with functions defined in them stored in *&lt;pathToFolderWithps1ScriptsContainingFunctions&gt;* for this to work :-)

### And this is how it will look like

%[https://youtu.be/R3GPfgTdwME]

---

# Why not just dot source the ps1 script files?
- dot sourcing can be even 10x slower than importing well made module (see bellow what this means)
- if you split your functions to different modules, just module, that contains function you want to use, will be imported by default (so again much faster)

---

# So how to do it the right way?
Define each function in a separate identically titled ps1 file (i.e. Hello-World.ps1 will contain function Hello-World etc). And from such ps1's generate psm1 modules automatically :)

**What to be aware of when you want generate module automatically:**
 - it is **unsafe to simply copy&paste ps1 content**, because there can be some code outside the function, therefore by importing generated module, that code would be automatically run
 - you have to **detect defined aliases** in ps1 and make them available in the module by exporting them
  - in [alias("HelloWorld")] definition, so as by using Set-Alias
 - you have to **detect functions names**, so you can export them directly in module instead of using wildcard '*'
  - using \* in *Export-ModuleMember* makes the module import **slow**
 - **validate function syntax** before export
 - if source ps1 scripts are located in GIT repository, **use last committed version** of the ps1 file, in case the file is modified (there is work in progress, so it is not safe to use actual script content in generated module yet)
 - replace invalid characters like en-dash or em-dash for regular dash
  - otherwise syntax checker will throw nonsense errors on you 

### PowerShell AST to the rescue!
Parsing PowerShell content using regex to get the right data would be quite hard, but luckily we have AST!

AST is abbreviation of **A**bstract **S**yntax **T**ree and it is built-in PowerShell feature for code analysis. In general it will create hierarchic tree of objects, representing given code. You can use it for example to extract various information from the code like variables, function definitions, function parameters, aliases etc.

For example:

- **to find variables defined in "C:\Scripts\ScriptWithVariables.ps1"**
```
$AST = [System.Management.Automation.Language.Parser]::ParseFile("C:\Scripts\ScriptWithVariables.ps1", [ref]$null, [ref]$null)
$variable = $AST.FindAll( { $args[0] -is [System.Management.Automation.Language.VariableExpressionAst ] }, $true)
$variable | Where-Object { $_.parent.left -or $_.parent.type -and ($_.parent.operator -eq 'Equals' -or $_.parent.parent.operator -eq 'Equals') }
``` 

- **to find function definitions in "C:\Scripts\ScriptWithFunction.ps1"**
```
$AST = [System.Management.Automation.Language.Parser]::ParseFile("C:\Scripts\ScriptWithFunction.ps1", [ref]$null, [ref]$null)
$AST.FindAll( {
    param([System.Management.Automation.Language.Ast] $AST)

    $AST -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
    # Class methods have a FunctionDefinitionAst under them as well, but we don't want them.
    ($PSVersionTable.PSVersion.Major -lt 5 -or
        $AST.Parent -isnot [System.Management.Automation.Language.FunctionMemberAst])
}, $false)
```

- **to find function \#requires in "C:\Scripts\ScriptWithRequires.ps1"**

```
$AST = [System.Management.Automation.Language.Parser]::ParseFile("C:\Scripts\ScriptWithRequires.ps1", [ref]$null, [ref]$null)
$AST.scriptRequirements
# will return something like
RequiredApplicationId :
RequiredPSVersion     :
RequiredPSEditions    : {}
RequiredModules       : {psasync}
RequiresPSSnapIns     : {}
RequiredAssemblies    : {}
IsElevationRequired   : False
```

- **to find function aliases defined as [Alias("Some-FunctionAlias")] in "C:\Scripts\ScriptWithAlias.ps1"**
```
$AST = [System.Management.Automation.Language.Parser]::ParseFile("C:\Scripts\ScriptWithAlias.ps1", [ref]$null, [ref]$null)
$AST.FindAll( {
        param([System.Management.Automation.Language.Ast] $AST)

        $ast -is [System.Management.Automation.Language.AttributeAst]
    }, $true) | ? { $_.parent.extent.text -match '^param' } | Select-Object -ExpandProperty PositionalArguments | Select-Object -ExpandProperty Value -ErrorAction SilentlyContinue # filter out aliases for function parameters
```

- **to find function aliases defined by Set-Alias in "C:\Scripts\ScriptWithAlias.ps1"**
```
$functionName = "Hello-World"
$AST = [System.Management.Automation.Language.Parser]::ParseFile("C:\Scripts\ScriptWithAlias.ps1", [ref]$null, [ref]$null)
$AST.EndBlock.Statements | ? { $_ -match "^\s*Set-Alias .+" -and $_ -match [regex]::Escape($functionName) } | % { $_.extent.text }
```

If you are interested in more examples of AST usage, check content of [pre-commit.ps1](https://github.com/ztrhgf/Powershell_CICD_repository/blob/master/repo_content_set_up/.githooks/pre-commit.ps1). And search there functions: _getParameterAST, _getAliasAST, _getFunctionAST

---

# And what about that GIT part?
First you have to recognize files, that are not ready for production (were modified since last pushed commit, or aren't commited yet)
```
# uncommited changed files
git ls-files -m --full-name
# untracked files
git ls-files --others --exclude-standard --full-name
```
Than you have to replace such files with their production ready variant (by getting content of last commited version from GIT) via
```
git show HEAD:/repository/scripts/someModifiedScript.ps1
```

# So what is the solution?
You can use my function [Export-ScriptsToModule](https://github.com/ztrhgf/useful_powershell_functions/blob/master/Export-ScriptsToModule.ps1) and use it to easily create your own modules :)
 - check TL;DR section for more information

> TIP
If you want to automate this even further use my [CI/CD PowerShell solution](https://github.com/ztrhgf/Powershell_CICD_repository) which uses same function in background and helps you to automate whole PowerShell content lifecycle. I will cover it in some following article.

If you have ANY questions, don't hesitate to contact me on [Twitter](https://twitter.com/AndrewZtrhgf)


