Skip to content

Commit ff89f56

Browse files
authored
Merge pull request #5 from msftrncs/reorganize
Reorganize and correct issues; array support from pipeline, support null values, correct empty property keys, fix empty array from pipeline null-valued expression error.
2 parents 5efb09f + 64f7a4c commit ff89f56

2 files changed

Lines changed: 131 additions & 102 deletions

File tree

ConvertTo-CSON.ps1

Lines changed: 130 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@ function ConvertTo-Cson {
3131
[AllowEmptyCollection()]
3232
[AllowEmptyString()]
3333
[AllowNull()]
34-
[object]$InputObject,
34+
[object] $InputObject,
3535

3636
[PSDefaultValue(Help = 'Tab')]
37-
[string]$Indent = "`t",
37+
[string] $Indent = "`t",
3838

3939
[ValidateRange(1, 100)]
40-
[int32]$Depth = 2,
40+
[int32] $Depth = 2,
4141

42-
[switch]$EnumsAsStrings
42+
[switch] $EnumsAsStrings
4343
)
4444
# write out a CSON document from the object supplied
4545
# $InputObject is an object, who's properties will be output as CSON. Hash tables are supported.
@@ -48,24 +48,20 @@ function ConvertTo-Cson {
4848

4949
# define a match evaluator for escaping characters
5050
$escape_replacer = {
51-
switch ($_) {
52-
{ $_.Groups[1].Success } {
53-
# group 1, control characters
54-
switch ($_.Value[0]) {
55-
<# appearing in order of expected frequency, from most frequent to least frequent #>
56-
([char]10) { '\n'; continue } # new line
57-
([char]9) { '\t'; continue } # tab
58-
([char]13) { '\r'; continue } # caridge return
59-
([char]12) { '\f'; continue } # new form
60-
([char]8) { '\b'; continue } # bell
61-
default { '\u{0:X4}' -f [int16]$_ } # unicode escape all others
62-
}
63-
continue
64-
}
65-
{ $_.Groups[2].Success } {
66-
# group 2, items that need `\` escape
67-
"\$($_.Value)"
51+
if ($_.Groups[1].Success) {
52+
# group 1, control characters
53+
switch ($_.Value[0]) {
54+
<# appearing in order of expected frequency, from most frequent to least frequent #>
55+
([char]10) { '\n'; continue } # new line
56+
([char]9) { '\t'; continue } # tab
57+
([char]13) { '\r'; continue } # caridge return
58+
([char]12) { '\f'; continue } # new form
59+
([char]8) { '\b'; continue } # bell
60+
default { '\u{0:X4}' -f [int16]$_ } # unicode escape all others
6861
}
62+
} elseif ($_.Groups[2].Success) {
63+
# group 2, items that need `\` escape
64+
"\$($_.Value)"
6965
}
7066
}
7167

@@ -76,105 +72,138 @@ function ConvertTo-Cson {
7672
"""$($_ -replace '([\x00-\x1F\x85\u2028\u2029])|([\\"]|#\{)', $escape_replacer)"""
7773
}
7874

79-
function writeProperty ([string]$name, $item, [string]$indention, [int32]$level) {
80-
# writing the property may require recursively breaking down the objects based on their type
81-
# name of the property is optional, but that is only intended for the first property object
75+
function writeObject ($item) {
8276

83-
function writeValue ($item, [string]$indention) {
84-
# write a property value
77+
function writeProperty ([string] $name, $value) {
78+
# write a property name and its value, which may require recursing back to writeObject
8579
"$indention$(
86-
if (($item -is [string]) -or ($item -is [char]) -or (($item -is [enum]) -and $EnumsAsStrings) -or ($level -ge $Depth)) {
87-
# handle strings or characters, or objects exceeding the max depth
88-
"$item" | writeStringValue
89-
}
90-
elseif ($item -is [boolean]) {
91-
# handle boolean type
92-
if ($item) {
93-
'true'
94-
}
95-
else {
96-
'false'
97-
}
98-
}
99-
elseif ($item -is [datetime]) {
100-
# specifically format date/time to ISO 8601
101-
$item.ToString('o') | writeStringValue
102-
}
103-
elseif ($item -isnot [enum]) {
104-
# assuming a [valuetype] that doesn't need special treatment
105-
$item
106-
}
107-
else {
108-
# specifically out the enum value
109-
$item.value__
80+
# if a property name is not all simple characters or starts with numeric digit, it must be quoted and escaped
81+
if (-not $name -or $name -match '[^\p{L}\d_]|^\d') {
82+
# property name requires escaping
83+
$name | writeStringValue
84+
}
85+
else {
86+
$name
87+
}
88+
):$(
89+
if (($level -gt $Depth) -or ($null -eq $value) -or ($value -is [ValueType]) -or ($value -is [string])) {
90+
" $(, $value | writeValue)" # comma forces $value to be treated as a whole object instead of being enumerated as subitems
91+
}
92+
elseif ($value -is [Collections.IList]) {
93+
" [$( # add array start token if value is an array
94+
if ($value.Count -eq 0) {
95+
']' # add array end token if value is an empty array
96+
}
97+
)"
98+
}
99+
)"
100+
if ($level -le $Depth) {
101+
# if exceeded Depth, value already written above
102+
if (($value -is [Collections.IList]) -and ($value.Count -ne 0)) {
103+
# handle nested non-empty arrays specially due to already emitted array start token
104+
$level++ # level increases for arrays or objects
105+
$value | writeArray # write the nested array
106+
"$indention]" # nested array end token
107+
} elseif ($value -and ($value -isnot [ValueType]) -and ($value -isnot [string])) {
108+
$indention = "$indention$Indent"
109+
writeObject $value # recurse the element to writeObject
110110
}
111-
)"
111+
}
112112
}
113113

114-
# write out key name, if one was supplied from the parent object
115-
if ($name) {
116-
"$indention$(
117-
# if a property name is not all simple characters or start with numeric digit, it must be quoted and escaped
118-
if ($name -match '[^\p{L}\d_]|^\d') {
119-
# property name requires escaping
120-
$name | writeStringValue
121-
}
122-
else {
123-
$name
124-
}
125-
):$(
126-
if (($item -is [array]) -and ($level -lt $Depth)) {
127-
' [' # add array start token if property is an array
128-
}
129-
elseif (($item -is [ValueType]) -or ($item -is [string]) -or ($level -ge $Depth)) {
130-
" $(writeValue $item '')"
114+
filter writeValue {
115+
# write a object property or array element simple value
116+
if ($null -eq $_) {
117+
'null'
118+
} elseif (($_ -is [char]) -or ($EnumsAsStrings -and ($_ -is [enum])) -or ($_ -isnot [ValueType])) {
119+
# handle strings or characters, or objects exceeding the max depth
120+
"$_" | writeStringValue
121+
} elseif ($_ -is [boolean]) {
122+
# handle boolean type
123+
if ($_) {
124+
'true'
125+
} else {
126+
'false'
131127
}
132-
)"
133-
}
134-
else {
135-
if (($item -is [array]) -and ($level -lt $Depth)) {
136-
"$indention[" # add array start token if property is an array
137-
}
138-
elseif (($item -is [valuetype]) -or ($item -is [string]) -or ($level -ge $Depth)) {
139-
writeValue $item "$indention"
128+
} elseif ($_ -is [datetime]) {
129+
# specifically format date/time to ISO 8601
130+
$_.ToString('o') | writeStringValue
131+
} elseif ($_ -isnot [enum]) {
132+
# assuming a [valuetype] that doesn't need special treatment
133+
$_
134+
} else {
135+
# specifically out the enum value
136+
$_.value__
140137
}
141138
}
142139

143-
if ($level -lt $Depth) {
144-
if ($item -is [array]) {
145-
# handle arrays, iterate through the items in the array
146-
foreach ($subitem in $item) {
147-
if (($subitem -is [valuetype]) -or ($subitem -is [string])) {
148-
writeValue $subitem "$indention$Indent"
149-
}
150-
elseif ($subitem -is [array]) {
151-
writeProperty '' $subitem "$indention$Indent" ($level + 1)
152-
}
153-
else {
154-
"$indention$indent{"
155-
writeProperty '' $subitem "$indention$Indent" ($level + 1)
156-
"$indention$Indent}"
140+
filter writeArray {
141+
begin {
142+
$indentionArray = "$indention$Indent"
143+
}
144+
process {
145+
# if depth not exceeded, check for a nested object
146+
if (($level -le $Depth) -and $_ -and ($_ -isnot [ValueType]) -and ($_ -isnot [string]) -and ($_ -isnot [Collections.IList])) {
147+
# an object is nested within the array element
148+
if ($(if ($_ -is [Collections.IDictionary]) { $_.get_Keys().Count } else { @($_.psobject.get_Properties()).Count } ) -gt 0) {
149+
"$indentionArray{" # object start token
150+
$indention = "$indentionArray$Indent"
151+
writeObject $_ # recurse the object to writeObject
152+
"$indentionArray}" # object end token
153+
} else {
154+
"$indentionArray{}" # empty object
157155
}
156+
} else {
157+
# for all other cases recurse the value back to writeObject
158+
$indention = $indentionArray
159+
writeObject $_
158160
}
159-
"$indention]"
160161
}
161-
elseif ($item -isnot [valuetype] -and $item -isnot [string]) {
162-
# handle objects by recursing with writeProperty
163-
if ($item.GetType().Name -in 'HashTable', 'OrderedDictionary') {
162+
}
163+
164+
if (($level -gt $Depth) -or ($null -eq $item) -or ($item -is [ValueType]) -or ($item -is [string])) {
165+
"$indention$(, $item | writeValue)" # comma forces $item to be treated as a whole object instead of being enumerated as subitems
166+
} else {
167+
$level++ # level increases for arrays or objects
168+
if ($item -is [Collections.IList]) {
169+
if ($item.Count -ne 0) {
170+
# handle non-empty arrays, iterate through the items in the array
171+
"$indention["
172+
$item | writeArray
173+
"$indention]"
174+
} else {
175+
"$indention[]" # indicate empty array
176+
}
177+
} elseif ($(if($item -is [Collections.IDictionary]) { $item.get_Keys().Count } else { @($item.psobject.get_Properties()).Count } ) -gt 0) {
178+
if ($item -is [Collections.IDictionary]) {
164179
# process what we assume is a hashtable object
165-
foreach ($hash in $item.GetEnumerator()) {
166-
writeProperty $hash.Key $hash.Value $(if ($level -ge 0) { "$indention$Indent" } else { $indention }) ($level + 1)
180+
foreach ($key in $item.get_Keys()) {
181+
# handle objects by recursing with writeProperty
182+
writeProperty $key $item[$key]
167183
}
168184
} else {
169-
# iterate through the items (force to a PSCustomObject for consistency)
170-
foreach ($property in ([PSCustomObject]$item).psobject.Properties) {
171-
writeProperty $property.Name $property.Value $(if ($level -ge 0) { "$indention$Indent" } else { $indention }) ($level + 1)
185+
# iterate through the objects properties
186+
foreach ($property in $item.psobject.Properties) {
187+
# handle objects by recursing with writeProperty
188+
writeProperty $property.Name $property.Value
172189
}
173190
}
191+
} else {
192+
"$indention{}" # indicate empty object
174193
}
175194
}
176195
}
177196

178-
# start writing the property list, the property list should be an object, has no name, and starts at base level
179-
(writeProperty '' $InputObject '' (-1)) -join $(if (-not $IsCoreCLR -or $IsWindows) { "`r`n" } else { "`n" })
197+
# start writing the input object starting with no indent at level 0
198+
[string] $indention = ''
199+
[int32] $level = 0
200+
(writeObject $(
201+
# we need to determine where our input is coming from, pipeline or parameter argument.
202+
if ($input -is [array] -and $input.Length -ne 0) {
203+
$input # input from pipeline
204+
} else {
205+
$InputObject # input from parameter argument
206+
}
207+
)
208+
) -join "$(if (-not $IsCoreCLR -or $IsWindows) { "`r" })`n"
180209
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ $grammar_cson_doc = Get-Content powershell.tmlanguage.json | ConvertFrom-Json |
2727

2828
#### InputObject
2929

30-
The PowerShell object which possesses the items to be output in CSON notation. This can be any object that can be represented as a PSCustomObject. This parameter may be received from the pipeline.
30+
The PowerShell object which possesses the items to be output in CSON notation. This can be any object that can be represented as a PSCustomObject. Hashtables with empty keys are also accepted. This parameter may be received from the pipeline.
3131

3232
#### Indent
3333

0 commit comments

Comments
 (0)