├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── build.ps1 ├── build.settings.ps1 ├── docs ├── .markdownlint.json └── en-US │ ├── Add-CDPath.md │ ├── Get-CDPath.md │ ├── Get-CDPathCandidate.md │ ├── Get-CDPathOption.md │ ├── Set-CDPath.md │ ├── Set-CDPathLocation.md │ ├── Set-CDPathOption.md │ ├── Update-Cdpath.md │ └── cdpath.md ├── src ├── cdpath.psd1 ├── cdpath.psm1 └── init.ps1 └── test └── cdpath.tests.ps1 /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | *.psd1 diff=asplaintext 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This .gitignore file was automatically created by Microsoft(R) Visual Studio. 3 | ################################################################################ 4 | 5 | *.suo 6 | *.user 7 | Release/ 8 | .vscode/.browse.VC.db -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "PowerShell", 6 | "request": "launch", 7 | "name": "PowerShell Launch tests in Temporary Console", 8 | "script": "${workspaceRoot}/test/cdpath.tests.ps1", 9 | "args": [ 10 | "-noprofile" 11 | ], 12 | "cwd": "${workspaceRoot}", 13 | "createTemporaryIntegratedConsole": true 14 | }, 15 | { 16 | "type": "PowerShell", 17 | "request": "launch", 18 | "name": "PowerShell Launch Current File in Temporary Console", 19 | "script": "${file}", 20 | "args": [ 21 | "-noprofile" 22 | ], 23 | "cwd": "${file}", 24 | "createTemporaryIntegratedConsole": true 25 | }, 26 | { 27 | "type": "PowerShell", 28 | "request": "launch", 29 | "name": "PowerShell Launch Current File", 30 | "script": "${file}", 31 | "args": [], 32 | "cwd": "${file}", 33 | "createTemporaryIntegratedConsole": true 34 | }, 35 | { 36 | "type": "PowerShell", 37 | "request": "launch", 38 | "name": "PowerShell Launch Current File w/Args Prompt", 39 | "script": "${file}", 40 | "args": [ 41 | "${command:SpecifyScriptArgs}" 42 | ], 43 | "cwd": "${file}" 44 | }, 45 | { 46 | "type": "PowerShell", 47 | "request": "attach", 48 | "name": "PowerShell Attach to Host Process", 49 | "processId": "${command:PickPSHostProcess}", 50 | "runspaceId": 1 51 | }, 52 | { 53 | "type": "PowerShell", 54 | "request": "launch", 55 | "name": "PowerShell Interactive Session", 56 | "cwd": "${workspaceRoot}" 57 | } 58 | ] 59 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true 8 | } 9 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // A task runner that invokes Pester to run all Pester tests under the 2 | // current workspace folder. 3 | // NOTE: This Test task runner requires an updated version of Pester (>=3.4.0) 4 | // in order for the problemMatcher to find failed test information (message, line, file). 5 | // If you don't have that version, you can update Pester from the PowerShell Gallery 6 | // with this command: 7 | // 8 | // PS C:\> Update-Module Pester 9 | // 10 | // If that gives an error like: 11 | // "Module 'Pester' was not installed by using Install-Module, so it cannot be updated." 12 | // then execute: 13 | // 14 | // PS C:\> Install-Module Pester -Scope CurrentUser -Force 15 | // 16 | // NOTE: The Clean, Build and Publish tasks require PSake. PSake can be installed 17 | // from the PowerShell Gallery with this command: 18 | // 19 | // PS C:\> Install-Module PSake -Scope CurrentUser -Force 20 | // 21 | // Available variables which can be used inside of strings. 22 | // ${workspaceRoot}: the root folder of the team 23 | // ${file}: the current opened file 24 | // ${fileBasename}: the current opened file's basename 25 | // ${fileDirname}: the current opened file's dirname 26 | // ${fileExtname}: the current opened file's extension 27 | // ${cwd}: the current working directory of the spawned process 28 | { 29 | "version": "2.0.0", 30 | // Start PowerShell 31 | "windows": { 32 | "options": { 33 | "shell": { 34 | "executable": "C:\\Program Files\\PowerShell\\6.0.2\\pwsh.exe", 35 | "args": [ 36 | "-NoProfile", 37 | "-ExecutionPolicy", 38 | "Bypass", 39 | "-Command" 40 | ] 41 | } 42 | },"echoCommand": true 43 | }, 44 | "linux": { 45 | "command": "/usr/bin/powershell", 46 | "args": [ 47 | "-NoProfile" 48 | ] 49 | }, 50 | "osx": { 51 | "command": "/usr/local/bin/powershell", 52 | "args": [ 53 | "-NoProfile" 54 | ] 55 | }, 56 | // Associate with test task runner 57 | "tasks": [ 58 | { 59 | "label": "Clean", 60 | "type": "shell", 61 | "command": "Invoke-PSake build.ps1 -taskList Clean" 62 | }, 63 | { 64 | "label": "Build", 65 | "type": "shell", 66 | "command": "Invoke-PSake build.ps1 -taskList Build", 67 | "group": { 68 | "kind": "build", 69 | "isDefault": true 70 | }, 71 | "problemMatcher": [] 72 | }, 73 | { 74 | "label": "Install", 75 | "type": "shell", 76 | "command": "Invoke-PSake build.ps1 -taskList Install", 77 | "problemMatcher": [] 78 | }, 79 | { 80 | "label": "Publish", 81 | "type": "shell", 82 | "command": "Invoke-PSake build.ps1 -taskList Publish", 83 | "problemMatcher": [] 84 | }, 85 | { 86 | "label": "Test", 87 | "group": { 88 | "kind": "test", 89 | "isDefault": true 90 | }, 91 | "type": "shell", 92 | "command": "Set-Location ${workspaceRoot}/test;Invoke-Pester -PesterOption @{IncludeVSCodeMarker=$true} ./cdpath.tests.ps1;", 93 | "problemMatcher": "$pester" 94 | } 95 | ] 96 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Staffan Gustafsson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CDPath 2 | ====== 3 | 4 | PowerShell module to make navigating your system a breeze. 5 | 6 | The main function of the module is Set-CDPathLocation (below referenced by 'cd') and supports the following: 7 | 8 | * Go to previous location without pushd/popd (cd -) 9 | * Go upwards multiple levels (cd ....) 10 | * Go to a directory in a predefined path 11 | * Integration with TabExpansion++ 12 | 13 | 14 | Usage 15 | ----- 16 | Assuming you've installed the module somewhere in your module path, just import the module in your profile, e.g.: 17 | ```powershell 18 | Import-Module CDPath 19 | ``` 20 | 21 | To setup the CDPath, i.e. the parent directories of the directories you most often navigate to, call 22 | ```powershell 23 | # a user who has github projects and a directory where corporate source code is stored 24 | # may set up the path like this 25 | Set-CDPath -Path ~\Documents\GitHub,d:\corpsrc,~\documents 26 | ``` 27 | The CDPath is persisted at ~/Documents/WindowsPowerShell/cdpath.txt 28 | 29 | Imagine the following directory structure 30 | ``` 31 | ~/Documents/ 32 | GitHub 33 | PSReadLine 34 | TabExpansionPlusPlus 35 | CDPath 36 | WindowsPowerShell 37 | ArgumentCompleters 38 | Modules 39 | D:\CorpSrc 40 | PSApi 41 | ToolingApi 42 | Frontend 43 | Backend 44 | ``` 45 | Navigation can then be done like this: 46 | 47 | ```powershell 48 | # go to PSReadline 49 | PS> cd psr 50 | 51 | go to ~\Documents\WindowsPowerShell\Modules 52 | cd win mod 53 | 54 | # go to D:\CorpSrc\ToolingApi 55 | cd too 56 | 57 | # go to ~\Documents\GitHub\TabExpansionPlusPlus 58 | cd tab 59 | # go up three levels 60 | cd .... 61 | # go back to previous directory 62 | cd - 63 | 64 | ``` 65 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules psake 2 | 3 | ############################################################################## 4 | # DO NOT MODIFY THIS FILE! Modify build.settings.ps1 instead. 5 | ############################################################################## 6 | 7 | ############################################################################## 8 | # This is the PowerShell Module psake build script. It defines the following tasks: 9 | # 10 | # Clean, Build, Sign, BuildHelp, Install, Test and Publish. 11 | # 12 | # The default task is Build. This task copies the appropriate files from the 13 | # $SrcRootDir under the $OutDir. Later, other tasks such as Sign and BuildHelp 14 | # will further modify the contents of $OutDir and add new files. 15 | # 16 | # The Sign task will only sign scripts if the $SignScripts variable is set to 17 | # $true. A code-signing certificate is required for this task to complete. 18 | # 19 | # The BuildHelp task invokes platyPS to generate markdown files from 20 | # comment-based help for your exported commands. platyPS then generates 21 | # a help file for your module from the markdown files. 22 | # 23 | # The Install task simplies copies the module folder under $OutDir to your 24 | # profile's Modules folder. 25 | # 26 | # The Test task invokes Pester on the $TestRootDir. 27 | # 28 | # The Publish task uses the Publish-Module command to publish 29 | # to either the PowerShell Gallery (the default) or you can change 30 | # the $PublishRepository property to the name of an alternate repository. 31 | # Note: the Publish task requires that the Test task execute without failures. 32 | # 33 | # You can exeute a specific task, such as the Test task by running the 34 | # following command: 35 | # 36 | # PS C:\> invoke-psake build.psake.ps1 -taskList Test 37 | # 38 | # You can execute the Publish task with the following command. 39 | # The first time you execute the Publish task, you will be prompted to enter 40 | # your PowerShell Gallery NuGetApiKey. After entering the key, it is encrypted 41 | # and stored so you will not have to enter it again. 42 | # 43 | # PS C:\> invoke-psake build.psake.ps1 -taskList Publish 44 | # 45 | # You can verify the stored and encrypted NuGetApiKey by running the following 46 | # command which will display a portion of your NuGetApiKey in plain text. 47 | # 48 | # PS C:\> invoke-psake build.psake.ps1 -taskList ShowApiKey 49 | # 50 | # You can store a new NuGetApiKey with this command. You can leave off 51 | # the -properties parameter and you'll be prompted for the key. 52 | # 53 | # PS C:\> invoke-psake build.psake.ps1 -taskList StoreApiKey -properties @{NuGetApiKey='test123'} 54 | # 55 | 56 | ############################################################################### 57 | # Dot source the user's customized properties and extension tasks. 58 | ############################################################################### 59 | . $PSScriptRoot\build.settings.ps1 60 | 61 | ############################################################################### 62 | # Private properties. 63 | ############################################################################### 64 | Properties { 65 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 66 | $ModuleOutDir = "$OutDir\$ModuleName" 67 | 68 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 69 | $UpdatableHelpOutDir = "$OutDir\UpdatableHelp" 70 | 71 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 72 | $SharedProperties = @{} 73 | 74 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 75 | $LineSep = "-" * 78 76 | } 77 | 78 | ############################################################################### 79 | # Core task implementations. Avoid modifying these tasks. 80 | ############################################################################### 81 | Task default -depends Build 82 | 83 | Task Init -requiredVariables OutDir { 84 | if (!(Test-Path -LiteralPath $OutDir)) { 85 | New-Item $OutDir -ItemType Directory -Verbose:$VerbosePreference > $null 86 | } 87 | else { 88 | Write-Verbose "$($psake.context.currentTaskName) - directory already exists '$OutDir'." 89 | } 90 | } 91 | 92 | Task Clean -depends Init -requiredVariables OutDir { 93 | # Maybe a bit paranoid but this task nuked \ on my laptop. Good thing I was not running as admin. 94 | if ($OutDir.Length -gt 3) { 95 | Get-ChildItem $OutDir | Remove-Item -Recurse -Force -Verbose:$VerbosePreference 96 | } 97 | else { 98 | Write-Verbose "$($psake.context.currentTaskName) - `$OutDir '$OutDir' must be longer than 3 characters." 99 | } 100 | } 101 | 102 | Task StageFiles -depends Init, Clean, BeforeStageFiles, CoreStageFiles, AfterStageFiles { 103 | } 104 | 105 | Task CoreStageFiles -requiredVariables ModuleOutDir, SrcRootDir { 106 | if (!(Test-Path -LiteralPath $ModuleOutDir)) { 107 | New-Item $ModuleOutDir -ItemType Directory -Verbose:$VerbosePreference > $null 108 | } 109 | else { 110 | Write-Verbose "$($psake.context.currentTaskName) - directory already exists '$ModuleOutDir'." 111 | } 112 | 113 | Copy-Item -Path $SrcRootDir\* -Destination $ModuleOutDir -Recurse -Exclude $Exclude -Verbose:$VerbosePreference 114 | } 115 | 116 | Task Build -depends Init, Clean, BeforeBuild, StageFiles, Analyze, Sign, AfterBuild { 117 | } 118 | 119 | Task Analyze -depends StageFiles ` 120 | -requiredVariables ModuleOutDir, ScriptAnalysisEnabled, ScriptAnalysisFailBuildOnSeverityLevel, ScriptAnalyzerSettingsPath { 121 | if (!$ScriptAnalysisEnabled) { 122 | "Script analysis is not enabled. Skipping $($psake.context.currentTaskName) task." 123 | return 124 | } 125 | 126 | if (!(Get-Module PSScriptAnalyzer -ListAvailable)) { 127 | "PSScriptAnalyzer module is not installed. Skipping $($psake.context.currentTaskName) task." 128 | return 129 | } 130 | 131 | "ScriptAnalysisFailBuildOnSeverityLevel set to: $ScriptAnalysisFailBuildOnSeverityLevel" 132 | 133 | $analysisResult = Invoke-ScriptAnalyzer -Path $ModuleOutDir -Settings $ScriptAnalyzerSettingsPath -Recurse -Verbose:$VerbosePreference 134 | $analysisResult | Format-Table 135 | switch ($ScriptAnalysisFailBuildOnSeverityLevel) { 136 | 'None' { 137 | return 138 | } 139 | 'Error' { 140 | Assert -conditionToCheck ( 141 | ($analysisResult | Where-Object Severity -eq 'Error').Count -eq 0 142 | ) -failureMessage 'One or more ScriptAnalyzer errors were found. Build cannot continue!' 143 | } 144 | 'Warning' { 145 | Assert -conditionToCheck ( 146 | ($analysisResult | Where-Object { 147 | $_.Severity -eq 'Warning' -or $_.Severity -eq 'Error' 148 | }).Count -eq 0) -failureMessage 'One or more ScriptAnalyzer warnings were found. Build cannot continue!' 149 | } 150 | default { 151 | Assert -conditionToCheck ( 152 | $analysisResult.Count -eq 0 153 | ) -failureMessage 'One or more ScriptAnalyzer issues were found. Build cannot continue!' 154 | } 155 | } 156 | } 157 | 158 | Task Sign -depends StageFiles -requiredVariables CertPath, SettingsPath, ScriptSigningEnabled { 159 | if (!$ScriptSigningEnabled) { 160 | "Script signing is not enabled. Skipping $($psake.context.currentTaskName) task." 161 | return 162 | } 163 | 164 | $validCodeSigningCerts = Get-ChildItem -Path $CertPath -CodeSigningCert -Recurse | Where-Object NotAfter -ge (Get-Date) 165 | if (!$validCodeSigningCerts) { 166 | throw "There are no non-expired code-signing certificates in $CertPath. You can either install " + 167 | "a code-signing certificate into the certificate store or disable script analysis in build.settings.ps1." 168 | } 169 | 170 | $certSubjectNameKey = "CertSubjectName" 171 | $storeCertSubjectName = $true 172 | 173 | # Get the subject name of the code-signing certificate to be used for script signing. 174 | if (!$CertSubjectName -and ($CertSubjectName = GetSetting -Key $certSubjectNameKey -Path $SettingsPath)) { 175 | $storeCertSubjectName = $false 176 | } 177 | elseif (!$CertSubjectName) { 178 | "A code-signing certificate has not been specified." 179 | "The following non-expired, code-signing certificates are available in your certificate store:" 180 | $validCodeSigningCerts | Format-List Subject, Issuer, Thumbprint, NotBefore, NotAfter 181 | 182 | $CertSubjectName = Read-Host -Prompt 'Enter the subject name (case-sensitive) of the certificate to use for script signing' 183 | } 184 | 185 | # Find a code-signing certificate that matches the specified subject name. 186 | $certificate = $validCodeSigningCerts | 187 | Where-Object { $_.SubjectName.Name -cmatch [regex]::Escape($CertSubjectName) } | 188 | Sort-Object NotAfter -Descending | Select-Object -First 1 189 | 190 | if ($certificate) { 191 | $SharedProperties.CodeSigningCertificate = $certificate 192 | 193 | if ($storeCertSubjectName) { 194 | SetSetting -Key $certSubjectNameKey -Value $certificate.SubjectName.Name -Path $SettingsPath 195 | "The new certificate subject name has been stored in ${SettingsPath}." 196 | } 197 | else { 198 | "Using stored certificate subject name $CertSubjectName from ${SettingsPath}." 199 | } 200 | 201 | $LineSep 202 | "Using code-signing certificate: $certificate" 203 | $LineSep 204 | 205 | $files = @(Get-ChildItem -Path $ModuleOutDir\* -Recurse -Include *.ps1, *.psm1) 206 | foreach ($file in $files) { 207 | $setAuthSigParams = @{ 208 | FilePath = $file.FullName 209 | Certificate = $certificate 210 | Verbose = $VerbosePreference 211 | } 212 | 213 | $result = Microsoft.PowerShell.Security\Set-AuthenticodeSignature @setAuthSigParams 214 | if ($result.Status -ne 'Valid') { 215 | throw "Failed to sign script: $($file.FullName)." 216 | } 217 | 218 | "Successfully signed script: $($file.Name)" 219 | } 220 | } 221 | else { 222 | $expiredCert = Get-ChildItem -Path $CertPath -CodeSigningCert -Recurse | 223 | Where-Object { ($_.SubjectName.Name -cmatch [regex]::Escape($CertSubjectName)) -and 224 | ($_.NotAfter -lt (Get-Date)) } 225 | Sort-Object NotAfter -Descending | Select-Object -First 1 226 | 227 | if ($expiredCert) { 228 | throw "The code-signing certificate `"$($expiredCert.SubjectName.Name)`" EXPIRED on $($expiredCert.NotAfter)." 229 | } 230 | 231 | throw 'No valid certificate subject name supplied or stored.' 232 | } 233 | } 234 | 235 | Task BuildHelp -depends Build, BeforeBuildHelp, GenerateMarkdown, GenerateHelpFiles, AfterBuildHelp { 236 | } 237 | 238 | Task GenerateMarkdown -requiredVariables DefaultLocale, DocsRootDir, ModuleName, ModuleOutDir { 239 | if (!(Get-Module platyPS -ListAvailable)) { 240 | "platyPS module is not installed. Skipping $($psake.context.currentTaskName) task." 241 | return 242 | } 243 | 244 | $moduleInfo = Import-Module $ModuleOutDir\$ModuleName.psd1 -Global -Force -PassThru 245 | 246 | try { 247 | if ($moduleInfo.ExportedCommands.Count -eq 0) { 248 | "No commands have been exported. Skipping $($psake.context.currentTaskName) task." 249 | return 250 | } 251 | 252 | if (!(Test-Path -LiteralPath $DocsRootDir)) { 253 | New-Item $DocsRootDir -ItemType Directory > $null 254 | } 255 | 256 | if (Get-ChildItem -LiteralPath $DocsRootDir -Filter *.md -Recurse) { 257 | Get-ChildItem -LiteralPath $DocsRootDir -Directory | ForEach-Object { 258 | Update-MarkdownHelp -Path $_.FullName -Verbose:$VerbosePreference > $null 259 | } 260 | } 261 | 262 | # ErrorAction set to SilentlyContinue so this command will not overwrite an existing MD file. 263 | New-MarkdownHelp -Module $ModuleName -Locale $DefaultLocale -OutputFolder $DocsRootDir\$DefaultLocale ` 264 | -WithModulePage -ErrorAction SilentlyContinue -Verbose:$VerbosePreference > $null 265 | } 266 | finally { 267 | Remove-Module $ModuleName 268 | } 269 | } 270 | 271 | Task GenerateHelpFiles -requiredVariables DocsRootDir, ModuleName, ModuleOutDir, OutDir { 272 | if (!(Get-Module platyPS -ListAvailable)) { 273 | "platyPS module is not installed. Skipping $($psake.context.currentTaskName) task." 274 | return 275 | } 276 | 277 | if (!(Get-ChildItem -LiteralPath $DocsRootDir -Filter *.md -Recurse -ErrorAction SilentlyContinue)) { 278 | "No markdown help files to process. Skipping $($psake.context.currentTaskName) task." 279 | return 280 | } 281 | 282 | $helpLocales = (Get-ChildItem -Path $DocsRootDir -Directory).Name 283 | 284 | # Generate the module's primary MAML help file. 285 | foreach ($locale in $helpLocales) { 286 | New-ExternalHelp -Path $DocsRootDir\$locale -OutputPath $ModuleOutDir\$locale -Force ` 287 | -ErrorAction SilentlyContinue -Verbose:$VerbosePreference > $null 288 | } 289 | } 290 | 291 | Task BuildUpdatableHelp -depends BuildHelp, BeforeBuildUpdatableHelp, CoreBuildUpdatableHelp, AfterBuildUpdatableHelp { 292 | } 293 | 294 | Task CoreBuildUpdatableHelp -requiredVariables DocsRootDir, ModuleName, UpdatableHelpOutDir { 295 | if (!(Get-Module platyPS -ListAvailable)) { 296 | "platyPS module is not installed. Skipping $($psake.context.currentTaskName) task." 297 | return 298 | } 299 | 300 | $helpLocales = (Get-ChildItem -Path $DocsRootDir -Directory).Name 301 | 302 | # Create updatable help output directory. 303 | if (!(Test-Path -LiteralPath $UpdatableHelpOutDir)) { 304 | New-Item $UpdatableHelpOutDir -ItemType Directory -Verbose:$VerbosePreference > $null 305 | } 306 | else { 307 | Write-Verbose "$($psake.context.currentTaskName) - directory already exists '$UpdatableHelpOutDir'." 308 | Get-ChildItem $UpdatableHelpOutDir | Remove-Item -Recurse -Force -Verbose:$VerbosePreference 309 | } 310 | 311 | # Generate updatable help files. Note: this will currently update the version number in the module's MD 312 | # file in the metadata. 313 | foreach ($locale in $helpLocales) { 314 | New-ExternalHelpCab -CabFilesFolder $ModuleOutDir\$locale -LandingPagePath $DocsRootDir\$locale\$ModuleName.md ` 315 | -OutputFolder $UpdatableHelpOutDir -Verbose:$VerbosePreference > $null 316 | } 317 | } 318 | 319 | Task GenerateFileCatalog -depends Build, BuildHelp, BeforeGenerateFileCatalog, CoreGenerateFileCatalog, AfterGenerateFileCatalog { 320 | } 321 | 322 | Task CoreGenerateFileCatalog -requiredVariables CatalogGenerationEnabled, CatalogVersion, ModuleName, ModuleOutDir, OutDir { 323 | if (!$CatalogGenerationEnabled) { 324 | "FileCatalog generation is not enabled. Skipping $($psake.context.currentTaskName) task." 325 | return 326 | } 327 | 328 | if (!(Get-Command Microsoft.PowerShell.Security\New-FileCatalog -ErrorAction SilentlyContinue)) { 329 | "FileCatalog commands not available on this version of PowerShell. Skipping $($psake.context.currentTaskName) task." 330 | return 331 | } 332 | 333 | $catalogFilePath = "$OutDir\$ModuleName.cat" 334 | 335 | $newFileCatalogParams = @{ 336 | Path = $ModuleOutDir 337 | CatalogFilePath = $catalogFilePath 338 | CatalogVersion = $CatalogVersion 339 | Verbose = $VerbosePreference 340 | } 341 | 342 | Microsoft.PowerShell.Security\New-FileCatalog @newFileCatalogParams > $null 343 | 344 | if ($ScriptSigningEnabled) { 345 | if ($SharedProperties.CodeSigningCertificate) { 346 | $setAuthSigParams = @{ 347 | FilePath = $catalogFilePath 348 | Certificate = $SharedProperties.CodeSigningCertificate 349 | Verbose = $VerbosePreference 350 | } 351 | 352 | $result = Microsoft.PowerShell.Security\Set-AuthenticodeSignature @setAuthSigParams 353 | if ($result.Status -ne 'Valid') { 354 | throw "Failed to sign file catalog: $($catalogFilePath)." 355 | } 356 | 357 | "Successfully signed file catalog: $($catalogFilePath)" 358 | } 359 | else { 360 | "No code-signing certificate was found to sign the file catalog." 361 | } 362 | } 363 | else { 364 | "Script signing is not enabled. Skipping signing of file catalog." 365 | } 366 | 367 | Move-Item -LiteralPath $newFileCatalogParams.CatalogFilePath -Destination $ModuleOutDir 368 | } 369 | 370 | Task Install -depends Build, BuildHelp, GenerateFileCatalog, BeforeInstall, CoreInstall, AfterInstall { 371 | } 372 | 373 | Task CoreInstall -requiredVariables ModuleOutDir { 374 | if (!(Test-Path -LiteralPath $InstallPath)) { 375 | Write-Verbose 'Creating install directory' 376 | New-Item -Path $InstallPath -ItemType Directory -Verbose:$VerbosePreference > $null 377 | } 378 | 379 | Copy-Item -Path $ModuleOutDir\* -Destination $InstallPath -Verbose:$VerbosePreference -Recurse -Force 380 | "Module installed into $InstallPath" 381 | } 382 | 383 | Task Test -depends Build -requiredVariables TestRootDir, ModuleName, CodeCoverageEnabled, CodeCoverageFiles, ExcludeTestTags { 384 | if (!(Get-Module Pester -ListAvailable)) { 385 | "Pester module is not installed. Skipping $($psake.context.currentTaskName) task." 386 | return 387 | } 388 | 389 | Import-Module Pester 390 | 391 | try { 392 | Microsoft.PowerShell.Management\Push-Location -LiteralPath $TestRootDir 393 | 394 | if ($TestOutputFile) { 395 | $testing = @{ 396 | OutputFile = $TestOutputFile 397 | OutputFormat = $TestOutputFormat 398 | PassThru = $true 399 | Verbose = $VerbosePreference 400 | } 401 | } 402 | else { 403 | $testing = @{ 404 | PassThru = $true 405 | Verbose = $VerbosePreference 406 | } 407 | } 408 | if ($excludeTestTags) { 409 | $testing.ExcludeTag = $excludeTestTags 410 | } 411 | 412 | # To control the Pester code coverage, a boolean $CodeCoverageEnabled is used. 413 | if ($CodeCoverageEnabled) { 414 | $testing.CodeCoverage = $CodeCoverageFiles 415 | } 416 | 417 | $testResult = Invoke-Pester @testing 418 | 419 | Assert -conditionToCheck ( 420 | $testResult.FailedCount -eq 0 421 | ) -failureMessage "One or more Pester tests failed, build cannot continue." 422 | 423 | if ($CodeCoverageEnabled) { 424 | $testCoverage = [int]($testResult.CodeCoverage.NumberOfCommandsExecuted / 425 | $testResult.CodeCoverage.NumberOfCommandsAnalyzed * 100) 426 | "Pester code coverage on specified files: ${testCoverage}%" 427 | } 428 | } 429 | finally { 430 | Microsoft.PowerShell.Management\Pop-Location 431 | Remove-Module $ModuleName -ErrorAction SilentlyContinue 432 | } 433 | } 434 | 435 | Task Publish -depends Build, Test, BuildHelp, GenerateFileCatalog, BeforePublish, CorePublish, AfterPublish { 436 | } 437 | 438 | Task CorePublish -requiredVariables SettingsPath, ModuleOutDir { 439 | $publishParams = @{ 440 | Path = $ModuleOutDir 441 | NuGetApiKey = $NuGetApiKey 442 | } 443 | 444 | # Publishing to the PSGallery requires an API key, so get it. 445 | if ($NuGetApiKey) { 446 | "Using script embedded NuGetApiKey" 447 | } 448 | elseif ($NuGetApiKey = GetSetting -Path $SettingsPath -Key NuGetApiKey) { 449 | "Using stored NuGetApiKey" 450 | } 451 | else { 452 | $promptForKeyCredParams = @{ 453 | DestinationPath = $SettingsPath 454 | Message = 'Enter your NuGet API key in the password field' 455 | Key = 'NuGetApiKey' 456 | } 457 | 458 | $cred = PromptUserForCredentialAndStorePassword @promptForKeyCredParams 459 | $NuGetApiKey = $cred.GetNetworkCredential().Password 460 | "The NuGetApiKey has been stored in $SettingsPath" 461 | } 462 | 463 | $publishParams = @{ 464 | Path = $ModuleOutDir 465 | NuGetApiKey = $NuGetApiKey 466 | } 467 | 468 | # If an alternate repository is specified, set the appropriate parameter. 469 | if ($PublishRepository) { 470 | $publishParams['Repository'] = $PublishRepository 471 | } 472 | 473 | # Consider not using -ReleaseNotes parameter when Update-ModuleManifest has been fixed. 474 | if ($ReleaseNotesPath) { 475 | $publishParams['ReleaseNotes'] = @(Get-Content $ReleaseNotesPath) 476 | } 477 | 478 | "Calling Publish-Module..." 479 | Publish-Module @publishParams 480 | } 481 | 482 | ############################################################################### 483 | # Secondary/utility tasks - typically used to manage stored build settings. 484 | ############################################################################### 485 | 486 | Task ? -description 'Lists the available tasks' { 487 | "Available tasks:" 488 | $psake.context.Peek().Tasks.Keys | Sort-Object 489 | } 490 | 491 | Task RemoveApiKey -requiredVariables SettingsPath { 492 | if (GetSetting -Path $SettingsPath -Key NuGetApiKey) { 493 | RemoveSetting -Path $SettingsPath -Key NuGetApiKey 494 | } 495 | } 496 | 497 | Task StoreApiKey -requiredVariables SettingsPath { 498 | $promptForKeyCredParams = @{ 499 | DestinationPath = $SettingsPath 500 | Message = 'Enter your NuGet API key in the password field' 501 | Key = 'NuGetApiKey' 502 | } 503 | 504 | PromptUserForCredentialAndStorePassword @promptForKeyCredParams 505 | "The NuGetApiKey has been stored in $SettingsPath" 506 | } 507 | 508 | Task ShowApiKey -requiredVariables SettingsPath { 509 | $OFS = "" 510 | if ($NuGetApiKey) { 511 | "The embedded (partial) NuGetApiKey is: $($NuGetApiKey[0..7])" 512 | } 513 | elseif ($NuGetApiKey = GetSetting -Path $SettingsPath -Key NuGetApiKey) { 514 | "The stored (partial) NuGetApiKey is: $($NuGetApiKey[0..7])" 515 | } 516 | else { 517 | "The NuGetApiKey has not been provided or stored." 518 | return 519 | } 520 | 521 | "To see the full key, use the task 'ShowFullApiKey'" 522 | } 523 | 524 | Task ShowFullApiKey -requiredVariables SettingsPath { 525 | if ($NuGetApiKey) { 526 | "The embedded NuGetApiKey is: $NuGetApiKey" 527 | } 528 | elseif ($NuGetApiKey = GetSetting -Path $SettingsPath -Key NuGetApiKey) { 529 | "The stored NuGetApiKey is: $NuGetApiKey" 530 | } 531 | else { 532 | "The NuGetApiKey has not been provided or stored." 533 | } 534 | } 535 | 536 | Task RemoveCertSubjectName -requiredVariables SettingsPath { 537 | if (GetSetting -Path $SettingsPath -Key CertSubjectName) { 538 | RemoveSetting -Path $SettingsPath -Key CertSubjectName 539 | } 540 | } 541 | 542 | Task StoreCertSubjectName -requiredVariables SettingsPath { 543 | $certSubjectName = 'CN=' 544 | $certSubjectName += Read-Host -Prompt 'Enter the certificate subject name for script signing. Use exact casing, CN= prefix will be added' 545 | SetSetting -Key CertSubjectName -Value $certSubjectName -Path $SettingsPath 546 | "The new certificate subject name '$certSubjectName' has been stored in ${SettingsPath}." 547 | } 548 | 549 | Task ShowCertSubjectName -requiredVariables SettingsPath { 550 | $CertSubjectName = GetSetting -Path $SettingsPath -Key CertSubjectName 551 | "The stored certificate is: $CertSubjectName" 552 | 553 | $cert = Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert | 554 | Where-Object { $_.Subject -eq $CertSubjectName -and $_.NotAfter -gt (Get-Date) } | 555 | Sort-Object -Property NotAfter -Descending | Select-Object -First 1 556 | 557 | if ($cert) { 558 | "A valid certificate for the subject $CertSubjectName has been found" 559 | } 560 | else { 561 | 'A valid certificate has not been found' 562 | } 563 | } 564 | 565 | ############################################################################### 566 | # Helper functions 567 | ############################################################################### 568 | 569 | function PromptUserForCredentialAndStorePassword { 570 | [Diagnostics.CodeAnalysis.SuppressMessage("PSProvideDefaultParameterValue", '')] 571 | param( 572 | [Parameter()] 573 | [ValidateNotNullOrEmpty()] 574 | [string] 575 | $DestinationPath, 576 | 577 | [Parameter(Mandatory)] 578 | [string] 579 | $Message, 580 | 581 | [Parameter(Mandatory, ParameterSetName = 'SaveSetting')] 582 | [string] 583 | $Key 584 | ) 585 | 586 | $cred = Get-Credential -Message $Message -UserName "ignored" 587 | if ($DestinationPath) { 588 | SetSetting -Key $Key -Value $cred.Password -Path $DestinationPath 589 | } 590 | 591 | $cred 592 | } 593 | 594 | function AddSetting { 595 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope = 'Function')] 596 | param( 597 | [Parameter(Mandatory)] 598 | [string]$Key, 599 | 600 | [Parameter(Mandatory)] 601 | [string]$Path, 602 | 603 | [Parameter(Mandatory)] 604 | [ValidateNotNull()] 605 | [object]$Value 606 | ) 607 | 608 | switch ($type = $Value.GetType().Name) { 609 | 'securestring' { $setting = $Value | ConvertFrom-SecureString } 610 | default { $setting = $Value } 611 | } 612 | 613 | if (Test-Path -LiteralPath $Path) { 614 | $storedSettings = Import-Clixml -Path $Path 615 | $storedSettings.Add($Key, @($type, $setting)) 616 | $storedSettings | Export-Clixml -Path $Path 617 | } 618 | else { 619 | $parentDir = Split-Path -Path $Path -Parent 620 | if (!(Test-Path -LiteralPath $parentDir)) { 621 | New-Item $parentDir -ItemType Directory > $null 622 | } 623 | 624 | @{$Key = @($type, $setting)} | Export-Clixml -Path $Path 625 | } 626 | } 627 | 628 | function GetSetting { 629 | param( 630 | [Parameter(Mandatory)] 631 | [string]$Key, 632 | 633 | [Parameter(Mandatory)] 634 | [string]$Path 635 | ) 636 | 637 | if (Test-Path -LiteralPath $Path) { 638 | $securedSettings = Import-Clixml -Path $Path 639 | if ($securedSettings.$Key) { 640 | switch ($securedSettings.$Key[0]) { 641 | 'securestring' { 642 | $value = $securedSettings.$Key[1] | ConvertTo-SecureString 643 | $cred = New-Object -TypeName PSCredential -ArgumentList 'jpgr', $value 644 | $cred.GetNetworkCredential().Password 645 | } 646 | default { 647 | $securedSettings.$Key[1] 648 | } 649 | } 650 | } 651 | } 652 | } 653 | 654 | function SetSetting { 655 | param( 656 | [Parameter(Mandatory)] 657 | [string]$Key, 658 | 659 | [Parameter(Mandatory)] 660 | [string]$Path, 661 | 662 | [Parameter(Mandatory)] 663 | [ValidateNotNull()] 664 | [object]$Value 665 | ) 666 | 667 | if (GetSetting -Key $Key -Path $Path) { 668 | RemoveSetting -Key $Key -Path $Path 669 | } 670 | 671 | AddSetting -Key $Key -Value $Value -Path $Path 672 | } 673 | 674 | function RemoveSetting { 675 | param( 676 | [Parameter(Mandatory)] 677 | [string]$Key, 678 | 679 | [Parameter(Mandatory)] 680 | [string]$Path 681 | ) 682 | 683 | if (Test-Path -LiteralPath $Path) { 684 | $storedSettings = Import-Clixml -Path $Path 685 | $storedSettings.Remove($Key) 686 | if ($storedSettings.Count -eq 0) { 687 | Remove-Item -Path $Path 688 | } 689 | else { 690 | $storedSettings | Export-Clixml -Path $Path 691 | } 692 | } 693 | else { 694 | Write-Warning "The build setting file '$Path' has not been created yet." 695 | } 696 | } 697 | -------------------------------------------------------------------------------- /build.settings.ps1: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Customize these properties and tasks for your module. 3 | ############################################################################### 4 | 5 | Properties { 6 | # ----------------------- Basic properties -------------------------------- 7 | 8 | # The root directories for the module's docs, src and test. 9 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 10 | $DocsRootDir = "$PSScriptRoot\docs" 11 | $SrcRootDir = "$PSScriptRoot\src" 12 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 13 | $TestRootDir = "$PSScriptRoot\test" 14 | 15 | # The name of your module should match the basename of the PSD1 file. 16 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 17 | $ModuleName = Get-Item $SrcRootDir/*.psd1 | 18 | Where-Object { $null -ne (Import-PowerShellDataFile -Path $_ -ErrorAction SilentlyContinue) } | 19 | Select-Object -First 1 | Foreach-Object BaseName 20 | 21 | # The $OutDir is where module files and updatable help files are staged for signing, install and publishing. 22 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 23 | $OutDir = "$PSScriptRoot\Release" 24 | 25 | # The local installation directory for the install task. Defaults to your home Modules location. 26 | # The test for $profile is for the Plaster AppVeyor build machine since it doesn't define $profile. 27 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 28 | $InstallPath = Join-Path (Split-Path $(if ($profile) {$profile} 29 | else {$Home}) -Parent) ` 30 | "Modules\$ModuleName\$((Import-PowerShellDataFile -Path $SrcRootDir\$ModuleName.psd1).ModuleVersion.ToString())" 31 | 32 | # Default Locale used for help generation, defaults to en-US. 33 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 34 | $DefaultLocale = 'en-US' 35 | 36 | # Items in the $Exclude array will not be copied to the $OutDir e.g. $Exclude = @('.gitattributes') 37 | # Typically you wouldn't put any file under the src dir unless the file was going to ship with 38 | # the module. However, if there are such files, add their $SrcRootDir relative paths to the exclude list. 39 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 40 | $Exclude = @() 41 | 42 | # ------------------ Script analysis properties --------------------------- 43 | 44 | # Enable/disable use of PSScriptAnalyzer to perform script analysis. 45 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 46 | $ScriptAnalysisEnabled = $false 47 | 48 | # When PSScriptAnalyzer is enabled, control which severity level will generate a build failure. 49 | # Valid values are Error, Warning, Information and None. "None" will report errors but will not 50 | # cause a build failure. "Error" will fail the build only on diagnostic records that are of 51 | # severity error. "Warning" will fail the build on Warning and Error diagnostic records. 52 | # "Any" will fail the build on any diagnostic record, regardless of severity. 53 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 54 | [ValidateSet('Error', 'Warning', 'Any', 'None')] 55 | $ScriptAnalysisFailBuildOnSeverityLevel = 'Error' 56 | 57 | # Path to the PSScriptAnalyzer settings file. 58 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 59 | $ScriptAnalyzerSettingsPath = "$PSScriptRoot\ScriptAnalyzerSettings.psd1" 60 | 61 | # ------------------- Script signing properties --------------------------- 62 | 63 | # Set to $true if you want to sign your scripts. You will need to have a code-signing certificate. 64 | # You can specify the certificate's subject name below. If not specified, you will be prompted to 65 | # provide either a subject name or path to a PFX file. After this one time prompt, the value will 66 | # saved for future use and you will no longer be prompted. 67 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 68 | $ScriptSigningEnabled = $false 69 | 70 | # Specify the Subject Name of the certificate used to sign your scripts. Leave it as $null and the 71 | # first time you build, you will be prompted to enter your code-signing certificate's Subject Name. 72 | # This variable is used only if $SignScripts is set to $true. 73 | # 74 | # This does require the code-signing certificate to be installed to your certificate store. If you 75 | # have a code-signing certificate in a PFX file, install the certificate to your certificate store 76 | # with the command below. You may be prompted for the certificate's password. 77 | # 78 | # Import-PfxCertificate -FilePath .\myCodeSigingCert.pfx -CertStoreLocation Cert:\CurrentUser\My 79 | # 80 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 81 | $CertSubjectName = $null 82 | 83 | # Certificate store path. 84 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 85 | $CertPath = "Cert:\" 86 | 87 | # -------------------- File catalog properties ---------------------------- 88 | 89 | # Enable/disable generation of a catalog (.cat) file for the module. 90 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 91 | $CatalogGenerationEnabled = $false 92 | 93 | # Select the hash version to use for the catalog file: 1 for SHA1 (compat with Windows 7 and 94 | # Windows Server 2008 R2), 2 for SHA2 to support only newer Windows versions. 95 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 96 | $CatalogVersion = 2 97 | 98 | # ---------------------- Testing properties ------------------------------- 99 | 100 | # Enable/disable Pester code coverage reporting. 101 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 102 | $CodeCoverageEnabled = $true 103 | 104 | # CodeCoverageFiles specifies the files to perform code coverage analysis on. This property 105 | # acts as a direct input to the Pester -CodeCoverage parameter, so will support constructions 106 | # like the ones found here: https://github.com/pester/Pester/wiki/Code-Coverage. 107 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 108 | $CodeCoverageFiles = "$SrcRootDir\*.psm1" 109 | 110 | # -------------------- Publishing properties ------------------------------ 111 | 112 | # Your NuGet API key for the PSGallery. Leave it as $null and the first time you publish, 113 | # you will be prompted to enter your API key. The build will store the key encrypted in the 114 | # settings file, so that on subsequent publishes you will no longer be prompted for the API key. 115 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 116 | $NuGetApiKey = 'asdfae' 117 | 118 | # Name of the repository you wish to publish to. If $null is specified the default repo (PowerShellGallery) is used. 119 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 120 | $PublishRepository = 'DICE' 121 | 122 | # Path to the release notes file. Set to $null if the release notes reside in the manifest file. 123 | # The contents of this file are used during publishing for the ReleaseNotes parameter. 124 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 125 | $ReleaseNotesPath = $null 126 | 127 | # ----------------------- Misc properties --------------------------------- 128 | 129 | # In addition, PFX certificates are supported in an interactive scenario only, 130 | # as a way to import a certificate into the user personal store for later use. 131 | # This can be provided using the CertPfxPath parameter. PFX passwords will not be stored. 132 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 133 | $SettingsPath = "$env:LOCALAPPDATA\Plaster\NewModuleTemplate\SecuredBuildSettings.clixml" 134 | 135 | # Specifies an output file path to send to Invoke-Pester's -OutputFile parameter. 136 | # This is typically used to write out test results so that they can be sent to a CI 137 | # system like AppVeyor. 138 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 139 | $TestOutputFile = $null 140 | 141 | # Exclude these tags from pester tests 142 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 143 | [string[]]$excludeTestTags = 'QE' 144 | 145 | # Specifies the test output format to use when the TestOutputFile property is given 146 | # a path. This parameter is passed through to Invoke-Pester's -OutputFormat parameter. 147 | [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] 148 | $TestOutputFormat = "NUnitXml" 149 | } 150 | 151 | ############################################################################### 152 | # Customize these tasks for performing operations before and/or after file staging. 153 | ############################################################################### 154 | 155 | # Executes before the StageFiles task. 156 | Task BeforeStageFiles { 157 | } 158 | 159 | 160 | Task InlineModules -requiredVariables ModuleOutDir, ModuleName { 161 | $psm1Path = "$ModuleOutDir/$ModuleName.psm1" 162 | $psm1Content = Get-Content -LiteralPath $psm1Path 163 | 164 | Select-String -LiteralPath $psm1Path -Pattern 'using module .\\([\w\.]+)' | Foreach-Object { 165 | $line = $_.LineNumber - 1 166 | $fileName = $_.Matches[0].Groups[1].Value 167 | $u, $c = (Get-Content -LiteralPath $ModuleOutDir/$fileName).Where( {$_ -match '^using namespace'}, 'Split') 168 | $psm1Content[$line] = $c.Where( {$_ -notlike 'using module*'}) -join [Environment]::NewLine 169 | $using += $u 170 | 171 | Remove-Item -LiteralPath $ModuleOutDir/$fileName 172 | } 173 | $using,$psm1Content = $psm1Content.Where( {$_ -match '^using namespace'}, 'Split') 174 | $psm1Content = ($using | Sort-Object -Unique) + $psm1Content 175 | Set-Content -LiteralPath $psm1Path -Value $psm1Content 176 | } 177 | 178 | # Executes after the StageFiles task. 179 | Task AfterStageFiles -depends InlineModules { 180 | 181 | } 182 | 183 | ############################################################################### 184 | # Customize these tasks for performing operations before and/or after Build. 185 | ############################################################################### 186 | 187 | # Executes before the BeforeStageFiles phase of the Build task. 188 | Task BeforeBuild { 189 | } 190 | 191 | # Executes after the Build task. 192 | Task AfterBuild { 193 | } 194 | 195 | ############################################################################### 196 | # Customize these tasks for performing operations before and/or after BuildHelp. 197 | ############################################################################### 198 | 199 | # Executes before the BuildHelp task. 200 | Task BeforeBuildHelp { 201 | } 202 | 203 | # Executes after the BuildHelp task. 204 | Task AfterBuildHelp { 205 | } 206 | 207 | ############################################################################### 208 | # Customize these tasks for performing operations before and/or after BuildUpdatableHelp. 209 | ############################################################################### 210 | 211 | # Executes before the BuildUpdatableHelp task. 212 | Task BeforeBuildUpdatableHelp { 213 | } 214 | 215 | # Executes after the BuildUpdatableHelp task. 216 | Task AfterBuildUpdatableHelp { 217 | } 218 | 219 | ############################################################################### 220 | # Customize these tasks for performing operations before and/or after GenerateFileCatalog. 221 | ############################################################################### 222 | 223 | # Executes before the GenerateFileCatalog task. 224 | Task BeforeGenerateFileCatalog { 225 | } 226 | 227 | # Executes after the GenerateFileCatalog task. 228 | Task AfterGenerateFileCatalog { 229 | } 230 | 231 | ############################################################################### 232 | # Customize these tasks for performing operations before and/or after Install. 233 | ############################################################################### 234 | 235 | # Executes before the Install task. 236 | Task BeforeInstall { 237 | } 238 | 239 | # Executes after the Install task. 240 | Task AfterInstall { 241 | } 242 | 243 | ############################################################################### 244 | # Customize these tasks for performing operations before and/or after Publish. 245 | ############################################################################### 246 | 247 | # Executes before the Publish task. 248 | Task BeforePublish { 249 | } 250 | 251 | # Executes after the Publish task. 252 | Task AfterPublish { 253 | } -------------------------------------------------------------------------------- /docs/.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false, 3 | "MD022": false, 4 | "MD031": false, 5 | "MD040": false 6 | } -------------------------------------------------------------------------------- /docs/en-US/Add-CDPath.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: cdpath-help.xml 3 | Module Name: cdpath 4 | online version: 5 | schema: 2.0.0 6 | --- 7 | 8 | # Add-CDPath 9 | 10 | ## SYNOPSIS 11 | Adds one or more paths to your cdpath 12 | 13 | ## SYNTAX 14 | 15 | ``` 16 | Add-CDPath [[-Path] ] [-Prepend] [] 17 | ``` 18 | 19 | ## DESCRIPTION 20 | This is useful if you have modified your path file with an external editor 21 | 22 | ## EXAMPLES 23 | 24 | ### Example 1 25 | ```powershell 26 | PS C:\> {{ Add example code here }} 27 | ``` 28 | 29 | {{ Add example description here }} 30 | 31 | ## PARAMETERS 32 | 33 | ### -Path 34 | {{Fill Path Description}} 35 | 36 | ```yaml 37 | Type: String[] 38 | Parameter Sets: (All) 39 | Aliases: 40 | 41 | Required: False 42 | Position: 0 43 | Default value: None 44 | Accept pipeline input: False 45 | Accept wildcard characters: False 46 | ``` 47 | 48 | ### -Prepend 49 | Specify Prepend to add the paths before the existing paths 50 | 51 | ```yaml 52 | Type: SwitchParameter 53 | Parameter Sets: (All) 54 | Aliases: 55 | 56 | Required: False 57 | Position: Named 58 | Default value: False 59 | Accept pipeline input: False 60 | Accept wildcard characters: False 61 | ``` 62 | 63 | ### CommonParameters 64 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see about_CommonParameters (). 65 | 66 | ## INPUTS 67 | 68 | ## OUTPUTS 69 | 70 | ## NOTES 71 | 72 | ## RELATED LINKS 73 | -------------------------------------------------------------------------------- /docs/en-US/Get-CDPath.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: cdpath-help.xml 3 | Module Name: cdpath 4 | online version: 5 | schema: 2.0.0 6 | --- 7 | 8 | # Get-CDPath 9 | 10 | ## SYNOPSIS 11 | {{Fill in the Synopsis}} 12 | 13 | ## SYNTAX 14 | 15 | ``` 16 | Get-CDPath [-Resolve] [] 17 | ``` 18 | 19 | ## DESCRIPTION 20 | {{Fill in the Description}} 21 | 22 | ## EXAMPLES 23 | 24 | ### Example 1 25 | ```powershell 26 | PS C:\> {{ Add example code here }} 27 | ``` 28 | 29 | {{ Add example description here }} 30 | 31 | ## PARAMETERS 32 | 33 | ### -Resolve 34 | {{Fill Resolve Description}} 35 | 36 | ```yaml 37 | Type: SwitchParameter 38 | Parameter Sets: (All) 39 | Aliases: 40 | 41 | Required: False 42 | Position: Named 43 | Default value: None 44 | Accept pipeline input: False 45 | Accept wildcard characters: False 46 | ``` 47 | 48 | ### CommonParameters 49 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see about_CommonParameters (). 50 | 51 | ## INPUTS 52 | 53 | ### None 54 | 55 | ## OUTPUTS 56 | 57 | ### System.String 58 | 59 | ## NOTES 60 | 61 | ## RELATED LINKS 62 | -------------------------------------------------------------------------------- /docs/en-US/Get-CDPathCandidate.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: cdpath-help.xml 3 | Module Name: cdpath 4 | online version: 5 | schema: 2.0.0 6 | --- 7 | 8 | # Get-CDPathCandidate 9 | 10 | ## SYNOPSIS 11 | {{Fill in the Synopsis}} 12 | 13 | ## SYNTAX 14 | 15 | ``` 16 | Get-CDPathCandidate [-path] [[-remaining] ] [] 17 | ``` 18 | 19 | ## DESCRIPTION 20 | {{Fill in the Description}} 21 | 22 | ## EXAMPLES 23 | 24 | ### Example 1 25 | ```powershell 26 | PS C:\> {{ Add example code here }} 27 | ``` 28 | 29 | {{ Add example description here }} 30 | 31 | ## PARAMETERS 32 | 33 | ### -path 34 | {{Fill path Description}} 35 | 36 | ```yaml 37 | Type: String 38 | Parameter Sets: (All) 39 | Aliases: 40 | 41 | Required: True 42 | Position: 0 43 | Default value: None 44 | Accept pipeline input: False 45 | Accept wildcard characters: False 46 | ``` 47 | 48 | ### -remaining 49 | {{Fill remaining Description}} 50 | 51 | ```yaml 52 | Type: String[] 53 | Parameter Sets: (All) 54 | Aliases: 55 | 56 | Required: False 57 | Position: 1 58 | Default value: None 59 | Accept pipeline input: False 60 | Accept wildcard characters: False 61 | ``` 62 | 63 | ### CommonParameters 64 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see about_CommonParameters (). 65 | 66 | ## INPUTS 67 | 68 | ### None 69 | 70 | ## OUTPUTS 71 | 72 | ### System.String 73 | 74 | ## NOTES 75 | 76 | ## RELATED LINKS 77 | -------------------------------------------------------------------------------- /docs/en-US/Get-CDPathOption.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: cdpath-help.xml 3 | Module Name: cdpath 4 | online version: 5 | schema: 2.0.0 6 | --- 7 | 8 | # Get-CDPathOption 9 | 10 | ## SYNOPSIS 11 | {{Fill in the Synopsis}} 12 | 13 | ## SYNTAX 14 | 15 | ``` 16 | Get-CDPathOption [] 17 | ``` 18 | 19 | ## DESCRIPTION 20 | {{Fill in the Description}} 21 | 22 | ## EXAMPLES 23 | 24 | ### Example 1 25 | ```powershell 26 | PS C:\> {{ Add example code here }} 27 | ``` 28 | 29 | {{ Add example description here }} 30 | 31 | ## PARAMETERS 32 | 33 | ### CommonParameters 34 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see about_CommonParameters (). 35 | 36 | ## INPUTS 37 | 38 | ### None 39 | 40 | ## OUTPUTS 41 | 42 | ### System.Object 43 | 44 | ## NOTES 45 | 46 | ## RELATED LINKS 47 | -------------------------------------------------------------------------------- /docs/en-US/Set-CDPath.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: cdpath-help.xml 3 | Module Name: cdpath 4 | online version: 5 | schema: 2.0.0 6 | --- 7 | 8 | # Set-CDPath 9 | 10 | ## SYNOPSIS 11 | {{Fill in the Synopsis}} 12 | 13 | ## SYNTAX 14 | 15 | ``` 16 | Set-CDPath [-Path] [] 17 | ``` 18 | 19 | ## DESCRIPTION 20 | {{Fill in the Description}} 21 | 22 | ## EXAMPLES 23 | 24 | ### Example 1 25 | ```powershell 26 | PS C:\> {{ Add example code here }} 27 | ``` 28 | 29 | {{ Add example description here }} 30 | 31 | ## PARAMETERS 32 | 33 | ### -Path 34 | {{Fill Path Description}} 35 | 36 | ```yaml 37 | Type: String[] 38 | Parameter Sets: (All) 39 | Aliases: 40 | 41 | Required: True 42 | Position: 0 43 | Default value: None 44 | Accept pipeline input: False 45 | Accept wildcard characters: False 46 | ``` 47 | 48 | ### CommonParameters 49 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see about_CommonParameters (). 50 | 51 | ## INPUTS 52 | 53 | ### None 54 | 55 | ## OUTPUTS 56 | 57 | ### System.Object 58 | 59 | ## NOTES 60 | 61 | ## RELATED LINKS 62 | -------------------------------------------------------------------------------- /docs/en-US/Set-CDPathLocation.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: cdpath-help.xml 3 | Module Name: cdpath 4 | online version: 5 | schema: 2.0.0 6 | --- 7 | 8 | # Set-CDPathLocation 9 | 10 | ## SYNOPSIS 11 | Changing location using by resolving a pattern agains a set of paths 12 | 13 | ## SYNTAX 14 | 15 | ``` 16 | Set-CDPathLocation [[-Path] ] [-Remaining ] [-Exact] [] 17 | ``` 18 | 19 | ## DESCRIPTION 20 | CDPath replaces the 'cd' alias with Set-CDPathLocation 21 | 22 | Parts of paths can be specified with a space. 23 | 24 | PS\> cd win mo 25 | 26 | will change directory to ~/documents/windowspowershell/modules 27 | 28 | ## EXAMPLES 29 | 30 | ### EXAMPLE 1 31 | ``` 32 | Set-CDPathLocation .... 33 | ``` 34 | 35 | The example changes the current location to the parent three levels up. 36 | .. 37 | (first parent) 38 | ... 39 | (second parent) 40 | .... 41 | (third parent) 42 | 43 | ### EXAMPLE 2 44 | ``` 45 | Set-CDLocation 'C:\Program Files (X86)\Microsoft Visual Studio 12' 46 | ``` 47 | 48 | ### EXAMPLE 3 49 | ``` 50 | Set-CDLocation ~ 51 | ``` 52 | 53 | Go back to 'C:\Program Files (X86)\Microsoft Visual Studio 12' 54 | 55 | Set-CDPathLocation \- 56 | 57 | ### EXAMPLE 4 58 | ``` 59 | Get-CDPath 60 | ``` 61 | 62 | ~/Documents/GitHub 63 | ~/Documents 64 | ~/corpsrc 65 | 66 | go to ~/documents/WindowsPowerShell/Modules/CDPath 67 | Set-CDLocation win mod cdp 68 | 69 | ## PARAMETERS 70 | 71 | ### -Exact 72 | {{Fill Exact Description}} 73 | 74 | ```yaml 75 | Type: SwitchParameter 76 | Parameter Sets: (All) 77 | Aliases: 78 | 79 | Required: False 80 | Position: Named 81 | Default value: False 82 | Accept pipeline input: False 83 | Accept wildcard characters: False 84 | ``` 85 | 86 | ### -Path 87 | {{Fill Path Description}} 88 | 89 | ```yaml 90 | Type: String 91 | Parameter Sets: (All) 92 | Aliases: 93 | 94 | Required: False 95 | Position: 0 96 | Default value: None 97 | Accept pipeline input: False 98 | Accept wildcard characters: False 99 | ``` 100 | 101 | ### -Remaining 102 | {{Fill Remaining Description}} 103 | 104 | ```yaml 105 | Type: String[] 106 | Parameter Sets: (All) 107 | Aliases: 108 | 109 | Required: False 110 | Position: Named 111 | Default value: None 112 | Accept pipeline input: False 113 | Accept wildcard characters: False 114 | ``` 115 | 116 | ### CommonParameters 117 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see about_CommonParameters (). 118 | 119 | ## INPUTS 120 | 121 | ## OUTPUTS 122 | 123 | ## NOTES 124 | 125 | ## RELATED LINKS 126 | -------------------------------------------------------------------------------- /docs/en-US/Set-CDPathOption.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: cdpath-help.xml 3 | Module Name: cdpath 4 | online version: 5 | schema: 2.0.0 6 | --- 7 | 8 | # Set-CDPathOption 9 | 10 | ## SYNOPSIS 11 | Customizes the behavior of Set-CDPathLocation in CDPath 12 | 13 | ## SYNTAX 14 | 15 | ``` 16 | Set-CDPathOption [-SetWindowTitle] [] 17 | ``` 18 | 19 | ## DESCRIPTION 20 | {{Fill in the Description}} 21 | 22 | ## EXAMPLES 23 | 24 | ### Example 1 25 | ```powershell 26 | PS C:\> {{ Add example code here }} 27 | ``` 28 | 29 | {{ Add example description here }} 30 | 31 | ## PARAMETERS 32 | 33 | ### -SetWindowTitle 34 | Indicates if the the window title should be changed when changing location 35 | 36 | ```yaml 37 | Type: SwitchParameter 38 | Parameter Sets: (All) 39 | Aliases: 40 | 41 | Required: False 42 | Position: Named 43 | Default value: False 44 | Accept pipeline input: False 45 | Accept wildcard characters: False 46 | ``` 47 | 48 | ### CommonParameters 49 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see about_CommonParameters (). 50 | 51 | ## INPUTS 52 | 53 | ## OUTPUTS 54 | 55 | ## NOTES 56 | 57 | ## RELATED LINKS 58 | -------------------------------------------------------------------------------- /docs/en-US/Update-Cdpath.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: cdpath-help.xml 3 | Module Name: cdpath 4 | online version: 5 | schema: 2.0.0 6 | --- 7 | 8 | # Update-Cdpath 9 | 10 | ## SYNOPSIS 11 | 12 | Read the cdpath from ~/.cdpath 13 | 14 | ## SYNTAX 15 | 16 | ``` 17 | Update-Cdpath [] 18 | ``` 19 | 20 | ## DESCRIPTION 21 | This is useful if you have modified your path file with an external editor 22 | 23 | ## EXAMPLES 24 | 25 | ### Example 1 26 | ```powershell 27 | PS C:\> {{ Add example code here }} 28 | ``` 29 | 30 | {{ Add example description here }} 31 | 32 | ## PARAMETERS 33 | 34 | ### CommonParameters 35 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see about_CommonParameters (). 36 | 37 | ## INPUTS 38 | 39 | ## OUTPUTS 40 | 41 | ## NOTES 42 | 43 | ## RELATED LINKS 44 | -------------------------------------------------------------------------------- /docs/en-US/cdpath.md: -------------------------------------------------------------------------------- 1 | --- 2 | Module Name: cdpath 3 | Module Guid: 5ca4809d-c4ec-4c3c-b21d-b42956302d99 4 | Download Help Link: {{Please enter FwLink manually}} 5 | Help Version: {{Please enter version of help manually (X.X.X.X) format}} 6 | Locale: en-US 7 | --- 8 | 9 | # cdpath Module 10 | ## Description 11 | {{Manually Enter Description Here}} 12 | 13 | ## cdpath Cmdlets 14 | ### [Add-CDPath](Add-CDPath.md) 15 | {{Manually Enter Add-CDPath Description Here}} 16 | 17 | ### [Get-CDPath](Get-CDPath.md) 18 | {{Manually Enter Get-CDPath Description Here}} 19 | 20 | ### [Get-CDPathCandidate](Get-CDPathCandidate.md) 21 | {{Manually Enter Get-CDPathCandidate Description Here}} 22 | 23 | ### [Get-CDPathOption](Get-CDPathOption.md) 24 | {{Manually Enter Get-CDPathOption Description Here}} 25 | 26 | ### [Set-CDPath](Set-CDPath.md) 27 | {{Manually Enter Set-CDPath Description Here}} 28 | 29 | ### [Set-CDPathLocation](Set-CDPathLocation.md) 30 | {{Manually Enter Set-CDPathLocation Description Here}} 31 | 32 | ### [Set-CDPathOption](Set-CDPathOption.md) 33 | {{Manually Enter Set-CDPathOption Description Here}} 34 | 35 | ### [Update-Cdpath](Update-Cdpath.md) 36 | {{Manually Enter Update-Cdpath Description Here}} 37 | 38 | -------------------------------------------------------------------------------- /src/cdpath.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/powercode/CDPath/0cebf060fd775aa08b5b8c9d20d0d9ebb3cb46be/src/cdpath.psd1 -------------------------------------------------------------------------------- /src/cdpath.psm1: -------------------------------------------------------------------------------- 1 | using namespace System.Collections.Generic 2 | using namespace System.Management.Automation 3 | using namespace System.Management.Automation.Language 4 | 5 | Set-StrictMode -Version Latest 6 | 7 | enum CandidateKind{ 8 | Global 9 | CurrentDirectory 10 | CDPath 11 | } 12 | 13 | class CandidatePair : System.IComparable { 14 | 15 | [string] $CDPath 16 | [string] $CandidatePath 17 | [int] $CDPathIndex 18 | [CandidateKind] $Kind 19 | 20 | CandidatePair([string] $cdPath, [string] $candidatePath, [int] $cdpathIndex, [CandidateKind] $kind) { 21 | $this.CDPath = $cdPath.TrimEnd([IO.Path]::DirectorySeparatorChar).TrimEnd([IO.Path]::AltDirectorySeparatorChar) 22 | $this.CandidatePath = if ($candidatePath.EndsWith(":\")) { 23 | $candidatePath 24 | } 25 | else { 26 | $candidatePath.TrimEnd([IO.Path]::DirectorySeparatorChar).TrimEnd([IO.Path]::AltDirectorySeparatorChar) 27 | } 28 | $this.CDPathIndex = $cdpathIndex 29 | $this.Kind = $kind 30 | } 31 | 32 | [string] ToString() {return $this.CandidatePath} 33 | 34 | [int] GetHashCode() {return "$($this.CandidatePath)$($this.CDPath)".GetHashCode()} 35 | 36 | [bool] Equals([object] $o) { 37 | $cp = $o -as [CandidatePair] 38 | return $null -ne $cp -and $cp.CDPath -eq $this.CDPath -and $cp.CandidatePath -eq $this.CandidatePath 39 | } 40 | 41 | [int] CompareTo([object] $o) { 42 | $cp = $o -as [CandidatePair] 43 | if ($null -eq $cp) {return -1} 44 | $res = $this.Kind.CompareTo($cp.Kind) 45 | if ($res -ne 0) { 46 | return $res 47 | } 48 | if ($this.Kind -eq [CandidateKind]::CDPath) { 49 | $res = $this.CDPathIndex.CompareTo($cp.CdPathIndex) 50 | if ($res -ne 0) { 51 | return $res 52 | } 53 | } 54 | return $this.CandidatePath.CompareTo($cp.CandidatePath) 55 | } 56 | } 57 | 58 | class CDPathData { 59 | [string[]] $CDPath 60 | [string] $PathFile 61 | [string] $PreviousWorkingDir = "~" 62 | [bool] $SetWindowTitle = $true 63 | 64 | CDPathData([string] $pathFile) { 65 | $this.Pathfile = $global:ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($pathFile) 66 | if (-not (Test-Path -PathType Leaf $this.PathFile)) { 67 | $this.SetPath("~") 68 | } 69 | $this.Update() 70 | } 71 | 72 | [void] Update() { 73 | $this.CDPath = foreach ($line in [io.File]::ReadAllLines($this.PathFile)) { 74 | $trimmedLine = $line.Trim() 75 | if ($trimmedLine) { 76 | $trimmedLine 77 | } 78 | } 79 | } 80 | [void] AddPath([string[]] $path, [bool] $prepend) { 81 | if ($prepend) { 82 | $this.CDPath = @($path) + $this.CDPath 83 | } 84 | else { 85 | $this.CDPath = $this.CDPath + $path 86 | } 87 | } 88 | [void] SetPath([string[]] $path) { 89 | [io.File]::WriteAllLines($this.PathFile, $path) 90 | $this.Update() 91 | } 92 | 93 | [string[]] GetExpandedCDPath(){ 94 | $provider=$null 95 | $res = [List[string]]::new() 96 | foreach($cdp in $this.CDPath){ 97 | $expanded = $global:ExecutionContext.InvokeCommand.ExpandString($cdp) 98 | try{ 99 | $r =$global:ExecutionContext.SessionState.Path.GetResolvedProviderPathFromPSPath($expanded, [ref] $provider) 100 | $res.AddRange($r) 101 | } 102 | catch{ 103 | Write-Error $_ 104 | } 105 | } 106 | return $res 107 | } 108 | 109 | [CandidatePair[]] GetCandidates([string] $path, [string[]] $remaining, [bool] $exact) { 110 | $provider = $Null 111 | 112 | # Resolve Path 113 | $c = switch ($path) { 114 | '' { '~'; break} 115 | '.' { $pwd.ProviderPath; break } 116 | '..' { (Get-Item -LiteralPath '..').FullName; break } 117 | '-' { $this.PreviousWorkingDir; break } 118 | '~' { (Resolve-path -LiteralPath .).Provider.Home; break } 119 | {$_ -match "^\.{3,}$"} { 120 | # .'ing shortcuts for ... and .... 121 | $Path = $path.Substring(1) -replace '\.', '..\' 122 | (Get-Item -LiteralPath $Path).FullName 123 | break 124 | } 125 | {[IO.Path]::IsPathRooted($_) -and !$remaining} {$path;break} 126 | } 127 | if ($c) { 128 | return [CandidatePair]::new($null, $c, -1, [CandidateKind]::Global) 129 | } 130 | 131 | 132 | # If there are extra arguments, create a globbing expression 133 | # so that cd a b c => cd a*\b*\c* 134 | [bool]$pathEndsWithSep = $false 135 | $pathSep = [IO.Path]::DirectorySeparatorChar 136 | if ($path.EndsWith($pathSep)) { 137 | $pathEndsWithSep = $true 138 | $path = $path.TrimEnd($pathSep) 139 | } 140 | if ($pathEndsWithSep -and !$remaining) { 141 | [string[]] $searchPaths = "$path$pathSep", $path 142 | } 143 | else { 144 | [string[]] $searchPaths = $path 145 | } 146 | 147 | if ($Remaining) { 148 | $rest = $Remaining -join "*$pathSep" 149 | if ($rest.EndsWith($pathSep)) { 150 | $rest = $rest.TrimEnd($pathSep) 151 | $searchPaths = $searchPaths.Foreach{"$_*$pathSep$rest\", "$_*$pathSep$rest"} 152 | } 153 | else { 154 | $searchPaths = $searchPaths.Foreach{"$_*$pathSep$rest"} 155 | } 156 | } 157 | [HashSet[CandidatePair]] $candidates = [HashSet[CandidatePair]]::new() 158 | 159 | foreach ($sp in $searchPaths) { 160 | $rsp = $global:ExecutionContext.SessionState.Path.GetresolvedProviderPathFromPSPath("$sp*", [ref] $provider) 161 | foreach ($r in $rsp) { 162 | if (Test-Path -PathType Container $r) { 163 | $candidates.Add([CandidatePair]::new($pwd.ProviderPath, $r, -1, [CandidateKind]::CurrentDirectory)) 164 | } 165 | } 166 | } 167 | 168 | [string[]] $expandedCdPaths = @($pwd.ProviderPath ) + $this.GetExpandedCDPath() 169 | for ($i = 0; $i -lt $expandedCdPaths.Length; $i++) { 170 | $ecp = $expandedCdPaths[$i] 171 | if (-not $ecp -or ($ecp -notlike "\*" -and $ecp -notmatch '^\w+:' )){ 172 | continue 173 | } 174 | [string]$resolvedCDPath = $global:ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ecp) 175 | if ($resolvedCDPath -eq $pwd.ProviderPath) { 176 | continue 177 | } 178 | foreach ($p in $searchPaths) { 179 | [string[]]$CandidatePaths = if ($exact -or $p.EndsWith($pathSep)) { 180 | Join-Path $resolvedCDPath $p 181 | } 182 | else { 183 | Join-Path $resolvedCDPath "$p*" -Resolve -ea SilentlyContinue 184 | 185 | } 186 | foreach ($cp in $CandidatePaths) { 187 | if ($cp -and (Test-Path -PathType Container $cp)) { 188 | $cpair = [CandidatePair]::new($resolvedCDPath, $cp, $i, [CandidateKind]::CDPath) 189 | $candidates.Add($cpair) 190 | } 191 | } 192 | } 193 | } 194 | 195 | return $candidates | Sort-Object 196 | } 197 | 198 | [string] ToString() { 199 | return $this.CDPath -join ';' 200 | } 201 | } 202 | 203 | 204 | $script:data = [CDPathData]::new('~/.cdpath') 205 | 206 | 207 | class CDPathCompleter : System.Management.Automation.IArgumentCompleter { 208 | [CDPathData] $data 209 | CDPathCompleter() { 210 | $this.Data = $script:data 211 | } 212 | CDPathCompleter([CDPathData] $data) { 213 | $this.data = $data 214 | } 215 | 216 | static [CompletionResult] ToCompletionResult([CandidatePair] $candidatePair) { 217 | 218 | $lt = $candidatePair.CandidatePath.Substring($candidatePair.CDPath.Length + 1) 219 | $ct = $lt 220 | if ($lt.Contains(' ')) { 221 | $ct = "'$lt'" 222 | } 223 | return [CompletionResult]::new($ct, $lt, [CompletionResultType]::ProviderContainer, $candidatePair.CandidatePath) 224 | } 225 | 226 | static [string[]] GetPathsFromCommandAst($commandAst) { 227 | $ce = $commandAst.CommandElements 228 | $paths = $ce.Where{$_ -is [StringConstantExpressionAst]} 229 | $count = $paths.Count - 1 230 | $res = [string[]]::new($count) 231 | for ($i = 0; $i -lt $res.Length; $i++) { 232 | $res[$i] = $paths[$i + 1].Value 233 | } 234 | return $res 235 | } 236 | 237 | [IEnumerable[CompletionResult]] CompleteRemaining([string] $wordToComplete, [CommandAst] $commandAst, [bool] $exact) { 238 | $paths = [CDPathCompleter]::GetPathsFromCommandAst($commandAst) 239 | $res = [System.Collections.Generic.List[CompletionResult]]::new($paths.Length) 240 | 241 | $candidates = $this.Data.GetCandidates($paths[0], $paths[1..($paths.Length - 1)], $exact) 242 | foreach ($c in $candidates) { 243 | if ($c.CDPath) { 244 | $cr = [CDPathCompleter]::ToCompletionResult($c) 245 | $res.Add($cr) 246 | } 247 | } 248 | return $res 249 | } 250 | 251 | [IEnumerable[CompletionResult]] CompletePath([string] $wordToComplete, [CommandAst] $commandAst, [bool] $exact) { 252 | if ($commandAst.CommandElements.Count -ne 2) { 253 | return $null 254 | } 255 | 256 | $paths = [CDPathCompleter]::GetPathsFromCommandAst($commandAst) 257 | $res = [System.Collections.Generic.List[CompletionResult]]::new($paths.Length) 258 | $candidates = $this.Data.GetCandidates($paths[0], $null, $exact) 259 | 260 | foreach ($c in $candidates) { 261 | if ($c.CDPath) { 262 | $cr = [CDPathCompleter]::ToCompletionResult($c) 263 | $res.Add($cr) 264 | } 265 | elseif ($c.CandidatePath -and ($wordToComplete.EndsWith([IO.Path]::DirectorySeparatorChar) -or $wordToComplete.EndsWith([IO.Path]::AltDirectorySeparatorChar))) { 266 | $cp = $c.CandidatePath + [IO.Path]::DirectorySeparatorChar 267 | foreach ($r in [CompletionCompleters]::CompleteFilename($cp)) { 268 | if ($r.ResultType -eq [CompletionResultType]::ProviderContainer) { 269 | $res.Add($r) 270 | } 271 | } 272 | } 273 | } 274 | return $res 275 | } 276 | 277 | 278 | [IEnumerable[CompletionResult]] CompleteArgument([string] $commandName, 279 | [string] $parameterName, 280 | [string] $wordToComplete, 281 | [System.Management.Automation.Language.CommandAst] $commandAst, 282 | [System.Collections.IDictionary] $fakeBoundParameters) { 283 | $exact = $fakeBoundParameters["Exact"] -eq $true 284 | switch ($parameterName) { 285 | 'Path' { 286 | return $this.CompletePath($wordToComplete, $commandAst, $exact); break 287 | } 288 | 'Remaining' { 289 | return $this.CompleteRemaining($wordToComplete, $commandAst, $exact) 290 | } 291 | } 292 | return $null 293 | } 294 | } 295 | 296 | 297 | function Get-CDPathCandidate { 298 | [OutputType([string])] 299 | param( 300 | [Parameter(Mandatory)] 301 | [string] $path, 302 | [string[]] $remaining 303 | ) 304 | $script:Data.GetCandidates($path, $remaining, $false) 305 | 306 | } 307 | 308 | function Set-CDPathLocation { 309 | [Alias('cd')] 310 | param( 311 | [Parameter(Position = 0)] 312 | [ArgumentCompleter([CDPathCompleter])] 313 | [string] $Path, 314 | [Parameter(ValueFromRemainingArguments)] 315 | [ArgumentCompleter([CDPathCompleter])] 316 | [string[]] $Remaining = $null, 317 | [switch] $Exact 318 | ) 319 | 320 | $candidates = $script:data.GetCandidates($Path, $Remaining, $Exact) 321 | if ($candidates) { 322 | $prev = $pwd.ProviderPath 323 | Set-Location $candidates[0] 324 | $script:data.PreviousWorkingDir = $prev 325 | if ($data.SetWindowTitle) { 326 | $host.UI.RawUI.WindowTitle = $pwd.ProviderPath 327 | } 328 | } 329 | else { 330 | Write-Error -Message "Cannot Resolve path $Path $Remaining." -Category ([ErrorCategory]::ObjectNotFound) 331 | } 332 | } 333 | 334 | function Update-Cdpath { 335 | $script:Data.Update() 336 | } 337 | 338 | function Add-CDPath { 339 | param( 340 | [string[]] $Path 341 | , 342 | # Specify Prepend to add the paths before the existing paths 343 | [Switch] $Prepend 344 | ) 345 | 346 | $script.Data.AddPath($Path, $Prepend) 347 | } 348 | 349 | 350 | function Get-CDPath { 351 | [CmdletBinding()] 352 | [OutputType([string])] 353 | param( 354 | [switch] $Resolve 355 | ) 356 | if ($Resolve){ 357 | $script:data.GetExpandedCDPath() 358 | } 359 | else { 360 | $script:data.CDPath 361 | } 362 | } 363 | 364 | function Get-CDPathOption { 365 | $script:Data 366 | } 367 | 368 | function Set-CDPath { 369 | param( 370 | [Parameter(Mandatory)] 371 | [string[]] $Path 372 | ) 373 | $script:Data.SetPath($Path) 374 | } 375 | 376 | function Set-CDPathOption { 377 | param( 378 | # Indicates if the the window title should be changed when changing location 379 | [Switch] $SetWindowTitle 380 | ) 381 | if ($PSBoundParameters.ContainsKey('SetWindowTitle')) { 382 | $script:Data.SetWindowTitle = [bool] $SetWindowTitle 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /src/init.ps1: -------------------------------------------------------------------------------- 1 | if (Test-Path alias:cd) 2 | { 3 | Remove-Item alias:cd 4 | } 5 | -------------------------------------------------------------------------------- /test/cdpath.tests.ps1: -------------------------------------------------------------------------------- 1 | using module ..\Release\CDPath\CDPath.psm1 2 | Describe 'cdpath' { 3 | push-location 4 | BeforeAll { 5 | mkdir TestDrive:\A\A\A 6 | mkdir TestDrive:\A\B 7 | mkdir TestDrive:\A\C 8 | mkdir TestDrive:\B\A 9 | mkdir TestDrive:\B\D 10 | mkdir TestDrive:\C\A 11 | Get-ChildItem TestDrive:\ -Recurse -Directory | ForEach-Object { 12 | $p = $_.fullname 13 | Set-Content -LiteralPath $p/file.txt -value 'text' 14 | } 15 | $script:TD = (Resolve-Path TestDrive:\).ProviderPath 16 | } 17 | 18 | BeforeEach { 19 | $env:TD = $null 20 | Set-Location TestDrive:\ 21 | } 22 | 23 | it 'can set CDPATH' { 24 | $c = [CDPathData]::new("TestDrive:\cdpath") 25 | $c.SetPath(('TestDrive:\A', 'TestDrive:\B')) 26 | $c.CDPath[0] | Should Be "TestDrive:\A" 27 | $c.CDPath[1] | Should Be "TestDrive:\B" 28 | } 29 | 30 | it 'can expand ~ in CDPATH' { 31 | $c = [CDPathData]::new("TestDrive:\cdpath") 32 | $c.SetPath("~") 33 | $e = $c.GetExpandedCDPath() 34 | $e | Should Be $pwd.Provider.Home 35 | } 36 | 37 | it 'can change to C:\' { 38 | $can = Get-CDPathCandidate C:\ 39 | $can[0] | Should -Be "C:\" 40 | } 41 | 42 | it 'can change to ~' { 43 | $provider = (Resolve-Path .).Provider 44 | Set-CDPathLocation ~ 45 | $pwd.ProviderPath | Should -Be $provider.Home 46 | } 47 | 48 | it 'can get candidate from ...' { 49 | $c = [CDPathData]::new("TestDrive:\cdpath") 50 | $c.SetPath(('TestDrive:\A', 'TestDrive:\B')) 51 | Set-Location TestDrive:\A\A\A 52 | $candidates = $c.GetCandidates("...", $Null, $false) 53 | $td = (Resolve-Path TestDrive:\).ProviderPath 54 | $candidates[0].CandidatePath | Should Be "${TD}A" 55 | } 56 | 57 | it 'can get candidate with ending dir sep' { 58 | $c = [CDPathData]::new("TestDrive:\cdpath") 59 | $c.SetPath(('TestDrive:\A', 'TestDrive:\B')) 60 | $candidates = $c.GetCandidates("A\", $Null, $false) 61 | $td = (Resolve-Path TestDrive:\).ProviderPath 62 | $candidates[0].CandidatePath | Should Be "${TD}A" 63 | } 64 | 65 | it 'expands cdpath strings' { 66 | $c = [CDPathData]::new("TestDrive:\cdpath") 67 | $c.SetPath(('TestDrive:\A', 'TestDrive:\B', '$env:TD\C')) 68 | $env:TD = "TestDrive:" 69 | $candidates = $c.GetCandidates("A", $Null, $false) 70 | $candidates.Count | Should Be 4 71 | $candidates[0].CandidatePath | Should Be "${TD}A" 72 | $candidates[2].CandidatePath | Should Be "${TD}B\A" 73 | $candidates[3].CandidatePath | Should Be "${TD}C\A" 74 | } 75 | 76 | it 'can get candidates from cdpath' { 77 | $c = [CDPathData]::new("TestDrive:\cdpath") 78 | $c.SetPath(('TestDrive:\A', 'TestDrive:\B')) 79 | $candidates = $c.GetCandidates("A", $Null, $false) 80 | $candidates.Count | Should Be 3 81 | $candidates[0].CandidatePath | Should Be "${TD}A" 82 | $candidates[2].CandidatePath | Should Be "${TD}B\A" 83 | } 84 | 85 | it 'sets existing folder as first candidate' { 86 | $c = [CDPathData]::new("TestDrive:\cdpath") 87 | $c.SetPath(('TestDrive:\A', 'TestDrive:\B')) 88 | Set-Location TestDrive:\ 89 | $candidates = $c.GetCandidates("A", $Null, $false) 90 | $candidates.Count | Should Be 3 91 | $candidates[0].CandidatePath | Should Be "${TD}A" 92 | $candidates[1].CandidatePath | Should Be "${TD}A\A" 93 | $candidates[2].CandidatePath | Should Be "${TD}B\A" 94 | } 95 | 96 | it 'gets multilevel paths' { 97 | $c = [CDPathData]::new("TestDrive:\cdpath") 98 | 99 | mkdir TestDrive:\B\A\DDD\EEE\FFF 100 | $c.SetPath(('TestDrive:\A', 'TestDrive:\B')) 101 | $candidates = $c.GetCandidates("A", ('d', 'e', 'f'), $false) 102 | $candidates.Count | Should Be 1 103 | $candidates[0].CandidatePath | Should Be "${TD}B\A\DDD\EEE\FFF" 104 | } 105 | 106 | it 'completes set-cdpathlocation top level' { 107 | $c = [CDPathData]::new("TestDrive:\cdpath") 108 | $c.SetPath(('TestDrive:\A', 'TestDrive:\B')) 109 | $cpm = [CDPathCompleter]::new($c) 110 | $commandAst = {Set-CDPathLocation A}.Ast.EndBlock.Statements.PipelineElements[0] 111 | $res = $cpm.CompleteArgument("Set-CDPathLocation", "Path", "A", $commandAst, @{}) 112 | $res.Count | Should Be 3 113 | $res[0].CompletionText | Should Be "A" 114 | $res[0].ToolTip | Should Be "${TD}A" 115 | $res[1].CompletionText | Should Be "A" 116 | $res[1].ToolTip | Should Be "${TD}A\A" 117 | } 118 | 119 | it 'completes set-cdpathlocation second level' { 120 | $c = [CDPathData]::new("TestDrive:\cdpath") 121 | mkdir TestDrive:\B\A1 122 | Set-Content TestDrive:\B\A.txt 'text' 123 | $c.SetPath(('TestDrive:\A', 'TestDrive:\B')) 124 | $cpm = [CDPathCompleter]::new($c) 125 | $commandAst = {Set-CDPathLocation B A}.Ast.EndBlock.Statements.PipelineElements[0] 126 | $res = $cpm.CompleteArgument("Set-CDPathLocation", "Remaining", "A", $commandAst, @{}) 127 | $res.Count | Should Be 2 128 | $res[0].CompletionText | Should Be "B\A" 129 | $res[0].ToolTip | Should Be "${TD}B\A" 130 | $res[1].CompletionText | Should Be "B\A1" 131 | $res[1].ToolTip | Should Be "${TD}B\A1" 132 | } 133 | 134 | it 'completes set-cdpathlocation with path separator' { 135 | $c = [CDPathData]::new("TestDrive:\cdpath") 136 | $c.SetPath(('TestDrive:\')) 137 | $cpm = [CDPathCompleter]::new($c) 138 | mkdir TestDrive:\A\B\C\D 139 | $commandAst = {Set-CDPathLocation A\B\C}.Ast.EndBlock.Statements.PipelineElements[0] 140 | $res = $cpm.CompleteArgument("Set-CDPathLocation", "Path", "A\B\C", $commandAst, @{}) 141 | $res.Count | Should Be 1 142 | $res[0].CompletionText | Should Be "A\B\C" 143 | $res[0].ToolTip | Should Be "${TD}A\B\C" 144 | } 145 | pop-location 146 | } 147 | --------------------------------------------------------------------------------