How can I convert JSON-like string to PowerShell object? - powershell

A Microsoft utility returns strings in the following format:
"Author: First.Last; Name: RootConfiguration; Version: 2.0.0; GenerationDate: 06/01/2022 13:18:10; GenerationHost: Server;"
I would like to convert those strings into simple objects. If this were true JSON, I'd just use ConvertFrom-JSON. To reinvent the wheel as little as possible, what's the most straightforward way to convert that into an object (with keys Author, Name, Version, GenerationDate, GenerationHost, with the obvious values. It's fine if the values are all treated as dumb strings.
If "you just have to grind it out by tokenizing the string bite by bite" is the answer, I can do that, but it seems there should be a simpler way, like if I could tell ConvertFrom-JSON (or even ConvertFrom-String!) "Do your thing, but process the semicolons as newlines, accept spaces on the right hand side, etc."

A solution that combines manual parsing with ConvertFrom-StringData, but note that input order of the entries isn't preserved, given that the latter returns a [hashtable] instance with inherently unordered entries:
# Sampe input string.
$str = 'Author: First.Last; Name: RootConfiguration; Version: 2.0.0; GenerationDate: 06/01/2022 13:18:10; GenerationHost: Server;'
# Replace ":" with "=", split into individual lines, so
# that ConvertFrom-StringData recognizes the format.
$str -replace ': ', '=' -replace '; ?', "`n" | ConvertFrom-StringData
# Note: The above outputs a [hashtable].
# You could cast it to [pscustomobject], as shown below,
# but the input order of entries is lost either way.
As zett42 points out, if the values (as opposed to the keys) in the input string contained \ chars., they'd need to be doubled in order to be retained as such - see his comment below.
A solution with manual parsing only:
# Sampe input string.
$str = 'Author: First.Last; Name: RootConfiguration; Version: 2.0.0; GenerationDate: 06/01/2022 13:18:10; GenerationHost: Server;'
# Initialize an ordered hashtable (dictionary)
$dict = [ordered] #{}
# Split the string by ";", then each entry into key and value by ":".
$str -split '; ?' |
ForEach-Object { $key, $value = $_ -split ': ', 2; $dict[$key] = $value }
# Convert the ordered hashtable (dictionary) to a custom object.
[pscustomobject] $dict

I usually don't answer questions that don't have a coding attempt but, figured this might help others. Given that the delimiter is a semicolon, I was thinking of converting to CSV first but, would have to worry about the header next. So, instead of converting to CSV, we can use the delimiter to split the results at that and process the values one at a time:
"Author: First.Last; Name: RootConfiguration; Version: 2.0.0; GenerationDate: 06/01/2022 13:18:10; GenerationHost: Server;".Split(";").Trim() |
ForEach-Object -Process {
$header,$value = $_ -split ":",2
New-Object -TypeName PSCustomObject #{
$header = $value
}
} | ConvertTo-Json
To make this work we need to split at just the first colon (:) leaving the rest intact; incase there's others in the value like you see in the GenerationDate property.
This was achieved using $_ -split ":",2.
Finally, the rest was just left to assign the header to and value to a PSCustomObject and conver the results to JSON using ConvertTo-Json.
Note: I am restricted to a "strict language mode" on my work system so it's best to use the type-accelarator of [PSCustomObject]#{..} to create the object, rather than New-Object.

Complementing the existing helpful answers, here is another one using the Regex.Matches() function:
$testInput = 'Author: First.Last; Name: RootConfiguration; Version: 2.0.0; GenerationDate: 06/01/2022 13:18:10; GenerationHost: Server;'
# Create a temporary, ordered Hashtable to collect keys and values in the original order.
$ht = [ordered] #{}
# Use a regular expression to find all key/value pairs.
foreach( $match in [regex]::Matches( $testInput, '\s*([^:]+):\s*([^;]+);') ) {
# Enter a key/value pair into the Hashtable
$ht[ $match.Groups[1] ] = $match.Groups[2]
}
# Convert the temporary Hashtable to PSCustomObject.
[PSCustomObject] $ht
Output:
Author : First.Last
Name : RootConfiguration
Version : 2.0.0
GenerationDate : 06/01/2022 13:18:10
GenerationHost : Server
The RegEx pattern consists of two capturing groups ( ), where the first one captures a key and the second one captures a value.
For a detailed explanation see regex101, where you can also play around with the pattern.

Related

Powershell passing multiple parameters from one script to another [duplicate]

I've seen the # symbol used in PowerShell to initialise arrays.
What exactly does the # symbol denote and where can I read more about it?
In PowerShell V2, # is also the Splat operator.
PS> # First use it to create a hashtable of parameters:
PS> $params = #{path = "c:\temp"; Recurse= $true}
PS> # Then use it to SPLAT the parameters - which is to say to expand a hash table
PS> # into a set of command line parameters.
PS> dir #params
PS> # That was the equivalent of:
PS> dir -Path c:\temp -Recurse:$true
PowerShell will actually treat any comma-separated list as an array:
"server1","server2"
So the # is optional in those cases. However, for associative arrays, the # is required:
#{"Key"="Value";"Key2"="Value2"}
Officially, # is the "array operator." You can read more about it in the documentation that installed along with PowerShell, or in a book like "Windows PowerShell: TFM," which I co-authored.
While the above responses provide most of the answer it is useful--even this late to the question--to provide the full answer, to wit:
Array sub-expression (see about_arrays)
Forces the value to be an array, even if a singleton or a null, e.g. $a = #(ps | where name -like 'foo')
Hash initializer (see about_hash_tables)
Initializes a hash table with key-value pairs, e.g.
$HashArguments = #{ Path = "test.txt"; Destination = "test2.txt"; WhatIf = $true }
Splatting (see about_splatting)
Let's you invoke a cmdlet with parameters from an array or a hash-table rather than the more customary individually enumerated parameters, e.g. using the hash table just above, Copy-Item #HashArguments
Here strings (see about_quoting_rules)
Let's you create strings with easily embedded quotes, typically used for multi-line strings, e.g.:
$data = #"
line one
line two
something "quoted" here
"#
Because this type of question (what does 'x' notation mean in PowerShell?) is so common here on StackOverflow as well as in many reader comments, I put together a lexicon of PowerShell punctuation, just published on Simple-Talk.com. Read all about # as well as % and # and $_ and ? and more at The Complete Guide to PowerShell Punctuation. Attached to the article is this wallchart that gives you everything on a single sheet:
You can also wrap the output of a cmdlet (or pipeline) in #() to ensure that what you get back is an array rather than a single item.
For instance, dir usually returns a list, but depending on the options, it might return a single object. If you are planning on iterating through the results with a foreach-object, you need to make sure you get a list back. Here's a contrived example:
$results = #( dir c:\autoexec.bat)
One more thing... an empty array (like to initialize a variable) is denoted #().
The Splatting Operator
To create an array, we create a variable and assign the array. Arrays are noted by the "#" symbol. Let's take the discussion above and use an array to connect to multiple remote computers:
$strComputers = #("Server1", "Server2", "Server3")<enter>
They are used for arrays and hashes.
PowerShell Tutorial 7: Accumulate, Recall, and Modify Data
Array Literals In PowerShell
I hope this helps to understand it a bit better.
You can store "values" within a key and return that value to do something.
In this case I have just provided #{a="";b="";c="";} and if not in the options i.e "keys" (a, b or c) then don't return a value
$array = #{
a = "test1";
b = "test2";
c = "test3"
}
foreach($elem in $array.GetEnumerator()){
if ($elem.key -eq "a"){
$key = $elem.key
$value = $elem.value
}
elseif ($elem.key -eq "b"){
$key = $elem.key
$value = $elem.value
}
elseif ($elem.key -eq "c"){
$key = $elem.key
$value = $elem.value
}
else{
Write-Host "No other value"
}
Write-Host "Key: " $key "Value: " $value
}

Hashset to keys and values

I have a problem in my script:
here I set a var remotefilehash:
$remotefilehash = $remotefiles -replace '^[a-f0-9]{32}( )', '$0= ' | ConvertFrom-StringData
this creates a hashtable
I then compare those hash's to what I have locally in a hashset
# Create a hash set from the local hashes.
$localHashSet = [System.Collections.Generic.HashSet[string]] $localmd5
# Loop over all remote hashes to find those not among the local hashes.
$diffmd5 = $remotefilehash.Keys.Where({ -not $localHashSet.Contains($_) })
this gets me the hashkeys but I subsequently also need to get the hash values for those keys that are found in the above where ...
this creates a hashtable
Actually, it creates an array of hashtables, because ConvertFrom-StringData turns each pipeline input object into a separate hashtable.
To create a single hashtable, join the input lines to form a single string first - note how -join "`n" is applied to the result of the -replace operation in order to form a single, multi-line string:
$remotefilehash =
($remotefiles -replace '^[a-f0-9]{32}( )', '$0= ' -join "`n") |
ConvertFrom-StringData
To enumerate the key-value pairs instead of just the keys (.Keys) of a hashtable(-like) object, you need to use its .GetEnumerator() method:
$diffmd5 =
$remotefilehash.GetEnumerator().Where({ -not $localHashSet.Contains($_.Key) })
The reason that an enumerator must be requested explicitly is that PowerShell by default considers a hashtable/dictionary a single object that is passed as a whole through the pipeline / passed to enumeration methods such as the .Where() and .ForEach() array methods.
Note that the output of the above command is not itself a hashtable, but a regular, array-like collection (of type [System.Collections.ObjectModel.Collection[psobject]) whose elements happen to be key-value pair objects.
As such, this output is enumerated in the pipeline.

Get CN value from ADUser DistinguishedName

I have a PS script that checks some custom user's properties in Active Directory.
One of the properties is "Manager".
$data = Get-ADUser $user -Properties * | Select-Object DisplayName, LockedOut, Enabled, LastLogonDate, PasswordExpired, EmailAddress, Company, Title, Manager, Office
Write-Host "9." $user "manager is" $data.manager -ForegroundColor Green
When I run the script I've got:
User's manager is CN=cool.boss,OU=Users,OU=SO,OU=PL,OU=RET,OU=HBG,DC=domain,DC=com
The problem is that text "OU=SO,OU=PL,OU=RET,OU=HBG,DC=domain,DC=com" will be different for some users
How can I modify output and remove everything except "cool.boss"?
Thank you in advance
This should be a more or less safe and still easy way to parse it:
($data.manager -split "," | ConvertFrom-StringData).CN
To complement the helpful answers here with PowerShell-idiomatic regex solutions:
Using -split, the regex-based string splitting operator:
$dn = 'CN=cool.boss,OU=Users,OU=SO,OU=PL,OU=RET,OU=HBG,DC=domain,DC=com'
($dn -split '(?:^|,)CN=|,')[1] # -> 'cool.boss'
Using -replace, the regex-based string substitution operator:
$dn = 'CN=cool.boss,OU=Users,OU=SO,OU=PL,OU=RET,OU=HBG,DC=domain,DC=com'
$dn -replace '(?:^|,)CN=([^,]+).*', '$1' # -> 'cool.boss'
Note:
The above solutions do not rely on a specific order of the name-value pairs (RDNs) in the input (that is, a CN entry needn't be the first one), but they do extract only the first CN entry's value, should multiple ones be present, and they do assume that (at least) one is present.
In principle, DNs (Distinguished Names), of which the input string is an example, can have , characters embedded in the values of the name-value pairs that make up a DN, escaped as \, (or, in hex notation, \2C); e.g., "CN=boss\, cool,OU=Users,..."
A truly robust solution would have to take that into account, and would ideally also unescape the resulting value; none of the existing answers do that as of this writing; see below.
Robustly parsing an LDAP/AD DN (Distinguished Name):
The following Split-DN function:
handles escaped, embedded , chars., as well as other escape sequences, correctly
unescapes the values, which includes not just removing syntactic \, but also converting escape sequences in the form \<hh>, where hh is a two-digit hex. number representing a character's code point, to the actual character they represent (e.g, \3C, is converted to a < character).
outputs an ordered hashtable whose keys are the name components (e.g., CN, OU), with the values for names that occur multiple times - such as OU - represented as an array.
Example call:
PS> Split-DN 'CN=I \3C3 Huckabees\, I do,OU=Users,OU=SO,OU=PL,OU=RET,OU=HBG,DC=domain,DC=com'
Name Value
---- -----
CN I <3 Huckabees, I do
OU {Users, SO, PL, RET…}
DC {domain, com}
Note how escape sequence \3C was converted to <, the character it represents, and how \, was recognized as an , embedded in the CN value.
Since the input string contained multiple OU and DC name-value pairs (so-called RDNs, relative distinguished names), their corresponding hashtable entries became arrays of values (signified in the truncated-for-display-only output with { ... }, with , separating the elements).
Function Split-DN's source code:
Note: For brevity, error handling and validation are omitted.
function Split-DN {
param(
[Parameter(Mandatory)]
[string] $DN
)
# Initialize the (ordered) output hashtable.
$oht = [ordered] #{}
# Split into name-value pairs, while correctly recognizing escaped, embedded
# commas.
$nameValuePairs = $DN -split '(?<=(?:^|[^\\])(?:\\\\)*),'
$nameValuePairs.ForEach({
# Split into name and value.
# Note: Names aren't permitted to contain escaped chars.
$name, $value = ($_ -split '=', 2).Trim()
# Unescape the value, if necessary.
if ($value -and $value.Contains('\')) {
$value = [regex]::Replace($value, '(?i)\\(?:[0-9a-f]){2}|\\.', {
$char = $args[0].ToString().Substring(1)
if ($char.Length -eq 1) { # A \<literal-char> sequence.
$char # Output the character itself, without the preceding "\"
}
else { # A \<hh> escape sequence, conver the hex. code point to a char.
[char] [uint16]::Parse($char, 'AllowHexSpecifier')
}
})
}
# Add an entry to the output hashtable. If one already exists for the name,
# convert the existing value to an array, if necessary, and append the new value.
if ($existingEntry = $oht[$name]) {
$oht[$name] = ([array] $existingEntry) + $value
}
else {
$oht[$name] = $value
}
})
# Output the hashtable.
$oht
}
You can use the .split() method to get what you want.
$DN = "CN=cool.boss,OU=Users,OU=SO,OU=PL,OU=RET,OU=HBG,DC=domain,DC =com"
$DN.Split(',').Split('=')[1]
What i'd recommend, is throwing it into another Get-ADUser to get the displayname for neater output(:
you could use regex for that:
$s = "CN=cool.boss,OU=Users,OU=SO,OU=PL,OU=RET,OU=HBG,DC=domain,DC =com"
$pattern = [regex]"CN=.*?OU"
$r = $pattern.Replace($s, "CN=OU")
$r

Question regarding incrementing a string value in a text file using Powershell

Just beginning with Powershell. I have a text file that contains the string "CloseYear/2019" and looking for a way to increment the "2019" to "2020". Any advice would be appreciated. Thank you.
If the question is how to update text within a file, you can do the following, which will replace specified text with more specified text. The file (t.txt) is read with Get-Content, the targeted text is updated with the String class Replace method, and the file is rewritten using Set-Content.
(Get-Content t.txt).Replace('CloseYear/2019','CloseYear/2020') | Set-Content t.txt
Additional Considerations:
General incrementing would require a object type that supports incrementing. You can isolate the numeric data using -split, increment it, and create a new, joined string. This solution assumes working with 32-bit integers but can be updated to other numeric types.
$str = 'CloseYear/2019'
-join ($str -split "(\d+)" | Foreach-Object {
if ($_ -as [int]) {
[int]$_ + 1
}
else {
$_
}
})
Putting it all together, the following would result in incrementing all complete numbers (123 as opposed to 1 and 2 and 3 individually) in a text file. Again, this can be tailored to target more specific numbers.
$contents = Get-Content t.txt -Raw # Raw to prevent an array output
-join ($contents -split "(\d+)" | Foreach-Object {
if ($_ -as [int]) {
[int]$_ + 1
}
else {
$_
}
}) | Set-Content t.txt
Explanation:
-split uses regex matching to split on the matched result resulting in an array. By default, -split removes the matched text. Creating a capture group using (), ensures the matched text displays as is and is not removed. \d+ is a regex mechanism matching a digit (\d) one or more (+) successive times.
Using the -as operator, we can test that each item in the split array can be cast to [int]. If successful, the if statement will evaluate to true, the text will be cast to [int], and the integer will be incremented by 1. If the -as operator is not successful, the pipeline object will remain as a string and just be output.
The -join operator just joins the resulting array (from the Foreach-Object) into a single string.
AdminOfThings' answer is very detailed and the correct answer.
I wanted to provide another answer for options.
Depending on what your end goal is, you might need to convert the date to a datetime object for future use.
Example:
$yearString = 'CloseYear/2019'
#convert to datetime
[datetime]$dateConvert = [datetime]::new((($yearString -split "/")[-1]),1,1)
#add year
$yearAdded = $dateConvert.AddYears(1)
#if you want to display "CloseYear" with the new date and write-host
$out = "CloseYear/{0}" -f $yearAdded.Year
Write-Host $out
This approach would allow you to use $dateConvert and $yearAdded as a datetime allowing you to accurately manipulate dates and cultures, for example.

Converting string with object layout to object

Some objects has been saved to a txt.file
looking like this:
#{flightNumber=01; flightDate=2010-01-10; flightIdentification=201001}
#{flightNumber=01; flightDate=2010-01-10; flightIdentification=201002}
and I'm trying to read them in another program and convert them back into objects. What bothers me is that it understands each of the "objects" as a string and I have been unable to cast it into an object.
$list = Get-Content -Path 'C:\Users\XXXXX\Downloads\TemplateObject.txt'
foreach (#object in $list) {
Write-Host $object.flightNumber
}
From what I've shown, I would expect to see 2 different objects with the variables flightNumber, flightDate and flightIdentification
I've tried piping it by using ConvertFrom-StringData
I've tried casting to an object
I expect 2 separate objects containing 3 variables in each.
Don't pipe objects directly to files!
As has been pointed out, take advantage of built-in options for serialization to disk, like ConvertTo-Csv/Export-Csv for flat objects, ConvertTo-Json or Export-Clixml for more complex objects.
As a one-off thing, if you need to recover and re-encode this data, you could use the regex -replace operator to add quotes around the values, at which point the parser should accept them as hashtable entries and you can cast it to an object:
$string = '#{flightNumber=01; flightDate=2010-01-10; flightIdentification=201001}'
# Place double-quotes around anything found between a `=` and `;` or `}`
$quotedString = $string -replace '(?<=\=)([^=;}]+)(?=\s*(?:;|}))', '"$1"'
# Parse the resulting string as if it was PowerShell code
$errors = #()
$objectAST = [System.Management.Automation.Language.Parser]::ParseInput($quotedString, [ref]$null,[ref]$errors)
$objects = if(-not $errors){
# This is pretty dangerous, you should NEVER do this in a production script
$objectAST.GetScriptBlock.Invoke() |ForEach-Object {
[pscustomobject]$_
}
}
# This variable now contains the re-animated objects
$objects
You can convert a string to a hashtable using convertfrom-stringdata after some manipulation:
$a = '#{flightNumber=01; flightDate=2010-01-10; flightIdentification=201001}'
$a = $a -replace '#{' -replace '}' -replace ';',"`n" | ConvertFrom-StringData
[pscustomobject]$a
flightNumber flightIdentification flightDate
------------ -------------------- ----------
01 201001 2010-01-10