├── LICENSE ├── Modules └── ParameterCache │ └── 1.0.0 │ ├── ParameterCache.psd1 │ └── loader.ps1 └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dr. Tobias Weltner 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. 22 | -------------------------------------------------------------------------------- /Modules/ParameterCache/1.0.0/ParameterCache.psd1: -------------------------------------------------------------------------------- 1 |  2 | @{ 3 | 4 | # Module Loader File 5 | RootModule = 'loader.ps1' 6 | 7 | # Version Number 8 | ModuleVersion = '1.0.0' 9 | 10 | # Unique Module ID 11 | GUID = '6b4b2a62-8ccb-4778-8273-459f4de3c7af' 12 | 13 | # Module Author 14 | Author = 'Dr. Tobias Weltner' 15 | 16 | # Company 17 | CompanyName = 'https://powershell.one' 18 | 19 | # Copyright 20 | Copyright = '(c) 2021 Dr. Tobias Weltner. All rights reserved.' 21 | 22 | # Module Description 23 | Description = 'provides attributes for parameters that cache previous values and autosuggest them on later use' 24 | 25 | # Minimum PowerShell Version Required 26 | PowerShellVersion = '5.1' 27 | 28 | # Name of Required PowerShell Host 29 | PowerShellHostName = '' 30 | 31 | CompatiblePSEditions = @('Desktop', 'Core') 32 | 33 | # Minimum Host Version Required 34 | PowerShellHostVersion = '' 35 | 36 | # List of exportable functions 37 | FunctionsToExport = 'Register-ParameterCache' 38 | 39 | # Private data that needs to be passed to this module 40 | PrivateData = @{ 41 | 42 | PSData = @{ 43 | 44 | # Tags applied to this module. These help with module discovery in online galleries. 45 | Tags = @( 46 | 'Attribute' 47 | 'Parameter' 48 | 'Autocomplete' 49 | 'Cache' 50 | 'SecureString' 51 | 'Security' 52 | 'powershell.one' 53 | 'Windows' 54 | 'MacOS' 55 | 'Linux' 56 | ) 57 | 58 | # A URL to the main website for this project. 59 | ProjectUri = 'https://github.com/TobiasPSP/ParameterCache' 60 | 61 | 62 | } # End of PSData hashtable 63 | 64 | } # End of PrivateData hashtable 65 | 66 | } -------------------------------------------------------------------------------- /Modules/ParameterCache/1.0.0/loader.ps1: -------------------------------------------------------------------------------- 1 |  2 | # this is used only to load the module and its attributes 3 | 4 | function Register-ParameterCache 5 | { 6 | <# 7 | .SYNOPSIS 8 | Loads the parameter attributes 9 | 10 | .DESCRIPTION 11 | Once you run this command, or manually import module "ParameterCache" (Import-Module -Name ParameterCache), 12 | you have access to the following attributes: 13 | 14 | [AutoLearn('user')] : caches the values for a parameter in list "user" 15 | [AutoComplete('user')] : autocompletes the previously entered values from list "user" 16 | [AutoLearnCredential('user')] : same for credentials (to secure storage) 17 | [AutocompleteCredential('user')] : same for credentials (from secure storage) 18 | 19 | .EXAMPLE 20 | Register-ParameterCache 21 | Call this before using the attributes, or make the module "ParameterCache" a prerequisite for your own modules. 22 | 23 | .LINK 24 | URLs to related sites 25 | #> 26 | } 27 | 28 | 29 | class AutoLearnAttribute : System.Management.Automation.ArgumentTransformationAttribute 30 | { 31 | # define path to store hint lists 32 | [string]$Path = "$env:temp\hints" 33 | 34 | # define id to manage multiple hint lists: 35 | [string]$Id = 'default' 36 | 37 | # define prefix character used to delete the hint list 38 | [char]$ClearKey = '!' 39 | 40 | # define parameterless constructor: 41 | AutoLearnAttribute() : base() 42 | {} 43 | 44 | # define constructor with parameter for id: 45 | AutoLearnAttribute([string]$Id) : base() 46 | { 47 | $this.Id = $Id 48 | } 49 | 50 | # Transform() is called whenever there is a variable or parameter assignment, and returns the value 51 | # that is actually assigned: 52 | [object] Transform([System.Management.Automation.EngineIntrinsics]$engineIntrinsics, [object] $inputData) 53 | { 54 | # make sure the folder with hints exists 55 | $exists = Test-Path -Path $this.Path 56 | if (!$exists) { $null = New-Item -Path $this.Path -ItemType Directory } 57 | 58 | # create filename for hint list 59 | $filename = '{0}.hint' -f $this.Id 60 | $hintPath = Join-Path -Path $this.Path -ChildPath $filename 61 | 62 | # use a hashtable to keep hint list 63 | $hints = @{} 64 | 65 | # read hint list if it exists 66 | $exists = Test-Path -Path $hintPath 67 | if ($exists) 68 | { 69 | Get-Content -Path $hintPath -Encoding Default | 70 | # remove leading and trailing blanks 71 | ForEach-Object { $_.Trim() } | 72 | # remove empty lines 73 | Where-Object { ![string]::IsNullOrEmpty($_) } | 74 | # add to hashtable 75 | ForEach-Object { 76 | # value is not used, set it to $true: 77 | $hints[$_] = $true 78 | } 79 | } 80 | 81 | # does the user input start with the clearing key? 82 | if ($inputData.StartsWith($this.ClearKey)) 83 | { 84 | # remove the prefix: 85 | $inputData = $inputData.SubString(1) 86 | 87 | # clear the hint list: 88 | $hints.Clear() 89 | } 90 | 91 | # add new value to hint list 92 | if(![string]::IsNullOrWhiteSpace($inputData)) 93 | { 94 | $hints[$inputData] = $true 95 | } 96 | # save hints list 97 | $hints.Keys | Sort-Object | Set-Content -Path $hintPath -Encoding Default 98 | 99 | # return the user input (if there was a clearing key at its start, 100 | # it is now stripped): 101 | return $inputData 102 | } 103 | } 104 | 105 | 106 | class AutoCompleteAttribute : System.Management.Automation.ArgumentCompleterAttribute 107 | { 108 | # define path to store hint lists 109 | [string]$Path = "$env:temp\hints" 110 | 111 | # define id to manage multiple hint lists: 112 | [string]$Id = 'default' 113 | 114 | # define parameterless constructor: 115 | AutoCompleteAttribute() : base([AutoCompleteAttribute]::_createScriptBlock($this)) 116 | {} 117 | 118 | # define constructor with parameter for id: 119 | AutoCompleteAttribute([string]$Id) : base([AutoCompleteAttribute]::_createScriptBlock($this)) 120 | { 121 | $this.Id = $Id 122 | } 123 | 124 | # create a static helper method that creates the scriptblock that the base constructor needs 125 | # this is necessary to be able to access the argument(s) submitted to the constructor 126 | # the method needs a reference to the object instance to (later) access its optional parameters: 127 | hidden static [ScriptBlock] _createScriptBlock([AutoCompleteAttribute] $instance) 128 | { 129 | $scriptblock = { 130 | # receive information about current state: 131 | param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) 132 | 133 | # create filename for hint list 134 | $filename = '{0}.hint' -f $instance.Id 135 | $hintPath = Join-Path -Path $instance.Path -ChildPath $filename 136 | 137 | # use a hashtable to keep hint list 138 | $hints = @{} 139 | 140 | # read hint list if it exists 141 | $exists = Test-Path -Path $hintPath 142 | if ($exists) 143 | { 144 | Get-Content -Path $hintPath -Encoding Default | 145 | # remove leading and trailing blanks 146 | ForEach-Object { $_.Trim() } | 147 | # remove empty lines 148 | Where-Object { ![string]::IsNullOrEmpty($_) } | 149 | # filter completion items based on existing text: 150 | Where-Object { $_.LogName -like "$wordToComplete*" } | 151 | # create argument completion results 152 | Foreach-Object { 153 | [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) 154 | } 155 | } 156 | }.GetNewClosure() 157 | return $scriptblock 158 | } 159 | } 160 | 161 | 162 | class AutoLearnCredentialAttribute : System.Management.Automation.ArgumentTransformationAttribute 163 | { 164 | [string]$Path = "$env:temp\hints" 165 | [string]$Id = 'default' 166 | [char]$ClearKey = '!' 167 | 168 | AutoLearnCredentialAttribute() : base() 169 | {} 170 | 171 | AutoLearnCredentialAttribute([string]$Id) : base() 172 | { 173 | $this.Id = $Id 174 | } 175 | 176 | # calculates md5 hash for usernames 177 | # hashes are used as keys for the serialized hashtable 178 | # declared as "static" because it has no relation to the attribute instance 179 | # and is simply a generic helper method: 180 | static [string] GetHash([string]$UserName) 181 | { 182 | $md5 = [System.Security.Cryptography.MD5CryptoServiceProvider]::new() 183 | $utf8 = [System.Text.UTF8Encoding]::new() 184 | return [System.BitConverter]::ToString($md5.ComputeHash($utf8.GetBytes($UserName.ToLower()))).Replace('-','') 185 | } 186 | 187 | [object] Transform([System.Management.Automation.EngineIntrinsics]$engineIntrinsics, [object] $inputData) 188 | { 189 | # make sure the folder with hints exists 190 | $exists = Test-Path -Path $this.Path 191 | if (!$exists) { $null = New-Item -Path $this.Path -ItemType Directory } 192 | 193 | # create filename for hint list 194 | $filename = '{0}.xmlhint' -f $this.Id 195 | $hintPath = Join-Path -Path $this.Path -ChildPath $filename 196 | 197 | # use a hashtable to keep hint list 198 | $hints = @{} 199 | 200 | # read hint list if it exists 201 | $exists = Test-Path -Path $hintPath 202 | if ($exists) 203 | { 204 | # hint list is xml data 205 | # it is a serialized hashtable and can be 206 | # deserialized via Import-CliXml if it exists 207 | # result is a hashtable: 208 | [System.Collections.Hashtable]$hints = Import-Clixml -Path $hintPath 209 | } 210 | 211 | # if the argument is a string... 212 | if ($inputData -is [string]) 213 | { 214 | # does username start with "!"? 215 | [bool]$promptAlways = $inputData.StartsWith($this.ClearKey) 216 | 217 | # if not,... 218 | if (!$promptAlways) 219 | { 220 | # get the md5 key for the entered username 221 | $key = [AutoLearnCredentialAttribute]::GetHash($inputData) 222 | # ...check to see if the username has been used before, 223 | # and re-use its credential (no need to enter password again) 224 | if ($hints.ContainsKey($key)) 225 | { 226 | # the hashtable contains username and password, so 227 | # create a credential from this: 228 | 229 | # convert username from securestring to plaintext: 230 | $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($hints[$key].UserName) 231 | $username = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) 232 | 233 | # construct the credential object: 234 | $credential = [System.Management.Automation.PSCredential]::new($username, $hints[$key].Password) 235 | return $credential 236 | } 237 | } 238 | else 239 | { 240 | # ...else, remove the "!" at the beginning and prompt 241 | # again for the password (this way, passwords can be updated) 242 | $inputData = $inputData.SubString(1) 243 | # delete the cached credentials 244 | $hints.Clear() 245 | } 246 | # ask for a credential: 247 | $cred = $engineIntrinsics.Host.UI.PromptForCredential("Enter password", "Please enter user account and password", $inputData, "") 248 | # get the md5 key for the entered username 249 | $key = [AutoLearnCredentialAttribute]::GetHash($cred.UserName) 250 | 251 | 252 | # add username and password to the hashtable: 253 | $hints[$key] = @{ 254 | # save username as securestring to make sure it gets encrypted too: 255 | UserName = $cred.UserName | ConvertTo-SecureString -AsPlainText -Force 256 | Password = $cred.Password 257 | } 258 | # update the hashtable and write it to file 259 | # passwords are automatically safely encrypted: 260 | $hints | Export-Clixml -Path $hintPath 261 | # return the credential: 262 | return $cred 263 | } 264 | # if a credential was submitted... 265 | elseif ($inputData -is [PSCredential]) 266 | { 267 | # get the encrypted key for the entered username 268 | $key = [AutoLearnCredentialAttribute]::GetHash($inputData.UserName) 269 | 270 | # save it to the hashtable: 271 | $hints[$key] = @{ 272 | UserName = $inputData.UserName | ConvertTo-SecureString -AsPlainText -Force 273 | Password = $inputData.Password 274 | } 275 | # update the hashtable and write it to file: 276 | $hints | Export-Clixml -Path $hintPath 277 | # return the credential: 278 | return $inputData 279 | } 280 | throw [System.InvalidOperationException]::new('Unexpected error.') 281 | } 282 | } 283 | 284 | 285 | class AutocompleteCredentialAttribute : System.Management.Automation.ArgumentCompleterAttribute 286 | { 287 | [string]$Path = "$env:temp\hints" 288 | [string]$Id = 'default' 289 | 290 | AutocompleteCredentialAttribute() : base([AutocompleteCredentialAttribute]::_createScriptBlock($this)) 291 | {} 292 | 293 | AutocompleteCredentialAttribute([string]$Id) : base([AutocompleteCredentialAttribute]::_createScriptBlock($this)) 294 | { 295 | $this.Id = $Id 296 | } 297 | 298 | hidden static [ScriptBlock] _createScriptBlock([AutocompleteCredentialAttribute] $instance) 299 | { 300 | $scriptblock = { 301 | param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) 302 | 303 | $filename = '{0}.xmlhint' -f $instance.Id 304 | $hintPath = Join-Path -Path $instance.Path -ChildPath $filename 305 | 306 | $exists = Test-Path -Path $hintPath 307 | if ($exists) 308 | { 309 | # read serialized hint hashtable if it exists... 310 | [System.Collections.Hashtable]$hints = Import-Clixml -Path $hintPath 311 | 312 | # hint the sorted list of cached user names 313 | 314 | # take the serialized hashtables. We can no longer use the hashtable keys 315 | # because they are just MD5 hashes: 316 | $hints.Values | 317 | ForEach-Object { 318 | # decrypt encrypted username 319 | $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($_.UserName) 320 | [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) 321 | } | 322 | Where-Object { $_ } | 323 | # ...that still match the current user input: 324 | Where-Object { $_.LogName -like "$wordToComplete*" } | 325 | Sort-Object | 326 | # return completion results: 327 | Foreach-Object { 328 | [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) 329 | } 330 | } 331 | }.GetNewClosure() 332 | return $scriptblock 333 | } 334 | } 335 | 336 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ParameterCache 2 | Home of the "ParameterCache" PowerShell module, adding cached parameter values and autocompletion 3 | 4 | ## Installation 5 | 6 | Install the module from the *PowerShell Gallery*: 7 | 8 | ```powershell 9 | Install-Module -Name ParameterCache -Scope CurrentUser 10 | ``` 11 | 12 | ## Loading 13 | 14 | Either run `Register-ParameterCache` to load the module implicitly, or import the module: 15 | 16 | ```powershell 17 | Import-Module -Name ParameterCache 18 | ``` 19 | 20 | ## Using 21 | 22 | The module provides you with four new attributes that can be used to decorate parameters (and variables in general). 23 | 24 | With these attributes, you can easily implement parameter value caching and autocompletion. 25 | 26 | ### Unprotected Caching 27 | 28 | For insensitive data like computer names or ip addresses, use `[AutoLearn()]` and `[AutoComplete()]`. Each attribute takes a string id so you can use different caching lists for different parameters. 29 | 30 | This is an example. Run the command multiple times, and see how each time you submit values to the parameters, the autocompletion lists grow: 31 | 32 | ```powershell 33 | function Connect-MyServer 34 | { 35 | param 36 | ( 37 | [string] 38 | [Parameter(Mandatory)] 39 | # auto-learn user names to user.hint 40 | [AutoLearn('user')] 41 | # auto-complete user names from user.hint 42 | [AutoComplete('user')] 43 | $UserName, 44 | 45 | [string] 46 | [Parameter(Mandatory)] 47 | # auto-learn computer names to server.hint 48 | [AutoLearn('server')] 49 | # auto-complete computer names from server.hint 50 | [AutoComplete('server')] 51 | $ComputerName 52 | ) 53 | 54 | "hello $Username, connecting you to $ComputerName" 55 | } 56 | ``` 57 | 58 | ### Safe Credential Store 59 | 60 | To cache credentials in a safe and encrypted way, use `[AutoLearnCredential()]` and `[AutocompleteCredential()]`: 61 | 62 | ```powershell 63 | function New-Login 64 | { 65 | param 66 | ( 67 | [PSCredential] 68 | [Parameter(Mandatory)] 69 | # cache credentials to username.xmlhint 70 | [AutoLearnCredential('usernameSafe')] 71 | # auto-complete user names from username.xmlhint 72 | [AutocompleteCredential('usernameSafe')] 73 | $Credential 74 | ) 75 | 76 | $username = $Credential.UserName 77 | Write-Host "hello $username!" 78 | return $Credential 79 | } 80 | ``` 81 | 82 | ## Notes 83 | 84 | The full article about the techniques used can be found here: https://powershell.one/powershell-internals/attributes/custom-attributes#final-example 85 | 86 | It is noteworthy that this module shows a simple way to *export PowerShell classes*. Typically, classes defined by PowerShell can be exported by modules only when you use the awkward *using module...* statement. `Import-Module` won't make such classes available in the caller context. 87 | 88 | The simple solution is to rename your module file inside the module from *.psm1* to *.ps1*, making it a plain script without separate scope. That will suffice like this module show-cases. 89 | --------------------------------------------------------------------------------