Bulk-Assign PowerShell Variables using Multi-Level [PSCustomObject] - powershell

TLDR
I'm trying to create a function that will take a Multi-Level [PSCustomObject], extract the Key/Value pairs (strings only), and use them to declare Individual Global Variables using Set-Variable.
Current Code
Set-Variable -Name 'NSOneDrive' -Value "D:\OneDrive - New Spectrum"
$StrykerDirs = [PSCustomObject]#{
'OneDrive' = [PSCustomObject]#{
'NSOneDrive' = "D:\OneDrive - New Spectrum"
'MyOneDrive' = "D:\OneDrive"
}
'Dev' = [PSCustomObject]#{
'DevDir' = "${NSOneDrive}\Dev"
'DevToolsDir' = [PSCustomObject]#{
'DevTools' = "${NSOneDrive}\Dev\_DevTools"
'Terminals' = [PSCustomObject]#{
'DT_Terminals' = "${NSOneDrive}\Dev\_DevTools\terminals"
'DT_PowerShell' = "${NSOneDrive}\Dev\_DevTools\terminals\PowerShell"
}
'Editors' = [PSCustomObject]#{
'DT_Editors' = "${NSOneDrive}\Dev\_DevTools\.editors"
}
}
'ProjectsDir' = [PSCustomObject]#{
'NSProjects' = "${NSOneDrive}\Projects\NewSpectrum"
'MyProjects' = "${NSOneDrive}\Projects\Personal"
}
}
}
$StrykerDirs |
ConvertTo-JSON -Depth 25 |
Tee-Object -FilePath ".\JSON\Stryker-Paths.json"
function Set-DirAliases {
[CmdletBinding()]
# I might add parameters after I know how to make the 'Process' work
Begin {
# Begin Process Block
}
Process {
ForEach ( $dir in $StrykerDirs ) {
where ( $_.GetType() -eq 'String' ) |
Set-Variable -Name "${key}" -Value "${value}"
# I know ${key} and ${value} won't work, but I'm not sure how to properly fill them
}
}
End {
# End Process Block
}
}
Goals
Simplifying Set-Location Navigation
First and foremost I obviously need to figure out how to make the above Process block work. Once I do, I'll be able to easily declare Directory Variables for use with Set-Location. This is only for streamlining variable declarations so I don't have to repeatedly declare them with a messy barrage of individual Set-Variable commands while also avoiding the use of long (sometimes very long) $Object.PropertyName 'variables'.
After I get a handle on this script, I'll be able to finish several other scripts and functions that use (more or less) the same basic process.
Add to $PROFILE
This particular script is going to be part of a 'Startups' section in my default $PROFILE (Microsoft.PowerShell_profile.ps1) so I can set the Directory Variables in-bulk and keep the $PROFILE script itself nice and clean.
The other scripts that I mentioned anbove are also going to be included in my $PROFILE Startups.
JSON Output
The script also exports a .json file so that, among other things, I can (hopefully) repeat the process down the road in my WSL Bash Profiles.
Param() Functionality
Eventually I want to add a Param() block so the function can be used outside of the script as well.

Related

PowerShell Jobs, writing to a file

Having some problems getting a Start-Job script block to output to a file. The following three lines of code work without any problem:
$about_name = "C:\0\ps_about_name.txt"
$about = get-help about_* | select Name,Synopsis
if (-not (Test-
Path $about_name)) { ($about | select Name | sort Name | Out-String).replace("[Aa]bout_", "") > $about_name }
The file is created in C:\0\
But I need to do a lot of collections like this, so I naturally looked at stacking them in parallel as separate jobs. I followed online examples and so put the last line in the above as a script block invoked by Start-Job:
Start-Job { if (-not (Test-Path $about_name)) { { ($about | select Name | sort Name | Out-String).replace("[Aa]bout_", "") > $about_name } }
The Job is created, goes to status Running, and then to status Completed, but no file is created. Without Start-Job, all works, with Start-Job, nothing... I've tried a lot of variations on this but cannot get it to create the file. Can someone advise what I am doing wrong in this please?
IMO, the simplest way to get around this problem by use of the $using scope modifier.
$about_name = "C:\0\ps_about_name.txt"
$about = get-help about_* | select Name,Synopsis
$sb = { if (-not (Test-Path $using:about_name)) {
$using:about.Name -replace '^about_' | Sort-Object > $using:about_name
}
}
Start-Job -Scriptblock $sb
Explanation:
$using allows you to access local variables in a remote command. This is particularly useful when running Start-Job and Invoke-Command. The syntax is $using:localvariable.
This particular problem is a variable scope issue. Start-Job creates a background job with its own scope. When using -Scriptblock parameter, you are working within that scope. It does not know about variables defined in your current scope/session. Therefore, you must use a technique that will define the variable within the scope, pass in the variable's value, or access the local scope from the script block. You can read more about scopes at About_Scopes.
As an aside, character sets [] are not supported in the .NET .Replace() method. You need to switch to -replace to utilize those. I updated the code to perform the replace using -replace case-insensitively.
HCM's perfectly fine solution uses a technique that passes the value into the job's script block. By defining a parameter within the script block, you can pass a value into that parameter by use of -ArgumentList.
Another option is to just define your variables within the Start-Job script block.
$sb = { $about_name = "C:\0\ps_about_name.txt"
$about = get-help about_* | select Name,Synopsis
if (-not (Test-Path $about_name)) {
$about.Name -replace '^about_' | Sort-Object > $about_name
}
}
Start-Job -Scriptblock $sb
You've got to send your parameters to your job.
This does not work:
$file = "C:\temp\_mytest.txt"
start-job {"_" | out-file $file}
While this does:
$file = "C:\temp\_mytest.txt"
start-job -ArgumentList $file -scriptblock {
Param($file)
"_" | out-file $file
}

How to restore language settings in Windows from a saved file using a powershell script?

I need to create a PowerShell script which will restore language settings and input methods (important) from a file. At first I thought it would be easy to use Get-WinUserLanguageList | Export-CliXML ./mylist.xml to save current settings and then $List = Import-CliXML ./mylist.xml, Set-WinUserLanguageList -LanguageList $List, however it does not work because the data imported from XML into the variable is deserialized and I get an exception:
Set-WinUserLanguageList : Cannot bind parameter 'LanguageList'. Cannot convert the "Microsoft.InternationalSettings.Commands.WinUserLanguage" value of type
"Deserialized.Microsoft.InternationalSettings.Commands.WinUserLanguage" to type
"Microsoft.InternationalSettings.Commands.WinUserLanguage".
I've tried using XML but failed, so I created a workaround which looks like this:
[CmdletBinding()]
param (
[switch]$GenerateList
)
function Generate-List { # Generates language files to restore from.
$GoodList = Get-WinUserLanguageList
[string[]]$LanguageTags = $GoodList.LanguageTag
$LanguageTags | Out-File .\LanguageTags.txt
[string[]]$InputMethods = $GoodList.InputMethodTips
$InputMethods | Out-File .\InputMethods.txt
} # Exporting languages and corresponding input methods in separate files. Can be improved.
if ($GenerateList -eq $true) {
Generate-List
} # Invokes a function based on a switch parameter.
function RestoreFrom-List {
$GoodList = Get-WinUserLanguageList # Make our variable of a proper type
$GoodList.Clear() # Clear the variable contents
[string[]]$LanguageTags = Get-Content .\LanguageTags.txt
[string[]]$InputMethods = Get-Content .\InputMethods.txt
foreach ($language in $LanguageTags) { # This loop fills $GoodList with proper values
$index = $LanguageTags.IndexOf($language)
$GoodList.Add($language) # Add a language to the list
$GoodList[$index].InputMethodTips.Clear() # Remove default input method
$GoodList[$index].InputMethodTips.Add($InputMethods[$index]) # Add an input method from a corresponding position in the saved txt file
}
Set-WinUserLanguageList $GoodList -force # Restore system languages and input methods using a freshly created list
}
RestoreFrom-List
I am new to PowerShell and I am sure this code is ugly and can be improved. Also, it feels that *-WinUserLanguageList cmdlets are a pain to work with - you have to use internal methods to change data instead of the universal Set-Property.
So far my script successfully exports language/input-method settings into two(!) txt(!!) files and doesn't work if a particular language has two or more input methods (because it dumps input methods into an array without relating them to a particular language and then retrieves them based on an index). Please help improving this.
Decided to revisit my old question and post an improved version of the script which uses JSON to store exported languages. Any number of languages and input methods is supported now, and they are stored as a properly structured JSON file. Handwriting and spellchecking settings are also preserved.
# Exports/imports windows language settings to/from a file
function ImportExport-Languages {
[CmdletBinding()]
param (
[switch]$GenerateList,
[string]$Path
)
## Export
function Export-Lang {
Get-WinUserLanguageList | ConvertTo-Json | Out-File $Path
}
## Import
function Import-Lang {
$importedFile = Get-Content $Path | ConvertFrom-Json
$langCollection = New-Object Microsoft.InternationalSettings.Commands.WinUserLanguage[] ""
foreach ($item in $importedFile)
{
$lang = [Microsoft.InternationalSettings.Commands.WinUserLanguage]::new($item.LanguageTag)
$lang.InputMethodTips.Clear() # clear default auto-generated input method
foreach ($inputMethod in $item.InputMethodTips)
{
$lang.InputMethodTips.Add($inputMethod)
}
$lang.Handwriting = $item.Handwriting
$lang.Spellchecking = $item.Spellchecking
$langCollection += $lang
}
Set-WinUserLanguageList $langCollection -Force
}
## Run
if ($GenerateList) { Export-Lang }
Import-Lang
}
ImportExport-Languages -Path "C:\MyFolder\MyFile.json" -GenerateList
Hi I modified it a little bit because it was not working on windows 10 pro. Changed the array to list of languages now works like a charm.
function ImportExport-Languages {
[CmdletBinding()]
param (
[switch]$GenerateList,
[string]$Path
)
## Export
function Export-Lang {
Get-WinUserLanguageList | ConvertTo-Json | Out-File $Path
}
## Import
function Import-Lang {
$importedFile = Get-Content $Path | ConvertFrom-Json
$langCollection = New-Object System.Collections.Generic.List[Microsoft.InternationalSettings.Commands.WinUserLanguage]
foreach ($item in $importedFile)
{
$lang = [Microsoft.InternationalSettings.Commands.WinUserLanguage]::new($item.LanguageTag)
$lang.InputMethodTips.Clear() # clear default auto-generated input method
foreach ($inputMethod in $item.InputMethodTips)
{
$lang.InputMethodTips.Add($inputMethod)
}
$lang.Handwriting = $item.Handwriting
$lang.Spellchecking = $item.Spellchecking
$langCollection.Add($lang)
}
Set-WinUserLanguageList $langCollection -Force
}
## Run
if ($GenerateList) { Export-Lang }
Import-Lang
}
# usage export ImportExport-Languages -Path "C:\Install\languageExport.json" -GenerateList
# usage import ImportExport-Languages -Path "C:\Install\languageExport.json"

Cannot modify a script-scoped variable from inside a function

I am currently making a script which is supposed to connect to 42 different local servers and getting the Users of a specific group (fjärrskrivbordsanvändare(Remote desktop users in swedish :D)) from active directory. After it has gotten all the users from the server it has to export the users to a file on MY desktop
The csv file has to look like this:
Company;Users
LawyerSweden;Mike
LawyerSweden;Jennifer
Stockholm Candymakers;Pedro
(Examples)
etc.
Here's the code as of now:
cls
$MolnGroup = 'fjärrskrivbordsanvändare'
$ActiveDirectory = 'activedirectory'
$script:CloudArray
Set-Variable -Name OutputAnvandare -Value ($null) -Scope Script
Set-Variable -Name OutputDomain -Value ($null) -Scope Script
function ReadInfo {
Write-Host("A")
Get-Variable -Exclude PWD,*Preference | Remove-Variable -EA 0
if (Test-Path "C:\file\frickin\path.txt") {
Write-Host("File found")
}else {
Write-Host("Error: File not found, filepath might be invalid.")
Exit
}
$filename = "C:\File\Freakin'\path\super.txt"
$Headers = "IPAddress", "Username", "Password", "Cloud"
$Importedcsv = Import-csv $filename -Delimiter ";" -Header $Headers
$PasswordsArray += #($Importedcsv.password)
$AddressArray = #($Importedcsv | ForEach-Object { $_.IPAddress } )
$UsernamesArray += #($Importedcsv.username)
$CloudArray += #($Importedcsv.cloud)
GetData
}
function GetData([int]$p) {
Write-Host("B")
for ($row = 1; $row -le $UsernamesArray.Length; $row++)
{
# (If the customer has cloud-service on server, proceed)
if($CloudArray[$row] -eq 1)
{
# Code below uses the information read in from a file to connect pc to server(s)
$secstr = New-Object -TypeName System.Security.SecureString
$PasswordsArray[$row].ToCharArray() | ForEach-Object {$secstr.AppendChar($_)}
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $UsernamesArray[$row], $secstr
# Runs command on server
$OutputAnvandare = Invoke-Command -computername $AddressArray[$row] -credential $cred -ScriptBlock {
Import-Module Activedirectory
foreach ($Anvandare in (Get-ADGroupMember fjärrskrivbordsanvändare))
{
$Anvandare.Name
}
}
$OutputDomain = Invoke-Command -computername $AddressArray[$row] -credential $cred -ScriptBlock {
Import-Module Activedirectory
foreach ($Anvandare in (Get-ADGroupMember fjärrskrivbordsanvändare))
{
gc env:UserDomain
}
}
$OutputDomain + $OutputAnvandare
}
}
}
function Export {
Write-Host("C")
# Variabler för att bygga up en CSV-fil genom Out-File
$filsökväg = "C:\my\file\path\Coolkids.csv"
$ColForetag = "Company"
$ColAnvandare = "Users"
$Emptyline = "`n"
$delimiter = ";"
for ($p = 1; $p -le $AA.Length; $p++) {
# writes out columns in the csv file
$ColForetag + $delimiter + $ColAnvandare | Out-File $filsökväg
# Writes out the domain name and the users
$OutputDomain + $delimiter + $OutputAnvandare | Out-File $filsökväg -Append
}
}
ReadInfo
Export
My problem is, I can't export the users or the domain. As you can see i tried to make the variables global to the whole script, but $outputanvandare and $outputdomain only contains the information i need inside of the foreach loop. If I try to print them out anywhere else, they're empty?!
This answer focuses on variable scoping, because it is the immediate cause of the problem.
However, it is worth mentioning that modifying variables across scopes is best avoided to begin with; instead, pass values via the success stream (or, less typically, via by-reference variables and parameters ([ref]).
To expound on PetSerAl's helpful comment on the question: The perhaps counter-intuitive thing about PowerShell variable scoping is that:
while you can see (read) variables from ancestral (higher-up) scopes (such as the parent scope) by referring to them by their mere name (e.g., $OutputDomain),
you cannot modify them by name only - to modify them you must explicitly refer to the scope that they were defined in.
Without scope qualification, assigning to a variable defined in an ancestral scope implicitly creates a new variable with the same name in the current scope.
Example that demonstrates the issue:
# Create empty script-level var.
Set-Variable -Scope Script -Name OutputDomain -Value 'original'
# This is the same as:
# $script:OutputDomain = 'original'
# Declare a function that reads and modifies $OutputDomain
function func {
# $OutputDomain from the script scope can be READ
# without scope qualification:
$OutputDomain # -> 'original'
# Try to modify $OutputDomain.
# !! Because $OutputDomain is ASSIGNED TO WITHOUT SCOPE QUALIFICATION
# !! a NEW variable in the scope of the FUNCTION is created, and that
# !! new variable goes out of scope when the function returns.
# !! The SCRIPT-LEVEL $OutputDomain is left UNTOUCHED.
$OutputDomain = 'new'
# !! Now that a local variable has been created, $OutputDomain refers to the LOCAL one.
# !! Without scope qualification, you cannot see the script-level variable
# !! anymore.
$OutputDomain # -> 'new'
}
# Invoke the function.
func
# Print the now current value of $OutputDomain at the script level:
$OutputDomain # !! -> 'original', because the script-level variable was never modified.
Solution:
There are several ways to add scope qualification to a variable reference:
Use a scope modifier, such as script in $script:OutputDomain.
In the case at hand, this is the simplest solution:
$script:OutputDomain = 'new'
Note that this only works with absolute scopes global, script, and local (the default).
A caveat re global variables: they are session-global, so a script assigning to a global variable could inadvertently modify a preexisting global variable, and, conversely, global variables created inside a script continue to exist after the script terminates.
Use Get/Set-Variable -Scope, which - in addition to supporting the absolute scope modifiers - supports relative scope references by 0-based index, where 0 represents the current scope, 1 the parent scope, and so on.
In the case at hand, since the script scope is the next higher scope,
Get-Variable -Scope 1 OutputDomain is the same as $script:OutputDomain, and
Set-Variable -Scope 1 OutputDomain 'new' equals $script:OutputDomain = 'new'.
(A rarely used alternative available inside functions and trap handlers is to use [ref], which allows modifying the variable in the most immediate ancestral scope in which it is defined: ([ref] $OutputDomain).Value = 'new', which, as PetSerAl points out in a comment, is the same as (Get-Variable OutputDomain).Value = 'new')
For more information, see:
Get-Help about_Variables
Get-Help about_Scopes
Finally, for the sake of completeness, Set-Variable -Option AllScope is a way to avoid having to use scope qualification at all (in all descendent scopes), because effectively then only a single variable by that name exists, which can be read and modified without scope qualification from any (descendent) scope.
# By defining $OutputDomain this way, all descendent scopes
# can both read and assign to $OutpuDomain without scope qualification
# (because the variable is effectively a singleton).
Set-Variable -Scope Script -Option AllScope -Name OutputDomain
However, I would not recommend it (at least not without adopting a naming convention), as it obscures the distinction between modifying local variables and all-scope variables:
in the absence of scope qualification, looking at a statement such as $OutputDomain = 'new' in isolation, you cannot tell if a local or an all-scope variable is being modified.
Since you've mentioned that you want to learn, I hope you'll pardon my answer, which is a bit longer than normal.
The issue that's impacting you here is PowerShell Variable Scoping. When you're commiting the values of $outputAvandare and $outputDomain, they only exist for as long as that function is running.
Function variables last until the function ends.
Script variables last until the script ends.
Session/global variables last until the session ends.
Environmental variable persist forever.
If you want to get the values out of them, you could make them Global variables instead, using this syntax:
$global:OutputAnvandare = blahblahblah
While that would be the easiest fix for your code, Global variables are frowned upon in PowerShell, since they subvert the normal PowerShell expectations of variable scopes.
Much better solution :)
Don't be dismayed, you're actually almost there with a really good solution that conforms to PowerShell design rules.
Today, your GetData function grabs the values that we want, but it only emits them to the console. You can see this in this line on GetData:
$OutputDomain + $OutputAnvandare
This is what we'd call emitting an object, or emiting data to the console. We need to STORE this data instead of just writing it. So instead of simply calling the function, as you do today, do this instead:
$Output = GetData
Then your function will run and grab all the AD Users, etc, and we'll grab the results and stuff them in $output. Then you can export the contents of $output later on.

Safe way to convert string literal without using Invoke-Expression [duplicate]

Imagine the following code:
# Script Start
$WelcomeMessage = "Hello $UserName, today is $($Date.DayOfWeek)"
..
..
# 100 lines of other functions and what not...
..
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$WelcomeMessage
}
This is a very basic example, but what it tries to show is a script where there is a $WelcomeMessage that the person running the script can set at the top of the script and controls how/what the message displayed is.
First thing's first: why do something like this? Well, if you're passing your script around to multiple people, they might want different messages. Maybe they don't like $($Date.DayOfWeek) and want to get the full date. Maybe they don't want to show the username, whatever.
Second, why put it at the top of the script? Simplicity. If you have 1000 lines in your script and messages like these spread all over the script, it makes it a nightmare for people to find and change these messages. We already do that for static messages, in the form of localized strings and stuff, so this is nothing new, except for the variable parts in it.
So, now to the issue. If you run that code and invoke Greet-User (assuming the functions/cmdlets for retrieving username and date actually exist and return something proper...) Greet-User will always return Hello , today is.
This is because the string is expanded when you declare it, at the top of the script, when neither $UserName nor $Date objects have a value.
A potential workaround would be to create the strings with single quotes, and use Invoke-Expression to expand them. But because of the spaces, that gets a bit messy. I.e.:
$WelcomeMessage = 'Hello $env:USERNAME'
Invoke-Expression $WelcomeMessage
This throws an error because of the space, to get it to work properly it would have to be declared as such:
$WelcomeMessage = 'Hello $env:USERNAME'
$InvokeExpression = "`"$WelcomeMessage`""
Messy...
Also, there's another problem in the form of code injection. Since we're allowing the user to write their own welcome message with no bounds specified, what's to prevent them from putting in something like...
$WelcomeMessage 'Hello $([void] (Remove-Item C:\Windows -Force -Recurse))'
(Yes, I know this will not delete everything but it is an example)
Granted this is a script and if they can modify that string they can also modify everything else on the script, but whereas the example I gave was someone maliciously taking advantage of the nature of the script, it can also happen that someone accidentally puts something in the string that ends up having unwanted consequences.
So... there's got to be a better way without the use of Invoke-Expression, I just can't quite thing of one so help would be appreciated :)
Embedding variables into strings is not the only way to create dynamic text, the way I would do it is like this:
$WelcomeMessage = 'Hello {0}, today is {1}'
# 100 lines of other functions and what not...
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$WelcomeMessage -f $Username, $Date
}
The canonical way to delay evaluation of expressions/variables in strings is to define them as single-quoted strings and use $ExecutionContext.InvokeCommand.ExpandString() later on.
Demonstration:
PS C:\> $s = '$env:COMPUTERNAME'
PS C:\> $s
$env:COMPUTERNAME
PS C:\> $ExecutionContext.InvokeCommand.ExpandString($s)
FOO
Applied to your sample code:
$WelcomeMessage = 'Hello $UserName, today is $($Date.DayOfWeek)'
...
...
...
function Greet-User {
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$ExecutionContext.InvokeCommand.ExpandString($WelcomeMessage)
}
Have you considered using a lambda expression; i.e. instead of defining the variable as a string value define it as a function, then invoke that function passing the relevant parameters at runtime.
$WelcomeMessage = {param($UserName,$Date);"Hello $UserName, today is $($Date.DayOfWeek) $([void](remove-item c:\test\test.txt))"}
#...
# 100 lines of other functions and what not...
#...
"testfile" >> c:\test\test.txt #ensure we have a test file to be deleted
function Get-UserNameFromSomewhereFancy(){return "myUsername";}
function Get-DateFromSomewhereFancy(){return (get-date);}
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$WelcomeMessage.invoke($username,$date)
}
cls
Greet-User
Update
If you only wish to allow variable replacement the below code would do the trick; but this fails to do more advanced functions (e.g. .DayOfWeek)
$WelcomeMessage = 'Hello $Username, today is $($Date.DayOfWeek) $([void](remove-item c:\test\test.txt))'
#...
# 100 lines of other functions and what not...
#...
"testfile" >> c:\test\test.txt #ensure we have a test file to be deleted
function Get-UserNameFromSomewhereFancy(){return "myUsername";}
function Get-DateFromSomewhereFancy(){return (get-date);}
function Resolve-WelcomeMessage(){
write-output {param($UserName,$Date);"$WelcomeMessage";}
}
function Greet-User
{
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
$temp = $WelcomeMessage
get-variable | ?{#('$','?','^') -notcontains $_.Name} | sort name -Descending | %{
$temp = $temp -replace ("\`${0}" -f $_.name),$_.value
}
$temp
}
cls
Greet-User
Update
To avoid code injection this makes use of -whatif; that will only help where the injected code supports the whatif functionality, but hopefully better than nothing...
Also the code now doesn't require parameters to be declared; but just takes those variables which are available at the time of execution.
$WelcomeMessage = {"Hello $Username, today is $($Date.DayOfWeek) $([void](remove-item c:\test\test.txt))"}
#...
# 100 lines of other functions and what not...
#...
function Get-UserNameFromSomewhereFancy(){return "myUsername";}
function Get-DateFromSomewhereFancy(){return (get-date);}
function Resolve-WelcomeMessage(){
write-output {param($UserName,$Date);"$WelcomeMessage";}
}
"testfile" >> c:\test\test.txt #ensure we have a test file to be deleted
function Greet-User {
[cmdletbinding(SupportsShouldProcess=$True)]
param()
begin {$original = $WhatIfPreference; $WhatIfPreference = $true;}
process {
$Username = Get-UserNameFromSomewhereFancy
$Date = Get-DateFromSomewhereFancy
& $WelcomeMessage
}
end {$WhatIfPreference = $original;}
}
cls
Greet-User

Assigning proper variable types when read from a config file [PowerShell]

I am trying to read values from a text file and keep them as variables to use in my script.
This config file contains strings, ints, booleans and an array that can contain strings, ints and booleans.
When I declare the variables outright, I have no problems. My script functions as expected. However when I am reading in the config file and trying to create variables based on that, I only get the variables declared as strings.
This creates my config file in the format I would like.
Function Create-Config() {
If (!(Test-Path config.txt)) {
$currentlocation=Get-Location
$parentfolder=(get-item $currentlocation).parent.FullName
New-Item config.txt -ItemType "file"
Add-Content config.txt "SERVER_NAME=MyServer"
Add-Content config.txt "SERVER_LOCATION=$currentlocation"
Add-Content config.txt "BACKUP_LOCATION=$parentfolder\backup"
Add-Content config.txt "CRAFTBUKKIT=craftbukkit.jar"
Add-Content config.txt "JAVA_FLAGS=-Xmx1G"
Add-Content config.txt "CRAFTBUKKIT_OPTIONS=-o True -p 1337"
Add-Content config.txt "TEST_DEPENDENCIES=True"
Add-Content config.txt "DELETE_LOG=True"
Add-Content config.txt "TAKE_BACKUP=True"
Add-Content config.txt "RESTART_PAUSE=5"
}
}
However, either I need to change how I create my config file, or change how I import those variables. I want the config file to be as simple as possible. I am using this code to import the values:
Function Load-Variables() {
Get-Content config.txt | Foreach-Object {
$var = $_.Split('=')
New-Variable -Name $var[0] -Scope Script -Value $var[1]
}
}
As you can see, I don't explicitly set the variable, since the variables from the config are different types (booleans, int, array, strings). However, PowerShell imports these all as strings. I can import all variables individually (which I may have to do) but I'm still feeling like I will be stuck on the array.
If I declare the array using this command:
New-Variable -Name CRAFTBUKKIT_OPTIONS -Option Constant -Value ([array]#('-o',$true,'-p',25565))
I get exactly what I want, but I need to import it from the config file instead of declaring the variable in my script. The java program is a bit finicky, so I cannot just import that value as a string, or it will not get passed properly and those options get ignored. I've found the only way it works is to have it as an array (as defined above). I also want to note that there could be many more config file options presented than in my example.
I am not sure what is the better approach - importing the variables to be declared correctly (what I would like to do), or assuming they cannot be imported as anything other than a string and then parsing that string into the proper variable types after.
I have tried declaring the variables before hand and using the Set-Variable command to set the values, but that doesn't work. It very much seems like my variables are being imported with Get-Content as strings from the start instead of the correct types.
Full script is here:
https://gist.github.com/TnTBass/4692f2a00fade7887ce4
Any help?
$types = #{
SERVER_NAME = {$args[0]}
SERVER_LOCATION = {$args[0]}
BACKUP_LOCATION = {$args[0]}
CRAFTBUKKIT = {$args[0]}
JAVA_FLAGS = {$args[0]}
CRAFTBUKKIT_OPTIONS = { ($args[0].split(' ')[0] -as [string]),
([bool]::Parse($args[0].split(' ')[1])),
($args[0].split(' ')[2] -as [string]),
($args[0].split(' ')[3] -as [int]) }
TEST_DEPENDENCIES = {[bool]::Parse($args[0])}
DELETE_LOG = {[bool]::Parse($args[0])}
TAKE_BACKUP = {[bool]::Parse($args[0])}
RESTART_PAUSE = {$args[0] -as [int]}
}
$ht = [ordered]#{}
gc config.txt |
foreach {
$parts = $_.split('=').trim()
$ht[$parts[0]] = &$types[$parts[0]] $parts[1]
}
New-object PSObject -Property $ht
SERVER_NAME : MyServer
SERVER_LOCATION : C:\testfiles
BACKUP_LOCATION : C:\\backup
CRAFTBUKKIT : craftbukkit.jar
JAVA_FLAGS : -Xmx1G
CRAFTBUKKIT_OPTIONS : {-o, True, -p, 1337}
TEST_DEPENDENCIES : True
DELETE_LOG : True
TAKE_BACKUP : True
RESTART_PAUSE : 5
The $types hash table uses parameter names from your configuration file for the keys, and script blocks that define the typing and data transformation that needs to be done on the string value for that parameter you're reading from the file. As each line is read in from the file, this part of the script:
$parts = $_.split('=').trim()
$ht[$parts[0]] = &$types[$parts[0]] $parts[1]
Splits it at the '=', then looks up the script block for that parameter and invokes it using the value as it's argument. The results are stored in a hash table ($ht), and then that's used to create an object. You can omit the object creation and just use the hash table to pass your config values if that's more appropriate for your application.
You might need to add some error trapping to test the input data and/or resulting values for production work. but I think the hash table of script blocks is a pretty clean way doing to present the typing and transformation, and should be fairly intuitive to read and easy to maintain in the script if you need to make changes. The first 5 parameters are string parameters, and are just returned as-is, but you can explicit cast them as [string] in the script block just for clarity.
Of course Powershell handles the variable values as strings. That's because it cannot tell string "1337" apart from integer 1337 without some extra help. In order to specify the data type, you need some metadata. There is an format just for that - XML. Now, you don't need to create an XML file by yourself. There are cmdlets Import-CliXML and Export-CliXML that manage Powershell object serialization.
One could for example save the configuration settings in a hash table and serialize it like so,
$cfgSettings = #{
"currentlocation" = "my current location";
"parentfolder" = "my backup location";
"SERVER_NAME" = "MyServer";
"SERVER_LOCATION" = $currentlocation;
"BACKUP_LOCATION" = "$parentfolder\backup";
"CRAFTBUKKIT" = "craftbukkit.jar";
"JAVA_FLAGS" = "-Xmx1G";
"CRAFTBUKKIT_OPTIONS" = "-o True -p 1337";
"TEST_DEPENDENCIES" = $true;
"DELETE_LOG" = $true;
"TAKE_BACKUP" = $true;
"RESTART_PAUSE" = 5
}
Export-Clixml -Path myConf.xml -InputObject $cfgSettings
The file contains serialized hashtable with data types. For example, DELETE_LOG is a boolean, RESTART_PAUSE an int and so on:
<En>
<S N="Key">DELETE_LOG</S>
<B N="Value">true</B>
</En>
<En>
<S N="Key">RESTART_PAUSE</S>
<I32 N="Value">5</I32>
</En>
<En>
<S N="Key">JAVA_FLAGS</S>
<S N="Value">-Xmx1G</S>
</En>
Repopulating and accessing the settings hashtable is not hard either:
$config = Import-CliXML myConf.xml
$config["DELETE_LOG"] # NB! Case sensitive, "delete_log" is different a key!
True
Edit
As per how to create the array, here is a sample that uses deserialized data.
Split the options and serialize the values:
$config = #{
"CRAFTBUKKIT_OPTION1" = "-o" ;
"CRAFTBUKKIT_OPTION2" = $true ;
"CRAFTBUKKIT_OPTION3" = "-p" ;
"CRAFTBUKKIT_OPTION4" = 1337 }
Export-Clixml -InputObject $config -Path .\temp\conf.xml
Deserialize the values and create an array out of them:
$config2 = Import-Clixml C:\temp\conf.xml
$array = #(
$config2["CRAFTBUKKIT_OPTION1"],
$config2["CRAFTBUKKIT_OPTION2"],
$config2["CRAFTBUKKIT_OPTION3"],
$config2["CRAFTBUKKIT_OPTION4"])
Print the array contents with type info:
$array | % { $("{0} {1}" -f $_, ($_.gettype().name)) }
# Output
-o String
True Boolean
-p String
1337 Int32