ArgParser.psm1
using namespace System.Reflection using namespace System.Collections.Generic using namespace System.Management.Automation using namespace System.Collections.ObjectModel #!/usr/bin/env pwsh #region Classes class Parsedarg { [int] $Index [string] $Name [bool] $IsKnown [bool] $IsArray [bool] $IsSwitch [bool] $HasEqualSign [Object] $DefaultValue [Type] $ParameterType Parsedarg() {} Parsedarg([Hashtable]$Object) { $Object.Keys.ForEach({ $this.$_ = $Object[$_] }) } } class ParamBase : ParameterInfo { [bool]$IsDynamic [System.Object]$Value [Collection[string]]$Aliases [Collection[Attribute]]$Attributes [IEnumerable[CustomAttributeData]]$CustomAttributes ParamBase([string]$Name) { [void][ParamBase]::From($Name, [System.Management.Automation.SwitchParameter], $null, [ref]$this) } ParamBase([Object[]]$argumentlist) { [void][ParamBase]::From([string]$argumentlist[0], [type]$argumentlist[1], [System.Object]$argumentlist[2], [ref]$this) } ParamBase([string]$Name, [type]$Type) { [void][ParamBase]::From($Name, $Type, $null, [ref]$this) } ParamBase([string]$Name, [System.Object]$value) { [void][ParamBase]::From($Name, ($value.PsObject.TypeNames[0] -as 'Type'), $value, [ref]$this) } ParamBase([string]$Name, [type]$Type, [System.Object]$value) { [void][ParamBase]::From($Name, $Type, $value, [ref]$this) } ParamBase([System.Management.Automation.ParameterMetadata]$ParameterMetadata, [System.Object]$value) { [void][ParamBase]::From($ParameterMetadata, $value, [ref]$this) } static hidden [ParamBase] From([string]$Name, [type]$Type, [System.Object]$value, [ref]$ref) { return [ParamBase]::From([System.Management.Automation.ParameterMetadata]::new($Name, $Type), $value, $ref) } static hidden [ParamBase] From([System.Management.Automation.ParameterMetadata]$ParameterMetadata, [System.Object]$value, [ref]$ref) { $Name = $ParameterMetadata.Name; if ([string]::IsNullOrWhiteSpace($ParameterMetadata.Name)) { throw [System.ArgumentNullException]::new('Name') } $PType = $ParameterMetadata.ParameterType; [ValidateNotNullOrEmpty()][type]$PType = $PType; if ($null -ne $value) { try { $ref.Value.Value = $value -as $PType; } catch { $InnrEx = [System.Exception]::new() $InnrEx = if ($null -ne $ref.Value.Value) { if ([Type]$ref.Value.Value.PsObject.TypeNames[0] -ne $PType) { [System.InvalidOperationException]::New('Operation is not valid due to ambigious parameter types') }else { $innrEx } } else { $innrEx } throw [System.Management.Automation.SetValueException]::new("Unable to set value for $($ref.Value.ToString()) parameter.", $InnrEx) } }; $ref.Value.Aliases = $ParameterMetadata.Aliases; $ref.Value.IsDynamic = $ParameterMetadata.IsDynamic; $ref.Value.Attributes = $ParameterMetadata.Attributes; $ref.Value.PsObject.properties.add([psscriptproperty]::new('Name', [scriptblock]::Create("return '$Name'"), { throw "'Name' is a ReadOnly property." })); $ref.Value.PsObject.properties.add([psscriptproperty]::new('IsSwitch', [scriptblock]::Create("return [bool]$([int]$ParameterMetadata.SwitchParameter)"), { throw "'IsSwitch' is a ReadOnly property." })); $ref.Value.PsObject.properties.add([psscriptproperty]::new('ParameterType', [scriptblock]::Create("return [Type]'$PType'"), { throw "'ParameterType' is a ReadOnly property." })); $ref.Value.PsObject.properties.add([psscriptproperty]::new('DefaultValue', [scriptblock]::Create('return $(switch ($this.ParameterType) { ([bool]) { $false } ([string]) { [string]::Empty } ([array]) { @() } ([hashtable]) { @{} } Default { $null } }) -as $this.ParameterType'), { throw "'DefaultValue' is a ReadOnly property." })); $ref.Value.PsObject.properties.add([psscriptproperty]::new('RawDefaultValue', [scriptblock]::Create('return $this.DefaultValue.ToString()'), { throw "'RawDefaultValue' is a ReadOnly property." })); $ref.Value.PsObject.properties.add([psscriptproperty]::new('HasDefaultValue', [scriptblock]::Create('return $($null -ne $this.DefaultValue)'), { throw "'HasDefaultValue' is a ReadOnly property." })); return $ref.Value } [string] ToString() { return '{0}${1}' -f $($this.IsSwitch ? '[switch]' : '[Parameter()]'), $this.Name } } <# .EXAMPLE [ParamSchema]@( ('font', [string], "NotoSansMono-Regular"), ('force', [switch], $false), ('verbose', [switch], $false) ) #> class ParamSchema { [bool] $IsReadOnly = $false hidden [Dictionary[string, ParamBase]] $_int_d ParamSchema() { [ParamSchema]::From($null, [ref]$this) } ParamSchema([Object[]]$params) { [ParamSchema]::From([ParamBase[]]$params, [ref]$this) } ParamSchema([hashtable]$params) { [ParamSchema]::From($params, [ref]$this) } ParamSchema([ParamBase[]]$params) { [ParamSchema]::From($params, [ref]$this) } static [ParamSchema] Create() { return [ParamSchema]::new() } static [ParamSchema] Create([Object[]]$params) { return [ParamSchema]::Create([ParamBase[]]$params) } static [ParamSchema] Create([hashtable]$params) { return [ParamSchema]::new($params) } static [ParamSchema] Create([ParamBase[]]$params) { return [ParamSchema]::new($params) } static hidden [ParamSchema] From([ParamBase[]]$params, [ref]$ref) { $ref.Value.__init__(); $params.ForEach({ $this._int_d.Add($_.Name, $_) }) return $ref.Value } static hidden [ParamSchema] From([hashtable]$params, [ref]$ref) { $ref.Value.__init__(); $params.Keys.ForEach({ $this._int_d.Add($_, [ParamBase]::new($_, $params.$_[0], $params.$_[1] ) ) }) return $ref.Value } hidden [void] __init__() { $this._int_d = [Dictionary[string, ParamBase]]::new() $this.PsObject.Properties.Add([psscriptproperty]::new('Count', { return $this._int_d.Count }, { throw "'Count' is a ReadOnly property." })) $this.PsObject.Properties.Add([psscriptproperty]::new('Keys', { return [System.Collections.Generic.ICollection[string]]$this._int_d.Keys }), { throw "'Keys' is a ReadOnly property." }) $this.PsObject.Properties.Add([psscriptproperty]::new('Values', { return [System.Collections.Generic.ICollection[ParamBase]]$this._int_d.Values }, { throw "'Values' is a ReadOnly property." })) } [void] Add([string]$key, [ParamBase]$value) { $this._int_d.Add($key, $value) } [void] Add([string]$key, [Object[]]$param) { $this.Add($key, [ParamBase]::new($param)) } [void] Add([KeyValuePair[string, ParamBase]] $item) { $this._int_d.Add($item.Key, $item.Value) } [bool] Contains([KeyValuePair[string, ParamBase]] $item) { return $this._int_d.ContainsKey($item.Key) -and [ParamBase]::Equals($this._int_d[$item.Key], $item.Value) } [bool] ContainsKey([string] $key) { return $this._int_d.ContainsKey($key) } [bool] Remove([string] $key) { return $this._int_d.Remove($key) } [bool] TryGetValue([string]$key, [ref]$value) { return $this._int_d.TryGetValue($key, [ref]$value) } [Dictionary[string, ParamBase]] ToDictionary() { return $this._int_d } [void] CopyTo([KeyValuePair[string, ParamBase][]] $argumentlist, [int] $argumentlistIndex) { $index = $argumentlistIndex foreach ($item in $this._int_d) { $argumentlist[$index] = $item $index++ } } [bool] Remove([KeyValuePair[string, ParamBase]] $item) { if ($this._int_d.ContainsKey($item.Key) -and [ParamBase]::Equals($this._int_d[$item.Key], $item.Value)) { return $this._int_d.Remove($item.Key) } return $false } [IEnumerator[KeyValuePair[string, ParamBase]]] GetEnumerator() { return $this._int_d.GetEnumerator() } [System.Collections.IEnumerator] IEnumerable_GetEnumerator() { return $this._int_d.GetEnumerator() } [ParamBase] get_Item([string]$key) { return $this._int_d[$key] } [void] set_Item([string]$key, [ParamBase]$value) { $this._int_d[$key] = $value } [void] set_Item([string]$key, [Object[]]$param) { $this.set_Item($key, [ParamBase]::new($param)) } [void] Clear() { $this._int_d.Clear() } } class ArgParser { hidden [Parsedarg[]]$INFERRED hidden [version]$VERSION = [version]'0.1.3' hidden [ValidateNotNullOrEmpty()][ParamSchema]$schema hidden [KeyValuePair[String, Parsedarg][]]$_map = @() hidden [ValidateNotNullOrEmpty()][string[]]$_array hidden [int]$ci = 0 ArgParser([Object[]]$schema) { $this.schema = [ParamSchema]::Create($schema) } ArgParser([ParamSchema]$schema) { $this.schema = $schema } [Dictionary[String, ParamBase]] Parse([string[]]$argumentlist) { $result = [Dictionary[String, ParamBase]]::New(); [void]$this.read_list($argumentlist); if ($this.INFERRED.Count -gt 0) { foreach ($item in $this.INFERRED) { $result.Add($item.Name, [ParamBase]::New($item.Name, $item.ParameterType, $this.get_value($item.Name))) } } return $result } [Dictionary[String, ParamBase]] Parse([string[]]$argumentlist, [Dictionary[ParameterMetadata, object]]$metadata) { $_schema = [Dictionary[String, ParamBase]]::New(); $metadata.Keys.ForEach({ $_schema.Add($_.Name, [ParamBase]::new($_.Name, $_.ParameterType, $metadata[$_])) }) return $this.Parse($argumentlist, $_schema) } [Object] get_value([string]$Name) { $value = $null; $done = $false # means we got a value or we just have to stop. $knval = $this.INFERRED.Where({ $_.Name -eq $Name })[0]; $this.ci = $knval.Index do { switch ($true) { $($this._map[$this.ci].Value.HasEqualSign -and !$this._map[$this.ci].Value.IsArray) { $value = $this._array[$this.ci].Substring($this._array[$this.ci].IndexOf('=') + 1); $done = $null -ne $value; if (!$done) { $this.ci++ } break } Default { $_values = @(); While (!$done -and $this.ci -lt $this._array.Count) { $_v = $this._array[$this.ci] if ($this._map[$this.ci].Value.HasEqualSign) { $_v = $_v.Substring($_v.IndexOf('=') + 1) } if (!$_v.StartsWith('-')) { $_values += $_v } $next = $this._array[$this.ci + 1] $cont = $next ? !$next.StartsWith('-') : $false $cont ? ($this.ci++) : ($done = $true) } $value = $_values -as $this._map[$knval.Index].Value.ParameterType } } if ($done) { $this.ci = 0 } } until ($done -or $this.ci -ge $this._array.Count) $result = ($null -eq $value) ? $knval.DefaultValue : $value return $result } [void] read_list([string[]]$argumentlist) { $this._array = $argumentlist for ($i = 0; $i -lt $this._array.Count; $i++) { $name = $this._array[$i] $name = $name.TrimStart('-'); $HasEqualSign = $name.Contains('=') $name = $HasEqualSign ? $name.Substring(0, $name.IndexOf('=')) : $name $scpv = $this.schema.get_Item($name) $Is_known_key = $this.schema.ContainsKey($name) $Is_Array = $Is_known_key ? $scpv.ParameterType.IsArray : $false $this._map += [KeyValuePair[String, Parsedarg]]::new($i, [Parsedarg]@{ Name = $name Index = $i IsKnown = $Is_known_key IsArray = $Is_Array IsSwitch = $Is_known_key ? $scpv.IsSwitch : $false DefaultValue = $Is_known_key ? $scpv.DefaultValue : $null HasEqualSign = $HasEqualSign ParameterType = $Is_known_key ? $scpv.ParameterType : $($Is_Array ? [string[]] : [string]) } ) } $this.INFERRED = $this._map.Value.Where({ $_.IsKnown }) | Select-Object * -Exclude IsKnown | Sort-Object -Unique Index } [string] MungeName([string]$name) { # converts parameter names from their command-line format (using dashes) to their property name format. return [string]::Join('', ($name.Split('-') | ForEach-Object { $_.Substring(0, 1).ToUpper() + $_.Substring(1) })) } [string] ToString() { return $this.INFERRED ? $this.INFERRED.ToString() : 'ArgParser' } } #endregion Classes # Types that will be available to users when they import the module. $typestoExport = @( [ArgParser], [ParamBase], [Parsedarg], [ParamSchema] ) $TypeAcceleratorsClass = [PsObject].Assembly.GetType('System.Management.Automation.TypeAccelerators') foreach ($Type in $typestoExport) { if ($Type.FullName -in $TypeAcceleratorsClass::Get.Keys) { $Message = @( "InvalidOperation : TypeAcceleratorAlreadyExists" "Unable to register type accelerator '$($Type.FullName)'" ) -join ' - ' $Message | Write-Warning } } # Add type accelerators for every exportable type. foreach ($Type in $typestoExport) { $TypeAcceleratorsClass::Add($Type.FullName, $Type) } # Remove type accelerators when the module is removed. $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { foreach ($Type in $typestoExport) { $TypeAcceleratorsClass::Remove($Type.FullName) } }.GetNewClosure(); $scripts = @(); $Public = Get-ChildItem "$PSScriptRoot/Public" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue $scripts += Get-ChildItem "$PSScriptRoot/Private" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue $scripts += $Public foreach ($file in $scripts) { Try { if ([string]::IsNullOrWhiteSpace($file.fullname)) { continue } . "$($file.fullname)" } Catch { Write-Warning "Failed to import function $($file.BaseName): $_" $host.UI.WriteErrorLine($_) } } $Param = @{ Function = $Public.BaseName Cmdlet = '*' Alias = '*' Verbose = $false } Export-ModuleMember @Param |