Translating PS script without psd1 file - powershell

I'm a casual code writer. I'm in the self-assigned thread to create a script to install a software only available as an exe file for Windows and customize a bit some defaults stored in an xml file. This is a step/step process for me, each one being through big reading on the net and many trials. Being ATM at 5% estimated of the travel an (other :-( ) idea hit my mind : making the job also for English speaking people in addition to French, and publish the script so that any other can easily add their own language strings for messages. I discovered the MS scripts internationalization way that uses DATA section(s), psd1 files and some related commands.
My goal is to supply a single ps1 file, dot. My latest idea, based on what I recently read, was to have all the strings in the ps1 file, then based on $PSUIlocale export/create a temporary .\LG-lg\SetupThisSoft.psd1 I could Import-LocalizedData... what I guess is stupid (why export when we have the strings within the file?).
Do you have any idea to reach the goal? I now have some (better?) idea, using an array of yet-translated-locales, e.g. $AvailLng = "fr-FR","en-EN" then having one DATA section for each $Item I could ForEach test against $PSUILocale, but I have no idea how to "point"/"enable" the good DATA section. "en-EN" would be the last $Item as a default/fallback when $PSUILocale doesn't match any previous $Item...
Thanks for ideas

Store your Data sections in a hashtable and use the locale identifier (fr-FR, en-US etc.) as the key:
# Store messages for multiple languages in $localizedMessages
$localizedMessages = #{
'en-US' = Data {
ConvertFrom-StringData #'
Error = an error occurred
Success = something wonderful happened
'#
}
'fr-FR' = Data {
ConvertFrom-StringData #'
Error = une erreur est survenue
Success = quelque chose de merveilleux est arrivé
'#
}
}
$defaultLanguage = 'fr-FR'
# Assign the appropriate language version to the variable that holds the messages for later use
$Messages = if($localizedMessages.ContainsKey($PSUICulture)){
$localizedMessages[$PSUICulture]
} else {
# If we don't have localized messages for the current culture, we fall back to our default language
$localizedMessages[$defaultLanguage]
}
# ...
# This will now throw a different error message based on the current UI culture
Write-Error $Messages['Error']

Related

Powershell circular dependencies

I have a scenario where I THINK a circular dependency is the right answer. In my actual code I have a class that contains some data that is used for token replacements. That class contains other classes for the data, and contains a method that does the lookup of token values and returns the value. However, those dependent classes need to be validated, and some of those values are dependent on lookups. So, I have mocked up this code to validate the approach.
class Tokens {
[Collections.Specialized.OrderedDictionary]$primaryData
[Collections.Specialized.OrderedDictionary]$secondaryData
Tokens ([Secondary]$seconday) {
$this.primaryData = [Ordered]#{
'1' = 'One'
'2' = 'Two'
'3' = 'Three'
'4' = 'Four'
'5' = 'Five'
}
$this.secondaryData = $seconday.secondaryData
}
[String] GetToken ([String]$library, [String]$item) {
return $this.$library.$item
}
[Void] SetToken ([String]$library, [String]$item, [String]$value) {
$this.$library.$item = $value
}
[String] ToString () {
[Collections.Generic.List[String]]$toString = [Collections.Generic.List[String]]::new()
foreach ($key in $this.primaryData.Keys) {
$toString.Add("$key : $($this.primaryData.$key)")
}
foreach ($key in $this.secondaryData.Keys) {
$toString.Add("$key : $($this.secondaryData.$key)")
}
return [string]::Join("`n", $toString)
}
}
class Secondary {
[Collections.Specialized.OrderedDictionary]$secondaryData
Secondary () {
$this.secondaryData = [Ordered]#{
'A' = 'a'
'B' = 'b'
'C' = 'c'
'D' = 'd'
'E' = 'e'
}
}
[Void] Update ([Tokens]$tokensReference) {
$tokensReference.SetToken('secondaryData', 'A', 'A')
$tokensReference.SetToken('secondaryData', 'B', "$($tokensReference.GetToken('secondaryData', 'A')) and $($tokensReference.GetToken('secondaryData', 'B'))")
}
[String] ToString () {
[Collections.Generic.List[String]]$toString = [Collections.Generic.List[String]]::new()
foreach ($key in $this.secondaryData.Keys) {
$toString.Add("$key : $($this.secondaryData.$key)")
}
return [string]::Join("`n", $toString)
}
}
CLS
$secondary = [Secondary]::new()
$tokens = [Tokens]::new($secondary)
$secondary.Update($tokens)
Write-Host "$($tokens.ToString())"
This is working, exactly as expected. however, the idea of circular dependency injection has my hair standing on end. Like, it could be a real problem, or at least is a code smell. So, my question is, am I on the right track, or is this a dead end and I just haven't found that end yet? Given that PowerShell isn't "fully" object oriented yet, I imagine there could be some uniquely PS related issues, and everything I have found searching for Powershell Circular Dependency talks about removing them. So far I haven't found anything about when it is appropriate and how to do it well.
And, assuming it is a valid approach, is there anything obvious in this simplified implementation that could lead to problems or could be better done some other way?
EDIT: OK, so perhaps I am going to refine my vocabulary a bit too. I was thinking circular dependency since Secondary is a dependency, or member perhaps, of Tokens, and then I update Secondary from inside Secondary, while referencing data from Secondary, via the method in Tokens.
To clarify (I hope) the ultimate goal, these lookups are for program specific data, which I have in XML files. So, for example the data file for Autodesk Revit 2021 would include these three items
<item id="GUID" type="literal">{7346B4A0-2100-0510-0000-705C0D862004}</item>
<item id="installDataKey" type="registryKeyPath">HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\[Revit 2021~GUID]</item>
<item id="displayName" type="registryPropertyValue" path="installDataKey">Revit 2021</item>
In actual use I would want to get the DisplayName property found in the key defined in <item id="installDataKey">, and if the value matches the value in <item id="displayName"> then I might also look for the value of the DisplayVersion property in the same key, and make decisions based on that. But because there are new versions every year, and 20+ different software packages to address, managing these data files is a pain. So I want to validate the data I have in the files against a machine that actually has the software installed, to be sure I have my data correct. Autodesk is famous for changing things for no good reason, and often for some very customer hostile reasons. So, things like referencing the GUID as data and reusing it as a token, i.e. the [Revit 2021~GUID] above, saves effort. So during the validation process only, I would want to set the GUID, then do the standard token replacement to convert HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\[Revit 2021~GUID] to HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{7346B4A0-2100-0510-0000-705C0D862004} and should that key actually be found, use it to validate about 20 other registry and file paths as well as the actual value of DisplayName. If everything validates I will sign the XML, and in actual use signed XML will basically treat everything as a literal and no validation is done, or rather it was prevalidated.
But before use a reference to [product~installDataKey] when the current product is Revit 2021 would resolve first to HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\[Revit 2021~GUID] then to HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{7346B4A0-2100-0510-0000-705C0D862004} at which point the code could use it as a registry path and see if Revit 2021 is in fact installed.
So, 99.9% of the time, I would instantiate Tokens with a constructor that just includes the full dataset and move on. But on the .1% of occasions where I am validating the data itself, I need to be able to read the xml, set the value for that GUID and immediately use the lookup to validate that Autodesk hasn't done something stupid like move some data out of the GUID key and into a secondary key. They have done that before.
I also might want to replace HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall with a token like [windows~uninstallKey] just to make life easier there too.
Hopefully that makes some sense. It's a mess to be sure, but anything to do with Autodesk is a mess.

O365 Powershell | Breaking up a long list into two sets of 100

I am looking to create a rule in Office 365 applied to all of the members in our org.
I would like this rule to append a warning on all incoming email from outside the organization with the same Display Names as our users.
When I attempt to apply it to all of the users in our org I get an error stating that the rule is too long.
In order to solve that I pulled a group, but I am still about 1000 characters over the limit.
I would like to make two variables, that each hold one half of the list, created by this command:
(Get-DistibutionGroupMember -Identity email#contoso.com -ResultSize Unlimited).DisplayName
I have attempted to modify the ResultSize parameter, but what I would need is result 1-100 and then 100-200 from the same list.
Another caveat to this problem is that the list cannot be static. It is something that the script will have to update every time it is run.
There is a sub-string command that you can use on a particular username that I have utilized when I made something for AD, but I am not aware of any way to break up a list like this.
If anyone has any other ways to solve this issue I would be more than open to any suggestion.
Thanks for taking the time to read this!
There are many ways of doing it. I found it very readable.
My favorite one is this one:
$ObjectList = 1..1000
$Step = 100
$counter = [pscustomobject] #{ Value = 0 }
$ObjectListSplitted = $ObjectList | Group-Object -Property { math]::Floor($counter.Value++ / $step) }
Then if you want to show the third subset just use this format :
$ObjectListSplitted[3].Group
Have a look to this solution already explained.
As a note other languages are capable of slicing an array of object with a start, stop and a step, have a look here if you're curious.

Arguments passed byReference and/or mutable constant issue

Not sure how I can do a minimal working code example here, but I will at least try to explain what is going on.
I have a utility that processes some text files to extract data, and then provides that data back in various ways depend on commend line options and some data in an XML file. There is a variety of "Processes" I could look for in the data, and I can output the final data in one or more formats (txt, csv, xml), with potentially different "Processes" being output to different file types. And since I am processing potentially hundreds of txt files, I want to multi-three this. So I have created a function, and since the mix of processes to monitor and output types to emit is the same for every txt file, which I then want to compile into a single big data structure at the end, I have created that final data structure as a hash of hashes, but empty of data. Then I use
Set-Variable resultsContainer -option:constant -value:$results
to make a constant, which is empty of data, to hand off to the function. the idea being I can hand the function an empty container plus the path of a txt file, and it can fill the container and return it, where I can in theory use it to fill the mutable data container. So I do a foreach on the txt files, and pass what SHOULD be an empty container each time, like this.
$journalResults = Invoke-PxParseJournal -journal "$source\$journal" -container $resultsContainer
However, instead of staying empty, as I would expect for a constant, I am effectively passing all the previous journals' data to each successive iteration of the function. I proved this to myself by initializing a counter to 1 and then running this loop after the Invoke-PxParseJournal call.
foreach ($process in $resultsContainer.Keys) {
foreach ($output in $resultsContainer.$process.Keys) {
foreach ($item in $resultsContainer.$process.$output) {
Write-Host "$(Split-Path $journal -leaf) $process $output $item$('!' * $count)"
}
}
}
After the first Invoke the loop produces nothing, but from there everything is appended. So I see results like this
journal.0002.txt Open_Project csv Gordon,1/20/2017 12:08:43 AM,Open an existing project,.\RFO_Benchmark - previous.rvt,0:00:22.012!!
journal.0003.txt Open_Project csv Gordon,1/20/2017 12:08:43 AM,Open an existing project,.\RFO_Benchmark - previous.rvt,0:00:22.012!!!
journal.0004.txt Open_Project csv Gordon,1/20/2017 12:08:43 AM,Open an existing project,.\RFO_Benchmark - previous.rvt,0:00:22.012!!!!
Identical repeats each time. Even odder, if I rerun the script in the console I WILL get an error saying
Set-Variable : Cannot overwrite variable resultsContainer because it is read-only or constant.
But still the results are data being appended. Now, my first thought was that because I was using the same variable name in the function as in the root script I was dealing with some scoping problem, so I changed the name of the variable in the function and gave it an alias, like this.
[Alias('container')]$parseJournal
I then populate and return the $parseJournal variable.
No change in behavior. Which now has me wondering if I just don't understand how parameters are passed. I had thought it was ByVal, but this is acting like it is ByReference, so even with the name change I am in fact just adding to the same data structure in memory each time.
Is there something obvious here that I am missing? FWIW< I am in PS 2.0 at the moment. I don't have a Win10 VM I can spin up easily at the moment to test there.

Convert BodyStream in MSMQ to text/XML via PoSH?

What I'm trying to do: PEEK multiple MSMQ messages (not RECEIVE) using PowerShell, then return the contents of BodyStream in XML format (it appears to be in Decimal) so that I can shred the XML in the message and return information from within it.
We're trying to look and see what's still in the queue, what they have in common, what the most recent values in the XML are, etc. I've gone looking which is how I've gotten this far, but I haven't even figured how to peek multiple messages yet.
Here's basic code that will PEEK the first message in the "mybundles" private queue - ideally I'd like to do many more, maybe even the entirety (though again, without pulling them off the queue)
[Reflection.Assembly]::LoadWithPartialName("System.Messaging")
$queue = new-object System.Messaging.MessageQueue "FormatName:DIRECT=OS:servername\private$\mybundles"
$queue.Formatter.TargetTypes = ,[string]
$peekmsg = $queue.Peek()
[string]$sr = $peekmsg.BodyStream.ToArray()
$sr
When I do this, it returns a string of numbers, which is a decimal representation of the text I want. How do I change that string of numbers (or straight from BodyStream to text/XML?
Looks like you do this:
$peekmsg = $queue.Peek()
$sr= $peekmsg.BodyStream.ToArray()
$enc = [System.Text.Encoding]::ASCII
$enc.GetString($sr)
I think I'd just do a foreach loop, once I figure out how to PEEK multiple.

Perl XML::SAX - character() method error

I'm new to using Perl XML::SAX and I encountered a problem with the characters event that is triggered. I'm trying to parse a very large XML file using perl.
My goal is to get the content of each tag (I do not know the tag names - given any xml file, I should be able to crack the record pattern and return every record with its data and tag like Tag:Data).
While working with small files, everything is ok. But when running on a large file, the characters{} event does partial reading of the content. There is no specific pattern in the way it cuts down the reading. Sometimes its the starting few characters of data and sometimes its last few characters and sometimes its just one letter from the actual data.
The Sax Parser is:
$myhandler = MyFilter->new();
$parser = XML::SAX::ParserFactory->parser(Handler => $myhandler);
$parser->parse_file($filename);
And, I have written my own Handler called MyFilter and overridding the character method of the parser.
sub characters {
my ($self, $element) = #_;
$globalvar = $element->{Data};
print "content is: $globalvar \n";
}
Even this print statement, reads the values partially at times.
I also tried loading the Parsesr Package before calling the $parser->parse() as:
$XML::SAX::ParserPackage = "XML::SAX::ExpatXS";
Stil doesn't work. Could anyone help me out here? Thanks in advance!
Sounds like you need XML::Filter::BufferText.
http://search.cpan.org/dist/XML-Filter-BufferText/BufferText.pm
From the description "One common cause of grief (and programmer error) is that XML parsers aren't required to provide character events in one chunk. They can, but are not forced to, and most don't. This filter does the trivial but oft-repeated task of putting all characters into a single event."
It's very easy to use once you have it installed and will solve your partial character data problem.