Создание модуля power shell

Несмотря на то, что в интернете вы можете найти очень много различных инструкций по поводу того, как создать свой собственный Powershell-модуль, даже официальная документация не дает полного понимания, как все-таки создать модуль правильно.

Я потратил несколько часов на изучение разных статей и, как обычно, пришел к своему подходу, который мне кажется достаточно удобным, гибким, и которым я хочу поделиться сейчас.

└─ScriptModuleRepositoryTemplate
  │   ScriptModuleRepositoryTemplate.psd1
  │   ScriptModuleRepositoryTemplate.psm1
  │   ScriptModuleRepositoryTemplate.Tests.ps1

At the top of the Pester ScriptModuleRepositoryTemplate.Tests.ps1 file I have this statement to import the module:

using module './ScriptModuleRepositoryTemplate.psm1'

The module is imported and the Pester tests run fine in Pwsh 7, but on Windows PowerShell 5.1 it throws the error:

System.IO.FileNotFoundException: The specified module 'D:\a\PowerShell.ScriptModuleRepositoryTemplate\PowerShell.ScriptModuleRepositoryTemplate\ScriptModuleRepositoryTemplate.psm1' was not loaded because no valid module file was found in any module directory.

If I instead use Import-Module like this:

Import-Module -Name "$PSScriptRoot/ScriptModuleRepositoryTemplate.psm1"

it works in both PowerShell 5.1 and 7, but of course Import-Module does not import classes and enums, so I need to use using module.

Both Pwsh and Windows PowerShell are using Pester v5.5.0, so I don’t think the issue is with Pester, but with Windows PowerShell itself.

I can reproduce the issue on my local machine by running Invoke-Pester ., as well as in GitHub Actions (see this workflow run in the open source project).

Does anyone have any ideas on how I can get using module './PathToModuleFile.psm1' to work properly?

#Requires -Version 3.0

<#

.SYNOPSIS

Compiling PowerShell Class Modules into C# DLLs.

 
.DESCRIPTION

This script compiles PowerShell Class Modules into C# Dynamic Link Libraries (DLLs).

The compiled file can be imported into PowerShell using the “using assembly .\Path.dll” directive,

or utilized with other .NET languages such as C# by adding the DLL to references and importing it using “using static ExampleName;”.

:/>  Как включить rdp windows 10 через командную строку

 
.PARAMETER Path

Specifies the path to the *.ps1 or *.psm1 script containing only a class and related namespaces.

 
.PARAMETER ScriptDefinition

Provides a string containing the class code.

 
.PARAMETER OutputAssembly

Specifies the path where the resulting *.cs or *.dll file will be saved.

 
.PARAMETER DebugMode

Enables the GetPowershell() method, as well as access to both ClassHandlers and other features.

 
.PARAMETER SkipUpdate

Doesn’t check for updates.

 
.EXAMPLE

Compile-Dll .\example.psm1 -o .\example.dll

 
.EXAMPLE

Compile-Dll .\example.psm1 -o .\example.cs

” “

 
        string Script = Encoding.UTF8.GetString(Convert.FromBase64String(“cGFyYW0gKFtzdHJpbmddJE1ldGhvZCwgW29iamVjdFtdXSRBcmd1bWVudHMsIFtoYXNodGFibGVdJFByb3BlcnRpZXMpDQpDbGFzc0hhbmRsZXIgJE1ldGhvZCAkQXJndW1lbnRzICRQcm9wZXJ0aWVzICRmYWxzZQ==”));

 
        Hashtable Properties = new Hashtable();’

“`n Properties.Add($(‘”‘ + $property.Keys + ‘”‘), $($property.Keys));`n”

“`n }`n`n”

 
        string Script = Encoding.UTF8.GetString(Convert.FromBase64String(“cGFyYW0gKFtzdHJpbmddJE1ldGhvZCwgW29iamVjdFtdXSRBcmd1bWVudHMsIFtoYXNodGFibGVdJFByb3BlcnRpZXMpDQpDbGFzc0hhbmRsZXIgJE1ldGhvZCAkQXJndW1lbnRzICRQcm9wZXJ0aWVzICR0cnVl”));

 
        Hashtable Properties = new Hashtable();’

“`n Properties.Add($(‘”‘ + $property.Keys + ‘”‘), $($property.Keys));`n”

“`n }`n”

” //Visible Class`n`n”

“`nSucesss, $((Get-Item $OutputAssembly).Name) has been compiled!”

What is a PowerShell Module?

about Modules – PowerShell

Explains how to install, import, and use PowerShell modules.

Создание модуля power shell

Put briefly, a PowerShell module — similar to a Python module — is a way to organize a collection of related code, variables, aliases, etc. in a contained workspace.

There are two types of PowerShell modules:

  • Script modules (PowerShell scripts)
  • Binary modules (compiled DLLs)

You can intermingle both scripts and compiled DLLs in a single module if the need arises, as there are performance benefits and other enhancements to running compiled code.

Module Loading Paths

If you inspect this PowerShell environment variable, you can see from which folders your PowerShell modules will be loaded.

$env:PSModulePath

PSModulePath environment variable

If you install a module from PowerShell Gallery, you can use the Scope parameter to determine which folder in $env:PSModulePath the module will be downloaded.

# Local User
# Install the module to C:\Users\user.name\Documents\WindowsPowerShell\Modules\ on Windows
# Install the module to /home/user.name/.local/share/powershell/Modules on Linux
Install-Module -Name <module-name> -Scope CurrentUser

# Global Module
# Install the module to C:\Program Files\WindowsPowerShell\Modules on Windows
# Install the module to /usr/local/share/powershell/Modules on Linux
Install-Module -Name <module-name>

Different ways to install modules from a network repository

Why mention this?

When to Create a Module

  1. Create a script for a distinct purpose or project
  2. Additional scripts continue to be added
    • Adding helper scripts
    • Or, extending the scope of the project
  3. The project scope continues to grow, we need a place to keep things organized

A PowerShell module — like a function — should be fine-tuned to a specific purpose. In other words, if you see your module becoming too monolithic and trying to be a one-size-fits-all tool, you might consider breaking it up into several individual modules.

Scaffolding the Module

Please note that this is the way that I create PowerShell modules, and should not be construed as the only way to create PS Modules.

Module Directory Structure

ModuleName
|___ModuleName.psd1
|___ModuleName.psm1
|___Private
|   |___ps1
|       |___Verb-Noun.ps1
|       |___Verb-Noun.ps1
|
|___Public
    |___ps1
        |___Verb-Noun.ps1
        |___Verb-Noun.ps1
  • ModuleName — Name the module after the project or its intended purpose
  • ModuleName.psd1 — This is the module manifest, we’ll cover this later
  • ModuleName.psm1 — This is your script module, we’ll cover this later as well
  • Private — This is the directory to store your private functions and variables (eg. helper functions)
    • ps1 — The PowerShell functions are stored here and are not directly referenced by the user
      • Verb-Noun.ps1 — the script file containing the private PowerShell function.
  • Public — This is directory to store your public functions and variables
    • ps1 — The PowerShell function code is stored here and these are the cmdlets the user will be directly running
      • Verb-Noun.ps1 — the script file containing the public PowerShell function

Commands to Create the Module Structure

# Variables
$year = (Get-Date).Year
$moduleName = 'TestModule'
$templateString = 'Test Module'
$version = '1.0'

# Create the "TestModule" top-level directory
New-Item -ItemType Directory -Name $moduleName

# Create subdirectories
#    TestModule
#    |___ ...
#    |___ ...
#    |___Private
#    |   |___ps1
#    |___ ...

New-Item -Path "$PWD\$moduleName\Private\ps1" -ItemType Directory -Force

# Create subdirectories
#    TestModule
#    |___ ...
#    |___ ...
#    |___ ...
#    |___Public
#        |___ps1

New-Item -Path "$PWD\$moduleName\Public\ps1" -ItemType Directory -Force

# Create the script module
#    TestModule
#    |___ ...
#    |___ TestModule.psm1

New-Item -Path "$PWD\$moduleName\$moduleName.psm1" -ItemType File

# Create the module manifest
#    TestModule
#    |___TestModule.psd1
#    |___ ...

$moduleManifestParameters = @{
    Path = "$PWD\$moduleName\$moduleName.psd1"
    Author = $templateString
    CompanyName = $templateString
    Copyright = "$year $templateString by Benjamin Heater"
    ModuleVersion = $version
    Description = $templateString
    RootModule = "$moduleName.psm1"
}
New-ModuleManifest @moduleManifestParameters

Inspecting the Manifest

ModuleName
|___ModuleName.psd1
|___ ...
|___ ...

The module manifest in relation to the directory structure

Get-Content "$PWD\TestModule\TestModule.psd1"`

Output the contents of the moudle manifest

Example Module Manifest

#
# Module manifest for module 'TestModule'
#
# Generated by: Test Module
#
# Generated on: 8/17/2023
#

@{

# Script module or binary module file associated with this manifest.
RootModule = 'TestModule.psm1'

# Version number of this module.
ModuleVersion = '1.0'

# Supported PSEditions
# CompatiblePSEditions = @()

# ID used to uniquely identify this module
GUID = '147cfba3-3c6d-4260-aeca-f99aa900cc0c'

# Author of this module
Author = 'Test Module'

# Company or vendor of this module
CompanyName = 'Test Module'

# Copyright statement for this module
Copyright = '2023 Test Module by Benjamin Heater'

# Description of the functionality provided by this module
Description = 'Test Module'

# Minimum version of the Windows PowerShell engine required by this module
# PowerShellVersion = ''

# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''

# Minimum version of the Windows PowerShell host required by this module
# PowerShellHostVersion = ''

# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# DotNetFrameworkVersion = ''

# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# CLRVersion = ''

# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''

# Modules that must be imported into the global environment prior to importing this module
# RequiredModules = @()

# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()

# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()

# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()

# Format files (.ps1xml) to be loaded when importing this module
# FormatsToProcess = @()

# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
# NestedModules = @()

# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = '*'

# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = '*'

# Variables to export from this module
VariablesToExport = '*'

# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = '*'

# DSC resources to export from this module
# DscResourcesToExport = @()

# List of all modules packaged with this module
# ModuleList = @()

# List of all files packaged with this module
# FileList = @()

# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{

    PSData = @{

        # Tags applied to this module. These help with module discovery in online galleries.
        # Tags = @()

        # A URL to the license for this module.
        # LicenseUri = ''

        # A URL to the main website for this project.
        # ProjectUri = ''

        # A URL to an icon representing this module.
        # IconUri = ''

        # ReleaseNotes of this module
        # ReleaseNotes = ''

    } # End of PSData hashtable

} # End of PrivateData hashtable

# HelpInfo URI of this module
# HelpInfoURI = ''

# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
# DefaultCommandPrefix = ''

}

When we ran the New-ModuleManifest command before, PowerShell added our inputs to a .psd1 file with the above template.

The module manifest is responsible for declaring some important things about our module, including:

  • The RootModule script (eg. TestModule.psm1)
  • The GUID to uniquely identify the module
  • FunctionsToExport for function names that should be executable by the user
  • CmdletsToExport for compiled cmdlets that should be executable by the user
  • VariablesToExport for any module-specific variables
  • AliasesToExport for any aliases defined in your functions or cmdlets
  • And, many more configurations

about Module Manifests – PowerShell

Describes the settings and practices for writing module manifest files.

Создание модуля power shell

Inspecting the Script Module

ModuleName
|___ ...
|___ModuleName.psm1
|___ ...

The script module in relation to the directory structure

Get-Content "$PWD\TestModule\TestModule.psm1"

Output the contents of the script module

Script Module I’ve Created for All My Modules

$directorySeparator = [System.IO.Path]::DirectorySeparatorChar
$moduleName = $PSScriptRoot.Split($directorySeparator)[-1]
$moduleManifest = $PSScriptRoot + $directorySeparator + $moduleName + '.psd1'
$publicFunctionsPath = $PSScriptRoot + $directorySeparator + 'Public' + $directorySeparator + 'ps1'
$privateFunctionsPath = $PSScriptRoot + $directorySeparator + 'Private' + $directorySeparator + 'ps1'
$currentManifest = Test-ModuleManifest $moduleManifest

$aliases = @()
$publicFunctions = Get-ChildItem -Path $publicFunctionsPath | Where-Object {$_.Extension -eq '.ps1'}
$privateFunctions = Get-ChildItem -Path $privateFunctionsPath | Where-Object {$_.Extension -eq '.ps1'}
$publicFunctions | ForEach-Object { . $_.FullName }
$privateFunctions | ForEach-Object { . $_.FullName }

$publicFunctions | ForEach-Object { # Export all of the public functions from this module

    # The command has already been sourced in above. Query any defined aliases.
    $alias = Get-Alias -Definition $_.BaseName -ErrorAction SilentlyContinue
    if ($alias) {
        $aliases += $alias
        Export-ModuleMember -Function $_.BaseName -Alias $alias
    }
    else {
        Export-ModuleMember -Function $_.BaseName
    }

}

$functionsAdded = $publicFunctions | Where-Object {$_.BaseName -notin $currentManifest.ExportedFunctions.Keys}
$functionsRemoved = $currentManifest.ExportedFunctions.Keys | Where-Object {$_ -notin $publicFunctions.BaseName}
$aliasesAdded = $aliases | Where-Object {$_ -notin $currentManifest.ExportedAliases.Keys}
$aliasesRemoved = $currentManifest.ExportedAliases.Keys | Where-Object {$_ -notin $aliases}

if ($functionsAdded -or $functionsRemoved -or $aliasesAdded -or $aliasesRemoved) {

    try {

        $updateModuleManifestParams = @{}
        $updateModuleManifestParams.Add('Path', $moduleManifest)
        $updateModuleManifestParams.Add('ErrorAction', 'Stop')
        if ($aliases.Count -gt 0) { $updateModuleManifestParams.Add('AliasesToExport', $aliases) }
        if ($publicFunctions.Count -gt 0) { $updateModuleManifestParams.Add('FunctionsToExport', $publicFunctions.BaseName) }

        Update-ModuleManifest @updateModuleManifestParams

    }
    catch {

        $_ | Write-Error

    }

}

I use the script module as an initialization script for when the module is first imported. Looking at the code from top to bottom, I’ll briefly summarize what the script module is doing.

  1. Dynamically determine the module name using $PSScriptRoot
  2. Assume the .psd1 file shares the same name as the parent directory
    (the .psd1 is invoking this .psm1 script module)
  3. Pull the current module manifest using Test-ModuleManifest, in order to compare and update things as necessary
  4. Discover all .ps1 files in private and public directories and source them in
  5. Loop over each public function and determine if any aliases are defined and export them as module members
  6. Do some comparisons against the current module manifest to see if any functions or aliases have been added or removed, and if so, update the module manifest

You could add additional logic to the .psm1 script module if there are things you’d like your module to do when it’s first imported.

Adding Code to the Module

ModuleName
|___ ...
|___ ...
|___Private <--- May not be referenced by user
|   |___ps1 <--- Script files
|       |___Verb-Noun.ps1 <--- Private function 1
|       |___Verb-Noun.ps1 <--- Private function 2
|       |___etc.
|
|___Public  <--- May be referenced by user
    |___ps1 <--- Script files
        |___Verb-Noun.ps1 <--- Public function 1
        |___Verb-Noun.ps1 <--- Public function 2
        |___etc.

The PowerShell code in relation to the directory structure

The examples here are going to be a bit contrived. The intent — however — is to keep this blog post as concise as possible while still providing clear examples of functionality.

Adding a Private Function

ModuleName
|___ ...
|___ ...
|___Private 
|   |___ps1 
|       |___Confirm-CoffeeTime.ps1
|       |___ ...
|
|___ ...

Private function in relation to the directory structure

function Confirm-CoffeeTime {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Regular', 'Decaf')]
        [String]$CoffeeType
    )
    
    begin {
        $currentDateTime = Get-Date
        $regularCoffeeHours = 4..20
        $decafCoffeeHours = 0..3 + 21..23
    }
    process {

        if ($CoffeeType -eq 'Regular') {
            
            if ($currentDateTime.Hour -in $regularCoffeeHours) {
                $result = $true
            }
            else {
                $result = $false
            }

        }
        else {
        
            if ($currentDateTime.Hour -in $decafCoffeeHours) {
                $result = $true
            }
            else {
                $result = $false
            }

        }

    }
    end {

        if ($result -eq $true) {
            return "Enjoy your cup of $CoffeeType coffee!"
        }
        else {
            throw "You may not drink $CoffeeType right now!"
        }

    }

}

Note the function name matches the file name ‘Confirm-CoffeeTime’

Adding a Public Function

ModuleName
|___ ...
|___ ...
|___ ...
|
|___Public
    |___ps1
        |___Get-Coffee.ps1
        |___ ...

Public function in relation to the directory structure

function Get-Coffee {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet('Regular', 'Decaf')]
        [String]$CoffeeType
    )
    
    begin {}
    process {

        try {
            # Call the private function to confirm coffee time
            $confirmation = Confirm-CoffeeTime -CoffeeType $CoffeeType
            Write-Host $confirmation -ForegroundColor Green
        }
        catch {
            throw $_
        }
        
    }
    end {}

}

Note the function name matches the file name ‘Get-Coffee’

Importing and Using the Module

What we should expect to happen:

  • Import the module
  • Run Get-Command -Module <module_name>
  • Observe that we have only one user-executable command
Import-Module .\TestModule

Importing our test module

Get-Command -Module TestModule

See which commands are provided by the module

Создание модуля power shell
As expected, the only available command is the ‘Get-Coffee’ command
Создание модуля power shell

Perfect! Our public Get-Coffee function is performing as expected — calling the private Confirm-CoffeeTime function and returning stdout or stderr.

Applying this to Real Projects

As mentioned before, the examples above are contrived, and not great at demonstrating how a PowerShell module could be used for real projects. Some examples of how you might create a PowerShell module:

  • API clients
  • CTF tools
  • Hacking tools
  • Forensic tools
  • Diagnostic tools
  • Etc.

Don’t create more work for yourself!

Creating a duplicate PowerShell module as a learning experience is absolutely fine. However, if there’s a module on GitHub or the PS Gallery that meets your needs, use that and consider contributing to the source code if it’s open source.

If you see an existing module, but aren’t satisfied with it for any reason, that would absolutely be a perfect opportunity to create your own solution.

Some Modules I’ve Written

GitHub – 0xBEN/PSProxmox: PowerShell module for interfacing with Proxmox API

PowerShell module for interfacing with Proxmox API – GitHub – 0xBEN/PSProxmox: PowerShell module for interfacing with Proxmox API

Создание модуля power shell

Good reference for my usage of public/private functions

GitHub – 0xBEN/PSToolbox: PowerShell module containing a set of generally useful tools.

PowerShell module containing a set of generally useful tools. – GitHub – 0xBEN/PSToolbox: PowerShell module containing a set of generally useful tools.

Создание модуля power shell

No usage of private functions, just bundling tools

Wrapping Up

I want to emphasize that this post is a demonstration of the way that I create PowerShell modules. It is not the only way to create PowerShell modules.

That said, I do hope that I gave you some ideas on how to get started with creating your own modules, and how you can keep your related code organized.

Установите необходимые утилиты

Кроме самого Powershell, Вам понадобится утилита командной строки nuget. Она будет нужна, чтобы паковать модуль в nuget-пакет и распространять модуль между другими компьютерами\пользователями.

Упаковка модуля

Попробуем запустить local-build.ps1 и посмотреть, что получится.

Разработка и распространение модуля на Powershell

Отлично! Nuget-пакет готов и лежит в папке My.OwnModule.Powershell\_bin\My.OwnModule.Powershell.0.0.3.nupkg. Попробуем его установить и воспользоваться.

Создайте необходимые файлы

Также нужно создать файлы, которые собственно и превращают набор Powershell-сриптов в модуль.

D:.
│   LICENSE
│   PSScriptAnalyzerSettings.psd1
│   README.md

└───My.OwnModule.Powershell
    │   local-build.ps1
    │   local-install.ps1
    │   My.OwnModule.Powershell.nuspec
    │   My.OwnModule.Powershell.psd1
    │   My.OwnModule.Powershell.psm1
    │
    ├───images
    │       icon.png
    │
    ├───private
    │       _stub.ps1
    │
    ├───public
    │   └───common
    │           Read-MyOwnJsonFile.ps1
    │
    └───_bin

Нас интересуют файлы, которые лежат в корневой папке модуля, в private и public. Остальные – это служебные файлы git-репозитория. Ниже привожу содержимое каждого файла с комментариями.

Придумайте префикс для названия командлетов\функций

Если вы разрабатываете несколько разных модулей (например, для разных компаний или команд), иногда в модулях может быть одна и та же функция. И если вы начнете сами использовать эти модули одновременно, при их импорте начнутся конфликты имен. Поэтому имеет смысл использовать для каждого модуля свой уникальный префикс.

В нашем примере я буду использовать MyOwn. Например стандартное имя New-Item превратится в New-MyOwnItem.

GitHub репозиторий

Вы можете найти все эти файлы в моем репозитории: https://github.com/vicioussn/My.OwnModule.Powershell.

Установка Powershell-модуля из nuget-пакета.

Разработка и распространение модуля на Powershell

Прекрасно, модуль готов к работе!

Там Verbose говорит, что версия 0.0.5 уже установлена и так далее. Это потому что я попытался понизить версию, вместо того, чтобы повысить. Не обращайте внимание.

Создайте дерево каталогов

Кроме корневой папки, которая будет называться так же, как и сам модуль, понадобится начальный набор каталогов.

D:.
└───My.OwnModule.Powershell
    ├───images
    ├───private
    ├───public
    │   └───common
    └───_bin

  • images. Тут будет лежать иконка для пакета, которая в теории будет видна в каком-нибудь репозитории типа Artifactory.
  • private. Тут будут лежать скрипты, которые должны быть доступны только для служебных нужд. Например, чтобы прочитать файл с диска с обработкой ошибок.
  • public. Тут будут лежать скрипты, в которых будут доступны непосредственно пользователям модуля.
    • public -> common. Эта папка нужна исключительно для структурирования и группировки файлов. Все внутри Public будет читаться рекурсивно.
  • _bin. Тут будут лежать пакеты с модулем, когда утилита nuget скомпилирует пакет.

Заполняем служебные файлы для Powershell-модуля

My.OwnModule.Powershell.nuspec

Этот файл нужен, чтобы nuget генерировал пакет.

<?xml version="1.0" encoding="utf-8"?>
<package >
  <metadata>
    <id>My.OwnModule.Powershell</id>
    <version>0.0.2</version>
    <authors>Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.</authors>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <icon>images\icon.png</icon>
    <description>
      There is no any description available at this time. Sorry for that.
    </description>
    <releaseNotes>Initial release.</releaseNotes>
    <tags>PSEdition_Core PSEdition_Desktop Linux Mac PSModule PSIncludes_Cmdlet</tags>
  </metadata>
</package>

My.OwnModule.Powershell.psd1

Этот файл – манифест, описание модуля с точки зрения самого Powershell. Тут хранятся различные сведения об авторе, о функциях, которые модуль будет предосталять и т.п.

Обратите внимание на строку 11 и параметр FunctionsToExport. Это функции, которые будут доступны пользователям вашего модуля. Поэтому, при добавлении новых функций в модуль, необходимо помнить и добавлять их сюда.

@{
    RootModule           = "My.OwnModule.Powershell.psm1"
    ModuleVersion        = "0.0.2"
    CompatiblePSEditions = "Desktop", "Core"
    GUID                 = "8f37ba7b-be77-4b6e-8ceb-d30c0c328674"
    Author               = "Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript."
    PowerShellVersion    = "7.0"

    ScriptsToProcess     = @();
       
    FunctionsToExport    = @(
        "Read-MyOwnJsonFile"
    );
    CmdletsToExport      = "*"
    VariablesToExport    = "*"
    AliasesToExport      = "*"
}

My.OwnModule.Powershell.psm1

Этот файл – скрипт, который выполняется, когда вы запускаете (или сама система запускает) Import-Module. Это самый главный файл в модуле, вообще говоря, это и есть сам модуль.

#Requires -Version 7.0


$public  = @( Get-ChildItem -Path $PSScriptRoot\public\*.ps1 -Recurse; );
$private = @( Get-ChildItem -Path $PSScriptRoot\private\*.ps1 -Recurse; );


# Dot source the files
foreach ($import in @($public + $private))
{
    try
    {
        Write-Verbose "Importing '$($import.FullName)'.";
        . $import.FullName;
    }
    catch
    {
        Write-Error -Message "Failed to import function $($import.fullname): $_";
    }
}


Export-ModuleMember -Function $public.Basename;

_stub.ps1

Я положил этот файл-заглушку, чтобы скрипт из psm1-файла смог нормально прочитать папку без возможных проблем. А вообще он полностью пустой.

Read-MyOwnJsonFile.ps1

Это файл командлета, который мы будем предоставлять нашим модулем. Обратите внимание, как он называется – Read-MyOwnJsonFile, всмысле что используется префикс для имен командлетов модуля.

local-build.ps1

Этот файл не является непосредственной частью Powershell-модуля. Это скрипт, который я использую для упаковки модуля в nuget-пакет, чтобы потом его можно было положить куда-нибудь в архив. Также этот скрипт в каждый запуск изменяет версию модуля на +1.

# Config
$moduleName = "My.OwnModule.Powershell";


# Clear old packages
# Get-ChildItem .\$moduleName\_bin | Remove-Item -Force -Verbose;


#region Get current package version and increment Patch
$version = Select-String -Path .\$moduleName\$moduleName.nuspec -Pattern "<version>(\d)\.(\d)\.(\d{1,})</version>";
[int]$majorVersion = $version | Select-Object @{name="version"; expression={$_.Matches.Groups[1].Value}} | select-object -ExpandProperty version;
[int]$minorVersion = $version | Select-Object @{name="version"; expression={$_.Matches.Groups[2].Value}} | select-object -ExpandProperty version;
[int]$patchVersion = $version | Select-Object @{name="version"; expression={$_.Matches.Groups[3].Value}} | select-object -ExpandProperty version;
$patchVersion++;
$nuspecVersionString = "<version>$majorVersion.$minorVersion.$patchVersion</version>";
$psd1VersionString = "ModuleVersion        = `"$majorVersion.$minorVersion.$patchVersion`"";
#endregion /Get current package version and increment Patch


#region Update package version
$a = Get-Content -Path ".\$moduleName\$moduleName.nuspec" | %{$_ -replace "<version>(\d)\.(\d)\.(\d{1,})<\/version>", $nuspecVersionString };
$a | Out-File ".\$moduleName\$moduleName.nuspec" -Verbose;

$a = Get-Content -Path ".\$moduleName\$moduleName.psd1" | %{$_ -replace "ModuleVersion        = `"\d\.\d.\d{1,}`"", $psd1VersionString };
$a | Out-File ".\$moduleName\$moduleName.psd1" -Verbose;
#endregion /Update package version


nuget pack ".\$moduleName\$moduleName.nuspec" -OutputDirectory .\$moduleName\_bin -Properties NoWarn=NU5111,NU5110,NU5100
$package = Get-ChildItem .\$moduleName\_bin\*.nupkg | Sort-Object -Property CreationTime | Select-Object -Last 1;

$package

local-install.ps1

Соответственно, этот кастомный скрипт – для установки модуля.

$moduleName = "My.OwnModule.Powershell";
$repoName = "My.OwnModule.Powershell.Repository";
$repoPath = "D:\Private\My.OwnModule.Powershell\My.OwnModule.Powershell\_bin\";

Register-PSRepository -Name $repoName -SourceLocation $repoPath -InstallationPolicy Trusted -ScriptSourceLocation $repoPath;

Install-Module -Name $moduleName -Repository $repoName -Verbose;

Get-Module $moduleName -ListAvailable;

Придумайте название

Это важно :-). Потом с этим названием вы будете долго жить, поэтому придумайте то, что вам потом будет приятно видеть каждый день =).

В моем случае я решил придерживаться следующего подхода: <код_заказчика\название заказчика>.<код_проекта>.Powershell.

Для целей этого гайда давайте делать модуль My.OwnModule.Powershell.

Оставьте комментарий