Showing preview only (217K chars total). Download the full file or copy to clipboard to get everything.
Repository: PoshCode/Configuration
Branch: main
Commit: d5f74a9ff405
Files: 48
Total size: 204.2 KB
Directory structure:
gitextract_4wdl1uu2/
├── .config/
│ └── dotnet-tools.json
├── .github/
│ └── workflows/
│ ├── Merge-Module.ps1
│ └── build.yml
├── .gitignore
├── .vscode/
│ ├── launch.json
│ ├── settings.json
│ ├── taskmarks.json
│ └── tasks.json
├── Benchmark/
│ ├── Benchmark.ps1
│ └── Data/
│ └── Configuration.psd1
├── Build.ps1
├── CHANGELOG.md
├── Examples/
│ ├── TestModuleOne/
│ │ ├── Configuration.psd1
│ │ ├── TestModuleOne.psd1
│ │ └── TestModuleOne.psm1
│ └── TestModuleTwo/
│ ├── Configuration.psd1
│ ├── TestModuleTwo.psd1
│ └── TestModuleTwo.psm1
├── GitVersion.yml
├── LICENSE
├── PSScriptAnalyzerSettings.psd1
├── README.md
├── ReBuild.ps1
├── RequiredModules.psd1
├── Source/
│ ├── Configuration.psd1
│ ├── Header/
│ │ └── param.ps1
│ ├── Private/
│ │ ├── InitializeStoragePaths.ps1
│ │ └── ParameterBinder.ps1
│ └── Public/
│ ├── Export-Configuration.ps1
│ ├── Get-ConfigurationPath.ps1
│ ├── Get-ParameterValue.ps1
│ ├── Import-Configuration.ps1
│ └── Import-ParameterConfiguration.ps1
├── Specs/
│ ├── Configuration.Steps.ps1
│ ├── Configuration.feature
│ ├── ConfiguredParameters.feature
│ ├── DefaultParameters.feature
│ ├── Layering.feature
│ ├── LocalStoragePath.feature
│ ├── LocalStoragePathLinux.feature
│ ├── Manifest.feature
│ ├── ScriptAnalyzer.Steps.ps1
│ ├── ScriptAnalyzer.feature
│ ├── Serialization.feature
│ └── TestVersion.feature
├── Test.ps1
├── bootstrap.ps1
└── build.psd1
================================================
FILE CONTENTS
================================================
================================================
FILE: .config/dotnet-tools.json
================================================
{
"version": 1,
"isRoot": true,
"tools": {
"gitversion.tool": {
"version": "5.6.0",
"commands": [
"dotnet-gitversion"
]
}
}
}
================================================
FILE: .github/workflows/Merge-Module.ps1
================================================
#requires -Module Configuration
[CmdletBinding()]
param(
$OutputModulePath,
$NestedModulePath
)
$OutputModule = Get-Module $OutputModulePath -ListAvailable
$NestedModule = Get-Module $NestedModulePath -ListAvailable
# Copy and then remove the extra output
Copy-Item -Path (Join-Path $NestedModule.ModuleBase Metadata.psm1) -Destination $OutputModule.ModuleBase
Remove-Item $NestedModule.ModuleBase -Recurse
# Because this is a double-module, combine the exports of both modules
# Put the ExportedFunctions of both in the manifest
Update-Metadata -Path $OutputModule.Path -PropertyName FunctionsToExport `
-Value @(
@(
$NestedModule.ExportedFunctions.Keys
$OutputModule.ExportedFunctions.Keys
) | Select-Object -Unique
# @('*')
)
# Put the ExportedAliases of both in the manifest
Update-Metadata -Path $OutputModule.Path -PropertyName AliasesToExport `
-Value @(
@(
$NestedModule.ExportedAliases.Keys
$OutputModule.ExportedAliases.Keys
) | Select-Object -Unique
# @('*')
)
================================================
FILE: .github/workflows/build.yml
================================================
name: Build on push
on: [push]
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: GitVersion
id: gitversion
uses: PoshCode/Actions/gitversion@v1
- name: Install-RequiredModules
uses: PoshCode/Actions/install-requiredmodules@v1
- name: Build Module
id: build
uses: PoshCode/actions/build-module@v1
with:
version: ${{ steps.gitversion.outputs.LegacySemVerPadded }}
destination: ${{github.workspace}}/output
- name: Upload Build Output
uses: actions/upload-artifact@v2
with:
name: Modules
path: ${{github.workspace}}/output
- name: Upload Tests
uses: actions/upload-artifact@v2
with:
name: PesterTests
path: ${{github.workspace}}/Specs
- name: Upload RequiredModules.psd1
uses: actions/upload-artifact@v2
with:
name: RequiredModules
path: ${{github.workspace}}/RequiredModules.psd1
- name: Upload PSScriptAnalyzerSettings.psd1
uses: actions/upload-artifact@v2
with:
name: ScriptAnalyzer
path: ${{github.workspace}}/PSScriptAnalyzerSettings.psd1
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
needs: build
steps:
- name: Download Build Output
uses: actions/download-artifact@v2
- uses: PoshCode/Actions/install-requiredmodules@v1
- uses: PoshCode/Actions/pester@v1
with:
codeCoveragePath: Modules/Configuration
moduleUnderTest: Configuration
additionalModulePaths: ${{github.workspace}}/Modules
- name: Upload Results
uses: actions/upload-artifact@v2
with:
name: Pester Results
path: ${{github.workspace}}/*.xml
================================================
FILE: .gitignore
================================================
/output/
/[0-9]*/
/.vs/
RequiredModules/
node_modules/
results.xml
coverage.xml
================================================
FILE: .vscode/launch.json
================================================
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "PowerShell",
"request": "launch",
"name": "Gherkin All Tests w/Code Coverage",
"preLaunchTask": "build",
"script": "Invoke-Gherkin",
"args": [
"-PesterOption @{ IncludeVSCodeMarker = $True }",
"-CodeCoverage Output\\*.psm1"
],
"cwd": "${workspaceFolder}",
"createTemporaryIntegratedConsole": true
},
{
"type": "PowerShell",
"request": "launch",
"name": "Gherkin Current Test File w/Args Prompt",
"preLaunchTask": "build",
"script": "$env:PSModulePath = '${workspaceFolder}\\Output;${env:PSModulePath};Import-Module Pester; Invoke-Gherkin -Path '${file}'",
"args": [
"-PesterOption @{ IncludeVSCodeMarker = $True }",
"${command:SpecifyScriptArgs}"
],
"cwd": "${workspaceFolder}",
"createTemporaryIntegratedConsole": true
},
{
"type": "PowerShell",
"request": "launch",
"name": "PowerShell Interactive Session",
"cwd": "${workspaceFolder}",
"createTemporaryIntegratedConsole": true
},
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"files.defaultLanguage": "powershell",
"powershell.codeFormatting.preset": "OTBS",
"powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline",
"powershell.codeFormatting.useCorrectCasing": true,
"powershell.scriptAnalysis.enable": true,
"powershell.scriptAnalysis.settingsPath": "PSScriptAnalyzerSettings.psd1"
}
================================================
FILE: .vscode/taskmarks.json
================================================
{
"activeTaskName": "default",
"tasks": [
{
"name": "default",
"files": []
}
]
}
================================================
FILE: .vscode/tasks.json
================================================
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "build",
"group": {
"kind": "build",
"isDefault": true
},
"type": "shell",
"command": "${workspaceFolder}\\build.ps1",
"args": [
"-OutputDirectory", "${workspaceFolder}"
],
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
}
},
{
"label": "analyze",
"group": "test",
"type": "shell",
"command": "Invoke-ScriptAnalyzer",
"args": [
"-Path", "(Get-Module Configuration -List | Sort Version -Desc | Select -First 1 -Expand ModuleBase)"
],
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
}
},
{
"label": "test",
"group": {
"kind": "test",
"isDefault": true
},
"type": "shell",
"options": {
"cwd": "${workspaceFolder}",
},
"command": "Invoke-Gherkin",
"args": [
"-Path", "${workspaceFolder}\\Specs",
"-PesterOption", "@{ IncludeVSCodeMarker = $True }",
"-CodeCoverage", "(Convert-Path (Join-Path (Split-Path (Get-Module Configuration -List | Sort Version -Desc | Select -First 1 -Expand ModuleBase)) *.psm1))"
],
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "shared",
"showReuseMessage": true,
"clear": true
}
}
]
}
================================================
FILE: Benchmark/Benchmark.ps1
================================================
[CmdletBinding()]
param([int]$Count = 10)
$dataPath = Join-Path $PSScriptRoot "../Benchmark/Data/Configuration.psd1"
foreach ($Version in Get-Module Configuration -ListAvailable | Sort-Object Version) {
Remove-Module Configuration -Force
Import-Module $Version.Path -Force
$Configuration = Get-Module Configuration
Write-Host "${fg:white}$($Configuration.Name) v$($Configuration.Version)$(if(($pre=$Configuration.PrivateData.PSData.PreRelease)) {"-$pre"})"
$ToTime = [System.Collections.Generic.List[timespan]]::new()
$FromTime = [System.Collections.Generic.List[timespan]]::new()
$Timer = [System.Diagnostics.Stopwatch]::new()
for ($i=0;$i -lt $Count; $i++) {
$Timer.Restart()
$inputObject = ConvertFrom-Metadata -InputObject (Get-Content -Raw $dataPath)
$FromTime.Add($Timer.Elapsed)
$outputObject = ConvertTo-Metadata -InputObject $inputObject
$ToTime.Add($Timer.Elapsed)
}
$From = $FromTime | Measure-Object TotalMilliseconds -Sum -Average -Maximum -Minimum
$To = $ToTime | Measure-Object TotalMilliseconds -Sum -Average -Maximum -Minimum
Write-Host " ${fg:grey}ConvertTo-Metadata completed in ${fg:Cyan}$($To.Average) milliseconds${fg:grey} on average. Max $($To.Maximum) to $($To.Minimum) minimum."
Write-Host " ${fg:grey}ConvertFrom-Metadata completed in ${fg:Cyan}$($From.Average) milliseconds${fg:grey} on average. Max $($From.Maximum) to $($From.Minimum) minimum."
}
================================================
FILE: Benchmark/Data/Configuration.psd1
================================================
@{
CurrentColorTheme = 'devblackops'
CurrentIconTheme = 'devblackops'
Themes = @{
Color = @{
devblackops = @{
Name = 'devblackops'
Types = @{
Directories = @{
'' = '[0m'
WellKnown = @{
docs = '00BFFF'
'.github' = 'C0C0C0'
media = 'D3D3D3'
tests = '87CEEB'
'.git' = 'FF4500'
'.vscode' = '87CEFA'
src = '00FF7F'
images = '9ACD32'
fonts = 'DC143C'
}
}
Files = @{
'.lua' = '87CEFA'
'.mpv' = 'FFA500'
'.mp4' = 'FFA500'
'.asc' = '66CDAA'
'.apx' = '20B2AA'
'.xml' = '98FB98'
'.vsix' = '6495ED'
'.mpg' = 'FFA500'
'.eps' = '20B2AA'
'.bmap' = 'DC143C'
'.pub' = '66CDAA'
'.pl' = '8A2BE2'
'.raw' = '20B2AA'
'.ini' = '6495ED'
WellKnown = @{
'firebase.json' = 'FFA500'
'.clang-tidy' = '87CEEB'
'code_of_conduct.txt' = 'FFFFE0'
'docker-compose.yml' = '4682B4'
Dockerfile = '4682B4'
'vue.config.js' = '778899'
'.gitlab-ci.yml' = 'FF4500'
'docker-compose.yaml' = '4682B4'
'CHANGELOG.md' = '98FB98'
'docker-compose.dev.yml' = '4682B4'
'.bowerrc' = 'CD5C5C'
'.gitkeep' = 'FF4500'
'.gitattributes' = 'FF4500'
'.nmpignore' = '00BFFF'
LICENSE = 'CD5C5C'
'.gitignore' = 'FF4500'
'.htaccess' = '9ACD32'
'.esmrc' = '6B8E23'
'.esformatter' = 'F4A460'
'LICENSE.txt' = 'CD5C5C'
'package-lock.json' = '6B8E23'
'.tsbuildinfo' = 'F4A460'
'docker-compose.staging.yml' = '4682B4'
'.nvmrc' = '6B8E23'
CHANGELOG = '98FB98'
'bitbucket-pipelines.yaml' = '87CEFA'
'authors.md' = 'FF6347'
'.DS_Store' = '696969'
'README.md' = '00FFFF'
'cdp.pid' = 'F4A460'
'composer.lock' = 'F4A460'
'.jsbeautifyrc' = 'F4A460'
'gulpfile.babel.js' = 'CD5C5C'
'docker-compose.test.yml' = '4682B4'
authors = 'FF6347'
'.azure-pipelines.yml' = '00BFFF'
'git-history' = 'FF4500'
'docker-compose.production.yml' = '4682B4'
'.travis.yml' = 'FFE4B5'
'.mrconfig' = '87CEEB'
README = '00FFFF'
'.jscsrc' = 'F4A460'
'manifest.mf' = '87CEEB'
'package.json' = '6B8E23'
'docker-compose.ci.yml' = '4682B4'
'tslint.json' = 'F4A460'
'.jshintrc' = 'F4A460'
'code_of_conduct.md' = 'FFFFE0'
'authors.txt' = 'FF6347'
'tsconfig.json' = 'F4A460'
'LICENSE.md' = 'CD5C5C'
'.buildignore' = '87CEEB'
'favicon.ico' = 'FFD700'
'CHANGELOG.txt' = '98FB98'
'.gitmodules' = 'FF4500'
'.jenkinsfile' = '6495ED'
'.jshintignore' = '87CEEB'
'gulpfile.js' = 'CD5C5C'
'docker-compose.local.yml' = '4682B4'
'bower.json' = 'CD5C5C'
'.npmrc' = '00BFFF'
'.clang-format' = '87CEEB'
'vue.config.ts' = '778899'
'.firebaserc' = 'FFA500'
'docker-compose.override.yml' = '4682B4'
'README.txt' = '00FFFF'
'docker-compose.prod.yml' = '4682B4'
'.yardopts' = '87CEEB'
'gulpfile.ts' = 'CD5C5C'
'bitbucket-pipelines.yml' = '87CEFA'
}
'.ps1' = '00BFFF'
'.dockerfile' = '4682B4'
'.suit' = 'DC143C'
'.fsi' = '00BFFF'
'.pem' = '66CDAA'
'.br' = 'DAA520'
'.cpp' = 'A9A9A9'
'.wmv' = 'FFA500'
'.properties' = '6495ED'
'.markdown' = '00BFFF'
'.sql' = 'FFD700'
'.dds' = '20B2AA'
'.cs' = '7B68EE'
'.aiff' = 'DB7093'
'.ps1xml' = '00BFFF'
'.bat' = '008000'
'.vcxitems.filters' = 'EE82EE'
'.vsxproj.filters' = 'EE82EE'
'.cert' = 'FF6347'
'.vob' = 'FFA500'
'.sqlite' = 'FFD700'
'.woff' = 'DC143C'
'.ttc' = 'DC143C'
'.exr' = '20B2AA'
'.webp' = '20B2AA'
'.manifest' = '98FB98'
'.hs' = '9932CC'
'.tif' = '20B2AA'
'.go' = '20B2AA'
'.json' = 'FFD700'
'.potx' = 'DC143C'
'.crt' = 'FF6347'
'' = '[0m'
'.jpg' = '20B2AA'
'.jb2' = '20B2AA'
'.rs' = 'FF4500'
'.yaml' = 'FF6347'
'.yuv' = 'FFA500'
'.dll' = '87CEEB'
'.flv' = 'FFA500'
'.mrg' = 'DC143C'
'.zip' = 'DAA520'
'.vue' = '20B2AA'
'.conf' = '6495ED'
'.sublime-project' = 'F4A460'
'.pdb' = 'FFD700'
'.jxr' = '20B2AA'
'.rtf' = '00BFFF'
'.xlsx' = '9ACD32'
'.ico' = '20B2AA'
'.js' = 'F0E68C'
'.m2v' = 'FFA500'
'.fpx' = '20B2AA'
'.exe' = '00FA9A'
'.pic' = '20B2AA'
'.cur' = '20B2AA'
'.cfg' = '6495ED'
'.pkb' = 'FFD700'
'.vscodeignore' = '6495ED'
'.brotli' = 'DAA520'
'.cer' = 'FF6347'
'.applescript' = '4682B4'
'.key' = '66CDAA'
'.settings' = '6495ED'
'.mov' = 'FFA500'
'.toml' = '6495ED'
'.tgz' = 'DAA520'
'.ppam' = 'DC143C'
'.ntf' = 'DC143C'
'.rb' = 'FF0000'
'.pgf' = '20B2AA'
'.m4a' = 'DB7093'
'.reg' = '6495ED'
'.gemfile' = 'FF0000'
'.vcxitems' = 'EE82EE'
'.rar' = 'DAA520'
'.xhtml' = 'CD5C5C'
'.html' = 'CD5C5C'
'.ppsx' = 'DC143C'
'.csx' = '7B68EE'
'.mjs' = 'F0E68C'
'.psb' = '20B2AA'
'.mkv' = 'FFA500'
'.xquery' = '98FB98'
'.tiff' = '20B2AA'
'.sln' = 'EE82EE'
'.less' = '6B8E23'
'.mpeg' = 'FFA500'
'.fonts' = 'DC143C'
'.ttf' = 'DC143C'
'.rst' = '00BFFF'
'.font' = 'DC143C'
'.ts' = 'F0E68C'
'.php' = '6A5ACD'
'.htm' = 'CD5C5C'
'.psd' = '20B2AA'
'.7z' = 'DAA520'
'.brk' = '20B2AA'
'.docx' = '00BFFF'
'.sh' = 'FF4500'
'.cljs' = '00FF7F'
'.props' = '6495ED'
svg = 'F4A460'
'.log' = 'F0E68C'
'.img' = '20B2AA'
'.dart' = '4682B4'
'.sui' = 'DC143C'
'.option' = '6495ED'
'.gbr' = '20B2AA'
'.bz' = 'DAA520'
'.avi' = 'FFA500'
'.suo' = 'EE82EE'
'.clixml' = '00BFFF'
'.jsx' = '20B2AA'
'.ics' = '00CED1'
'.pdf' = 'CD5C5C'
'.css' = '87CEFA'
pssc = '00BFFF'
'.pptx' = 'DC143C'
'.bmp' = '20B2AA'
'.dockerignore' = '4682B4'
'.potm' = 'DC143C'
'.txt' = '00CED1'
'.xls' = '9ACD32'
'.clj' = '00FF7F'
'.psc1' = '00BFFF'
'.fnt' = 'DC143C'
'.vcxproj' = 'EE82EE'
'.ogv' = 'FFA500'
'.dng' = '20B2AA'
'.woff2' = 'DC143C'
'.sln.dotsettings' = '6495ED'
'.dlc' = '6495ED'
'.psql' = 'FFD700'
'.pps' = 'DC143C'
'.code-workplace' = '6495ED'
'.flac' = 'DB7093'
'.exs' = '8B4513'
'.mp3' = 'DB7093'
'.gzip' = 'DAA520'
'.xaml' = '87CEFA'
'.otf' = 'DC143C'
'.psm1' = '00BFFF'
'.tsx' = '20B2AA'
'.c' = 'A9A9A9'
'.leex' = '8B4513'
'.pbm' = '20B2AA'
'.jbig2' = '20B2AA'
'.rmvb' = 'FFA500'
'.resx' = '98FB98'
'.groovy' = '87CEFA'
'.ogg' = 'FFA500'
'.qt' = 'FFA500'
'.rm' = 'FFA500'
'.fsx' = '00BFFF'
'.mpe' = 'FFA500'
'.sass' = 'FF00FF'
'.pgsql' = 'FFD700'
'.mp2' = 'FFA500'
'.code-workspace' = '00BFFF'
'.prefs' = '6495ED'
'.vsixmanifest' = '6495ED'
'.elm' = '9932CC'
'.xsd' = '98FB98'
'.postgres' = 'FFD700'
'.cmd' = '008000'
'.eex' = '8B4513'
'.xsl' = '98FB98'
'.asp' = 'CD5C5C'
'.wma' = 'DB7093'
'.tsv' = '9ACD32'
'.config' = '6495ED'
'.xslt' = '98FB98'
'.psd1' = '00BFFF'
'.csproj' = 'EE82EE'
'.ppsm' = 'DC143C'
'.odttf' = 'DC143C'
'.sublime-workspace' = 'F4A460'
'.cljc' = '00FF7F'
'.erb' = 'FF0000'
'.pfx' = 'FF6347'
'.gifv' = 'FFA500'
'.webm' = 'FFA500'
'.sln.dotsettings.user' = '6495ED'
'.erl' = 'FF6347'
'.lock' = 'DAA520'
'.project' = '98FB98'
'.ex' = '8B4513'
'.jng' = '20B2AA'
'.fs' = '00BFFF'
'.gz' = 'DAA520'
'.accdb' = 'FFD700'
'.plist' = '98FB98'
'.chm' = '87CEEB'
'.xz' = 'DAA520'
'.eot' = 'DC143C'
'.html_vm' = 'CD5C5C'
'.user' = '00BFFF'
'.dtd' = '98FB98'
'.tsbuildinfo' = 'FFD700'
'.tmLanguage' = '98FB98'
'.iml' = '98FB98'
'.ami' = '20B2AA'
'.jpeg' = '20B2AA'
'.esx' = 'F0E68C'
'.gpg' = '66CDAA'
'.md' = '00BFFF'
'.gif' = '20B2AA'
'.bzip2' = 'DAA520'
'.patch' = 'FF4500'
'.bpg' = '20B2AA'
'.vbs' = 'EE82EE'
'.pptm' = 'DC143C'
'.ruleset' = 'EE82EE'
'.pks' = 'FFD700'
'.vb' = 'EE82EE'
'.csv' = '9ACD32'
'.doc' = '00BFFF'
'.yml' = 'FF6347'
'.fsproj' = '00BFFF'
'.prop' = '6495ED'
'.ppa' = 'DC143C'
'.tar' = 'DAA520'
'.ppt' = 'DC143C'
'.mdb' = 'FFD700'
'.png' = '20B2AA'
}
}
}
}
Icon = @{
devblackops = @{
Name = 'devblackops'
Types = @{
Directories = @{
WellKnown = @{
'.github' = 'nf-custom-folder_github'
docs = 'nf-fa-folder'
'.git' = 'nf-custom-folder_git'
images = 'nf-mdi-folder_image'
'.vscode' = 'nf-custom-folder_config'
}
'' = 'nf-fa-folder'
}
Files = @{
'.conf' = 'nf-fa-gear'
'.vob' = 'nf-fa-file_video_o'
'.php' = 'nf-dev-php'
'.mjs' = 'nf-dev-javascript'
'.hs' = 'nf-dev-haskell'
'.erl' = 'nf-dev-erlang'
'.ppt' = 'nf-mdi-file_powerpoint'
'.xslt' = 'nf-mdi-xml'
'.toml' = 'nf-fa-gear'
'.user' = 'nf-mdi-visualstudio'
WellKnown = @{
'.jshintignore' = 'nf-fa-gear'
'docker-compose.ci.yml' = 'nf-dev-docker'
'.firebaserc' = 'nf-dev-firebase'
'CHANGELOG.txt' = 'nf-fae-checklist_o'
'code_of_conduct.md' = 'nf-mdi-check_circle'
'.gitmodules' = 'nf-dev-git'
'bitbucket-pipelines.yaml' = 'nf-dev-bitbucket'
'.esmrc' = 'nf-dev-nodejs_small'
'.bowerrc' = 'nf-dev-bower'
'cdp.pid' = 'nf-seti-json'
'docker-compose.override.yml' = 'nf-dev-docker'
'README.md' = 'nf-mdi-library_books'
'authors.md' = 'nf-oct-person'
'manifest.mf' = 'nf-fa-gear'
'.tsbuildinfo' = 'nf-seti-json'
'.jshintrc' = 'nf-seti-json'
'gulpfile.ts' = 'nf-dev-gulp'
'.travis.yml' = 'nf-dev-travis'
'docker-compose.production.yml' = 'nf-dev-docker'
'gulpfile.babel.js' = 'nf-dev-gulp'
'.yardopts' = 'nf-fa-gear'
'.mrconfig' = 'nf-fa-gear'
'gulpfile.js' = 'nf-dev-gulp'
'.gitignore' = 'nf-dev-git'
'.jscsrc' = 'nf-seti-json'
'git-history' = 'nf-dev-git'
'authors.txt' = 'nf-oct-person'
'docker-compose.staging.yml' = 'nf-dev-docker'
'tslint.json' = 'nf-seti-json'
'package-lock.json' = 'nf-dev-nodejs_small'
'bower.json' = 'nf-dev-bower'
'package.json' = 'nf-dev-nodejs_small'
'.DS_Store' = 'nf-fa-file_o'
'.nmpignore' = 'nf-dev-npm'
'docker-compose.prod.yml' = 'nf-dev-docker'
'.gitkeep' = 'nf-dev-git'
'code_of_conduct.txt' = 'nf-mdi-check_circle'
'.buildignore' = 'nf-fa-gear'
'docker-compose.local.yml' = 'nf-dev-docker'
'tsconfig.json' = 'nf-seti-json'
'.gitattributes' = 'nf-dev-git'
'docker-compose.dev.yml' = 'nf-dev-docker'
'.azure-pipelines.yml' = 'nf-mdi-azure'
'.htaccess' = 'nf-mdi-xml'
'docker-compose.yaml' = 'nf-dev-docker'
'docker-compose.test.yml' = 'nf-dev-docker'
'firebase.json' = 'nf-dev-firebase'
'bitbucket-pipelines.yml' = 'nf-dev-bitbucket'
'favicon.ico' = 'nf-seti-favicon'
'composer.lock' = 'nf-seti-json'
'.clang-format' = 'nf-fa-gear'
'.jenkinsfile' = 'nf-dev-jenkins'
'.jsbeautifyrc' = 'nf-seti-json'
'.gitlab-ci.yml' = 'nf-fa-gitlab'
'vue.config.ts' = 'nf-mdi-vuejs'
'CHANGELOG.md' = 'nf-fae-checklist_o'
'.clang-tidy' = 'nf-fa-gear'
CHANGELOG = 'nf-fae-checklist_o'
'README.txt' = 'nf-mdi-library_books'
'docker-compose.yml' = 'nf-dev-docker'
LICENSE = 'nf-mdi-certificate'
README = 'nf-mdi-library_books'
'.npmrc' = 'nf-dev-npm'
Dockerfile = 'nf-dev-docker'
'.nvmrc' = 'nf-dev-nodejs_small'
'.esformatter' = 'nf-seti-json'
'vue.config.js' = 'nf-mdi-vuejs'
authors = 'nf-oct-person'
}
'.flac' = 'nf-fa-file_audio_o'
'.suo' = 'nf-dev-visualstudio'
'.bmp' = 'nf-fa-file_image_o'
'.ntf' = 'nf-fa-font'
'.asp' = 'nf-seti-html'
'.clixml' = 'nf-dev-code_badge'
'.log' = 'nf-mdi-view_list'
'.pgsql' = 'nf-dev-database'
'.settings' = 'nf-fa-gear'
'.htm' = 'nf-seti-html'
'.gpg' = 'nf-fa-key'
'.exr' = 'nf-fa-file_image_o'
'.rst' = 'nf-dev-markdown'
'.erb' = 'nf-oct-ruby'
'.config' = 'nf-fa-gear'
'.exs' = 'nf-custom-elixir'
'.cljc' = 'nf-dev-clojure'
'.font' = 'nf-fa-font'
'.ppa' = 'nf-mdi-file_powerpoint'
'.csv' = 'nf-mdi-file_excel'
'.lock' = 'nf-fa-lock'
'.pptx' = 'nf-mdi-file_powerpoint'
'.fsi' = 'nf-dev-fsharp'
'.dockerignore' = 'nf-dev-docker'
'.m2v' = 'nf-fa-file_video_o'
'.cmd' = 'nf-custom-msdos'
'.plist' = 'nf-mdi-xml'
'.vscodeignore' = 'nf-fa-gear'
'.doc' = 'nf-mdi-file_word'
'.ps1' = 'nf-dev-terminal_badge'
'.avi' = 'nf-fa-file_video_o'
'.png' = 'nf-fa-file_image_o'
'.woff' = 'nf-fa-font'
'.xsd' = 'nf-mdi-xml'
'.ts' = 'nf-seti-typescript'
'.cer' = 'nf-fa-certificate'
'.cljs' = 'nf-dev-clojure'
'.pem' = 'nf-fa-key'
'.prefs' = 'nf-fa-gear'
'.yaml' = 'nf-mdi-format_align_left'
'.tiff' = 'nf-fa-file_image_o'
'.ex' = 'nf-custom-elixir'
'.fsx' = 'nf-dev-fsharp'
'.ami' = 'nf-fa-file_image_o'
'.fnt' = 'nf-fa-font'
'.patch' = 'nf-dev-git'
'.esx' = 'nf-dev-javascript'
'.sublime-project' = 'nf-dev-sublime'
'.csx' = 'nf-mdi-language_csharp'
'.fonts' = 'nf-fa-font'
'.sh' = 'nf-oct-terminal'
'.pptm' = 'nf-mdi-file_powerpoint'
'.cert' = 'nf-fa-certificate'
'.bat' = 'nf-custom-msdos'
'.xz' = 'nf-oct-file_zip'
'.zip' = 'nf-oct-file_zip'
'.rm' = 'nf-fa-file_video_o'
'.dll' = 'nf-fa-archive'
'.csproj' = 'nf-dev-visualstudio'
'.cs' = 'nf-mdi-language_csharp'
'.psb' = 'nf-fa-file_image_o'
'.ttc' = 'nf-fa-font'
'.pgf' = 'nf-fa-file_image_o'
'.tmLanguage' = 'nf-mdi-xml'
'.webm' = 'nf-fa-file_video_o'
'.gzip' = 'nf-oct-file_zip'
'.jpeg' = 'nf-fa-file_image_o'
'.webp' = 'nf-fa-file_image_o'
'.mp4' = 'nf-fa-file_video_o'
'.jxr' = 'nf-fa-file_image_o'
'.vcxitems' = 'nf-dev-visualstudio'
'.mov' = 'nf-fa-file_video_o'
'.xquery' = 'nf-mdi-xml'
'.ppsx' = 'nf-mdi-file_powerpoint'
'.gbr' = 'nf-fa-file_image_o'
'.gemfile' = 'nf-oct-ruby'
'.vsxproj.filters' = 'nf-dev-visualstudio'
'.asc' = 'nf-fa-key'
'.reg' = 'nf-fa-gear'
'.sln' = 'nf-dev-visualstudio'
'.json' = 'nf-seti-json'
'.html_vm' = 'nf-seti-html'
'.vbs' = 'nf-dev-visualstudio'
'.mpg' = 'nf-fa-file_video_o'
svg = 'nf-mdi-svg'
'.option' = 'nf-fa-gear'
'.js' = 'nf-dev-javascript'
'.manifest' = 'nf-mdi-xml'
'.go' = 'nf-dev-go'
'.mpe' = 'nf-fa-file_video_o'
'.accdb' = 'nf-dev-database'
'.mrg' = 'nf-fa-font'
'.eot' = 'nf-fa-font'
'.postgres' = 'nf-dev-database'
'.prop' = 'nf-fa-gear'
'.jng' = 'nf-fa-file_image_o'
'.docx' = 'nf-mdi-file_word'
'.resx' = 'nf-mdi-xml'
'.vsix' = 'nf-fa-gear'
'.psd1' = 'nf-dev-terminal_badge'
'.txt' = 'nf-mdi-file_document'
'.potx' = 'nf-mdi-file_powerpoint'
'.vue' = 'nf-mdi-vuejs'
'.yuv' = 'nf-fa-file_video_o'
'.jbig2' = 'nf-fa-file_image_o'
'.bpg' = 'nf-fa-file_image_o'
'.pkb' = 'nf-dev-database'
'.project' = 'nf-mdi-xml'
'.dng' = 'nf-fa-file_image_o'
'.odttf' = 'nf-fa-font'
'.dtd' = 'nf-mdi-xml'
'.bz' = 'nf-oct-file_zip'
'.pbm' = 'nf-fa-file_image_o'
'.fsproj' = 'nf-dev-fsharp'
'.props' = 'nf-fa-gear'
'.iml' = 'nf-mdi-xml'
'.qt' = 'nf-fa-file_video_o'
'.md' = 'nf-dev-markdown'
'.mdb' = 'nf-dev-database'
'.fpx' = 'nf-fa-file_image_o'
'.jb2' = 'nf-fa-file_image_o'
'.tgz' = 'nf-oct-file_zip'
'.clj' = 'nf-dev-clojure'
'.sass' = 'nf-dev-sass'
'.pic' = 'nf-fa-file_image_o'
pssc = 'nf-dev-terminal_badge'
'.wma' = 'nf-fa-file_audio_o'
'.rb' = 'nf-oct-ruby'
'.pps' = 'nf-mdi-file_powerpoint'
'.eex' = 'nf-custom-elixir'
'.code-workplace' = 'nf-fa-gear'
'.lua' = 'nf-seti-lua'
'.m4a' = 'nf-fa-file_audio_o'
'.vcxitems.filters' = 'nf-dev-visualstudio'
'.ogg' = 'nf-fa-file_video_o'
'.html' = 'nf-seti-html'
'.ics' = 'nf-fa-calendar'
'.eps' = 'nf-fa-file_image_o'
'.raw' = 'nf-fa-file_image_o'
'.xhtml' = 'nf-seti-html'
'.properties' = 'nf-fa-gear'
'.less' = 'nf-dev-less'
'.mpeg' = 'nf-fa-file_video_o'
'.gz' = 'nf-oct-file_zip'
'.xaml' = 'nf-mdi-xaml'
'.yml' = 'nf-mdi-format_align_left'
'.groovy' = 'nf-dev-groovy'
'.aiff' = 'nf-fa-file_audio_o'
'.mp3' = 'nf-fa-file_audio_o'
'.pl' = 'nf-dev-perl'
'.jpg' = 'nf-fa-file_image_o'
'.vb' = 'nf-dev-visualstudio'
'.jsx' = 'nf-dev-react'
'.brotli' = 'nf-oct-file_zip'
'.psc1' = 'nf-dev-terminal_badge'
'.ini' = 'nf-fa-gear'
'.vsixmanifest' = 'nf-fa-gear'
'.gif' = 'nf-fa-file_image_o'
'.xml' = 'nf-mdi-xml'
'.dockerfile' = 'nf-dev-docker'
'.ruleset' = 'nf-dev-visualstudio'
'.tsx' = 'nf-dev-react'
'.crt' = 'nf-fa-certificate'
'.applescript' = 'nf-dev-apple'
'.cfg' = 'nf-fa-gear'
'.tsbuildinfo' = 'nf-seti-json'
'.ttf' = 'nf-fa-font'
'.vcxproj' = 'nf-dev-visualstudio'
'.bzip2' = 'nf-oct-file_zip'
'.suit' = 'nf-fa-font'
'.chm' = 'nf-mdi-help_box'
'.rtf' = 'nf-mdi-file_word'
'.cur' = 'nf-fa-file_image_o'
'.xls' = 'nf-mdi-file_excel'
'.xlsx' = 'nf-mdi-file_excel'
'.7z' = 'nf-oct-file_zip'
'.sui' = 'nf-fa-font'
'.br' = 'nf-oct-file_zip'
'.bmap' = 'nf-fa-font'
'.psm1' = 'nf-dev-terminal_badge'
'.ppsm' = 'nf-mdi-file_powerpoint'
'.css' = 'nf-dev-css3'
'.leex' = 'nf-custom-elixir'
'.key' = 'nf-fa-key'
'.markdown' = 'nf-dev-markdown'
'.psql' = 'nf-dev-database'
'.sln.dotsettings.user' = 'nf-fa-gear'
'.rs' = 'nf-dev-rust'
'.dart' = 'nf-dev-dart'
'.mkv' = 'nf-fa-file_video_o'
'.mp2' = 'nf-fa-file_video_o'
'.pub' = 'nf-fa-key'
'.dds' = 'nf-fa-file_image_o'
'.mpv' = 'nf-fa-file_video_o'
'.code-workspace' = 'nf-mdi-visualstudio'
'.pdb' = 'nf-dev-database'
'.tif' = 'nf-fa-file_image_o'
'.pks' = 'nf-dev-database'
'.pfx' = 'nf-fa-certificate'
'.elm' = 'nf-custom-elm'
'.sqlite' = 'nf-dev-database'
'.otf' = 'nf-fa-font'
'.cpp' = 'nf-mdi-language_cpp'
'.sublime-workspace' = 'nf-dev-sublime'
'.brk' = 'nf-fa-file_image_o'
'.woff2' = 'nf-fa-font'
'.pdf' = 'nf-mdi-file_pdf'
'.rar' = 'nf-oct-file_zip'
'.flv' = 'nf-fa-file_video_o'
'.sql' = 'nf-dev-database'
'.psd' = 'nf-fa-file_image_o'
'.img' = 'nf-fa-file_image_o'
'.exe' = 'nf-mdi-application'
'.gifv' = 'nf-fa-file_video_o'
'.c' = 'nf-mdi-language_c'
'.ico' = 'nf-fa-file_image_o'
'.ps1xml' = 'nf-dev-terminal_badge'
'.apx' = 'nf-fa-file_image_o'
'.fs' = 'nf-dev-fsharp'
'.ogv' = 'nf-fa-file_video_o'
'.tar' = 'nf-oct-file_zip'
'.sln.dotsettings' = 'nf-fa-gear'
'.wmv' = 'nf-fa-file_video_o'
'.rmvb' = 'nf-fa-file_video_o'
'.xsl' = 'nf-mdi-xml'
'.tsv' = 'nf-mdi-file_excel'
'.potm' = 'nf-mdi-file_powerpoint'
'.ppam' = 'nf-mdi-file_powerpoint'
'.dlc' = 'nf-fa-gear'
'' = 'nf-fa-file'
}
}
}
}
}
}
================================================
FILE: Build.ps1
================================================
#requires -Module @{ModuleName = "ModuleBuilder"; ModuleVersion = "2.0.0"}, Configuration
[CmdletBinding()]
param(
# A specific folder to build into
$OutputDirectory,
# The version of the output module
[Alias("ModuleVersion")]
[string]$SemVer
)
Push-Location $PSScriptRoot -StackName BuildTestStack
if (-not $Semver -and (Get-Command gitversion -ErrorAction Ignore)) {
if ($semver = gitversion -showvariable SemVer) {
$null = $PSBoundParameters.Add("SemVer", $SemVer)
}
}
try {
Build-Module @PSBoundParameters -Target CleanBuild
} finally {
Pop-Location -StackName BuildTestStack
}
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.6.0] - 2023-08-24
### Added
- Get-ParameterValue. Get parameter values from PSBoundParameters + DefaultValues and optionally, a configuration file.
### Fixed
- Only call Add-MetadataConverter at load if converters are supplied at load time.
## [1.5.1] - 2022-06-06
### Fixed
- Stop re-importing the metadata module at import
## [1.5.0] - 2021-07-03
### Removed
This is the first release without the Metadata module included. This module is now available as a separate module on the PowerShell Gallery.
### Added
Test runs on GitHub Actions now include Linux and Mac OS.
AllowedVariables now flow through the whole module (and into calls to the Metadata module).
================================================
FILE: Examples/TestModuleOne/Configuration.psd1
================================================
@{
Address = "http://PoshCode.org"
Credential = $null
}
================================================
FILE: Examples/TestModuleOne/TestModuleOne.psm1
================================================
# If your default configuration has some blank settings, you can do something like this:
# Assume I have a mandatory credential:
function ImportConfiguration {
$Configuration = Import-Configuration
if(!$Configuration.Credential.Password.Length) {
Write-Warning "Thanks for using the Acme Industries Module, please run Set-AimConfiguration to configure."
throw "Module not configured. Run Set-AimConfiguration"
}
$Configuration
}
function Set-AimConfiguration {
[CmdletBinding()]
param(
[Parameter(Mandatory,ValueFromPipelineByPropertyName)]
[string]$Address,
[Parameter(Mandatory,ValueFromPipelineByPropertyName)]
[ValidateScript({
if ($_.Password.Length -eq 0) {
throw "Credential must include a password."
}
$true
})]
[PSCredential]$Credential
)
end {
$PSBoundParameters | Export-Configuration
}
}
# Test for it **during** module import:
try {
$null = ImportConfiguration
} catch {
# Hide the error on import, just warn them
Write-Host "You must configure module to avoid this warning on first run. Use Set-AimConfiguration" -ForegroundColor Black -BackgroundColor Yellow
}
================================================
FILE: Examples/TestModuleTwo/Configuration.psd1
================================================
@{
Address = "http://PoshCode.org"
}
================================================
FILE: Examples/TestModuleTwo/TestModuleTwo.psm1
================================================
# If your default configuration has valid defaults, but you still want them to review it,
# Provide a public Get-Configuration and test the path(s):
Write-Verbose "No Settings"
function TestStoragePath {
$Path = Get-StoragePath
Test-Path (Join-Path $Path "Configuration.psd1")
}
function Set-AimConfiguration {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string]$Address
)
process {
$PSBoundParameters | Export-Configuration
}
}
function Get-AimConfiguration {
Import-Configuration
}
if (!(TestStoragePath -Verbose)) {
Write-Warning "Not Configured"
Write-Host @"
Welcome first-time users:
You should review and approve the configuration of this module by running:
`$Configuration = Get-AimConfiguration
`$Configuration
And then review the settings. When you're satisfied, approve them by:
Set-AimConfiguration @Configuration
"@ -ForegroundColor Black -BackgroundColor Yellow
}
================================================
FILE: GitVersion.yml
================================================
mode: Mainline
commit-message-incrementing: MergeMessageOnly
assembly-versioning-format: '{Major}.{Minor}.{Patch}.{env:BUILDCOUNT ?? 0}'
assembly-informational-format: '{NuGetVersionV2}+Build.{env:BUILDCOUNT ?? 0}.Date.{CommitDate}.Branch.{env:SAFEBRANCHNAME ?? unknown}.Sha.{Sha}'
commit-date-format: yyyyMMddTHHmmss
branches:
master:
increment: Patch
is-release-branch: true
pull-request:
tag: rc
increment: Patch
feature:
regex: .*/
tag: useBranchName
increment: Patch
source-branches: ['master', 'feature']
track-merge-target: true
release:
tag: ''
regex: releases?[/-]\d+\.\d+\.\d+
increment: Patch
is-release-branch: true
================================================
FILE: LICENSE
================================================
Copyright (c) 2015 Joel Bennett
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: PSScriptAnalyzerSettings.psd1
================================================
@{
Severity = @('Error', 'Warning')
ExcludeRules = @('PSAvoidUsingDeprecatedManifestFields', 'PSPossibleIncorrectUsageOfAssignmentOperator')
}
================================================
FILE: README.md
================================================
[](https://github.com/PoshCode/Configuration/actions/workflows/build.yml)
# The Configuration Module
Configuration commands for storing and loading settings (usually just hashtables, but can be any PSObject). Just pipe your hashtable to `Export-Configuration` and load it back with `Import-Configuration`!
- Ship default configuration files with your module
- Automatically determines where to save your settings
- Supports Windows roaming profiles
- Supports XDG settings for Linux (and MacOS)
- Allow users to configure settings using our commands, or your own custom commands
- Supports _layered_ configurations:
- Machine-level config
- User override files
- Supports automatic configuration of parameters values for a command
- Reads `Noun` configuration files in your working directory
- Filters only values which apply to the current command
- Supports recursively defining defaults in folders
- Supports combining parameter values from PSBoundParameters, DefaultValues, and an optional configuration file.
These modules work back to much older versions of PowerShell, but are currently being tested on Windows PowerShell and PowerShell 7. They depend on the Metadata module for serialization of objects to psd1 format.
## Metadata commands for working with .psd1 are in the [Metadata](https://github.com/PoshCode/Metadata) module
- Manipulating metadata files
- Extensible serialization of types
- Built in support for DateTime, Version, Guid, SecureString, ScriptBlocks and more
- Lets you store almost anything in readable metadata (.psd1) files
- Serializing (`Export`) to metadata (.psd1) files
- Deserializing (`Import`) from metadata (.psd1) files
It supports WindowsPowerShell, as well as PowerShell Core on Windows, Linux and OS X.
## Installation
```posh
Install-Module Configuration
```
## Usage
The Configuration module is designed to be used by other modules (or from scripts) to allow the storage of configuration data (generally, hashtables, but any PSObject).
In its simplest form, you add a `Configuration.psd1` file to a module you're authoring, and put your default settings in it -- perhaps something as simple as this:
```posh
@{
DriveName = "data"
}
```
Then, in your module, you import those settings _in a function_ when you need them, or expose them to your users like this:
```posh
function Get-FaqConfig {
Import-Configuration
}
```
Perhaps, in a simple case like this one, you might write a wrapper function so your users can get _and set_ that one configuration option directly:
```posh
function Get-DataDriveName {
$script:Config = Import-Configuration
$config.DriveName
}
function Set-DataDriveName {
param([Parameter(Mandatory)][String]$Name)
@{ DriveName = $Name} | Export-Config
}
```
Of course, you could have imported the configuration, edited that one setting, and then exported the whole config, but you can also just export a few settings, because `Import-Configuration` supports a layered configuration. More on that in a moment, but first, let's talk about how this all works.
### Versioning
Versioning your configuration is supported, but is only done explicitly (in `Import-Configuration`, `Export-Configuration`, and `Get-ConfigurationPath`). Whenever you need to change your module's configuration in an incompatible way, you can write a migration function that runs at import-time in your new version, something like this:
```powershell
# Specify a script-level version number
$ConfigVersion = @{ Version = 1.1 }
function MigrateData {
# Specify the version you want to migrate
$OldVersion = @{ <# I didn't specify a version at first #> }
# If there are no configuration files, migrate them
if(!(Get-ConfigurationPath @ConfigVersion | Get-ChildItem -File)) {
# Import the old config
$oldConfig = Import-Configuration @OldVersion
# Transform your configuration however you like
$newConfig = @{ PSDriveName = $existing.DriveName }
# Export the new config
$newConfig | Export-Configuration @ConfigVersion
}
}
# Call your migration function during module import
MigrateData
```
Then you just need to be sure you specify the `@ConfigVersion` whenever you call `Import-Configuration` elsewhere in your module.
Note that configuration files are not currently deleted by Uninstall-Module, so they are never automatically cleaned up.
# How it works
The Configuration module works by serializing PowerShell hashtables or custom objects into PowerShell data language in a `Configuration.psd1` file!
## Configuration path
When you `Export-Configuration` you can set the `-Scope`, which determines where the Configuration.psd1 are stored:
- **User** exports to `$Env:LocalAppData` or `~/.config/`
- **Enterprise** exports to `$Env:AppData` (the roaming path) or `~/.local/share/`
- **Machine** exports to `$Env:ProgramData` or `/etc/xdg/`
Note that the linux paths are controlled by XDG environment variables, and the default paths can be overriden by manually editing the Configuration module manifest.
Within that folder, the Configuration module root is "PowerShell," followed by either a company or author and the module name -- within which your configuration file(s) are stored.
From a module that uses Configuration, you can call the `Get-ConfigurationPath` command to get the path to that folder, and since the folder is created for you, you can use it store other files, like cached images, etc.
## Layered Configuration
In addition to automatically determining the storage path, the configuration module supports layered configuration, so that you can have defaults you ship with your module, or configure default at the enterprise or machine level, and still allow users to override the settings. When you call `Import-Configuration` from within a module, it automatically imports _all_ the available files and updates the configuration object which is returned at the end:
1. First, it imports the default Configuration.psd1 from the module's folder.
2. Then it imports machine-wide settings (e.g. the ProgramData folder)
3. Then it imports the users' enterprise roaming settings (e.g. from AppData\Roaming)
4. Finally it imports the users' local settings (from AppData\Local)
Any missing files are just skipped, and each layer of settings updates the settings from the previous layers, so if you don't set a setting in one layer, the setting from the previous layers persists.
However, it's up to individual users and module authors to take advantage of this..
## Serialization
The actual serialization commands (with the `Metadata` noun) are: ConvertFrom, ConvertTo, Import and Export. By default, the Configuration serializer can handle a variety of custom PSObjects, hashtables, and arrays recursively, and has specific handling for booleans, strings and numbers, as well as Versions, GUIDs, and DateTime, DateTimeOffset, and even ScriptBlocks and PSCredential objects.
**Important note:** PSCredentials are stored using ConvertTo-SecureString, and currently only work on Windows. They should be stored in the user scope, since they're serialized per-user, per-machine, using the Windows Data Protection API.
In other words, it handles everything you're likely to need in a configuration file. However, it also has support for adding additional type serializers via the `Add-MetadataConverter` command. If you want to store anything that doesn't work, please raise an issue :wink:.
### One little catch
The configuration module uses the caller's scope to determine the name of the module (and Company or Author name) that is asking for configuration. For this reason you normally just call `Import-Configuration` from within _a function_ **in** your module (to make sure the callstack shows the module scope).
The _very important_ side effect is that you _must not_ change the module name nor the author of your module if you're using this Configuration module, or you will need to manually call `Import-Configuration` with the old information and then `Export` those settings to the new location (see the )
### Using the cmdlets from outside a module
It is possible to use the commands to Import and Export the configuration for a module from outside the module (or from the main module body, instead of a function), simply pipe the ModuleInfo to `Import-Configuration`. To continue our example from earlier:
```posh
$Config = Get-Module DataModule | Import-Configuration
$Config.DriveName = "DataDrive"
Get-Module DataModule | Export-Configuration $Config
```
Note that if you look at the parameter sets for `Import-Configuration` you will find that you can also just pass the the `-Author` (or `-CompanyName`) and module `-Name` by hand, but you must be sure to get them exactly right, or you'll import nothing...
```posh
$Config = Import-Configuration -Name DataModule -Author HuddledMasses.org
```
Because of how easily this can go wrong, I strongly recommend you don't use this syntax -- but if you do, be aware that you must also specify the `-DefaultPath` if you want to load the default configuration file from the module folder.
# A little history
The Configuration module is something I first wrote as part of the PoshCode packaging module and have been meaning to pull out for awhile.
I finally started working on this while I work on writing the Gherkin support for Pester. That support was merged into Pester with the 4.0 release, and I'm using it for the tests in this module.
In any case, this module is mostly code ported from my PoshCode module as I develop the specs (the .feature files) and the Gherkin support to run them! Anything you see here has better than 95% code coverage in the feature and step files, which are executable via `Invoke-Gherkin`.
For the tests to work, you need to make sure that the module isn't already loaded, because the tests import it with the file paths mocked for testing:
```posh
Remove-Module Configuration -ErrorAction SilentlyContinue
Invoke-Gherkin -CodeCoverage *.psm1
```
================================================
FILE: ReBuild.ps1
================================================
#requires -Version "4.0" -Module PackageManagement, Pester
[CmdletBinding()]
param(
# The step(s) to run. Defaults to "Clean", "Update", "Build", "Test", "Package"
# You may also "Publish"
# It's also acceptable to skip the "Clean" and particularly "Update" steps
[ValidateSet("Clean", "Update", "Build", "Test", "Package", "Publish")]
[string[]]$Step = @("Clean", "Update", "Build", "Test"),
# The path to the module to build. Defaults to the folder this script is in.
[Alias("PSPath")]
[string]$Path = $PSScriptRoot,
# The name of the module to build.
# Default is hardcoded to "Configuration" because AppVeyor forces checkout to lowercase path name
[string]$ModuleName = "Configuration",
# The target framework for .net (for packages), with fallback versions
# The default supports PS3: "net40","net35","net20","net45"
# To only support PS4, use: "net45","net40","net35","net20"
# To support PS2, you use: "net35","net20"
[string[]]$TargetFramework = @("net40","net35","net20","net45"),
# The revision number (pulled from the environment in AppVeyor)
[Nullable[int]]$RevisionNumber = ${Env:APPVEYOR_BUILD_NUMBER},
[ValidateNotNullOrEmpty()]
[String]$CodeCovToken = ${ENV:CODECOV_TOKEN},
# The default language is your current UICulture
[Globalization.CultureInfo]$DefaultLanguage = $((Get-Culture).Name)
)
$Script:TraceVerboseTimer = New-Object System.Diagnostics.Stopwatch
$Script:TraceVerboseTimer.Start()
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
function init {
#.Synopsis
# The init step always has to run.
# Calculate your paths and so-on here.
[CmdletBinding()]
param()
# Calculate Paths
# The output path is just a temporary output and logging location
$Script:OutputPath = Join-Path $Path output
if(Test-Path $OutputPath -PathType Leaf) {
throw "Cannot create folder for Configuration because there's a file in the way at '$OutputPath'"
}
if(!(Test-Path $OutputPath -PathType Container)) {
$null = New-Item $OutputPath -Type Directory -Force
}
# We expect the source for the module in a subdirectory called one of three things:
$Script:SourcePath = "src", "source", ${ModuleName} | ForEach-Object { Join-Path $Path $_ -Resolve -ErrorAction Ignore } | Select-Object -First 1
if(!$SourcePath) {
Write-Warning "This Build script expects a 'Source' or '$ModuleName' folder to be alongside it."
throw "Can't find module source folder."
}
$Script:ManifestPath = Join-Path $SourcePath "${ModuleName}.psd1" -Resolve -ErrorAction Ignore
if(!$ManifestPath) {
Write-Warning "This Build script expects a '${ModuleName}.psd1' in the '$SourcePath' folder."
throw "Can't find module source files"
}
$Script:TestPath = "Tests", "Specs" | ForEach-Object { Join-Path $Path $_ -Resolve -ErrorAction Ignore } | Select-Object -First 1
if(!$TestPath) {
Write-Warning "This Build script expects a 'Tests' or 'Specs' folder to contain tests."
}
# Calculate Version here, because we need it for the release path
[Version]$Script:Version = Get-Module $ManifestPath -ListAvailable | Select-Object -ExpandProperty Version
# If the RevisionNumber is specified as ZERO, this is a release build ...
# If the RevisionNumber is not specified, this is a dev box build
# If the RevisionNumber is specified, we assume this is a CI build
if($Script:RevisionNumber -ge 0) {
# For CI builds we don't increment the build number
$Script:Build = if($Version.Build -le 0) { 0 } else { $Version.Build }
} else {
# For dev builds, assume we're working on the NEXT release
$Script:Build = if($Version.Build -le 0) { 1 } else { $Version.Build + 1}
}
if([string]::IsNullOrEmpty($RevisionNumber) -or $RevisionNumber -eq 0) {
$Script:Version = New-Object Version $Version.Major, $Version.Minor, $Build
} else {
$Script:Version = New-Object Version $Version.Major, $Version.Minor, $Build, $RevisionNumber
}
# The release path is where the final module goes
$Script:ReleasePath = Join-Path $Path $Version
$Script:ReleaseManifest = Join-Path $ReleasePath "${ModuleName}.psd1"
}
function clean {
#.Synopsis
# Clean output and old log
[CmdletBinding()]
param(
[Parameter()]
[string]$ReleasePath = $Script:ReleasePath,
# Also clean packages
[Switch]$Packages
)
Trace-Message "OUTPUT Release Path: $ReleasePath"
if(Test-Path $ReleasePath) {
Trace-Message " Clean up old build"
Trace-Message "DELETE $ReleasePath\"
Remove-Item $ReleasePath -Recurse -Force
}
if(Test-Path $Path\packages) {
Trace-Message "DELETE $Path\packages"
# force reinstall by cleaning the old ones
Remove-Item $Path\packages\ -Recurse -Force
}
if(Test-Path $Path\packages\build.log) {
Trace-Message "DELETE $OutputPath\build.log"
Remove-Item $OutputPath\build.log -Recurse -Force
}
}
function update {
#.Synopsis
# Nuget restore and git submodule update
#.Description
# This works like nuget package restore, but using PackageManagement
# The benefit of using PackageManagement is that you can support any provider and any source
# However, currently only the nuget providers supports a -Destination
# So for most cases, you could use nuget restore instead:
# nuget restore $(Join-Path $Path packages.config) -PackagesDirectory "$Path\packages" -ExcludeVersion -PackageSaveMode nuspec
[CmdletBinding()]
param(
# Force reinstall
[switch]$Force=$($Step -contains "Clean"),
# Remove packages first
[switch]$Clean
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
Trace-Message "UPDATE $ModuleName in $Path"
if(Test-Path (Join-Path $Path packages.config)) {
if(!($Name = Get-PackageSource | Where-Object Location -eq 'https://www.nuget.org/api/v2' | ForEach-Object Name)) {
Write-Warning "Adding NuGet package source"
$Name = Register-PackageSource NuGet -Location 'https://www.nuget.org/api/v2' -ForceBootstrap -ProviderName NuGet | Where-Object Name
}
if($Force -and (Test-Path $Path\packages)) {
# force reinstall by cleaning the old ones
remove-item $Path\packages\ -Recurse -Force
}
if(Test-Path $Path\packages\ -PathType Leaf) {
throw "Cannot create folder for Configuration because there's a file in the way at '$Path\packages\'"
}
if(!(Test-Path $Path\packages\ -PathType Container)) {
$null = New-Item $Path\packages\ -Type Directory -Force
}
# Remember, as of now, only nuget actually supports the -Destination flag
foreach($Package in ([xml](Get-Content .\packages.config)).packages.package) {
Trace-Message "Installing $($Package.id) v$($Package.version) from $($Package.Source)"
$null = Install-Package -Name $Package.id -RequiredVersion $Package.version -Source $Package.Source -Destination $Path\packages -Force:$Force -ErrorVariable failure
if($failure) {
throw "Failed to install $($package.id), see errors above."
}
}
}
# we also check for git submodules...
git submodule update --init --recursive
}
function build {
[CmdletBinding()]
param()
Trace-Message "BUILDING: $ModuleName from $Path"
# Copy NuGet dependencies
$PackagesConfig = (Join-Path $Path packages.config)
if(Test-Path $PackagesConfig) {
Trace-Message " Copying Packages"
foreach($Package in ([xml](Get-Content $PackagesConfig)).packages.package) {
$LibPath = "$ReleasePath\lib"
$folder = Join-Path $Path "packages\$($Package.id)*"
# The git NativeBinaries are special -- we need to copy all the "windows" binaries:
if($Package.id -eq "LibGit2Sharp.NativeBinaries") {
$targets = Join-Path $folder 'libgit2\windows'
$LibPath = Join-Path $LibPath "NativeBinaries"
} else {
# Check for each TargetFramework, in order of preference, fall back to using the lib folder
$targets = ($TargetFramework -replace '^','lib\') + 'lib' | ForEach-Object { Join-Path $folder $_ }
}
$PackageSource = Get-Item $targets -ErrorAction SilentlyContinue | Select-Object -First 1 -Expand FullName
if(!$PackageSource) {
throw "Could not find a lib folder for $($Package.id) from package. You may need to run Setup.ps1"
}
Trace-Message "robocopy $PackageSource $LibPath /E /NP /LOG+:'$OutputPath\build.log' /R:2 /W:15"
$null = robocopy $PackageSource $LibPath /E /NP /LOG+:"$OutputPath\build.log" /R:2 /W:15
if($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 1 -and $LASTEXITCODE -ne 3) {
throw "Failed to copy Package $($Package.id) (${LASTEXITCODE}), see build.log for details"
}
}
}
$RootModule = Get-Module $ManifestPath -ListAvailable | Select-Object -ExpandProperty RootModule
if (!$RootModule) {
$RootModule = Get-Module $ManifestPath -ListAvailable | Select-Object -ExpandProperty ModuleToProcess
if (!$RootModule) {
$RootModule = "${ModuleName}.psm1"
}
}
$ReleaseModule = Join-Path $ReleasePath ${RootModule}
## Copy PowerShell source Files (support for my new Public|Private folders, and the old simple copy way)
# if the Source folder has "Public" and optionally "Private" in it, then the psm1 must be assembled:
if(Test-Path (Join-Path $SourcePath Public) -Type Container){
Trace-Message " Collating Module Source"
if(Test-Path $ReleasePath -PathType Leaf) {
throw "Cannot create folder for Configuration because there's a file in the way at '$ReleasePath'"
}
if(!(Test-Path $ReleasePath -PathType Container)) {
$null = New-Item $ReleasePath -Type Directory -Force
}
Trace-Message " Setting content for $ReleaseModule"
$FunctionsToExport = Join-Path $SourcePath Public\*.ps1 -Resolve | ForEach-Object { [System.IO.Path]::GetFileNameWithoutExtension($_) }
Set-Content $ReleaseModule ((
(Get-Content (Join-Path $SourcePath Private\*.ps1) -Raw) +
(Get-Content (Join-Path $SourcePath Public\*.ps1) -Raw)) -join "`r`n`r`n`r`n") -Encoding UTF8
# If there are any folders that aren't Public, Private, Tests, or Specs ...
$OtherFolders = Get-ChildItem $SourcePath -Directory -Exclude Public, Private, Tests, Specs
# Then we need to copy everything in them
Copy-Item $OtherFolders -Recurse -Destination $ReleasePath
# Finally, we need to copy any files in the Source directory
Get-ChildItem $SourcePath -File |
Where-Object Name -ne $RootModule |
Copy-Item -Destination $ReleasePath
Update-Manifest $ReleaseManifest -Property FunctionsToExport -Value $FunctionsToExport
} else {
# Legacy modules just have "stuff" in the source folder and we need to copy all of it
Trace-Message " Copying Module Source"
Trace-Message "COPY $SourcePath\"
$null = robocopy $SourcePath\ $ReleasePath /E /NP /LOG+:"$OutputPath\build.log" /R:2 /W:15
if($LASTEXITCODE -ne 3 -AND $LASTEXITCODE -ne 1) {
throw "Failed to copy Module (${LASTEXITCODE}), see build.log for details"
}
}
# Copy the readme file as an about_ help
$ReadMe = Join-Path $Path Readme.md
if(Test-Path $ReadMe -PathType Leaf) {
$LanguagePath = Join-Path $ReleasePath $DefaultLanguage
if(Test-Path $LanguagePath -PathType Leaf) {
throw "Cannot create folder for Configuration because there's a file in the way at '$LanguagePath'"
}
if(!(Test-Path $LanguagePath -PathType Container)) {
$null = New-Item $LanguagePath -Type Directory -Force
}
$about_module = Join-Path $LanguagePath "about_${ModuleName}.help.txt"
if(!(Test-Path $about_module)) {
Trace-Message "Turn readme into about_module"
Copy-Item -LiteralPath $ReadMe -Destination $about_module
}
}
## Update the PSD1 Version:
Trace-Message " Update Module Version"
Push-Location $ReleasePath
try {
Import-Module $ReleaseModule -Force
$FileList = Get-ChildItem -Recurse -File | Resolve-Path -Relative
Update-Metadata -Path $ReleaseManifest -PropertyName 'ModuleVersion' -Value $Version
Update-Metadata -Path $ReleaseManifest -PropertyName 'FileList' -Value $FileList
Import-Module $ReleaseManifest -Force
} finally {
Pop-Location
}
(Get-Module $ReleaseManifest -ListAvailable | Out-String -stream) -join "`n" | Trace-Message
}
function test {
[CmdletBinding()]
param(
[Switch]$Quiet,
[Switch]$ShowWip,
[int]$FailLimit=${Env:ACCEPTABLE_FAILURE},
[ValidateNotNullOrEmpty()]
[String]$JobID = ${Env:APPVEYOR_JOB_ID}
)
if(!$TestPath) {
Write-Warning "No tests folder found. Invoking Pester in root: $Path"
$TestPath = $Path
}
Trace-Message "TESTING: $ModuleName with $TestPath"
Trace-Message "TESTING $ModuleName v$Version" -Verbose:(!$Quiet)
Remove-Module $ModuleName -ErrorAction SilentlyContinue
Write-Host $(prompt) -NoNewLine
Write-Host Remove-Module $ModuleName -ErrorAction SilentlyContinue
$Options = @{
OutputFormat = "NUnitXml"
OutputFile = (Join-Path $OutputPath TestResults.xml)
}
if($Quiet) { $Options.Quiet = $Quiet }
if(!$ShowWip){ $Options.ExcludeTag = @("wip") }
Set-Content "$TestPath\VersionSpecific.Steps.ps1" "
BeforeEachFeature {
Remove-Module 'Configuration' -ErrorAction Ignore -Force
Import-Module '$ReleasePath\${ModuleName}.psd1' -Force
}
AfterEachFeature {
Remove-Module 'Configuration' -ErrorAction Ignore -Force
Import-Module '$ReleasePath\${ModuleName}.psd1' -Force
}
AfterEachScenario {
if(Test-Path '$ReleasePath\${ModuleName}.psd1.backup') {
Remove-Item '$ReleasePath\${ModuleName}.psd1'
Rename-Item '$ReleasePath\${ModuleName}.psd1.backup' '$ReleasePath\${ModuleName}.psd1'
}
}
"
# Show the commands they would have to run to get these results:
Write-Host $(prompt) -NoNewLine
Write-Host Import-Module $ReleasePath\${ModuleName}.psd1 -Force
Write-Host $(prompt) -NoNewLine
# TODO: Update dependency to Pester 4.0 and use just Invoke-Pester
if(Get-Command Invoke-Gherkin -ErrorAction SilentlyContinue) {
Write-Host Invoke-Gherkin -Path $TestPath -CodeCoverage "$ReleasePath\*.psm1" -PassThru @Options
$TestResults = Invoke-Gherkin -Path $TestPath -CodeCoverage "$ReleasePath\*.psm1" -PassThru @Options
}
# Write-Host Invoke-Pester -Path $TestPath -CodeCoverage "$ReleasePath\*.psm1" -PassThru @Options
# $TestResults = Invoke-Pester -Path $TestPath -CodeCoverage "$ReleasePath\*.psm1" -PassThru @Options
Remove-Module $ModuleName -ErrorAction SilentlyContinue
$script:failedTestsCount = 0
$script:passedTestsCount = 0
foreach($result in $TestResults)
{
if($result -and $result.CodeCoverage.NumberOfCommandsAnalyzed -gt 0)
{
$script:failedTestsCount += $result.FailedCount
$script:passedTestsCount += $result.PassedCount
$CodeCoverageTitle = 'Code Coverage {0:F1}%' -f (100 * ($result.CodeCoverage.NumberOfCommandsExecuted / $result.CodeCoverage.NumberOfCommandsAnalyzed))
# TODO: this file mapping does not account for the new Public|Private module source (and I don't know how to make it do so)
# Map file paths, e.g.: \1.0 back to \src
for($i=0; $i -lt $TestResults.CodeCoverage.HitCommands.Count; $i++) {
$TestResults.CodeCoverage.HitCommands[$i].File = $TestResults.CodeCoverage.HitCommands[$i].File.Replace($ReleasePath, $SourcePath)
}
for($i=0; $i -lt $TestResults.CodeCoverage.MissedCommands.Count; $i++) {
$TestResults.CodeCoverage.MissedCommands[$i].File = $TestResults.CodeCoverage.MissedCommands[$i].File.Replace($ReleasePath, $SourcePath)
}
if($result.CodeCoverage.MissedCommands.Count -gt 0) {
$result.CodeCoverage.MissedCommands |
ConvertTo-Html -Title $CodeCoverageTitle |
Out-File (Join-Path $OutputPath "CodeCoverage-${Version}.html")
}
if(${CodeCovToken})
{
# TODO: https://github.com/PoshCode/PSGit/blob/dev/test/Send-CodeCov.ps1
Trace-Message "Sending CI Code-Coverage Results" -Verbose:(!$Quiet)
$response = &"$TestPath\Send-CodeCov" -CodeCoverage $result.CodeCoverage -RepositoryRoot $Path -OutputPath $OutputPath -Token ${CodeCovToken}
Trace-Message $response.message -Verbose:(!$Quiet)
}
}
}
# If we're on AppVeyor ....
if(Get-Command Add-AppveyorCompilationMessage -ErrorAction SilentlyContinue) {
Add-AppveyorCompilationMessage -Message ("{0} of {1} tests passed" -f @($TestResults.PassedScenarios).Count, (@($TestResults.PassedScenarios).Count + @($TestResults.FailedScenarios).Count)) -Category $(if(@($TestResults.FailedScenarios).Count -gt 0) { "Warning" } else { "Information"})
Add-AppveyorCompilationMessage -Message ("{0:P} of code covered by tests" -f ($TestResults.CodeCoverage.NumberOfCommandsExecuted / $TestResults.CodeCoverage.NumberOfCommandsAnalyzed)) -Category $(if($TestResults.CodeCoverage.NumberOfCommandsExecuted -lt $TestResults.CodeCoverage.NumberOfCommandsAnalyzed) { "Warning" } else { "Information"})
}
if(${JobID}) {
if(Test-Path $Options.OutputFile) {
Trace-Message "Sending Test Results to AppVeyor backend" -Verbose:(!$Quiet)
$wc = New-Object 'System.Net.WebClient'
if($response = $wc.UploadFile("https://ci.appveyor.com/api/testresults/nunit/${JobID}", $Options.OutputFile)) {
if($text = [System.Text.Encoding]::ASCII.GetString($response)) {
Trace-Message $text -Verbose:(!$Quiet)
} else {
Trace-Message "No text in response from AppVeyor" -Verbose:(!$Quiet)
}
} else {
Trace-Message "No response when calling UploadFile to AppVeyor" -Verbose:(!$Quiet)
}
} else {
Write-Warning "Couldn't find Test Output: $($Options.OutputFile)"
}
}
if($FailedTestsCount -gt $FailLimit) {
$exception = New-Object AggregateException "Failed Scenarios:`n`t`t'$($TestResults.FailedScenarios.Name -join "'`n`t`t'")'"
$errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, "FailedScenarios", "LimitsExceeded", $TestResults
$PSCmdlet.ThrowTerminatingError($errorRecord)
}
}
function package {
[CmdletBinding()]
param()
Trace-Message "robocopy '$ReleasePath' '${OutputPath}\${ModuleName}' /MIR /NP "
$null = robocopy $ReleasePath "${OutputPath}\${ModuleName}" /MIR /NP /LOG+:"$OutputPath\build.log"
# Obviously this should be Publish-Module, but this works on appveyor
$zipFile = Join-Path $OutputPath "${ModuleName}-${Version}.zip"
Add-Type -assemblyname System.IO.Compression.FileSystem
Remove-Item $zipFile -ErrorAction SilentlyContinue
Trace-Message "ZIP $zipFile"
[System.IO.Compression.ZipFile]::CreateFromDirectory((Join-Path $OutputPath $ModuleName), $zipFile)
# You can add other artifacts here
ls $OutputPath -File
}
function Trace-Message {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[string]$Message,
[switch]$AsWarning,
[switch]$ResetTimer,
[switch]$KillTimer,
[Diagnostics.Stopwatch]$Stopwatch
)
begin {
if($Stopwatch) {
$Script:TraceTimer = $Stopwatch
$Script:TraceTimer.Start()
}
if(!(Test-Path Variable:Script:TraceTimer)) {
$Script:TraceTimer = New-Object System.Diagnostics.Stopwatch
$Script:TraceTimer.Start()
}
if($ResetTimer)
{
$Script:TraceTimer.Restart()
}
}
process {
$Script = Split-Path $MyInvocation.ScriptName -Leaf
$Command = (Get-PSCallStack)[1].Command
if($Script -ne $Command) {
$Message = "{0} - at {1} Line {2} ({4}) | {3}" -f $Message, $Script, $MyInvocation.ScriptLineNumber, $TraceTimer.Elapsed, $Command
} else {
$Message = "{0} - at {1} Line {2} | {3}" -f $Message, $Script, $MyInvocation.ScriptLineNumber, $TraceTimer.Elapsed
}
if($AsWarning) {
Write-Warning $Message
} else {
Write-Verbose $Message
}
}
end {
if($KillTimer) {
$Script:TraceTimer.Stop()
$Script:TraceTimer = $null
}
}
}
# First call to Trace-Message, pass in our TraceTimer to make sure we time EVERYTHING.
Trace-Message "BUILDING: $ModuleName in $Path" -Stopwatch $TraceVerboseTimer
Push-Location $Path
init
foreach($s in $step){
Trace-Message "Invoking Step: $s"
&$s
}
Pop-Location
Trace-Message "FINISHED: $ModuleName in $Path" -KillTimer
================================================
FILE: RequiredModules.psd1
================================================
@{
Configuration = "[1.3.1,2.0)"
Metadata = "1.5.*"
Pester = "4.10.*"
ModuleBuilder = "2.0.*"
PSScriptAnalyzer = "1.19.1"
}
================================================
FILE: Source/Configuration.psd1
================================================
@{
# Script module or binary module file associated with this manifest.
ModuleToProcess = 'Configuration.psm1'
# Version number of this module.
ModuleVersion = '1.5.0'
# ID used to uniquely identify this module
GUID = 'e56e5bec-4d97-4dfd-b138-abbaa14464a6'
# Author of this module
Author = @('Joel Bennett')
# Company or vendor of this module
CompanyName = 'HuddledMasses.org'
# Copyright statement for this module
Copyright = 'Copyright (c) 2014-2021 by Joel Bennett, all rights reserved.'
# Description of the functionality provided by this module
Description = 'A module for storing and reading configuration values, with full PS Data serialization, automatic configuration for modules and scripts, etc.'
# Exports - populated by the build
FunctionsToExport = @('*')
CmdletsToExport = @()
VariablesToExport = @()
AliasesToExport = @('Get-StoragePath', 'Get-ManifestValue', 'Update-Manifest')
RequiredModules = @('Metadata')
# List of all files packaged with this module
FileList = @('.\Configuration.psd1','.\Configuration.psm1')
PrivateData = @{
# Allows overriding the default paths where Configuration stores it's configuration
# Within those folders, the module assumes a "powershell" folder and creates per-module configuration folders
PathOverride = @{
# Where the user's personal configuration settings go.
# Highest presedence, overrides all other settings.
# Defaults to $Env:LocalAppData on Windows
# Defaults to $Env:XDG_CONFIG_HOME elsewhere ($HOME/.config/)
UserData = ""
# On some systems there are "roaming" user configuration stored in the user's profile. Overrides machine configuration
# Defaults to $Env:AppData on Windows
# Defaults to $Env:XDG_CONFIG_DIRS elsewhere (or $HOME/.local/share/)
EnterpriseData = ""
# Machine specific configuration. Overrides defaults, but is overriden by both user roaming and user local settings
# Defaults to $Env:ProgramData on Windows
# Defaults to /etc/xdg elsewhere
MachineData = ""
}
# PSData is module packaging and gallery metadata embedded in PrivateData
# It's for the PoshCode and PowerShellGet modules
# We had to do this because it's the only place we're allowed to extend the manifest
# https://connect.microsoft.com/PowerShell/feedback/details/421837
PSData = @{
# The semver pre-release version information
PreRelease = ''
# Keyword tags to help users find this module via navigations and search.
Tags = @('Development','Configuration','Settings','Storage')
# The web address of this module's project or support homepage.
ProjectUri = "https://github.com/PoshCode/Configuration"
# The web address of this module's license. Points to a page that's embeddable and linkable.
LicenseUri = "http://opensource.org/licenses/MIT"
# Release notes for this particular version of the module
ReleaseNotes = '
- Extract the Metadata module
- Add support for arbitrary AllowedVariables
'
}
}
}
================================================
FILE: Source/Header/param.ps1
================================================
# Allows you to override the Scope storage paths (e.g. for testing)
param(
$Converters = @{},
$EnterpriseData,
$UserData,
$MachineData
)
if ($Converters.Count) {
Add-MetadataConverter $Converters
}
================================================
FILE: Source/Private/InitializeStoragePaths.ps1
================================================
function InitializeStoragePaths {
[CmdletBinding()]
param(
$EnterpriseData,
$UserData,
$MachineData
)
$PathOverrides = $MyInvocation.MyCommand.Module.PrivateData.PathOverride
# Where the user's personal configuration settings go.
# Highest presedence, overrides all other settings.
if ([string]::IsNullOrWhiteSpace($UserData)) {
if (!($UserData = $PathOverrides.UserData)) {
if ($IsLinux -or $IsMacOs) {
# Defaults to $Env:XDG_CONFIG_HOME on Linux or MacOS ($HOME/.config/)
if (!($UserData = $Env:XDG_CONFIG_HOME)) {
$UserData = Join-Path $HOME .config/
}
} else {
# Defaults to $Env:LocalAppData on Windows
if (!($UserData = $Env:LocalAppData)) {
$UserData = [Environment]::GetFolderPath("LocalApplicationData")
}
}
}
}
# On some systems there are "roaming" user configuration stored in the user's profile. Overrides machine configuration
if ([string]::IsNullOrWhiteSpace($EnterpriseData)) {
if (!($EnterpriseData = $PathOverrides.EnterpriseData)) {
if ($IsLinux -or $IsMacOs) {
# Defaults to the first value in $Env:XDG_CONFIG_DIRS on Linux or MacOS (or $HOME/.local/share/)
if (!($EnterpriseData = @($Env:XDG_CONFIG_DIRS -split ([IO.Path]::PathSeparator))[0] )) {
$EnterpriseData = Join-Path $HOME .local/share/
}
} else {
# Defaults to $Env:AppData on Windows
if (!($EnterpriseData = $Env:AppData)) {
$EnterpriseData = [Environment]::GetFolderPath("ApplicationData")
}
}
}
}
# Machine specific configuration. Overrides defaults, but is overriden by both user roaming and user local settings
if ([string]::IsNullOrWhiteSpace($MachineData)) {
if (!($MachineData = $PathOverrides.MachineData)) {
if ($IsLinux -or $IsMacOs) {
# Defaults to /etc/xdg elsewhere
$XdgConfigDirs = $Env:XDG_CONFIG_DIRS -split ([IO.Path]::PathSeparator) | Where-Object { $_ -and (Test-Path $_) }
if (!($MachineData = if ($XdgConfigDirs.Count -gt 1) {
$XdgConfigDirs[1]
})) {
$MachineData = "/etc/xdg/"
}
} else {
# Defaults to $Env:ProgramData on Windows
if (!($MachineData = $Env:ProgramAppData)) {
$MachineData = [Environment]::GetFolderPath("CommonApplicationData")
}
}
}
}
Join-Path $EnterpriseData powershell
Join-Path $UserData powershell
Join-Path $MachineData powershell
}
$EnterpriseData, $UserData, $MachineData = InitializeStoragePaths -EnterpriseData $EnterpriseData -UserData $UserData -MachineData $MachineData
================================================
FILE: Source/Private/ParameterBinder.ps1
================================================
function ParameterBinder {
if (!$Module) {
[System.Management.Automation.PSModuleInfo]$Module = . {
$Command = ($CallStack)[0].InvocationInfo.MyCommand
$mi = if ($Command.ScriptBlock -and $Command.ScriptBlock.Module) {
$Command.ScriptBlock.Module
} else {
$Command.Module
}
if ($mi -and $mi.ExportedCommands.Count -eq 0) {
if ($mi2 = Get-Module $mi.ModuleBase -ListAvailable | Where-Object { ($_.Name -eq $mi.Name) -and $_.ExportedCommands } | Select-Object -First 1) {
$mi = $mi2
}
}
$mi
}
}
if (!$CompanyName) {
[String]$CompanyName = . {
if ($Module) {
$CName = $Module.CompanyName -replace "[$([Regex]::Escape(-join[IO.Path]::GetInvalidFileNameChars()))]", "_"
if ($CName -eq "Unknown" -or -not $CName) {
$CName = $Module.Author
if ($CName -eq "Unknown" -or -not $CName) {
$CName = "AnonymousModules"
}
}
$CName
} else {
"AnonymousScripts"
}
}
}
if (!$Name) {
[String]$Name = $(if ($Module) {
$Module.Name
} <# else { ($CallStack)[0].InvocationInfo.MyCommand.Name } #>)
}
if (!$DefaultPath -and $Module) {
[String]$DefaultPath = $(if ($Module) {
Join-Path $Module.ModuleBase Configuration.psd1
})
}
}
================================================
FILE: Source/Public/Export-Configuration.ps1
================================================
function Export-Configuration {
<#
.Synopsis
Exports a configuration object to a specified path.
.Description
Exports the configuration object to a file, by default, in the Roaming AppData location
NOTE: this exports the FULL configuration to this file, which will override both defaults and local machine configuration when Import-Configuration is used.
.Example
@{UserName = $Env:UserName; LastUpdate = [DateTimeOffset]::Now } | Export-Configuration
This example shows how to use Export-Configuration in your module to cache some data.
.Example
Get-Module Configuration | Export-Configuration @{UserName = $Env:UserName; LastUpdate = [DateTimeOffset]::Now }
This example shows how to use Export-Configuration to export data for use in a specific module.
#>
# PSSCriptAnalyzer team refuses to listen to reason. See bugs: #194 #283 #521 #608
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Callstack', Justification = 'This is referenced in ParameterBinder')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Module', Justification = 'This is referenced in ParameterBinder')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'DefaultPath', Justification = 'This is referenced in ParameterBinder')]
[CmdletBinding(DefaultParameterSetName = '__ModuleInfo', SupportsShouldProcess)]
param(
# Specifies the objects to export as metadata structures.
# Enter a variable that contains the objects or type a command or expression that gets the objects.
# You can also pipe objects to Export-Metadata.
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
$InputObject,
# Serialize objects as hashtables
[switch]$AsHashtable,
# A callstack. You should not ever pass this.
# It is used to calculate the defaults for all the other parameters.
[Parameter(ParameterSetName = "__CallStack")]
[System.Management.Automation.CallStackFrame[]]$CallStack = $(Get-PSCallStack),
# The Module you're importing configuration for
[Parameter(ParameterSetName = "__ModuleInfo", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[System.Management.Automation.PSModuleInfo]$Module,
# An optional module qualifier (by default, this is blank)
[Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
[Alias("Author")]
[String]$CompanyName,
# The name of the module or script
# Will be used in the returned storage path
[Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
[String]$Name,
# DefaultPath is IGNORED.
# The parameter was here to match Import-Configuration, but it is meaningless in Export-Configuration
# The only reason I haven't removed it is that I don't want to break any code that might be using it.
# TODO: If we release a breaking changes Configuration 2.0, remove this parameter
[Parameter(ParameterSetName = "ManualOverride", ValueFromPipelineByPropertyName = $true)]
[Alias("ModuleBase")]
[String]$DefaultPath,
# The scope to save at, defaults to Enterprise (which returns a path in "RoamingData")
[Parameter(ParameterSetName = "ManualOverride")]
[ValidateSet("User", "Machine", "Enterprise")]
[string]$Scope = "Enterprise",
# The version for saved settings -- if set, will be used in the returned path
# NOTE: this is *NOT* calculated from the CallStack
[Version]$Version
)
process {
. ParameterBinder
if (!$Name) {
throw "Could not determine the storage name, Export-Configuration should only be called from inside a script or module, or by piping ModuleInfo to it."
}
$Parameters = @{
CompanyName = $CompanyName
Name = $Name
}
if ($Version) {
$Parameters.Version = $Version
}
$MachinePath = Get-ConfigurationPath @Parameters -Scope $Scope
$ConfigurationPath = Join-Path $MachinePath "Configuration.psd1"
$InputObject | Export-Metadata $ConfigurationPath -AsHashtable:$AsHashtable
}
}
================================================
FILE: Source/Public/Get-ConfigurationPath.ps1
================================================
function Get-ConfigurationPath {
#.Synopsis
# Gets an storage path for configuration files and data
#.Description
# Gets an AppData (or roaming profile) or ProgramData path for configuration and data storage. The folder returned is guaranteed to exist (which means calling this function actually creates folders).
#
# Get-ConfigurationPath is designed to be called from inside a module function WITHOUT any parameters.
#
# If you need to call Get-ConfigurationPath from outside a module, you should pipe the ModuleInfo to it, like:
# Get-Module Powerline | Get-ConfigurationPath
#
# As a general rule, there are three scopes which result in three different root folders
# User: $Env:LocalAppData
# Machine: $Env:ProgramData
# Enterprise: $Env:AppData (which is the "roaming" folder of AppData)
#
#.NOTES
# 1. This command is primarily meant to be used in modules, to find a place where they can serialize data for storage.
# 2. It's techincally possible for more than one module to exist with the same name.
# The command uses the Author or Company as a distinguishing name.
#
#.Example
# $CacheFile = Join-Path (Get-ConfigurationPath) Data.clixml
# $Data | Export-CliXML -Path $CacheFile
#
# This example shows how to use Get-ConfigurationPath with Export-CliXML to cache data as clixml from inside a module.
[Alias("Get-StoragePath")]
# [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Callstack', Justification = 'This is referenced in ParameterBinder')]
# [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Module', Justification = 'This is referenced in ParameterBinder')]
[CmdletBinding(DefaultParameterSetName = '__ModuleInfo')]
param(
# The scope to save at, defaults to Enterprise (which returns a path in "RoamingData")
[ValidateSet("User", "Machine", "Enterprise")]
[string]$Scope = "Enterprise",
# A callstack. You should not ever pass this.
# It is used to calculate the defaults for all the other parameters.
[Parameter(ParameterSetName = "__CallStack")]
[System.Management.Automation.CallStackFrame[]]$CallStack = $(Get-PSCallStack),
# The Module you're importing configuration for
[Parameter(ParameterSetName = "__ModuleInfo", ValueFromPipeline = $true)]
[System.Management.Automation.PSModuleInfo]$Module,
# An optional module qualifier (by default, this is blank)
[Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
[Alias("Author")]
[String]$CompanyName,
# The name of the module or script
# Will be used in the returned storage path
[Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
[String]$Name,
# The version for saved settings -- if set, will be used in the returned path
# NOTE: this is *NOT* calculated from the CallStack
[Version]$Version,
# By default, Get-ConfigurationPath creates the folder if it doesn't already exist
# This switch allows overriding that behavior: if set, does not create missing paths
[Switch]$SkipCreatingFolder
)
begin {
$PathRoot = $(switch ($Scope) {
"Enterprise" {
$EnterpriseData
}
"User" {
$UserData
}
"Machine" {
$MachineData
}
# This should be "Process" scope, but what does that mean?
# "AppDomain" { $MachineData }
default {
$EnterpriseData
}
})
if (Test-Path $PathRoot) {
$PathRoot = Resolve-Path $PathRoot
} elseif (!$SkipCreatingFolder) {
Write-Warning "The $Scope path $PathRoot cannot be found"
}
}
process {
. ParameterBinder
if (!$Name) {
Write-Error "Empty Name ($Name) in $($PSCmdlet.ParameterSetName): $($PSBoundParameters | Format-List | Out-String)"
throw "Could not determine the storage name, Get-ConfigurationPath should only be called from inside a script or module."
}
$CompanyName = $CompanyName -replace "[$([Regex]::Escape(-join[IO.Path]::GetInvalidFileNameChars()))]", "_"
if ($CompanyName -and $CompanyName -ne "Unknown") {
$PathRoot = Join-Path $PathRoot $CompanyName
}
$PathRoot = Join-Path $PathRoot $Name
if ($Version) {
$PathRoot = Join-Path $PathRoot $Version
}
if (Test-Path $PathRoot -PathType Leaf) {
throw "Cannot create folder for Configuration because there's a file in the way at $PathRoot"
}
if (!$SkipCreatingFolder -and !(Test-Path $PathRoot -PathType Container)) {
$null = New-Item $PathRoot -Type Directory -Force
}
# Note: this used to call Resolve-Path
$PathRoot
}
}
================================================
FILE: Source/Public/Get-ParameterValue.ps1
================================================
function Get-ParameterValue {
<#
.SYNOPSIS
Get parameter values from PSBoundParameters + DefaultValues and optionally, a configuration file
.DESCRIPTION
This function gives command authors an easy way to combine default parameter values and actual arguments.
It also supports user-specified default parameter values loaded from a configuration file.
It returns a hashtable (like PSBoundParameters) which combines these parameter defaults with parameter values passed by the caller.
#>
[CmdletBinding()]
param(
# The base name of a configuration file to read defaults from
# If specified, the command will read a ".psd1" file with this name
# Suggested Value: $MyInvocation.MyCommand.Noun
[string]$FromFile,
# If your configuration file has defaults for multiple commands, pass
# the top-level key which contains defaults for this invocation
[string]$CommandKey,
# Allows extending the valid variables which are allowed to be referenced in configuration
# BEWARE: This exposes the value of these variables in the calling context to the configuration file
# You are reponsible to only allow variables which you know are safe to share
[String[]]$AllowedVariables
)
$CallersInvocation = $PSCmdlet.SessionState.PSVariable.GetValue("MyInvocation")
$BoundParameters = @{} + $CallersInvocation.BoundParameters
$AllParameters = $CallersInvocation.MyCommand.Parameters
if ($FromFile) {
$FromFile = [IO.Path]::ChangeExtension($FromFile, ".psd1")
}
$FileDefaults = if ($FromFile -and (Test-Path $FromFile)) {
$MetadataOptions = @{
AllowedVariables = $AllowedVariables
PSVariable = $PSCmdlet.SessionState.PSVariable
ErrorAction = "SilentlyContinue"
}
Write-Debug "Importing $FromFile"
$FileValues = Import-Metadata $FromFile @MetadataOptions
if ($CommandKey) {
$FileValues = $FileValues.$CommandKey
}
$FileValues
} else {
@{}
}
# Don't support getting common parameters from the config file
$CommonParameters = [System.Management.Automation.Cmdlet]::CommonParameters +
[System.Management.Automation.Cmdlet]::OptionalCommonParameters
# Layer the defaults below config below actual parameter values
foreach ($parameter in $AllParameters.GetEnumerator().Where({ $_.Key -notin $CommonParameters })) {
Write-Debug " Parameter: $($parameter.key)"
$key = $parameter.Key
# Support parameter aliases in the config file by changing the alias to the parameter name
# If the value is not in the file defaults AND was not set by the user ...
if ($FromFile -and -not $FileDefaults.ContainsKey($key) -and -not $BoundParameters.ContainsKey($key)) {
# Check if any of the aliases are in the file defaults
Write-Debug " Aliases: $($parameter.Value.Aliases -join ', ')"
foreach ($k in @($parameter.Value.Aliases)) {
if ($null -ne $k -and $FileDefaults.ContainsKey($k)) {
Write-Debug " ... Update FileDefaults[$key] from $k"
$FileDefaults[$key] = $FileDefaults[$k]
$null = $FileDefaults.Remove($k)
break
}
}
}
# Bound parameter values > build.psd1 values > default parameters values
if ($CallersInvocation) {
# If it's in the file defaults (now) AND it was not already set at a higher precedence
if ($FromFile -and $FileDefaults.ContainsKey($Parameter) -and -not ($BoundParameters.ContainsKey($Parameter))) {
Write-Debug "Export $Parameter = $($FileDefaults[$Parameter])"
$BoundParameters[$Parameter] = $FileDefaults[$Parameter]
# Set the variable in the _callers_ SessionState as well as our return hashtable
$PSCmdlet.SessionState.PSVariable.Set($Parameter, $FileDefaults[$Parameter])
# If it's still NOT in the file defaults and was not already set, check if there's a default value
} elseif (-not $FileDefaults.ContainsKey($key) -and -not $BoundParameters.ContainsKey($key)) {
# Reading the current value of the $key variable returns either the bound parameter or the default
if ($null -ne ($value = $PSCmdlet.SessionState.PSVariable.Get($key).Value)) {
Write-Debug " From Default: $($BoundParameters[$key] -join ', ')"
if ($value -ne ($null -as $parameter.Value.ParameterType)) {
$BoundParameters[$key] = $value
}
}
# Otherwise, it was set by the user, or ...
} elseif ($BoundParameters[$key]) {
Write-Debug " From Parameter: $($BoundParameters[$key] -join ', ')"
# We'll set it from the file
} elseif ($FileDefaults[$key]) {
Write-Debug " From File: $($FileDefaults[$key] -join ', ')"
$BoundParameters[$key] = $FileDefaults[$key]
}
}
}
$BoundParameters
}
================================================
FILE: Source/Public/Import-Configuration.ps1
================================================
function Import-Configuration {
#.Synopsis
# Import the full, layered configuration for the module.
#.Description
# Imports the DefaultPath Configuration file, and then imports the Machine, Roaming (enterprise), and local config files, if they exist.
# Each configuration file is layered on top of the one before (so only needs to set values which are different)
#.Example
# $Configuration = Import-Configuration
#
# This example shows how to use Import-Configuration in your module to load the cached data
#
#.Example
# $Configuration = Get-Module Configuration | Import-Configuration
#
# This example shows how to use Import-Configuration in your module to load data cached for another module
#
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Callstack', Justification = 'This is referenced in ParameterBinder')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Module', Justification = 'This is referenced in ParameterBinder')]
# [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'DefaultPath', Justification = 'This is referenced in ParameterBinder')]
[CmdletBinding(DefaultParameterSetName = '__CallStack')]
param(
# A callstack. You should not ever pass this.
# It is used to calculate the defaults for all the other parameters.
[Parameter(ParameterSetName = "__CallStack")]
[System.Management.Automation.CallStackFrame[]]$CallStack = $(Get-PSCallStack),
# The Module you're importing configuration for
[Parameter(ParameterSetName = "__ModuleInfo", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[System.Management.Automation.PSModuleInfo]$Module,
# An optional module qualifier (by default, this is blank)
[Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
[Alias("Author")]
[String]$CompanyName,
# The name of the module or script
# Will be used in the returned storage path
[Parameter(ParameterSetName = "ManualOverride", Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
[String]$Name,
# The full path (including file name) of a default Configuration.psd1 file
# By default, this is expected to be in the same folder as your module manifest, or adjacent to your script file
[Parameter(ParameterSetName = "ManualOverride", ValueFromPipelineByPropertyName = $true)]
[Alias("ModuleBase")]
[String]$DefaultPath,
# The version for saved settings -- if set, will be used in the returned path
# NOTE: this is *never* calculated, if you use version numbers, you must manage them on your own
[Version]$Version,
# If set (and PowerShell version 4 or later) preserve the file order of configuration
# This results in the output being an OrderedDictionary instead of Hashtable
[Switch]$Ordered,
# Allows extending the valid variables which are allowed to be referenced in configuration
# BEWARE: This exposes the value of these variables in the calling context to the configuration file
# You are reponsible to only allow variables which you know are safe to share
[String[]]$AllowedVariables
)
begin {
# Write-Debug "Import-Configuration for module $Name"
}
process {
. ParameterBinder
if (!$Name) {
throw "Could not determine the configuration name. When you are not calling Import-Configuration from a module, you must specify the -Author and -Name parameter"
}
$MetadataOptions = @{
AllowedVariables = $AllowedVariables
PSVariable = $PSCmdlet.SessionState.PSVariable
Ordered = $Ordered
ErrorAction = "Ignore"
}
if ($DefaultPath -and (Test-Path $DefaultPath -Type Container)) {
$DefaultPath = Join-Path $DefaultPath Configuration.psd1
}
$Configuration = if ($DefaultPath -and (Test-Path $DefaultPath)) {
Import-Metadata $DefaultPath @MetadataOptions
} else {
@{}
}
# Write-Debug "Module Configuration: ($DefaultPath)`n$($Configuration | Out-String)"
$Parameters = @{
CompanyName = $CompanyName
Name = $Name
}
if ($Version) {
$Parameters.Version = $Version
}
$MachinePath = Get-ConfigurationPath @Parameters -Scope Machine -SkipCreatingFolder
$MachinePath = Join-Path $MachinePath Configuration.psd1
$Machine = if (Test-Path $MachinePath) {
Import-Metadata $MachinePath @MetadataOptions
} else {
@{}
}
# Write-Debug "Machine Configuration: ($MachinePath)`n$($Machine | Out-String)"
$EnterprisePath = Get-ConfigurationPath @Parameters -Scope Enterprise -SkipCreatingFolder
$EnterprisePath = Join-Path $EnterprisePath Configuration.psd1
$Enterprise = if (Test-Path $EnterprisePath) {
Import-Metadata $EnterprisePath @MetadataOptions
} else {
@{}
}
# Write-Debug "Enterprise Configuration: ($EnterprisePath)`n$($Enterprise | Out-String)"
$LocalUserPath = Get-ConfigurationPath @Parameters -Scope User -SkipCreatingFolder
$LocalUserPath = Join-Path $LocalUserPath Configuration.psd1
$LocalUser = if (Test-Path $LocalUserPath) {
Import-Metadata $LocalUserPath @MetadataOptions
} else {
@{}
}
# Write-Debug "LocalUser Configuration: ($LocalUserPath)`n$($LocalUser | Out-String)"
$Configuration | Update-Object $Machine |
Update-Object $Enterprise |
Update-Object $LocalUser
}
}
================================================
FILE: Source/Public/Import-ParameterConfiguration.ps1
================================================
function Import-ParameterConfiguration {
<#
.SYNOPSIS
Loads a metadata file based on the calling command name and combines the values there with the parameter values of the calling function.
.DESCRIPTION
This function gives command authors and users an easy way to let the default parameter values of the command be set by a configuration file in the folder you call it from.
Normally, you have three places to get parameter values from. In priority order, they are:
- Parameters passed by the caller always win
- The PowerShell $PSDefaultParameterValues hashtable appears to the function as if the user passed it
- Default parameter values (defined in the function)
If you call this command at the top of a function, it overrides (only) the default parameter values with
- Values from a manifest file in the present working directory ($pwd)
.EXAMPLE
Given that you've written a script like:
function New-User {
[CmdletBinding()]
param(
$FirstName,
$LastName,
$UserName,
$Domain,
$EMail,
$Department,
[hashtable]$Permissions
)
Import-ParameterConfiguration -Recurse
# Possibly calculated based on (default) parameter values
if (-not $UserName) { $UserName = "$FirstName.$LastName" }
if (-not $EMail) { $EMail = "$UserName@$Domain" }
# Lots of work to create the user's AD account, email, set permissions etc.
# Output an object:
[PSCustomObject]@{
PSTypeName = "MagicUser"
FirstName = $FirstName
LastName = $LastName
EMail = $EMail
Department = $Department
Permissions = $Permissions
}
}
You could create a User.psd1 in a folder with just:
@{ Domain = "HuddledMasses.org" }
Now the following command would resolve the `User.psd1`
And the user would get an appropriate email address automatically:
PS> New-User Joel Bennett
FirstName : Joel
LastName : Bennett
EMail : Joel.Bennett@HuddledMasses.org
.EXAMPLE
Import-ParameterConfiguration works recursively (up through parent folders)
That means it reads config files in the same way git reads .gitignore,
with settings in the higher level files (up to the root?) being
overridden by those in lower level files down to the WorkingDirectory
Following the previous example to a ridiculous conclusion,
we could automate creating users by creating a tree like:
C:\HuddledMasses\Security\Admins\ with a User.psd1 in each folder:
# C:\HuddledMasses\User.psd1:
@{
Domain = "HuddledMasses.org"
}
# C:\HuddledMasses\Security\User.psd1:
@{
Department = "Security"
Permissions = @{
Access = "User"
}
}
# C:\HuddledMasses\Security\Admins\User.psd1
@{
Permissions = @{
Access = "Administrator"
}
}
And then switch to the Admins directory and run:
PS> New-User Joel Bennett
FirstName : Joel
LastName : Bennett
EMail : Joel.Bennett@HuddledMasses.org
Department : Security
Permissions : { Access = Administrator }
.EXAMPLE
Following up on our earlier example, let's look at a way to use that -FileName parameter.
If you wanted to use a different configuration files than your Noun, you can pass the file name in.
You could even use one of your parameters to generate the file name. If we modify the function like ...
function New-User {
[CmdletBinding()]
param(
$FirstName,
$LastName,
$UserName,
$Domain,
$EMail,
$Department,
[hashtable]$Permissions
)
Import-ParameterConfiguration -FileName "${Department}User.psd1"
# Possibly calculated based on (default) parameter values
if (-not $UserName) { $UserName = "$FirstName.$LastName" }
if (-not $EMail) { $EMail = "$UserName@$Domain" }
# Lots of work to create the user's AD account and email etc.
[PSCustomObject]@{
PSTypeName = "MagicUser"
FirstName = $FirstName
LastName = $LastName
EMail = $EMail
# Passthru for testing
Permissions = $Permissions
}
}
Now you could create a `SecurityUser.psd1`
@{
Domain = "HuddledMasses.org"
Permissions = @{
Access = "Administrator"
}
}
And run:
PS> New-User Joel Bennett -Department Security
#>
[CmdletBinding()]
param(
# The folder the configuration should be read from. Defaults to the current working directory
[string]$WorkingDirectory = $pwd,
# The name of the configuration file.
# The default value is your command's Noun, with the ".psd1" extention.
# So if you call this from a command named Build-Module, the noun is "Module" and the config $FileName is "Module.psd1"
[string]$FileName,
# If set, considers configuration files in the parent, and it's parent recursively
[switch]$Recurse,
# Allows extending the valid variables which are allowed to be referenced in configuration
# BEWARE: This exposes the value of these variables in the calling context to the configuration file
# You are reponsible to only allow variables which you know are safe to share
[String[]]$AllowedVariables
)
$CallersInvocation = $PSCmdlet.SessionState.PSVariable.GetValue("MyInvocation")
$BoundParameters = @{} + $CallersInvocation.BoundParameters
$AllParameters = $CallersInvocation.MyCommand.Parameters.Keys
if (-not $PSBoundParameters.ContainsKey("FileName")) {
$FileName = "$($CallersInvocation.MyCommand.Noun).psd1"
}
$MetadataOptions = @{
AllowedVariables = $AllowedVariables
PSVariable = $PSCmdlet.SessionState.PSVariable
ErrorAction = "SilentlyContinue"
}
do {
$FilePath = Join-Path $WorkingDirectory $FileName
Write-Debug "Initializing parameters for $($CallersInvocation.InvocationName) from $(Join-Path $WorkingDirectory $FileName)"
if (Test-Path $FilePath) {
$ConfiguredDefaults = Import-Metadata $FilePath @MetadataOptions
foreach ($Parameter in $AllParameters) {
# If it's in the defaults AND it was not already set at a higher precedence
if ($ConfiguredDefaults.ContainsKey($Parameter) -and -not ($BoundParameters.ContainsKey($Parameter))) {
Write-Debug "Export $Parameter = $($ConfiguredDefaults[$Parameter])"
$BoundParameters.Add($Parameter, $ConfiguredDefaults[$Parameter])
# This "SessionState" is the _callers_ SessionState, not ours
$PSCmdlet.SessionState.PSVariable.Set($Parameter, $ConfiguredDefaults[$Parameter])
}
}
}
Write-Debug "Recurse:$Recurse -and $($BoundParameters.Count) of $($AllParameters.Count) Parameters and $WorkingDirectory"
} while ($Recurse -and ($AllParameters.Count -gt $BoundParameters.Count) -and ($WorkingDirectory = Split-Path $WorkingDirectory))
}
================================================
FILE: Specs/Configuration.Steps.ps1
================================================
#requires -Module Configuration
#using module Configuration
$PSModuleAutoLoadingPreference = "None"
# Fix IsLinux on Windows PowerShell 5.x
if (!(Test-Path Variable:Global:IsLinux -ErrorAction SilentlyContinue)){
$Global:IsLinux = $False
}
# NOTE THIS FAKE IMPLEMENTS THE INTERFACE DuckType style, WITHOUT SAYING SO
# Unfortunately, this C# class is what's actually used by the tests
# When it SHOULD be the PowerShell class below
Add-Type -TypeDefinition @'
using System;
using System.Collections;
public class TestClass : Hashtable {
public string Name { get; set; }
public string TestMetadata { get; set; }
public TestClass() {
TestMetadata = "@{ Values = @{ User = 'Jaykul' } Name = 'Joel' }";
}
public string ToPsMetadata() {
return TestMetadata;
}
public void FromPsMetadata(string Metadata) {
Metadata = System.Text.RegularExpressions.Regex.Replace(Metadata.Trim(), @"\s+", " ");
if (Metadata != TestMetadata) {
throw new ArgumentException("Metadata doesn't match expected value: [" + Metadata + "] [" + TestMetadata + "]");
}
Name = "Joel";
Add("User", "Jaykul");
}
}
'@
InModuleScope Pester {
Import-Module Configuration
class TestClass : Hashtable, IPsMetadataSerializable {
[string]$Name
[string] ToPsMetadata() {
return ConvertTo-Metadata -InputObject @{
Name = $this.Name
Values = @{ } + $this
}
}
[void] FromPsMetadata([string]$Metadata) {
$self = ConvertFrom-Metadata -InputObject $Metadata
$this.PSBase.Name = $self.Name
foreach ($key in $self.Values.Keys) {
$null = $this.Add($key, $self.Values[$key])
}
}
}
}
function global:GetModuleBase {
$Module = Get-Module "Configuration" -ListAvailable |
Sort-Object Version -Descending |
Select-Object -First 1
Import-Module "$($Module.ModuleBase)/Configuration.psd1" -Scope Global
$Module.ModuleBase
}
Given 'the configuration module is imported on Linux:' {
$ModuleBase = GetModuleBase
Remove-Module "Configuration" -ErrorAction Ignore -Force
if (!(Test-Path Variable:IsLinux -ErrorAction SilentlyContinue)){
$Global:IsLinux = $True
Import-Module $ModuleBase/Configuration.psd1 -Scope Global
Remove-Variable IsLinux -Scope Global
} elseif (!$IsLinux) {
Set-Variable IsLinux $True -Force -Option ReadOnly, AllScope -Scope Global
Import-Module $ModuleBase/Configuration.psd1 -Scope Global
Set-Variable IsLinux $False -Force -Option ReadOnly, AllScope -Scope Global
}
}
Given 'the configuration module is imported with testing paths on Linux:' {
param($Table)
$ModuleBase = GetModuleBase
Copy-Item $ModuleBase/Configuration.psd1 -Destination $ModuleBase/Configuration.psd1.backup
Update-Metadata -Path $ModuleBase/Configuration.psd1 -PropertyName 'PrivateData.PathOverride.MachineData' -Value $Table.Machine
Update-Metadata -Path $ModuleBase/Configuration.psd1 -PropertyName 'PrivateData.PathOverride.EnterpriseData' -Value $Table.Enterprise
Update-Metadata -Path $ModuleBase/Configuration.psd1 -PropertyName 'PrivateData.PathOverride.UserData' -Value $Table.User
Remove-Module "Configuration" -ErrorAction Ignore -Force
if (!(Test-Path Variable:IsLinux -ErrorAction SilentlyContinue)) {
$Global:IsLinux = $True
Import-Module $ModuleBase/Configuration.psd1 -Scope Global
Remove-Variable IsLinux
} elseif (!$IsLinux) {
Set-Variable IsLinux $True -Force -Option ReadOnly, AllScope -Scope Global
Import-Module $ModuleBase/Configuration.psd1 -Scope Global
Set-Variable IsLinux $False -Force -Option ReadOnly, AllScope -Scope Global
} else {
Import-Module $ModuleBase/Configuration.psd1 -Scope Global
}
Update-Metadata -Path $ModuleBase/Configuration.psd1 -PropertyName 'PrivateData.PathOverride.MachineData' -Value ""
Update-Metadata -Path $ModuleBase/Configuration.psd1 -PropertyName 'PrivateData.PathOverride.EnterpriseData' -Value ""
Update-Metadata -Path $ModuleBase/Configuration.psd1 -PropertyName 'PrivateData.PathOverride.UserData' -Value ""
}
Given 'the configuration module is imported with testing paths:' {
param($Table)
$ModuleBase = GetModuleBase
Copy-Item $ModuleBase/Configuration.psd1 -Destination $ModuleBase/Configuration.psd1.backup
Update-Metadata -Path $ModuleBase/Configuration.psd1 -PropertyName 'PrivateData.PathOverride.MachineData' -Value $Table.Machine
Update-Metadata -Path $ModuleBase/Configuration.psd1 -PropertyName 'PrivateData.PathOverride.EnterpriseData' -Value $Table.Enterprise
Update-Metadata -Path $ModuleBase/Configuration.psd1 -PropertyName 'PrivateData.PathOverride.UserData' -Value $Table.User
Remove-Module "Configuration" -ErrorAction Ignore -Force
Import-Module $ModuleBase/Configuration.psd1 -Scope Global
Update-Metadata -Path $ModuleBase/Configuration.psd1 -PropertyName 'PrivateData.PathOverride.MachineData' -Value ""
Update-Metadata -Path $ModuleBase/Configuration.psd1 -PropertyName 'PrivateData.PathOverride.EnterpriseData' -Value ""
Update-Metadata -Path $ModuleBase/Configuration.psd1 -PropertyName 'PrivateData.PathOverride.UserData' -Value ""
}
Given 'the configuration module is imported with a URL converter' {
param($Table)
$ModuleBase = GetModuleBase
Remove-Module "Configuration" -ErrorAction Ignore -Force
Import-Module $ModuleBase/Configuration.psd1 -Args @{
[Uri] = { "Uri '$_' " }
"Uri" = {
param([string]$Value)
[Uri]$Value
}
} -Scope Global
}
Given 'the configuration module is imported' {
param($Table)
$ModuleBase = GetModuleBase
Remove-Module "Configuration" -ErrorAction Ignore -Force
Import-Module $ModuleBase/Configuration.psd1 -Scope Global
}
Given 'the manifest module is imported' {
param($Table)
$ModuleBase = GetModuleBase
Remove-Module "Configuration", Manifest
Import-Module $ModuleBase/Manifest.psm1 -Scope Global
}
Given "a module with(?:\s+\w+ name '(?<name>.+?)'|\s+\w+ the company '(?<company>.+?)'|\s+\w+ the author '(?<author>.+?)')+" {
param($name, $Company = "", $Author = "")
$ModulePath = "TestDrive:/Modules/$name"
Remove-Module $name -ErrorAction Ignore
Remove-Item $ModulePath -Recurse -ErrorAction Ignore
if(Test-Path $ModulePath -PathType Leaf) {
throw "Cannot create folder for Configuration because there's a file in the way at '$ModulePath'"
}
if(!(Test-Path $ModulePath -PathType Container)) {
$null = New-Item $ModulePath -Type Directory -Force
}
$Env:PSModulePath = $Env:PSModulePath + ";TestDrive:/Modules" -replace "(;TestDrive:/Modules)+?$", ";TestDrive:/Modules"
Set-Content $ModulePath/${Name}.psm1 "
`$Script:ConfigurationPath = Get-ConfigurationPath -Scope User -ErrorAction SilentlyContinue
`$Script:Configuration = Import-Configuration -ErrorAction SilentlyContinue
function GetConfiguration { `$Script:Configuration }
function GetConfigurationPath { `$Script:ConfigurationPath }
function GetStoragePath { Get-ConfigurationPath @Args }
function ImportConfiguration { Import-Configuration }
function ImportConfigVersion { Import-Configuration -Version 2.0 }
filter ExportConfiguration { `$_ | Export-Configuration }
filter ExportConfigVersion { `$_ | Export-Configuration -Version 2.0 }
"
New-ModuleManifest $ModulePath/${Name}.psd1 -RootModule ./${Name}.psm1 -Description "A Super Test Module" -Company $Company -Author $Author
# New-ModuleManifest sets things even when we don't want it to:
if(!$Author) {
Set-Content $ModulePath/${Name}.psd1 ((Get-Content $ModulePath/${Name}.psd1) -Replace "^(Author.*)$", '#$1')
}
if(!$Company) {
Set-Content $ModulePath/${Name}.psd1 ((Get-Content $ModulePath/${Name}.psd1) -Replace "^(Company.*)$", '#$1')
}
Import-Module $ModulePath/${Name}.psd1
}
Then "the user configuration path at load time should (\w+) (.+)$" {
param($Comparator, $Path)
[string[]]$Path = $Path -split "\s*and\s*" | %{ $_.Trim("['`"]") }
$LocalStoragePath = GetConfigurationPath
foreach($PathAssertion in $Path) {
$LocalStoragePath -replace "\\", "/" | Should $Comparator $PathAssertion
}
}
Then "the module's user path at load time should (\w+) (.+)$" {
param($Comparator, $Path)
[string[]]$Path = $Path -split "\s*and\s*" | %{ $_.Trim("['`"]") }
$LocalStoragePath = GetConfigurationPath
$LocalStoragePath = $LocalStoragePath -replace "C:[\\\/]etc", "/etc"
$LocalStoragePath = $LocalStoragePath -replace "^$([regex]::escape($Home.TrimEnd("/\")))", "~"
foreach ($PathAssertion in $Path) {
$LocalStoragePath -replace "\\", "/" | Should $Comparator $PathAssertion
}
}
When "the module's (\w+) path should (\w+) (.+)$" {
param($Scope, $Comparator, $Path)
[string[]]$Path = $Path -split "\s*and\s*" | %{ $_.Trim("['`"]") }
foreach($PathAssertion in $Path) {
try {
# if you're not an administrator, you're going to get Access ... denied
$LocalStoragePath = GetStoragePath -Scope $Scope
} catch {
# this would make most tests fail, because the folder won't exist
$LocalStoragePath = GetStoragePath -Scope $Scope -SkipCreatingFolder
}
# This is because of the mock I wrote to test the linux logic on Windows
if(!$IsLinux -and $PathAssertion -match "\^~?/") {
$LocalStoragePath = $LocalStoragePath -replace "C:[\\\/]etc","/etc"
}
# This is just because I want to be able to write ~/ in the paths in tests instead of $Home/
if ($PathAssertion -match "\^~?/") {
$LocalStoragePath = $LocalStoragePath -replace "^$([regex]::escape($Home.TrimEnd("/\")))","~"
}
#Write-Host $LocalStoragePath -ForegroundColor Yellow
$LocalStoragePath -replace "\\", "/" | Should $Comparator $PathAssertion
}
}
Then "the script's (\w+) path should (\w+) (.+)$" {
param($Scope, $Comparator, $Path)
[string[]]$Path = $Path -split "\s*and\s*" | % { $_.Trim("['`"]") }
$LocalStoragePath = iex "TestDrive:/${ScriptName}.ps1"
foreach ($PathAssertion in $Path) {
$LocalStoragePath -replace "\\","/" | Should $Comparator $PathAssertion
}
}
When "the resulting path should (\w+) (.+)$" {
param($Comparator, $Path)
[string[]]$Path = $Path -split "\s*and\s*" | %{ $_.Trim("['`"]") }
foreach($PathAssertion in $Path) {
$folder -replace "\\", "/" | Should $Comparator $PathAssertion
}
}
Given "a script with the name '(.+)' that calls Get-ConfigurationPath with no parameters" {
param($name)
Set-Content "TestDrive:/${name}.ps1" "Get-ConfigurationPath"
$ScriptName = $Name
}
Given "a script with the name '(?<File>.+)' that calls Get-ConfigurationPath (?:-Name (?<Name>\w*) ?|-Author (?<Author>\w*) ?){2}" {
param($File, $Name, $Author)
Set-Content "TestDrive:/${File}.ps1" "Get-ConfigurationPath -Name $Name -Author $Author"
$ScriptName = $File
}
Then "the script should throw an exception$" {
{ $LocalStoragePath = iex "TestDrive:/${ScriptName}.ps1" } | Should throw
}
When "the module's storage path should end with a version number if one is passed in" {
(GetStoragePath -Version "2.0") -replace "\\", "/" | Should Match "/2.0$"
(GetStoragePath -Version "4.0") -replace "\\", "/" | Should Match "/4.0$"
}
When "a settings hashtable" {
param($hashtable)
$Settings = iex "[ordered]$hashtable"
}
Given "a settings object" {
param($hashtable)
$Settings = iex "[PSCustomObject]$hashtable"
}
When "we update the settings with" {
param($hashtable)
$Update = if($hashtable) {
iex $hashtable
} else {
$null
}
$Settings = $Settings | Update-Object $Update
}
When "we say (?<property>.*) is important and update with" {
param([string[]]$property, $hashtable)
$Update = if ($hashtable) {
iex $hashtable
}
$Settings = $Settings | Update-Object -UpdateObject $Update -Important $property
}
Given "a (?:settings file|module manifest) named (\S+)(?:(?: in the (?<Scope>\S+) folder)|(?: for version (?<Version>[0-9.]+)))*" {
param($fileName, $hashtable, $Scope = $null, $Version = $null)
if ($Scope -in "current","parent") {
$folder = "TestDrive:/Level1/Level2/"
} elseif ($Scope -and $Version) {
$folder = GetStoragePath -Scope $Scope -Version $Version
} elseif ($Scope) {
$folder = GetStoragePath -Scope $Scope
} elseif ($Version) {
$folder = GetStoragePath -Version $Version
} elseif ($ModulePath -and (Test-Path "$ModulePath")) {
$folder = $ModulePath
} else {
$folder = "TestDrive:/"
}
$SettingsFile = Join-Path $folder $fileName
$Parent = Split-Path $SettingsFile
if(Test-Path $Parent -PathType Leaf) {
throw "Cannot create folder for Configuration because there's a file in the way at '$Parent'"
}
if(!(Test-Path $Parent -PathType Container)) {
$null = New-Item $Parent -Type Directory -Force
}
if ($Scope -in "current","parent") {
Push-Location "TestDrive:/Level1/Level2/"
}
if ($Scope -eq "parent") {
$Parent = Split-Path $Parent
$SettingsFile = Join-Path $Parent $fileName
}
# Write-Verbose "Creating $SettingsFile" -Verbose
Set-Content $SettingsFile -Value $hashtable
}
Then "the settings object MyPath should match the file's path" {
$Settings.MyPath | Convert-Path | Should Be (Convert-Path ${SettingsFile})
}
When "a settings hashtable with an? (.+) in it" {
param($type)
$Settings = @{
UserName = $Env:UserName
}
switch($type) {
"NULL" {
$Settings.TestCase = $Null
}
"Enum" {
$Settings.TestCase = [Security.PolicyLevelType]::Enterprise
}
"String" {
$Settings.TestCase = "Test"
}
"Number" {
$Settings.OneTestCase = 42
$Settings.TwoTestCase = 42.9
}
"Array" {
$Settings.TestCase = "One", "Two", "Three"
}
"Boolean" {
$Settings.OneTestCase = $True
$Settings.TwoTestCase = $False
}
"DateTime" {
$Settings.TestCase = Get-Date
}
"DateTimeOffset" {
$Settings.TestCase = [DateTimeOffset](Get-Date)
}
"GUID" {
$Settings.TestCase = [GUID]::NewGuid()
}
"PSObject" {
$Settings.TestCase = New-Object PSObject -Property @{ Name = $Env:UserName }
}
"PSCredential" {
$Settings.TestCase = New-Object PSCredential @("UserName", (ConvertTo-SecureString -AsPlainText -Force -String "Password"))
}
"SecureString" {
$Settings.TestCase = ConvertTo-SecureString -AsPlainText -Force -String "Password"
}
"ScriptBlock" {
$Settings.TestCase = { Get-ChildItem }
}
"SwitchParameter" {
$Settings.TestCase = [switch]$true
}
"Uri" {
$Settings.TestCase = [Uri]"http://HuddledMasses.org"
}
"Hashtable" {
$Settings.TestCase = @{ Key = "Value"; ANother = "Value" }
}
"ConsoleColor" {
$Settings.TestCase = [ConsoleColor]::Red
}
default {
throw "missing test type"
}
}
}
When "we add a converter for (.*) types" {
param($Type)
switch ($Type) {
"Uri" {
Add-MetadataConverter @{
[Uri] = { "Uri '$_' " }
"Uri" = {
param([string]$Value)
[Uri]$Value
}
}
}
default {
throw "missing converter type"
}
}
}
When "we convert the settings to metadata" {
$SettingsMetadata = ConvertTo-Metadata $Settings
# # Write-Debug $SettingsMetadata
$Wide = $Host.UI.RawUI.WindowSize.Width
# Write-Verbose $SettingsMetadata
}
When "we export to a settings file named (.*)" {
param($fileName)
if(!$ModulePath -or !(Test-Path $ModulePath)) {
$ModulePath = "TestDrive:/"
}
$SettingsFile = Join-Path $ModulePath $fileName
$File = $Settings | Export-Metadata ${SettingsFile} -Passthru
$File.FullName | Should Be (Convert-Path $SettingsFile)
}
When "we convert the metadata to an object" {
$Settings = ConvertFrom-Metadata $SettingsMetadata
Write-Verbose (($Settings | Out-String -Stream | % TrimEnd) -join "`n")
}
When "we import the file to an object" {
$Settings = Import-Metadata ${SettingsFile}
Write-Verbose (($Settings | Out-String -Stream | % TrimEnd) -join "`n")
}
When "we import the file with ordered" {
$Settings = Import-Metadata ${SettingsFile} -Ordered
Write-Verbose (($Settings | Out-String -Stream | % TrimEnd) -join "`n")
}
When "we import the folder path" {
$Settings = Import-Metadata (Split-Path ${SettingsFile})
Write-Verbose (($Settings | Out-String -Stream | % TrimEnd) -join "`n")
}
When "trying to import the file to an object should throw(.*)" {
param([string]$Message)
{ $Settings = Import-Metadata ${SettingsFile} } | Should Throw $Message.trim()
}
When "the string version should (\w+)\s*(.*)?" {
param($operator, $data)
# Normalize line endings, because the module does:
$meta = ($SettingsMetadata -replace "\r?\n","`n")
$data = $data.trim('"''') -replace "\r?\n","`n"
# And then actually test it
$meta | Should $operator $data
}
When "the settings file should (\w+)\s*(.*)?" {
param($operator, $data)
# Normalize line endings, because the module does:
$data = [regex]::escape(($data -replace "\r?\n","`n")) -replace '\\n','\r?\n'
if($operator -eq "Contain"){
(Get-Content ${SettingsFile} -raw) -match $data | Should Be $True
} else {
${SettingsFile} | Should $operator $data
}
}
Given "the settings file does not exist" {
#
if(!$ModulePath -or !(Test-Path $ModulePath)) {
$ModulePath = "TestDrive:/"
}
if(!${SettingsFile}) {
$SettingsFile = Join-Path $ModulePath "NoSuchFile.psd1"
}
if(Test-Path $SettingsFile) {
Remove-Item $SettingsFile
}
}
Given "the configuration module exports IPsMetadataSerializable" {
"IPsMetadataSerializable" -as [Type] | Should -Not -BeNullOrEmpty
[IPsMetadataSerializable].IsInterface | Should -Be $true
}
Given "a TestClass that implements IPsMetadataSerializable" {
# We're duck typing it, so no interface ...
# [TestClass].ImplementedInterfaces |
# Where Name -eq IPsMetadataSerializable |
# Should -Not -BeNullOrEmpty
}
# This step will create verifiable/counting loggable mocks for Write-Warning, Write-Error, Write-Verbose
Given "we expect an? (?<type>warning|error|verbose) in the (?<module>.*) module" {
param($type, $module)
$ErrorModule = $module
# The Metadata module hides itself a little bit
if($Type -eq "Error" -and ($ErrorModule -eq "Metadata")) {
Mock -Module $ErrorModule WriteError { Write-Host " WriteError: $Message" -Foreground Red } -Verifiable
} else {
Mock -Module $ErrorModule Write-$type { Write-Host " Write-Error: $Message" -Foreground Red } -Verifiable
}
}
# The error is logged exactly 1 time
# Then the error is logged exactly 2 times
# Then the warning is logged 3 times
# Then the error is logged
# this step lets us verify the number of calls to those three mocks
When "the (?<type>warning|error|verbose) is logged(?: (?<exactly>exactly) (\d+) times?)?" {
param($count, $exactly, $type)
$param = @{}
if($count) {
$param.Exactly = $Exactly -eq "Exactly"
$param.Times = $count
}
if($Type -eq "Error" -and ($ErrorModule -eq "Metadata")) {
Assert-MockCalled -Module $ErrorModule -Command WriteError @param
} else {
Assert-MockCalled -Module $ErrorModule -Command Write-$type @param
}
}
When "we add a converter that's not a scriptblock" {
Add-MetadataConverter @{
"Uri" = "
param([string]$Value)
[Uri]$Value
"
}
}
When "we add a converter with a number as a key" {
Add-MetadataConverter @{
42 = {
param([string]$Value)
$Value
}
}
}
Then "the (?:settings|output) object should be of type (.*)" {
param([Type]$Type)
$Settings | Should BeOfType $Type
}
Then "the (?:settings|output) object should have (.*) in the PSTypeNames" {
param([string]$Type)
$Settings.PSTypeNames -eq $Type | Should Be $Type
}
Then "the (?:settings|output) object's (.*) should (be of type|be) (.*)" {
param([String]$Parameter, [String]$operator, $Expected)
$Value = $Settings
Set-StrictMode -Off
foreach($property in $Parameter.Split(".")) {
$value = $value.$property
}
$operator = $operator -replace " "
if($Operator -eq "be" -and $Expected -eq "null") {
$value | Should BeNullOrEmpty
} else {
$value | Should $operator $Expected
}
}
Then "Key (\d+) is (\w+)" {
param([int]$index, [string]$name)
$Settings.Keys | Select -Index $index | Should Be $Name
}
Given "a mock PowerShell version (.*)" {
param($version)
$PSVersion = [Version]$version
$PSDefaultParameterValues."Test-PSVersion:Version" = $PSVersion
}
When "we fake version 2.0 in the Metadata module" {
&(Get-Module Configuration) {
&(Get-Module Metadata) {
$PSDefaultParameterValues."Test-PSVersion:Version" = [Version]"2.0"
}
}
}
When "we're using PowerShell 4 or higher in the Metadata module" {
&(Get-Module Configuration) {
&(Get-Module Metadata) {
$null = $PSDefaultParameterValues.Remove("Test-PSVersion:Version")
$PSVersionTable.PSVersion -ge ([Version]"4.0") | Should Be $True
}
}
}
Given "the actual PowerShell version" {
$PSVersion = $PSVersionTable.PSVersion
$null = $PSDefaultParameterValues.Remove("Test-PSVersion:Version")
}
Then "the Version -(..) (.*)" {
param($comparator, $version)
if($version -eq "the version") {
[Version]$version = $PSVersion
} else {
[Version]$version = $version
}
$test = @{ $comparator = $version }
Test-PSVersion @test | Should Be $True
}
When "I call Import-Configuration" {
$Settings = ImportConfiguration
Write-Verbose (($Settings | Out-String -Stream | % TrimEnd) -join "`n")
}
When "the ModuleInfo is piped to Import-Configuration" {
$Settings = Get-Module SuperTestModule | Import-Configuration -ErrorAction Stop
Write-Verbose (($Settings | Out-String -Stream | % TrimEnd) -join "`n")
}
When "the ModuleInfo is piped to Get-ConfigurationPath" {
$folder = Get-Module SuperTestModule | Get-ConfigurationPath -ErrorAction Stop
}
When "I call Import-Configuration with a Version" {
$Settings = ImportConfigVersion
Write-Verbose (($Settings | Out-String -Stream | % TrimEnd) -join "`n")
}
When "I call Export-Configuration with" {
param($configuration)
iex "$configuration" | ExportConfiguration
}
When "I call Export-Configuration with a Version" {
param($configuration)
iex "$configuration" | ExportConfigVersion
}
When "I call Get-Metadata (\S+)(?: (\S+))?" {
param($path, $name)
Push-Location $ModulePath
try {
if($name) {
$Result = Get-Metadata $path $name
} else {
$Result = Get-Metadata $path
}
} finally {
Pop-Location
}
}
When "I call Update-Metadata (\S+)(?: (\S+))?" {
param($path, $name)
Push-Location $ModulePath
try {
if($name) {
$Result = Update-Metadata $path $name
} else {
$Result = Update-Metadata $path
}
} finally {
Pop-Location
}
}
When "I call Update-Metadata (\S+) -Increment (\S+)" {
param($path, $name)
Push-Location $ModulePath
try {
$Result = Update-Metadata $path -Increment $name
} finally {
Pop-Location
}
}
Then "the result should be @\((.*)\)" {
param($value)
@($Result).ForEach{ "'$_'" } -join ", " | Should Be $value
}
Then "the result should be (?!@|`")(.*)" {
param($value)
$Result | Should Be $value
}
Then "the string result should be \`"(.*)\`"" {
param($value)
"$Result" | Should Be $value
}
Then "a settings file named (\S+) should exist(?:(?: in the (?<Scope>\S+) folder)|(?: for version (?<Version>[0-9.]+)))*" {
param($fileName, $hashtable, $Scope = $null, $Version = $null)
if($Scope -and $Version) {
$folder = GetStoragePath -Scope $Scope -Version $Version
} elseif($Scope) {
$folder = GetStoragePath -Scope $Scope
} elseif($Version) {
$folder = GetStoragePath -Version $Version
} elseif(Test-Path "${ModulePath}") {
$folder = $ModulePath
} else {
$folder = "TestDrive:/"
}
$SettingsFile = Join-Path $folder $fileName
$SettingsFile | Should Exist
}
Given "a passthru command '(?<Command>[A-Z][a-z]+-[A-Z][a-z]+)' with (?<Parameters>.*) parameters" {
param($Command, $Parameters)
[string[]]$Parameters = $Parameters -split "\s*and\s*" | % { $_.Trim("['`"]") }
$Function = "
function $Command {
param(`$$($Parameters -join ", `$"))
`$global:DebugPreference = 'Continue'
Import-ParameterConfiguration
@{ $(foreach ($name in $Parameters) {
"`n $Name = `$$Name"
})
}
`$global:DebugPreference = 'SilentlyContinue'
}
"
Invoke-Expression $Function
}
Given "a passthru command '(?<Command>[A-Z][a-z]+-[A-Z][a-z]+)' with (?<Parameters>.*) parameters that calls Get-ParameterValue(?<FromFile> with a file config)?" {
param($Command, $Parameters, $FromFile)
[string[]]$Parameters = $Parameters -split "\s*and\s*"
$Function = "
function $Command {
param(
`$$($Parameters -join ", `$"),
[Alias('Alias')]
`$ExtraParameter
)
`$global:DebugPreference = 'Continue'
Get-ParameterValue $(if($FromFile){ "-FromFile Verb.psd1" })
`$global:DebugPreference = 'SilentlyContinue'
}
"
Invoke-Expression $Function
}
Given "an example New-User command" {
function New-User {
[CmdletBinding()]
param(
$FirstName,
$LastName,
$UserName,
$Domain,
$EMail,
$Department,
[hashtable]$Permissions
)
Import-ParameterConfiguration -Recurse -FileName "${Department}User.psd1"
# Possibly calculated based on (default) parameter values
if (-not $UserName) { $UserName = "$FirstName.$LastName" }
if (-not $EMail) { $EMail = "$UserName@$Domain" }
# Lots of work to create the user's AD account, email, set permissions etc.
# Output an object:
[PSCustomObject]@{
PSTypeName = "MagicUser"
FirstName = $FirstName
LastName = $LastName
EMail = $EMail
Department = $Department
Permissions = $Permissions
}
}
}
When "I call (Test-Verb|New-User)(?<Parameters> .*)?" {
param($Command, $Parameters)
try {
# Write-Verbose "$Command $Parameters" -Verbose
# $global:DebugPreference = 'Continue'
$Settings = Invoke-Command ([ScriptBlock]::Create("$Command $Parameters"))
# $global:DebugPreference = 'SilentlyContinue'
# Write-Verbose (($Settings | Out-String -Stream | % TrimEnd) -join "`n") -Verbose
} finally {
Pop-Location
}
}
Given "we define (?<Name>[\:\w]+) = (?<Value>.*)" {
param($Name, $Value)
if ($Name -match "^env:") {
Set-Content $Name $Value
} else {
# we shouldn't have to set it global, but because of pester ...
Set-Variable $Name $Value -Scope Global
}
}
When "we import the file allowing variables (?<Variables>.*)" {
param($Variables)
$Variables = $Variables.Trim() -split "\s*,\s*"
$Settings = Import-Metadata ${SettingsFile} -AllowedVariables $Variables
Write-Verbose (($Settings | Out-String -Stream | ForEach-Object TrimEnd) -join "`n")
}
================================================
FILE: Specs/Configuration.feature
================================================
Feature: Module Configuration
As a PowerShell Module Author
I need to be able to store settings
And override them per-user
Background:
Given the configuration module is imported with testing paths:
| Enterprise | User | Machine |
| TestDrive:/EnterprisePath | TestDrive:/UserPath | TestDrive:/MachinePath |
@Modules @Import
Scenario: Loading Default Settings
Given a module with the name 'MyModule1'
And a settings file named Configuration.psd1
"""
@{
UserName = 'Joel'
Age = 42
}
"""
When I call Import-Configuration
Then the settings object should be of type hashtable
And the settings object's UserName should be of type String
And the settings object's Age should be of type Int32
@Modules @EndUsers
Scenario: End users should be able to read the configuration data for a module
Given a module with the name 'SuperTestModule' by the company 'PoshCode' and the author 'Jaykul'
And a settings file named Configuration.psd1
"""
@{
UserName = 'Joel'
Age = 42
}
"""
When the ModuleInfo is piped to Import-Configuration
Then the settings object should be of type hashtable
And the settings object's UserName should be of type String
And the settings object's Age should be of type Int32
@Modules @Import @EndUsers
Scenario: End users should be able to work with configuration data outside the module
Given a module with the name 'SuperTestModule'
And a settings file named Configuration.psd1 in the Enterprise folder
"""
@{
UserName = 'Joel'
Age = 42
}
"""
When the ModuleInfo is piped to Import-Configuration
Then the settings object should be of type hashtable
And the settings object's UserName should be of type String
And the settings object's Age should be of type Int32
@Modules @Import
Scenario: SxS Versions
Given a module with the name 'MyModule1'
And a settings file named Configuration.psd1 in the Enterprise folder
"""
@{
FullName = 'John Smith'
BirthDay = @{
Month = 'December'
Day = 22
}
}
"""
And a settings file named Configuration.psd1 in the Enterprise folder for version 2.0
"""
@{
FullName = 'Joel Bennett'
UserName = 'Jaykul'
Birthday = @{
Month = 'May'
}
}
"""
When I call Import-Configuration with a version
Then the settings object should be of type hashtable
And the settings object's UserName should be Jaykul
And the settings object's FullName should be Joel Bennett
And the settings object's BirthDay should be of type hashtable
And the settings object's BirthDay.Month should be May
And the settings object's BirthDay.Day should be null
@Modules @Export
Scenario: Exporting creates the expected files
Given a module with the name 'MyModule1' and the author 'Bob'
And a settings file named Configuration.psd1
"""
@{
FullName = 'John Smith'
UserName = 'Jaykul'
BirthDay = @{
Month = 'December'
Day = 22
}
}
"""
When I call Export-Configuration with
"""
@{
FullName = 'Joel Bennett'
Birthday = @{
Month = 'May'
}
}
"""
Then a settings file named Configuration.psd1 should exist in the Enterprise folder
When I call Import-Configuration
Then the settings object should be of type hashtable
And the settings object's UserName should be Jaykul
And the settings object's FullName should be Joel Bennett
And the settings object's BirthDay should be of type hashtable
And the settings object's BirthDay.Month should be May
And the settings object's BirthDay.Day should be 22
@Modules @Export
Scenario: Exporting supports versions
Given a module with the name 'MyModule1' and the author 'Bob'
And a settings file named Configuration.psd1
"""
@{
FullName = 'John Smith'
UserName = 'Jaykul'
BirthDay = @{
Month = 'December'
Day = 22
}
}
"""
When I call Export-Configuration with a version
"""
@{
FullName = 'Joel Bennett'
Birthday = @{
Month = 'May'
}
}
"""
Then a settings file named Configuration.psd1 should exist in the Enterprise folder for version 2.0
When I call Import-Configuration
Then the settings object should be of type hashtable
And the settings object's FullName should be John Smith
And the settings object's BirthDay.Month should be December
When I call Import-Configuration with a version
Then the settings object should be of type hashtable
And the settings object's UserName should be Jaykul
And the settings object's FullName should be Joel Bennett
And the settings object's BirthDay should be of type hashtable
And the settings object's BirthDay.Month should be May
And the settings object's BirthDay.Day should be 22
# @WIP
# Scenario: Migrate settings only once
# Given MyModule has a new version
# And I have some settings from an old version
# When I load the settings in the new module
# Then the settings from the old version should be copied
# And MyModule should be able to migrate them
# But they should save only to the new version
================================================
FILE: Specs/ConfiguredParameters.feature
================================================
Feature: Configure Command From Working Directory
There is a command to support loading default parameter values from the working directory
Background:
Given the configuration module is imported with testing paths:
| Enterprise | User | Machine |
| TestDrive:/EnterprisePath | TestDrive:/UserPath | TestDrive:/MachinePath |
@Functions @Import
Scenario: Loading Default Settings
Given a passthru command 'Test-Verb' with UserName and Age parameters
And a settings file named Verb.psd1 in the current folder
"""
@{
UserName = 'Joel'
Age = 42
}
"""
When I call Test-Verb
Then the output object's userName should be Joel
And the output object's Age should be 42
@Functions @Import
Scenario: Overriding Default Settings
Given a passthru command 'Test-Verb' with UserName and Age parameters
And a settings file named Verb.psd1 in the current folder
"""
@{
UserName = 'Joel'
Age = 42
}
"""
When I call Test-Verb Mark
Then the output object's userName should be Mark
And the output object's Age should be 42
@Functions @Import
Scenario: Overriding Default Settings Works on any Parameter
Given a passthru command 'Test-Verb' with UserName and Age parameters
And a settings file named Verb.psd1 in the current folder
"""
@{
UserName = 'Joel'
Age = 42
}
"""
When I call Test-Verb -Age 10
Then the output object's userName should be Joel
And the output object's Age should be 10
@Functions @Import
Scenario: New-User Example
Given an example New-User command
And a settings file named User.psd1 in the current folder
"""
@{
Domain = 'HuddledMasses.org'
}
"""
When I call New-User Joel Bennett
Then the output object's EMail should be Joel.Bennett@HuddledMasses.org
@Functions @Import
Scenario: New-User Example Two (overwriting)
Given an example New-User command
And a settings file named User.psd1 in the current folder
"""
@{
Permissions = @{
Access = "Administrator"
}
}
"""
And a settings file named User.psd1 in the parent folder
"""
@{
Department = "Security"
Permissions = @{
Access = "User"
}
}
"""
And a settings file named User.psd1
"""
@{
Domain = "HuddledMasses.org"
}
"""
When I call New-User Joel Bennett
Then the output object's EMail should be Joel.Bennett@HuddledMasses.org
And the output object's Department should be Security
And the output object's Permissions should be of type [hashtable]
And the output object's Permissions.Access should be Administrator
@Functions @Import
Scenario: New-User Example Three
Given an example New-User command
And a settings file named SecurityUser.psd1 in the current folder
"""
@{
Domain = 'HuddledMasses.org'
Permissions = @{
Access = "Administrator"
}
}
"""
When I call New-User Joel Bennett -Department Security
Then the output object's EMail should be Joel.Bennett@HuddledMasses.org
And the output object's Permissions should be of type [hashtable]
And the output object's Permissions.Access should be of type [string]
And the output object's Permissions.Access should be Administrator
================================================
FILE: Specs/DefaultParameters.feature
================================================
Feature: Get PSBoundParameters plus default values plus a config file
There is a command to support merging PSBoundParameters with parmeter default values
That command supports overwriting the default values with values from a config file
Background:
Given the configuration module is imported
@Functions @ParameterValue
Scenario: Loading Default Settings
Given a passthru command 'Test-Verb' with UserName and Age=42 parameters that calls Get-ParameterValue
When I call Test-Verb Joel
Then the output object's UserName should be Joel
And the output object's Age should be 42
@Functions @ParameterValue
Scenario: Loading Default Settings
Given a passthru command 'Test-Verb' with UserName and Age=12 parameters that calls Get-ParameterValue
When I call Test-Verb Joel
Then the output object's UserName should be Joel
And the output object's Age should be 12
@Functions @ParameterValue
Scenario: Overriding Default Settings
Given a passthru command 'Test-Verb' with UserName='Sarah' and Age=12 parameters that calls Get-ParameterValue
When I call Test-Verb Joel 24
Then the output object's UserName should be Joel
And the output object's Age should be 24
@Functions @ParameterValue
Scenario: Configuration file
Given a passthru command 'Test-Verb' with UserName='Sarah' and Age=12 parameters that calls Get-ParameterValue with a file config
And a settings file named Verb.psd1 in the current folder
"""
@{
UserName = 'Joel'
Age = 42
}
"""
When I call Test-Verb -Age 10
Then the output object's userName should be Joel
And the output object's Age should be 10
@Functions @ParameterValue
Scenario: Configuration file with aliases
Given a passthru command 'Test-Verb' with UserName='Sarah' and Age=12 parameters that calls Get-ParameterValue with a file config
And a settings file named Verb.psd1 in the current folder
"""
@{
UserName = 'Joel'
Age = 42
Alias = 'Supports Aliases'
}
"""
When I call Test-Verb -Age 10
Then the output object's userName should be Joel
And the output object's Age should be 10
And the output object's ExtraParameter should be Supports Aliases
================================================
FILE: Specs/Layering.feature
================================================
Feature: Multiple settings files should layer
As a module author, I want to distribute a default config with my module so that by default it has settings
As a machine administrator, I want to save corporate defaults so that users start with the right configuration
As a user I want to save my own preferences because those other guys are frequently wrong
And our custom settings shouldn't be overwritten on upgrade
Background:
Given the configuration module is imported with testing paths:
| Enterprise | User | Machine |
| TestDrive:/EnterprisePath | TestDrive:/UserPath | TestDrive:/MachinePath |
@Modules @Import @Layering
Scenario: Loading LocalMachine Overrides
Given a module with the name 'MyModule1'
And a settings file named Configuration.psd1
"""
@{
UserName = 'Joel'
Age = 42
}
"""
And a settings file named Configuration.psd1 in the Machine folder
"""
@{
UserName = 'Joel Bennett'
}
"""
When I call Import-Configuration
Then the settings object should be of type hashtable
And the settings object's UserName should be Joel Bennett
And the settings object's Age should be 42
@Modules @Import @Layering
Scenario: Multi-level Overrides
Given a module with the name 'MyModule1'
And a settings file named Configuration.psd1
"""
@{
FullName = 'John Smith'
BirthDay = @{
Month = 'December'
Day = 22
}
}
"""
And a settings file named Configuration.psd1 in the Enterprise folder
"""
@{
FullName = 'Joel Bennett'
UserName = 'Jaykul'
Birthday = @{
Month = 'May'
}
}
"""
When I call Import-Configuration
Then the settings object should be of type hashtable
And the settings object's UserName should be Jaykul
And the settings object's FullName should be Joel Bennett
And the settings object's BirthDay should be of type hashtable
And the settings object's BirthDay.Month should be May
And the settings object's BirthDay.Day should be 22
@Modules @Import @Layering
Scenario: Object Property Overrides
Given a module with the name 'MyModule1'
And a settings file named Configuration.psd1
"""
@{
FullName = 'John Smith'
BirthDay = PSObject @{
Month = 'December'
Day = 25
}
}
"""
And a settings file named Configuration.psd1 in the Enterprise folder
"""
@{
FullName = 'Joel Bennett'
UserName = 'Jaykul'
Birthday = @{
Month = 'May'
}
}
"""
And a settings file named Configuration.psd1 in the User folder
"""
@{
FullName = 'Joel Bennett'
UserName = 'Jaykul'
Birthday = PSObject @{
Day = 22
}
}
"""
When I call Import-Configuration
Then the settings object should be of type hashtable
And the settings object's UserName should be Jaykul
And the settings object's FullName should be Joel Bennett
And the settings object's BirthDay should be of type PSCustomObject
And the settings object's BirthDay.Month should be May
And the settings object's BirthDay.Day should be 22
@Modules @Import @Layering
Scenario: Loading User Overrides
Given a module with the name 'MyModule1'
And a settings file named Configuration.psd1
"""
@{
UserName = 'John Smith'
Age = 24
}
"""
And a settings file named Configuration.psd1 in the Machine folder
"""
@{
UserName = 'Jaykul'
}
"""
And a settings file named Configuration.psd1 in the Enterprise folder
"""
@{
Age = 42
}
"""
And a settings file named Configuration.psd1 in the User folder
"""
@{
FullName = 'Joel Bennett'
}
"""
When I call Import-Configuration
Then the settings object should be of type hashtable
And the settings object's UserName should be Jaykul
And the settings object's FullName should be Joel Bennett
And the settings object's Age should be 42
@Modules @Import @Layering
Scenario: Multi-level Overrides
Given a module with the name 'MyModule1'
And a settings file named Configuration.psd1 in the Enterprise folder
"""
@{
FullName = 'Joel Bennett'
UserName = 'Jaykul'
Birthday = @{
Month = 'May'
Day = 22
}
}
"""
When I call Import-Configuration
Then the settings object should be of type hashtable
And the settings object's UserName should be Jaykul
And the settings object's FullName should be Joel Bennett
And the settings object's BirthDay should be of type hashtable
And the settings object's BirthDay.Month should be May
And the settings object's BirthDay.Day should be 22
@Layering @WIP
Scenario: Save shouldn't overwrite default settings
Given a module with the name 'MyModule1'
And a settings file named Configuration.psd1
"""
@{
UserName = 'Joel'
Age = 42
}
"""
When I call Export-Configuration with
"""
@{
UserName = 'Joel Bennett'
}
"""
# The settings file should not be changed:
Then the settings file should contain
"""
@{
UserName = 'Joel'
Age = 42
}
"""
================================================
FILE: Specs/LocalStoragePath.feature
================================================
@StoragePath
Feature: Automatically Calculate Local Storage Paths
In order for module settings to survive upgrades
A PowerShell Module Author
Needs a place outside their module to save settings
For developer guidelines, see: http://msdn.microsoft.com/en-us/library/windows/apps/hh465094.aspx
We create a module-specific storage location inside the operating-system specified data paths:
By default, we store in the $Env:AppData user roaming path that Windows synchronizes (C:/Users/USERNAME/AppData/Roaming)
But we support using the $Env:ProgramData machine-local path instead (C:/ProgramData)
As well as the machine-specific $Env:LocalAppData user data path (C:/Users/USERNAME/AppData/Local)
Background:
Given the configuration module is imported with testing paths:
| Enterprise | User | Machine |
| TestDrive:/EnterprisePath | TestDrive:/UserPath | TestDrive:/MachinePath |
@Scripts
Scenario: Scripts will fail unless they specify the names
Given a script with the name 'SuperTestScript' that calls Get-ConfigurationPath with no parameters
Then the script should throw an exception
Given a script with the name 'SuperTestScript' that calls Get-ConfigurationPath -Name TestScript -Author Author
Then the script's Enterprise path should match '^TestDrive:/EnterprisePath/' and 'Author/TestScript'
@Modules
Scenario Outline: Modules storage paths work at load time
Given a module with the name 'SimpleTest' by the author 'Joel Bennett'
Then the module's user path at load time should match '^TestDrive:/UserPath/' and '/Joel Bennett/SimpleTest$'
And the module's user path should exist already
@Modules
Scenario Outline: Modules get automatic storage paths
Given a module with the name '<modulename>' by the author 'Joel Bennett'
Then the module's Enterprise path should match '^TestDrive:/EnterprisePath/' and '/Joel Bennett/<modulename>$'
And the module's Enterprise path should exist already
"""
There is a <modulename>
"""
Examples: A few different module names
| modulename |
| SuperTestModule |
| AnotherTestModule |
| ThirdModuleName |
@Modules
Scenario Outline: Modules get automatic storage paths on Linux
Given a module with the name '<modulename>' by the author 'Joel Bennett'
Then the module's Enterprise path should match '^TestDrive:/EnterprisePath/' and '/Joel Bennett/<modulename>$'
And the module's Enterprise path should exist already
"""
There is a <modulename>
"""
Examples: A few different module names
| modulename |
| SuperTestModule |
| AnotherTestModule |
| ThirdModuleName |
@Modules
Scenario Outline: There should be a way to store settings at the Machine and User scope too
Given a module with the name '<modulename>' with the author ''
Then the module's <scope> path should match '^<rootpattern>' and '/AnonymousModules/<modulename>$'
And the module's <scope> path should exist already
Examples:
| scope | modulename | rootpattern |
| Enterprise | SuperTestModule | TestDrive:/EnterprisePath |
| Machine | SuperTestModule | TestDrive:/MachinePath |
| User | SuperTestModule | TestDrive:/UserPath |
@Modules
Scenario Outline: To allow us to upgrade, settings should be versionable
Given a module with the name 'SuperTestModule' by the author 'Joel'
Then the module's Enterprise path should match '^TestDrive:/EnterprisePath/' and '/Joel/SuperTestModule$'
But the module's storage path should end with a version number if one is passed in
@Modules
Scenario Outline: To support differentiation, settings should support a company name instead
Given a module with the name 'SuperTestModule' by the company 'PoshCode' and the author 'Jaykul'
Then the module's Enterprise path should match '^TestDrive:/EnterprisePath/' and '/PoshCode/SuperTestModule$'
And the module's storage path should end with a version number if one is passed in
@Modules @EndUsers
Scenario: End users should be able to find the storage path for a module
Given a module with the name 'SuperTestModule' by the company 'PoshCode' and the author 'Jaykul'
When the ModuleInfo is piped to Get-ConfigurationPath
Then the resulting path should match '^TestDrive:/EnterprisePath/' and '/PoshCode/SuperTestModule$'
================================================
FILE: Specs/LocalStoragePathLinux.feature
================================================
@StoragePath
Feature: Automatically Calculate Local Storage Paths on Linux
In order for module settings to survive upgrades
A PowerShell Module Author
Needs a place outside their module to save settings
For developer guidelines, see: http://msdn.microsoft.com/en-us/library/windows/apps/hh465094.aspx
We create a module-specific storage location inside the operating-system specified data paths:
By default, we store in the $Env:AppData user roaming path that Windows synchronizes (C:/Users/USERNAME/AppData/Roaming)
But we support using the $Env:ProgramData machine-local path instead (C:/ProgramData)
As well as the machine-specific $Env:LocalAppData user data path (C:/Users/USERNAME/AppData/Local)
@Modules @Linux
Scenario Outline: On Linux the default configuration paths are different
Given the configuration module is imported on Linux:
Given a module with the name '<modulename>' with the author 'Jaykul'
Then the module's <scope> path should match '^<rootpattern>' and '/Jaykul/<modulename>$'
# And the module's <scope> path should exist already
Examples:
| scope | modulename | rootpattern |
| Enterprise | SuperTestModule | ~/.local/share |
| Machine | SuperTestModule | /etc/xdg |
| User | SuperTestModule | ~/.config |
@Modules @Linux
Scenario Outline: Modules storage paths work at load time on Linux
Given the configuration module is imported on Linux:
Given a module with the name 'SimpleTest' by the author 'Joel Bennett'
Then the module's user path at load time should match '^~/.config' and '/Joel Bennett/SimpleTest$'
And the module's user path should exist already
@Modules @Linux
Scenario Outline: There should be a way to store settings at the Machine and User scope on Linux too
Given the configuration module is imported with testing paths on Linux:
| Enterprise | User | Machine |
| TestDrive:/EnterprisePath | TestDrive:/UserPath | TestDrive:/MachinePath |
Given a module with the name '<modulename>' with the author ''
Then the module's <scope> path should match '^<rootpattern>' and '/AnonymousModules/<modulename>$'
And the module's <scope> path should exist already
Examples:
| scope | modulename | rootpattern |
| Enterprise | SuperTestModule | TestDrive:/EnterprisePath |
| Machine | SuperTestModule | TestDrive:/MachinePath |
| User | SuperTestModule | TestDrive:/UserPath |
================================================
FILE: Specs/Manifest.feature
================================================
Feature: Manifest Read and Write
As a PowerShell Module Author
I want to easily edit my manifest as part of my build script
Background:
Given the configuration module is imported with testing paths:
| Enterprise | User | Machine |
| TestDrive:/EnterprisePath | TestDrive:/UserPath | TestDrive:/MachinePath |
@Modules @Import
Scenario: Read ModuleVersion from a module manifest by default
Given a module with the name 'ModuleName'
And a module manifest named ModuleName.psd1
"""
@{
# Script module or binary module file associated with this manifest.
ModuleToProcess = './Configuration.psm1'
# Version number of this module.
ModuleVersion = '0.4'
# ID used to uniquely identify this module
GUID = 'e56e5bec-4d97-4dfd-b138-abbaa14464a6'
}
"""
When I call Get-Metadata ModuleName.psd1
Then the result should be 0.4
Scenario: Read a named value from a module manifest
Given a module with the name 'ModuleName'
And a module manifest named ModuleName.psd1
"""
@{
# Script module or binary module file associated with this manifest.
ModuleToProcess = './Configuration.psm1'
# Version number of this module.
ModuleVersion = '0.4'
Description = 'This is the day'
}
"""
When I call Get-Metadata ModuleName.psd1 Description
Then the result should be This is the day
Scenario: Read a named value from a module manifest PrivateData
Given a module with the name 'ModuleName'
And a module manifest named ModuleName.psd1
"""
@{
# Script module or binary module file associated with this manifest.
ModuleToProcess = './Configuration.psm1'
# Version number of this module.
ModuleVersion = '0.4'
PrivateData = @{
MyVeryOwnKey = "Test Value"
}
}
"""
When I call Get-Metadata ModuleName.psd1 MyVeryOwnKey
Then the result should be Test Value
Scenario: Read the release notes from a module manifest PSData
Given a module with the name 'ModuleName'
And a module manifest named ModuleName.psd1
"""
@{
# Script module or binary module file associated with this manifest.
ModuleToProcess = './Configuration.psm1'
# Version number of this module.
ModuleVersion = '0.4'
PrivateData = @{
MyVeryOwnKey = "Test Value"
PSData = @{
ReleaseNotes = "Nothing has changed"
}
}
}
"""
When I call Get-Metadata ModuleName.psd1 ReleaseNotes
Then the result should be Nothing has changed
Scenario: Attempt to read a non-existent value
Given a module with the name 'ModuleName'
And a module manifest named ModuleName.psd1
"""
@{
# Script module or binary module file associated with this manifest.
ModuleToProcess = './Configuration.psm1'
# Version number of this module.
ModuleVersion = '0.4'
}
"""
Given we expect an error in the Metadata module
When I call Get-Metadata ModuleName.psd1 NoSuchThing
Then the error is logged exactly 1 time
Scenario: Update the module version by default
Given a module with the name 'ModuleName'
And a module manifest named ModuleName.psd1
"""
@{
# Script module or binary module file associated with this manifest.
ModuleToProcess = './Configuration.psm1'
# Version number of this module.
ModuleVersion = '0.4'
}
"""
When I call Update-Metadata ModuleName.psd1
And I call Get-Metadata ModuleName.psd1
Then the result should be 0.4.1
Scenario: Update the module major version
Given a module with the name 'ModuleName'
And a module manifest named ModuleName.psd1
"""
@{
# Script module or binary module file associated with this manifest.
ModuleToProcess = './Configuration.psm1'
# Version number of this module.
ModuleVersion = '0.4'
}
"""
When I call Update-Metadata ModuleName.psd1 -Increment Major
And I call Get-Metadata ModuleName.psd1
Then the result should be 1.0
Scenario: Update the module minor version
Given a module with the name 'ModuleName'
And a module manifest named ModuleName.psd1
"""
@{
# Script module or binary module file associated with this manifest.
ModuleToProcess = './Configuration.psm1'
# Version number of this module.
ModuleVersion = '0.4'
}
"""
When I call Update-Metadata ModuleName.psd1 -Increment Minor
And I call Get-Metadata ModuleName.psd1
Then the result should be 0.5
Scenario: Update the module minor version when it's 0
Given a module with the name 'ModuleName'
And a module manifest named ModuleName.psd1
"""
@{
# Script module or binary module file associated with this manifest.
ModuleToProcess = './Configuration.psm1'
# Version number of this module.
ModuleVersion = '1.0'
}
"""
When I call Update-Metadata ModuleName.psd1 -Increment Minor
And I call Get-Metadata ModuleName.psd1
Then the result should be 1.1
Scenario: Update the module build version
Given a module with the name 'ModuleName'
And a module manifest named ModuleName.psd1
"""
@{
# Script module or binary module file associated with this manifest.
ModuleToProcess = './Configuration.psm1'
# Version number of this module.
ModuleVersion = '0.4.1'
}
"""
When I call Update-Metadata ModuleName.psd1 -Increment Build
And I call Get-Metadata ModuleName.psd1
Then the result should be 0.4.2
Scenario: Update the module build version when it's 0
Given a module with the name 'ModuleName'
And a module manifest named ModuleName.psd1
"""
@{
# Script module or binary module file associated with this manifest.
ModuleToProcess = './Configuration.psm1'
# Version number of this module.
ModuleVersion = '0.4'
}
"""
When I call Update-Metadata ModuleName.psd1 -Increment Build
And I call Get-Metadata ModuleName.psd1
Then the result should be 0.4.1
Scenario: Update the module revision
Given a module with the name 'ModuleName'
And a module manifest named ModuleName.psd1
"""
@{
# Script module or binary module file associated with this manifest.
ModuleToProcess = './Configuration.psm1'
# Version number of this module.
ModuleVersion = '4.3.2.1'
}
"""
When I call Update-Metadata ModuleName.psd1 -Increment Revision
And I call Get-Metadata ModuleName.psd1
Then the result should be 4.3.2.2
Scenario: Update the module revision when the build isn't set
Given a module with the name 'ModuleName'
And a module manifest named ModuleName.psd1
"""
@{
# Script module or binary module file associated with this manifest.
ModuleToProcess = './Configuration.psm1'
# Version number of this module.
ModuleVersion = '0.4'
}
"""
When I call Update-Metadata ModuleName.psd1 -Increment Revision
And I call Get-Metadata ModuleName.psd1
Then the result should be 0.4.0.1
@Regression
Scenario: Get Arrays from a metadata file
Given a module with the name 'ModuleName'
And a module manifest named ModuleName.psd1
"""
@{
# Script module or binary module file associated with this manifest.
ModuleToProcess = './Configuration.psm1'
# Version number of this module.
ModuleVersion = '0.4'
AliasesToExport = @('Get-StoragePath', 'Get-ManifestValue', 'Update-Manifest')
}
"""
When I call Get-Metadata ModuleName.psd1 AliasesToExport
Then the result should be @('Get-StoragePath', 'Get-ManifestValue', 'Update-Manifest')
================================================
FILE: Specs/ScriptAnalyzer.Steps.ps1
================================================
# Generate ScriptAnalyzer.feature
$Path = GetModuleBase
# The name (or path) of a settings file to be used.
[string]$Settings = "PSScriptAnalyzerSettings.psd1"
Write-Verbose "Resolve settings '$Settings'"
if (Test-Path $Settings) {
$Settings = Resolve-Path $Settings
} else {
Set-Location $PSScriptRoot\..
if (Test-Path $Settings) {
$Settings = Resolve-Path $Settings
} else {
foreach ($directory in Get-ChildItem -Directory) {
$search = Join-Path $directory.FullName $Settings
if (Test-Path $search) {
$Settings = Resolve-Path $search
break
}
}
}
}
$ExcludeRules = @()
$Rules = @(
if (!(Test-Path $Settings)) {
Write-Warning "Could not find a 'PSScriptAnalyzerSettings.psd1'"
} else {
Write-Verbose "Loading $Settings"
$Config = Import-LocalizedData -BaseDirectory ([IO.Path]::GetDirectoryName($Settings)) -FileName ([IO.Path]::GetFileName($Settings))
$ExcludeRules = @($Config.ExcludeRules)
if ($Config.CustomRulePath -and (Test-Path $Config.CustomRulePath)) {
$CustomRules = @{
CustomRulePath = @($Config.CustomRulePath)
RecurseCustomRulePath = [bool]$Config.RecurseCustomRulePath
}
Get-ScriptAnalyzerRule @CustomRules
}
}
Get-ScriptAnalyzerRule
) | Where-Object RuleName -notin $ExcludeRules
Set-Content "$PSScriptRoot\ScriptAnalyzer.feature" @"
# This feature file is re-generated by ScriptAnalyzer.Steps.ps1 whenever the tests are run
@ScriptAnalyzer
Feature: Passes Script Analyzer
This module should pass Invoke-ScriptAnalyzer with flying colors
Scenario: ScriptAnalyzer on the compiled module output
Given the configuration module is imported
When we run ScriptAnalyzer on '$Path' with '$Settings'
$( foreach ($Rule in $Rules.RuleName) {"
Then it passes the ScriptAnalyzer rule $Rule"
})
"@
When "we run ScriptAnalyzer on '(?<Path>.*)' with '(?<Settings>.*)'" {
param($Path, $Settings)
try {
$script:ScriptAnalyzerResults = Invoke-ScriptAnalyzer @PSBoundParameters
} catch {
Write-Warning "Exception running script analyzer on $($_.TargetObject)"
Write-Warning $($_.Exception.StackTrace)
throw $_
}
}
Then "it passes the ScriptAnalyzer rule (?<RuleName>.*)" {
param($RuleName)
$rule = $script:ScriptAnalyzerResults.Where({$_.RuleName -eq $RuleName})
if ($rule) { # ScriptAnalyzer only has results for failed tests
throw ([Management.Automation.ErrorRecord]::new(
([Exception]::new(($rule.ForEach{$_.ScriptName + ":" + $_.Line + " " + $_.Message} -join "`n"))),
"ScriptAnalyzerViolation",
"SyntaxError",
$rule))
}
}
================================================
FILE: Specs/ScriptAnalyzer.feature
================================================
# This feature file is re-generated by ScriptAnalyzer.Steps.ps1 whenever the tests are run
@ScriptAnalyzer
Feature: Passes Script Analyzer
This module should pass Invoke-ScriptAnalyzer with flying colors
Scenario: ScriptAnalyzer on the compiled module output
Given the configuration module is imported
When we run ScriptAnalyzer on 'C:\Users\Jaykul\Projects\Modules\Configuration\1.6.0' with 'C:\Users\Jaykul\Projects\Modules\Configuration\PSScriptAnalyzerSettings.psd1'
Then it passes the ScriptAnalyzer rule PSAlignAssignmentStatement
Then it passes the ScriptAnalyzer rule PSAvoidUsingCmdletAliases
Then it passes the ScriptAnalyzer rule PSAvoidAssignmentToAutomaticVariable
Then it passes the ScriptAnalyzer rule PSAvoidDefaultValueSwitchParameter
Then it passes the ScriptAnalyzer rule PSAvoidDefaultValueForMandatoryParameter
Then it passes the ScriptAnalyzer rule PSAvoidUsingEmptyCatchBlock
Then it passes the ScriptAnalyzer rule PSAvoidGlobalAliases
Then it passes the ScriptAnalyzer rule PSAvoidGlobalFunctions
Then it passes the ScriptAnalyzer rule PSAvoidGlobalVars
Then it passes the ScriptAnalyzer rule PSAvoidInvokingEmptyMembers
Then it passes the ScriptAnalyzer rule PSAvoidLongLines
Then it passes the ScriptAnalyzer rule PSAvoidMultipleTypeAttributes
Then it passes the ScriptAnalyzer rule PSAvoidNullOrEmptyHelpMessageAttribute
Then it passes the ScriptAnalyzer rule PSAvoidOverwritingBuiltInCmdlets
Then it passes the ScriptAnalyzer rule PSAvoidUsingPositionalParameters
Then it passes the ScriptAnalyzer rule PSReservedCmdletChar
Then it passes the ScriptAnalyzer rule PSReservedParams
Then it passes the ScriptAnalyzer rule PSAvoidSemicolonsAsLineTerminators
Then it passes the ScriptAnalyzer rule PSAvoidShouldContinueWithoutForce
Then it passes the ScriptAnalyzer rule PSAvoidTrailingWhitespace
Then it passes the ScriptAnalyzer rule PSAvoidUsingUsernameAndPasswordParams
Then it passes the ScriptAnalyzer rule PSAvoidUsingBrokenHashAlgorithms
Then it passes the ScriptAnalyzer rule PSAvoidUsingComputerNameHardcoded
Then it passes the ScriptAnalyzer rule PSAvoidUsingConvertToSecureStringWithPlainText
Then it passes the ScriptAnalyzer rule PSAvoidUsingDoubleQuotesForConstantString
Then it passes the ScriptAnalyzer rule PSAvoidUsingInvokeExpression
Then it passes the ScriptAnalyzer rule PSAvoidUsingPlainTextForPassword
Then it passes the ScriptAnalyzer rule PSAvoidUsingWMICmdlet
Then it passes the ScriptAnalyzer rule PSAvoidUsingWriteHost
Then it passes the ScriptAnalyzer rule PSUseCompatibleCommands
Then it passes the ScriptAnalyzer rule PSUseCompatibleSyntax
Then it passes the ScriptAnalyzer rule PSUseCompatibleTypes
Then it passes the ScriptAnalyzer rule PSMisleadingBacktick
Then it passes the ScriptAnalyzer rule PSMissingModuleManifestField
Then it passes the ScriptAnalyzer rule PSPlaceCloseBrace
Then it passes the ScriptAnalyzer rule PSPlaceOpenBrace
Then it passes the ScriptAnalyzer rule PSPossibleIncorrectComparisonWithNull
Then it passes the ScriptAnalyzer rule PSPossibleIncorrectUsageOfRedirectionOperator
Then it passes the ScriptAnalyzer rule PSProvideCommentHelp
Then it passes the ScriptAnalyzer rule PSReviewUnusedParameter
Then it passes the ScriptAnalyzer rule PSUseApprovedVerbs
Then it passes the ScriptAnalyzer rule PSUseBOMForUnicodeEncodedFile
Then it passes the ScriptAnalyzer rule PSUseCmdletCorrectly
Then it passes the ScriptAnalyzer rule PSUseCompatibleCmdlets
Then it passes the ScriptAnalyzer rule PSUseConsistentIndentation
Then it passes the ScriptAnalyzer rule PSUseConsistentWhitespace
Then it passes the ScriptAnalyzer rule PSUseCorrectCasing
Then it passes the ScriptAnalyzer rule PSUseDeclaredVarsMoreThanAssignments
Then it passes the ScriptAnalyzer rule PSUseLiteralInitializerForHashtable
Then it passes the ScriptAnalyzer rule PSUseOutputTypeCorrectly
Then it passes the ScriptAnalyzer rule PSUseProcessBlockForPipelineCommand
Then it passes the ScriptAnalyzer rule PSUsePSCredentialType
Then it passes the ScriptAnalyzer rule PSShouldProcess
Then it passes the ScriptAnalyzer rule PSUseShouldProcessForStateChangingFunctions
Then it passes the ScriptAnalyzer rule PSUseSingularNouns
Then it passes the ScriptAnalyzer rule PSUseSupportsShouldProcess
Then it passes the ScriptAnalyzer rule PSUseToExportFieldsInManifest
Then it passes the ScriptAnalyzer rule PSUseUsingScopeModifierInNewRunspaces
Then it passes the ScriptAnalyzer rule PSUseUTF8EncodingForHelpFile
Then it passes the ScriptAnalyzer rule PSDSCDscExamplesPresent
Then it passes the ScriptAnalyzer rule PSDSCDscTestsPresent
Then it passes the ScriptAnalyzer rule PSDSCReturnCorrectTypesForDSCFunctions
Then it passes the ScriptAnalyzer rule PSDSCUseIdenticalMandatoryParametersForDSC
Then it passes the ScriptAnalyzer rule PSDSCUseIdenticalParametersForDSC
Then it passes the ScriptAnalyzer rule PSDSCStandardDSCFunctionsInResource
Then it passes the ScriptAnalyzer rule PSDSCUseVerboseMessageInDSCResource
================================================
FILE: Specs/Serialization.feature
================================================
Feature: Serialize Hashtables or Custom Objects
To allow users to configure module preferences without editing their profiles
A PowerShell Module Author
Needs to serialize a preferences object in a user-editable format we call metadata
Background:
Given the configuration module is imported with testing paths:
| Enterprise | User | Machine |
| TestDrive:/EnterprisePath | TestDrive:/UserPath | TestDrive:/MachinePath |
@Serialization
Scenario: Serialize a hashtable to string
Given a settings hashtable
"""
@{ UserName = "Joel"; BackgroundColor = "Black"}
"""
When we convert the settings to metadata
Then the string version should be
"""
@{
UserName = 'Joel'
BackgroundColor = 'Black'
}
"""
@Serialization @ConsoleColor
Scenario: Serialize a ConsoleColor to string
Given a settings hashtable
"""
@{ UserName = "Joel"; BackgroundColor = [ConsoleColor]::Black }
"""
When we convert the settings to metadata
Then the string version should be
"""
@{
UserName = 'Joel'
BackgroundColor = (ConsoleColor Black)
}
"""
@Serialization
Scenario: Should be able to serialize core types:
Given a settings hashtable with a String in it
When we convert the settings to metadata
Then the string version should match 'TestCase = ([''"])[^\1]+\1'
Given a settings hashtable with a Boolean in it
When we convert the settings to metadata
Then the string version should match 'TestCase = \`$(True|False)'
Given a settings hashtable with a NULL in it
When we convert the settings to metadata
Then the string version should match 'TestCase = ""'
Given a settings hashtable with a Number in it
When we convert the settings to metadata
Then the string version should match 'TestCase = \d+'
@Serialization
Scenario: Should be able to serialize a array
Given a settings hashtable with an Array in it
When we convert the settings to metadata
Then the string version should match 'TestCase = ([^,]*,)+[^,]*'
@Serialization
Scenario: Should be able to serialize nested hashtables
Given a settings hashtable with a hashtable in it
When we convert the settings to metadata
Then the string version should match 'TestCase = @{'
@Serialization @SecureString @PSCredential @CRYPT32
Scenario Outline: Should be able to serialize PSCredential
Given a settings hashtable with a PSCredential in it
When we convert the settings to metadata
Then the string version should match "TestCase = \(?PSCredential"
@Serialization @SecureString @CRYPT32
Scenario Outline: Should be able to serialize SecureStrings
Given a settings hashtable with a SecureString in it
When we convert the settings to metadata
Then the string version should match "TestCase = \(?ConvertTo-SecureString [a-z0-9]+"
@Serialization @CRYPT32
Scenario Outline: Should support a few additional types
Given a settings hashtable with a <type> in it
When we convert the settings to metadata
Then the string version should match "TestCase = \(?<type> "
Examples:
| type |
| DateTime |
| DateTimeOffset |
| GUID |
| PSObject |
| PSCredential |
| ConsoleColor |
@Serialization
Scenario: PSCustomObject preserves PSTypeNames
Given a settings object
"""
@{
PSTypeName = 'Whatever.User'
FirstName = 'Joel'
LastName = 'Bennett'
UserName = 'Jaykul'
Homepage = [Uri]"http://HuddledMasses.org"
}
"""
When we export to a settings file named Configuration.psd1
And we import the file to an object
Then the settings object should have Whatever.User in the PSTypeNames
@Serialization @Enum
Scenario: Unsupported types should be serialized as strings
Given a settings hashtable with an Enum in it
Then we expect a warning in the Metadata module
When we convert the settings to metadata
And the warning is logged
@Serialization @Error @Converter
Scenario: Invalid converters should write non-terminating errors
Given we expect an error in the Metadata module
When we add a converter that's not a scriptblock
And we add a converter with a number as a key
Then the error is logged exactly 2 times
@Serialization @Uri @Converter
Scenario: Developers should be able to add support for other types
Given a settings hashtable with a Uri in it
When we add a converter for Uri types
And we convert the settings to metadata
Then the string version should match "TestCase = \(?Uri '.*'"
@Serialization @File
Scenario: Developers should be able to export straight to file
Given a settings hashtable
"""
@{
UserName = 'Joel'
Age = 42
}
"""
When we export to a settings file named Configuration.psd1
Then the settings file should contain
"""
@{
UserName = 'Joel'
Age = 42
}
"""
@Deserialization @Uri @Converter
Scenario: I should be able to import serialized data
Given a settings hashtable
"""
@{
UserName = 'Joel'
Age = 42
LastUpdated = (Get-Date).Date
Homepage = [Uri]"http://HuddledMasses.org"
}
"""
Then the settings object's Homepage should be of type Uri
And we add a converter for Uri types
And we convert the settings to metadata
When we convert the metadata to an object
Then the settings object should be of type hashtable
Then the settings object's UserName should be of type String
Then the settings object's Age should be of type Int32
Then the settings object's LastUpdated should be of type DateTime
Then the settings object's Homepage should be of type Uri
@DeSerialization @SecureString @PSCredential @CRYPT32
Scenario Outline: I should be able to import serialized credentials and secure strings
Given a settings hashtable
"""
@{
Credential = [PSCredential]::new("UserName",(ConvertTo-SecureString Password -AsPlainText -Force))
Password = ConvertTo-SecureString Password -AsPlainText -Force
}
"""
When we convert the settings to metadata
Then the string version should match "Credential = \(?PSCredential"
And the string version should match "Password = \(?ConvertTo-SecureString [\"a-z0-9]*"
When we convert the metadata to an object
Then the settings object should be of type hashtable
Then the settings object's Credential should be of type PSCredential
Then the settings object's Password should be of type SecureString
@Serialization @SecureString @CRYPT32
Scenario Outline: Should be able to serialize SecureStrings
Given a settings hashtable with a SecureString in it
When we convert the settings to metadata
Then the string version should match "TestCase = \(?ConvertTo-SecureString [a-z0-9]+"
@Deserialization @Uri @Converter
Scenario: I should be able to import serialized data even in PowerShell 2
Given a settings hashtable
"""
@{
UserName = New-Object PSObject -Property @{ FirstName = 'Joel'; LastName = 'Bennett' }
Age = [Version]4.2
LastUpdated = [DateTimeOffset](Get-Date).Date
GUID = [GUID]::NewGuid()
Color = [ConsoleColor]::Red
}
"""
And we fake version 2.0 in the Metadata module
And we add a converter for Uri types
And we convert the settings to metadata
When we convert the metadata to an object
Then the settings object should be of type hashtable
And the settings object's UserName should be of type PSObject
And the settings object's Age should be of type String
And the settings object's LastUpdated should be of type DateTimeOffset
And the settings object's GUID should be of type GUID
And the settings object's Color should be of type ConsoleColor
@Deserialization @Uri @Converter
Scenario: I should be able to add converters at import time
Given the configuration module is imported with a URL converter
And a settings hashtable
"""
@{
UserName = 'Joel'
Age = 42
Homepage = [Uri]"http://HuddledMasses.org"
}
"""
Then the settings object's Homepage should be of type Uri
And we convert the settings to metadata
Then the string version should match
"""
Homepage = \(?Uri 'http://HuddledMasses.org/'
"""
When we convert the metadata to an object
Then the settings object should be of type hashtable
And the settings object's UserName should be of type String
And the settings object's Age should be of type Int32
And the settings object's Homepage should be of type Uri
@Deserialization @File
Scenario: I should be able to import serialized data from files even in PowerShell 2
Given a module with the name 'TestModule1'
Given a settings file named Configuration.psd1
"""
@{
UserName = 'Joel'
Age = 42
}
"""
And we fake version 2.0 in the Metadata module
When we import the file to an object
Then the settings object should be of type hashtable
And the settings object's UserName should be of type
gitextract_4wdl1uu2/ ├── .config/ │ └── dotnet-tools.json ├── .github/ │ └── workflows/ │ ├── Merge-Module.ps1 │ └── build.yml ├── .gitignore ├── .vscode/ │ ├── launch.json │ ├── settings.json │ ├── taskmarks.json │ └── tasks.json ├── Benchmark/ │ ├── Benchmark.ps1 │ └── Data/ │ └── Configuration.psd1 ├── Build.ps1 ├── CHANGELOG.md ├── Examples/ │ ├── TestModuleOne/ │ │ ├── Configuration.psd1 │ │ ├── TestModuleOne.psd1 │ │ └── TestModuleOne.psm1 │ └── TestModuleTwo/ │ ├── Configuration.psd1 │ ├── TestModuleTwo.psd1 │ └── TestModuleTwo.psm1 ├── GitVersion.yml ├── LICENSE ├── PSScriptAnalyzerSettings.psd1 ├── README.md ├── ReBuild.ps1 ├── RequiredModules.psd1 ├── Source/ │ ├── Configuration.psd1 │ ├── Header/ │ │ └── param.ps1 │ ├── Private/ │ │ ├── InitializeStoragePaths.ps1 │ │ └── ParameterBinder.ps1 │ └── Public/ │ ├── Export-Configuration.ps1 │ ├── Get-ConfigurationPath.ps1 │ ├── Get-ParameterValue.ps1 │ ├── Import-Configuration.ps1 │ └── Import-ParameterConfiguration.ps1 ├── Specs/ │ ├── Configuration.Steps.ps1 │ ├── Configuration.feature │ ├── ConfiguredParameters.feature │ ├── DefaultParameters.feature │ ├── Layering.feature │ ├── LocalStoragePath.feature │ ├── LocalStoragePathLinux.feature │ ├── Manifest.feature │ ├── ScriptAnalyzer.Steps.ps1 │ ├── ScriptAnalyzer.feature │ ├── Serialization.feature │ └── TestVersion.feature ├── Test.ps1 ├── bootstrap.ps1 └── build.psd1
Condensed preview — 48 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (221K chars).
[
{
"path": ".config/dotnet-tools.json",
"chars": 167,
"preview": "{\n \"version\": 1,\n \"isRoot\": true,\n \"tools\": {\n \"gitversion.tool\": {\n \"version\": \"5.6.0\",\n \"commands\": [\n"
},
{
"path": ".github/workflows/Merge-Module.ps1",
"chars": 1262,
"preview": "#requires -Module Configuration\n[CmdletBinding()]\nparam(\n $OutputModulePath,\n $NestedModulePath\n)\n$OutputModule = "
},
{
"path": ".github/workflows/build.yml",
"chars": 1943,
"preview": "name: Build on push\non: [push]\njobs:\n build:\n runs-on: windows-latest\n steps:\n - name: Checkout\n uses"
},
{
"path": ".gitignore",
"chars": 81,
"preview": "/output/\n/[0-9]*/\n\n/.vs/\nRequiredModules/\nnode_modules/\n\nresults.xml\ncoverage.xml"
},
{
"path": ".vscode/launch.json",
"chars": 1460,
"preview": "{\r\n // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\r\n \"version\": \"0.2.0\",\r\n \"con"
},
{
"path": ".vscode/settings.json",
"chars": 371,
"preview": "{\n \"files.defaultLanguage\": \"powershell\",\n \"powershell.codeFormatting.preset\": \"OTBS\",\n \"powershell.codeFormatt"
},
{
"path": ".vscode/taskmarks.json",
"chars": 93,
"preview": "{\n\t\"activeTaskName\": \"default\",\n\t\"tasks\": [\n\t\t{\n\t\t\t\"name\": \"default\",\n\t\t\t\"files\": []\n\t\t}\n\t]\n}"
},
{
"path": ".vscode/tasks.json",
"chars": 2271,
"preview": "{\r\n // See https://go.microsoft.com/fwlink/?LinkId=733558\r\n // for the documentation about the tasks.json format\r\n"
},
{
"path": "Benchmark/Benchmark.ps1",
"chars": 1474,
"preview": "[CmdletBinding()]\nparam([int]$Count = 10)\n$dataPath = Join-Path $PSScriptRoot \"../Benchmark/Data/Configuration.psd1\"\n\nfo"
},
{
"path": "Benchmark/Data/Configuration.psd1",
"chars": 24486,
"preview": "\n@{\n CurrentColorTheme = 'devblackops'\n CurrentIconTheme = 'devblackops'\n Themes = @{\n Color = @{\n devblackop"
},
{
"path": "Build.ps1",
"chars": 650,
"preview": "#requires -Module @{ModuleName = \"ModuleBuilder\"; ModuleVersion = \"2.0.0\"}, Configuration\r\n[CmdletBinding()]\r\nparam(\r\n "
},
{
"path": "CHANGELOG.md",
"chars": 950,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
},
{
"path": "Examples/TestModuleOne/Configuration.psd1",
"chars": 63,
"preview": "@{\n Address = \"http://PoshCode.org\"\n Credential = $null\n}"
},
{
"path": "Examples/TestModuleOne/TestModuleOne.psm1",
"chars": 1250,
"preview": "# If your default configuration has some blank settings, you can do something like this:\n# Assume I have a mandatory cre"
},
{
"path": "Examples/TestModuleTwo/Configuration.psd1",
"chars": 40,
"preview": "@{\n Address = \"http://PoshCode.org\"\n}"
},
{
"path": "Examples/TestModuleTwo/TestModuleTwo.psm1",
"chars": 996,
"preview": "# If your default configuration has valid defaults, but you still want them to review it,\n# Provide a public Get-Configu"
},
{
"path": "GitVersion.yml",
"chars": 713,
"preview": "mode: Mainline\r\ncommit-message-incrementing: MergeMessageOnly\r\n\r\nassembly-versioning-format: '{Major}.{Minor}.{Patch}.{e"
},
{
"path": "LICENSE",
"chars": 1056,
"preview": "Copyright (c) 2015 Joel Bennett\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this so"
},
{
"path": "PSScriptAnalyzerSettings.psd1",
"chars": 154,
"preview": "@{\n Severity = @('Error', 'Warning')\n ExcludeRules = @('PSAvoidUsingDeprecatedManifestFields', 'PSPossibleInco"
},
{
"path": "README.md",
"chars": 10334,
"preview": "[](https://github.com/Po"
},
{
"path": "ReBuild.ps1",
"chars": 22502,
"preview": "#requires -Version \"4.0\" -Module PackageManagement, Pester\r\n[CmdletBinding()]\r\nparam(\r\n # The step(s) to run. Default"
},
{
"path": "RequiredModules.psd1",
"chars": 167,
"preview": "@{\n Configuration = \"[1.3.1,2.0)\"\n Metadata = \"1.5.*\"\n Pester = \"4.10.*\"\n ModuleBuilder"
},
{
"path": "Source/Configuration.psd1",
"chars": 3126,
"preview": "@{\n\n# Script module or binary module file associated with this manifest.\nModuleToProcess = 'Configuration.psm1'\n\n# Vers"
},
{
"path": "Source/Header/param.ps1",
"chars": 219,
"preview": "# Allows you to override the Scope storage paths (e.g. for testing)\nparam(\n $Converters = @{},\n $EnterpriseData,\n "
},
{
"path": "Source/Private/InitializeStoragePaths.ps1",
"chars": 3031,
"preview": "function InitializeStoragePaths {\n [CmdletBinding()]\n param(\n $EnterpriseData,\n $UserData,\n $"
},
{
"path": "Source/Private/ParameterBinder.ps1",
"chars": 1613,
"preview": "function ParameterBinder {\n if (!$Module) {\n [System.Management.Automation.PSModuleInfo]$Module = . {\n "
},
{
"path": "Source/Public/Export-Configuration.ps1",
"chars": 4593,
"preview": "function Export-Configuration {\n <#\n .Synopsis\n Exports a configuration object to a specified path."
},
{
"path": "Source/Public/Get-ConfigurationPath.ps1",
"chars": 5247,
"preview": "function Get-ConfigurationPath {\n #.Synopsis\n # Gets an storage path for configuration files and data\n #.Desc"
},
{
"path": "Source/Public/Get-ParameterValue.ps1",
"chars": 5320,
"preview": "function Get-ParameterValue {\n <#\n .SYNOPSIS\n Get parameter values from PSBoundParameters + Default"
},
{
"path": "Source/Public/Import-Configuration.ps1",
"chars": 5959,
"preview": "function Import-Configuration {\n #.Synopsis\n # Import the full, layered configuration for the module.\n #.Desc"
},
{
"path": "Source/Public/Import-ParameterConfiguration.ps1",
"chars": 8312,
"preview": "function Import-ParameterConfiguration {\n <#\n .SYNOPSIS\n Loads a metadata file based on the calling"
},
{
"path": "Specs/Configuration.Steps.ps1",
"chars": 28759,
"preview": "#requires -Module Configuration\n#using module Configuration\n\n$PSModuleAutoLoadingPreference = \"None\"\n# Fix IsLinux on Wi"
},
{
"path": "Specs/Configuration.feature",
"chars": 6270,
"preview": "Feature: Module Configuration\n As a PowerShell Module Author\n I need to be able to store settings\n And override"
},
{
"path": "Specs/ConfiguredParameters.feature",
"chars": 4032,
"preview": "Feature: Configure Command From Working Directory\n\n There is a command to support loading default parameter values fr"
},
{
"path": "Specs/DefaultParameters.feature",
"chars": 2533,
"preview": "Feature: Get PSBoundParameters plus default values plus a config file\r\n\r\n There is a command to support merging PSBou"
},
{
"path": "Specs/Layering.feature",
"chars": 6616,
"preview": "Feature: Multiple settings files should layer\r\n As a module author, I want to distribute a default config with my mod"
},
{
"path": "Specs/LocalStoragePath.feature",
"chars": 4855,
"preview": "@StoragePath\r\nFeature: Automatically Calculate Local Storage Paths\r\n In order for module settings to survive upgrades"
},
{
"path": "Specs/LocalStoragePathLinux.feature",
"chars": 2760,
"preview": "@StoragePath\r\nFeature: Automatically Calculate Local Storage Paths on Linux\r\n In order for module settings to survive"
},
{
"path": "Specs/Manifest.feature",
"chars": 9538,
"preview": "Feature: Manifest Read and Write\r\n As a PowerShell Module Author\r\n I want to easily edit my manifest as part of my"
},
{
"path": "Specs/ScriptAnalyzer.Steps.ps1",
"chars": 2833,
"preview": "# Generate ScriptAnalyzer.feature\n$Path = GetModuleBase\n\n# The name (or path) of a settings file to be used.\n[string]$Se"
},
{
"path": "Specs/ScriptAnalyzer.feature",
"chars": 5571,
"preview": "# This feature file is re-generated by ScriptAnalyzer.Steps.ps1 whenever the tests are run\n@ScriptAnalyzer\nFeature: Pass"
},
{
"path": "Specs/Serialization.feature",
"chars": 20307,
"preview": "Feature: Serialize Hashtables or Custom Objects\r\n To allow users to configure module preferences without editing thei"
},
{
"path": "Specs/TestVersion.feature",
"chars": 1030,
"preview": "@Version\nFeature: A Mockable PowerShell Version test\n To allow testing for the version of PowerShell while mocking th"
},
{
"path": "Test.ps1",
"chars": 1994,
"preview": "<#\r\n .SYNOPSIS\r\n Invoke-Gherkin against a specific version in output\r\n#>\r\n[CmdletBinding()]\r\nparam(\r\n # A s"
},
{
"path": "bootstrap.ps1",
"chars": 1816,
"preview": "using namespace Microsoft.PowerShell.Commands\r\n<#\r\n.SYNOPSIS\r\n Installs and imports modules listed in RequiredModules"
},
{
"path": "build.psd1",
"chars": 241,
"preview": "@{\n ModuleManifest = \"Source/Configuration.psd1\"\n OutputDirectory = \"../\"\n SourceDirectories"
}
]
// ... and 2 more files (download for full content)
About this extraction
This page contains the full source code of the PoshCode/Configuration GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 48 files (204.2 KB), approximately 50.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.