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 = @{ '' = '' 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' '' = '' '.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 ================================================ [![Build Status](https://github.com/PoshCode/Configuration/actions/workflows/build.yml/badge.svg)](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 '(?.+?)'|\s+\w+ the company '(?.+?)'|\s+\w+ the 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 '(?.+)' that calls Get-ConfigurationPath (?:-Name (?\w*) ?|-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 (?.*) 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 (?\S+) folder)|(?: for 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? (?warning|error|verbose) in the (?.*) 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 (?warning|error|verbose) is logged(?: (?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 (?\S+) folder)|(?: for 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 '(?[A-Z][a-z]+-[A-Z][a-z]+)' with (?.*) 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 '(?[A-Z][a-z]+-[A-Z][a-z]+)' with (?.*) parameters that calls Get-ParameterValue(? 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)(? .*)?" { 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 (?[\:\w]+) = (?.*)" { 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 (?.*)" { 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 '' by the author 'Joel Bennett' Then the module's Enterprise path should match '^TestDrive:/EnterprisePath/' and '/Joel Bennett/$' And the module's Enterprise path should exist already """ There is a """ 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 '' by the author 'Joel Bennett' Then the module's Enterprise path should match '^TestDrive:/EnterprisePath/' and '/Joel Bennett/$' And the module's Enterprise path should exist already """ There is a """ 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 '' with the author '' Then the module's path should match '^' and '/AnonymousModules/$' And the module's 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 '' with the author 'Jaykul' Then the module's path should match '^' and '/Jaykul/$' # And the module's 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 '' with the author '' Then the module's path should match '^' and '/AnonymousModules/$' And the module's 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 '(?.*)' with '(?.*)'" { 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 (?.*)" { 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 in it When we convert the settings to metadata Then the string version should match "TestCase = \(? " 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 String And the settings object's Age should be of type Int32 @Deserialization @File Scenario: I should be able to import serialized data regardless of file extension Given a module with the name 'TestModule1' Given a settings file named Settings.data """ @{ UserName = 'Joel' Age = 42 } """ When we import the file 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 @Deserialization @File Scenario: Imported metadata files should be able to use PSScriptRoot Given a module with the name 'TestModule1' Given a settings file named Configuration.psd1 """ @{ MyPath = Join-Path $PSScriptRoot "Configuration.psd1" } """ And we're using PowerShell 4 or higher 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 MyPath should be of type String And the settings object MyPath should match the file's path @Deserialization @File Scenario: Bad data should generate useful errors Given a module with the name 'TestModule1' Given a settings file named Configuration.psd1 """ @{ UserName = } """ Then trying to import the file to an object should throw """ Missing statement after '=' in hash literal. """ @Deserialization @File Scenario: Disallowed commands should generate useful errors Given a module with the name 'TestModule1' Given a settings file named Configuration.psd1 """ @{ UserName = New-Object PSObject -Property @{ First = "Joel" } } """ Then trying to import the file to an object should throw """ The command 'New-Object' is not allowed in restricted language mode or a Data section. """ @Serialization @Deserialization @File Scenario: Handling the default module manifest Given a module with the name 'TestModule1' Given a settings file named ModuleName/ModuleName.psd1 """ @{ UserName = 'Joel' Age = 42 } """ When we import the folder path 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 @Serialization @Deserialization @File Scenario: Errors when you import missing files Given the settings file does not exist And we expect an error in the metadata module When we import the file to an object Then the error is logged @UpdateObject Scenario: Update A Hashtable Given a settings hashtable """ @{ UserName = 'Joel' Age = 41 Homepage = [Uri]"http://HuddledMasses.org" } """ When we update the settings with """ @{ Age = 42 } """ Then the settings object's UserName should be Joel And the settings object's Age should be 42 @UpdateObject Scenario: Update an Object Given a settings object """ @{ PSTypeName = 'User' FirstName = 'Joel' LastName = 'Bennett' UserName = 'Jaykul' Homepage = [Uri]"http://HuddledMasses.org" } """ When we update the settings with """ @{ Age = 42 } """ Then the settings object should have User in the PSTypeNames And the settings object's UserName should be Jaykul And the settings object's Age should be 42 @UpdateObject Scenario: Try to Update An Object With Nothing Given a settings hashtable """ @{ UserName = 'Joel' Age = 41 Homepage = [Uri]"http://HuddledMasses.org" } """ When we update the settings with """ """ Then the settings object's UserName should be Joel And the settings object's Age should be 41 @UpdateObject Scenario: Update a hashtable with important properties Given a settings object """ @{ PSTypeName = 'User' FirstName = 'Joel' LastName = 'Bennett' UserName = 'Jaykul' Age = 12 Homepage = [Uri]"http://HuddledMasses.org" } """ When we say UserName is important and update with """ @{ UserName = 'JBennett' Age = 42 } """ Then the settings object's UserName should be Jaykul And the settings object's Age should be 42 And the settings object should have User in the PSTypeNames @Serialization @Deserialization @File Scenario: I should be able to import a manifest in order Given a module with the name 'TestModule1' Given a settings file named Configuration.psd1 """ @{ UserName = 'Joel' Age = 42 FullName = 'Joel Bennett' } """ When we import the file with ordered Then the settings object should be of type Collections.Specialized.OrderedDictionary And the settings object's UserName should be of type String And the settings object's Age should be of type Int32 And Key 0 is UserName And Key 1 is Age And Key 2 is FullName @Serialization @Deserialization @File Scenario: The ordered hashtable should recurse Given a module with the name 'TestModule1' Given a settings file named Configuration.psd1 """ @{ Age = 42 FullName = @{ FirstName = 'Joel' LastName = 'Bennett' } } """ When we import the file with ordered Then the settings object should be of type Collections.Specialized.OrderedDictionary And the settings object's FullName should be of type Collections.Specialized.OrderedDictionary @Regression @Serialization Scenario: Arrays of custom types Given the configuration module is imported with a URL converter And a settings hashtable """ @{ UserName = 'Joel' Domains = [Uri]"http://HuddledMasses.org", [Uri]"http://PoshCode.org", [Uri]"http://JoelBennett.net" } """ When we convert the settings to metadata Then the string version should match "Domains = @\(\(?\s*Uri" And the string version should match "Uri 'http://huddledmasses.org/'" And the string version should match "Uri 'http://poshcode.org'" @Serialization @ScriptBlock Scenario Outline: Should be able to serialize ScriptBlocks Given a settings hashtable with a ScriptBlock in it When we convert the settings to metadata Then the string version should match "TestCase = \(?ScriptBlock '" @Serialization Scenario Outline: Should serialize Switch statements as booleans Given a settings hashtable with a SwitchParameter in it When we convert the settings to metadata Then the string version should match "TestCase = \`$True" @Serialization Scenario: Has an IPsMetadataSerializable Interface Given the configuration module exports IPsMetadataSerializable And a TestClass that implements IPsMetadataSerializable And a settings file named Configuration.psd1 """ FromPsMetadata TestClass " @{ Values = @{ User = 'Jaykul' } Name = 'Joel' } " """ When we import the file to an object Then the settings object should be of type TestClass And the settings object's User should be Jaykul And the settings object's Name should be Joel And the settings object's Keys should be User @Serialization Scenario: Allows specifying a list of allowed variables Given a settings file named Configuration.psd1 """ @{ UserName = "${Env:UserName}" Age = 42 FullName = $FullName } """ And we define FullName = Joel Bennett And we define Env:UserName = Jaykul When we import the file allowing variables FullName, Env:UserName And the settings object's UserName should be Jaykul And the settings object's FullName should be Joel Bennett ================================================ FILE: Specs/TestVersion.feature ================================================ @Version Feature: A Mockable PowerShell Version test To allow testing for the version of PowerShell while mocking the version A PowerShell Module Author Needs a way to test the current version that can be mocked @Changes Scenario: Test the current PowerShell version Given the actual PowerShell version Then the Version -eq the Version And the Version -lt 10.0 And the Version -le 10.0 And the Version -gt 1.0 And the Version -ge 1.0 And the Version -ge the Version And the Version -le the Version And the Version -ne 1.0 And the Version -ne 10.0 Scenario: Test an old PowerShell version Given a mock PowerShell version 2.0 Then the Version -eq 2.0 And the Version -lt 10.0 And the Version -le 10.0 And the Version -gt 1.0 And the Version -ge 1.0 And the Version -ge 2.0 And the Version -le 2.0 And the Version -ne 1.0 And the Version -ne 10.0 ================================================ FILE: Test.ps1 ================================================ <# .SYNOPSIS Invoke-Gherkin against a specific version in output #> [CmdletBinding()] param( # A specific folder the build is in $OutputDirectory = $PSScriptRoot, # The version of the output module [Alias("ModuleVersion")] [string]$SemVer ) Push-Location $PSScriptRoot -StackName BuildTestStack if (!$SemVer -and (Get-Command gitversion -ErrorAction Ignore)) { $SemVer = gitversion -showvariable nugetversion } Write-Host "OutputDirectory: $OutputDirectory" Write-Host "SemVer: $SemVer" try { if (Test-Path $OutputDirectory) { # Get the part of the output path that we need to add to the PSModulePath if ($OutputDirectory -match "Configuration$") { $OutputDirectory = Split-Path $OutputDirectory } if (-not (@($Env:PSModulePath.Split([IO.Path]::PathSeparator)) -contains $OutputDirectory)) { Write-Verbose "Adding $($OutputDirectory) to PSModulePath" $Env:PSModulePath = $OutputDirectory + [IO.Path]::PathSeparator + $Env:PSModulePath } } $Specs = @{ Path = Join-Path $PSScriptRoot Specs } # Just to make sure everything is kosher, run tests in a clean session $PSModulePath = $Env:PSModulePath Invoke-Command { # We need to make sure that the PSModulePath has our output at the front $Env:PSModulePath = $OutputDirectory + [IO.Path]::PathSeparator + $Env:PSModulePath Write-Host "Testing Configuration $SemVer" $SemVer = ($SemVer -split "-")[0] # We need to make sure we have loaded ONLY the right version of the module Get-Module Configuration -All | Remove-Module -ErrorAction SilentlyContinue -Force $Specs["CodeCoverage"] = Import-Module Configuration -RequiredVersion $SemVer -PassThru | Select-Object -Expand Path Invoke-Gherkin @Specs } } finally { Pop-Location -StackName BuildTestStack } ================================================ FILE: bootstrap.ps1 ================================================ using namespace Microsoft.PowerShell.Commands <# .SYNOPSIS Installs and imports modules listed in RequiredModules if they're missing .EXAMPLE .\bootstrap Run the bootstrap interactively, with a prompt for each module to be installed .EXAMPLE .\bootstrap -Confirm:$false Run the bootstrap and install all the modules without prompting #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] param( # Override the default scope if you wish -- now that PowerShellGet has sane defaults, we use their default [ValidateSet("CurrentUser", "AllUsers")] $Scope = "CurrentUser" ) [ModuleSpecification[]]$RequiredModules = Import-LocalizedData -BaseDirectory $PSScriptRoot -FileName RequiredModules # Force Policy to Trusted so we can install without prompts and without -Force which is bad $Policy = (Get-PSRepository PSGallery).InstallationPolicy Set-PSRepository PSGallery -InstallationPolicy Trusted try { # Check for the modules by hand so that "RequiredVersion" is treated as "MinimumVersion" $RequiredModules.Where{ -not (Get-Module $_.Name -ListAvailable | Where-Object Version -ge $_.RequiredVersion) } | # Install missing modules with -AlloClobber and -SkipPublisherCheck because PowerShellGet requires both Install-Module -Scope $Scope -Repository PSGallery -SkipPublisherCheck -AllowClobber -Verbose -Confirm:$($ConfirmPreference -ne "None") # Put Policy back so we don't needlessly change environments permanently } finally { Set-PSRepository PSGallery -InstallationPolicy $Policy } # Since we're now allowing newer versions, import the newest available $RequiredModules.ForEach{ Get-Module $_.Name -ListAvailable | Sort-Object Version | Select-Object -Last 1 } | Import-Module ================================================ FILE: build.psd1 ================================================ @{ ModuleManifest = "Source/Configuration.psd1" OutputDirectory = "../" SourceDirectories = @("Private", "Public") Prefix = "Header\param.ps1" VersionedOutputDirectory = $true }