public/Set-CdExtrasOption.ps1

<#
.SYNOPSIS
Update cd-extras option ('AUTO_CD', 'CD_PATH', ...etc).

.PARAMETER Option
The option to update.

.PARAMETER Value
The new value.

.PARAMETER Options
A dictionary of options and values to update.

.EXAMPLE
PS C:\> setocd AUTO_CD

Enables AUTO_CD

.EXAMPLE
PS C:\> setocd AUTO_CD $false

Disables AUTO_CD

.EXAMPLE
PS C:\> Set-CdExtrasOption -Option CD_PATH -Value @('/temp')

Set the directory search paths to the single directory, '/temp'

.EXAMPLE
PS C:\> Set-CdExtrasOption -Options ([ordered]@{ MaxRecentDirs = 100; AUTO_CD = $false })

Set several options from any dictionary that implements System.Collections.IDictionary.
#>

function Set-CdExtrasOption {

  [OutputType([void], [bool])]
  [CmdletBinding(DefaultParameterSetName = 'Set')]
  param (
    [ArgumentCompleter( { $global:cde | Get-Member -Type Property -Name "$($args[2])*" | % Name })]
    [Parameter(ParameterSetName = 'Set', Mandatory, Position = 0)]
    [string] $Option,
    [Parameter(ParameterSetName = 'Set', Position = 1, ValueFromPipeline)]
    $Value,

    [Parameter(ParameterSetName = 'SetMany', Mandatory, Position = 0)]
    [Collections.IDictionary] $Options,

    [Parameter(ParameterSetName = 'Validate', Mandatory)]
    [switch] $Validate
  )

  # first update the $cde variable per the given settings
  if ($PSCmdlet.ParameterSetName -eq 'Set' -or $PSCmdlet.ParameterSetName -eq 'SetMany') {

    $flags = @(
      'AUTO_CD'
      'CDABLE_VARS'
      'ColorCompletion'
      'IndexedCompletion'
      'RecentDirsFallThrough'
    )

    $completionTypes = @(
      'PathCompletions'
      'DirCompletions'
      'FileCompletions'
    )

    $optionNames = if ($PSCmdlet.ParameterSetName -eq 'Set') { @($Option) } else { @($Options.Keys) }
    $publicPropertyNames = @($Global:cde | Get-Member -Type Property).Name
    $updates = @()

    foreach ($optionName in $optionNames) {
      $propertyName = $publicPropertyNames | Where-Object { $_ -eq $optionName } | Select-Object -First 1
      if (!$propertyName) {
        $exception = [ArgumentException]::new("Unknown cd-extras option '$optionName'.")
        $errorRecord = [Management.Automation.ErrorRecord]::new(
          $exception,
          'UnknownCdExtrasOption',
          [Management.Automation.ErrorCategory]::InvalidArgument,
          $optionName
        )
        $PSCmdlet.ThrowTerminatingError($errorRecord)
      }
      $property = $Global:cde.PSObject.Properties[$propertyName]

      $optionValue = if ($PSCmdlet.ParameterSetName -eq 'SetMany') { $Options[$optionName] } else { $Value }
      if ($PSCmdlet.ParameterSetName -eq 'Set' -and $null -eq $optionValue -and $property.Name -in $flags) {
        $optionValue = $true
      }

      $propertyType = [type]$property.TypeNameOfValue
      try {
        $convertedValue = [Management.Automation.LanguagePrimitives]::ConvertTo($optionValue, $propertyType)
      }
      catch {
        $exception = [ArgumentException]::new(
          "Invalid value for cd-extras option '$($property.Name)'. Expected $propertyType.",
          $_.Exception
        )
        $errorRecord = [Management.Automation.ErrorRecord]::new(
          $exception,
          'InvalidCdExtrasOptionValue',
          [Management.Automation.ErrorCategory]::InvalidArgument,
          $optionValue
        )
        $PSCmdlet.ThrowTerminatingError($errorRecord)
      }

      if ($property.Name -in $completionTypes) {
        $combined = @($property.Value)
        foreach ($completion in $convertedValue) {
          if ($combined -notcontains $completion) { $combined += $completion }
        }
        $convertedValue = [string[]]$combined
      }

      $updates += [pscustomobject]@{ Name = $property.Name; Value = $convertedValue }
    }

    foreach ($update in $updates) {
      $Global:cde.($update.Name) = $update.Value
    }
  }

  # then perform various side effects based on the current settings
  if ($cde.RECENT_DIRS_FILE) {

    $path = $cde.RECENT_DIRS_FILE -replace '~', $HOME
    $cde.RECENT_DIRS_FILE = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( $path )

    OpenRecentStore
  }

  $cde.RECENT_DIRS_EXCLUDE = $cde.RECENT_DIRS_EXCLUDE.ForEach{ Resolve-Path $_ }

  if ($cde.DirCompletions) {
    RegisterCompletions $cde.DirCompletions 'Path' { CompletePaths -dirsOnly @args }
  }
  if ($cde.FileCompletions) {
    RegisterCompletions $cde.FileCompletions 'Path' { CompletePaths -filesOnly @args }
  }
  if ($cde.PathCompletions) {
    RegisterCompletions $cde.PathCompletions 'Path' { CompletePaths @args }
  }

  $isUnderTest = { $Script:__cdeUnderTest -and !($Script:__cdeUnderTest = $false) }
  if ($cde.AUTO_CD) {
    CommandNotFound @(AutoCd) $isUnderTest
  }
  else {
    CommandNotFound @() $isUnderTest
  }

  # can be used to ensure side effects have run without actually changing any options
  # this is used when the $cde variable is updated directly
  if ($Validate) { return $true }
}