How to create an array to hold the values of several automatically generated checkboxes in Tk Tcl - arrays

I have a list of objects of unknown length, I want to create 2 lists of checkboxes, in each list a checkbox for each line of the list. I also need to be able to get to the checkboxes and check their value.
Since I didn't find a way to get the value of a checkbox from its path, I wanted to create a global array that will hold the variables for the check boxes, and tried the fallowing:
global cb
set i 0
foreach port $ports {
set to_cb [checkbutton $to.cb_to_$i -variable [list $cb(to$i)] -text $port -command [list __sp_from_to_changed $from $to]]
set from_cb [checkbutton $from.cb_from_$i -variable [list $cb(from$i)] -text $port -command [list __sp_from_to_changed $from $to]]
grid $to_cb -row [expr $i + 2] -sticky nsew
grid $from_cb -row [expr $i + 2] -sticky nsew
incr i
When I source it get the error can't read "cb(to0)": no such variable, how can I assign a variable inside an array for a check box, or better yet, is there a way to get the current value directly from the checkbox without using intermediate variables?

Checkbuttons always need to be bound to a variable (there is a default, but you don't want to use that). What you need to do is to generate the name of a variable that is unique for each checkbutton; an array element is an excellent choice here.
So what's the problem in your code? It's simply that you're trying to dereference the variables by putting a $ in front of them; unlike some other languages, Tcl always uses the $ to mean “read this variable” (except in regular expressions). You also don't need to wrap the variable name in list; that would only be useful if you putting it into a generated script. Instead, you need lines like this:
set to_cb [checkbutton $to.cb_to_$i -variable cb(to$i) -text $port \
-command [list __sp_from_to_changed to $i]]
set from_cb [checkbutton $from.cb_from_$i -variable cb(from$i) -text $port \
-command [list __sp_from_to_changed from $i]]
The changes here are:
Removed the extra generated complexity from the -variable option's value; a bare array element name is good enough.
Updated the arguments to __sp_from_to_changed so that they say what has actually been modified (the first argument is now to or from, and the second is the index; from that, you should be able to construct all the element names you need quite trivially).
Added a backslash to those long lines so I could break them up for readability. :-)

Related

Powershell arrayslicing Get-PSReadLine

trying to array slice the get-psreadlineoption command. I want to select all strings containing the word 'Color' and then input those to 'set-psreadline' to set all colors. I do not understand how this all works in powershell. It's an array, right? I can just loop over it like this:
foreach($a in $i) { $i; if ($i -contains 'MemberColor') {$i;} }
But that gives no output. I also cannot access it as an array:
get-psreadlineoption[21..36]
get-psreadlineoption['EditMode']
Does not work. Basically what I want to do is:
foreach (get-psreadlines[21..36]) { $array = append(<string that contains 'Color'>) } foreach ($a in $array) { set-psreadline($a, ...)}
How would I go about doing this?
(P.S. I just want to set all those colors to the same color in a concise manner as possible)
The solution depends on what version of the PSReadLine module you're using:
PSReadline v2.x, which ships with PowerShell (Core) 7+.
PSReadline v1.x, which Windows PowerShell versions ship with in Windows 10+ / Windows Sever 2016+, although it is possible to install 2.x on demand in Windows PowerShell 5.0 and 5.1 (Install-Module PSReadLine)
Use (Get-Module PSReadLine).Version.ToString() to see which version you're using.
PSReadline v2.x solution:
To get the names of all color properties, via filtering the names by those ending in color (case-insensitively), use reflection, which PowerShell facilitates via the intrinsic .psobject property:
(Get-PSReadLineOption).psobject.Properties.Name -like '*color'
Note:
A quick way to print all property names of interest, including their values, is to use
Get-PSReadLineOption | Select-Object *color. However, the result is a [pscustomobject], not a hashtable, and in order to convert the former to the latter you'd again need the same technique shown below.
However, as an alternative to setting colors via Set-PSReadLineOption (as shown below), you may also use the approach described for PSReadLine v1.x in the bottom section (e.g., (Get-PSReadlineOption).CommentColor = 'DarkGray'), in which case you can use the property names shown in the output from Get-PSReadLineOption | Select-Object *color as-is. Note that the property names differ between v2.x and v1.x; also, as with Set-PSReadLineOption -Colors below, setting background colors requires the use of VT escape sequences.
Caveat: These property names require a transformation in order to be usable as the keys of the hashtable you can pass to Set-PSReadLineOption -Colors (as of PSReadLine 2.1.0):
The suffix Color must be removed, and in the case of DefaultTokenColor, the suffix TokenColor, which the following -replace operation does:
(Get-PSReadLineOption).psobject.Properties.Name -like '*color' `
-replace '(Token)?Color$'
This yields the following color key names (as of PSReadLine 2.1.0):
ContinuationPrompt
Default
Comment
Keyword
String
Operator
Variable
Command
Parameter
Type
Number
Member
Emphasis
Error
Selection
InlinePrediction
You can now pass a hashtable that selectively updates colors; e.g. the following sets the Comment (CommentColor) color to dark gray (while leaving all other colors untouched):
Set-PSReadLineOption -Color #{ Comment = 'DarkGray' }
A color value can either be:
One of the values of the [ConsoleColor] enumeration type; e.g. [ConsoleColor]::DarkGray, which you may pass as string 'DarkGray', as shown above.
A VT (Virtual Terminal) / ANSI escape sequence. Note that use of such sequences is a must if you want to set the background color for a given setting; e.g., the following sets the comment color to dark gray (90) on a white background (47)
Set-PSReadLineOption -Color #{ Comment = "`e[90;47m" }
To create a hashtable comprising all color keys, using the current property values as a starting point, use the following:
$allColors = #{}
(Get-PSReadLineOption).psobject.Properties.
Where({ $_.Name -like '*color' }).
ForEach({ $allColors[$_.Name -replace '(Token)?Color$'] = $_.Value })
Note: When you print $allColors, the Value column may appear to be empty, but the values are there, in the form of - themselves invisible - Virtual Terminal (VT) control sequences.
PSReadline v1.x solution (older versions of Windows PowerShell):
Unlike the v2.x version:
Set-PSReadlineOption in v1.x uses the combination of the -TokenKind and -ForegroundColor / -BackgroundColor parameters for the various colors and also, for general color settings, individual parameters (e.g. -ErrorForegroundColor) (rather than the single v2.x -Colors parameter that accepts a hashtable); also, all color settings are split into foreground and background values.
The color values are limited to the values of the [ConsoleColor] enumeration type, such as 'DarkGray' ([ConsoleColor]::DarkGray)
For a unified approach to setting colors, you may forgo Set-PSReadLineOption and directly set the properties of the Microsoft.PowerShell.PSConsoleReadLineOptions instance that Get-PSReadLineOption returns.
For instance, the following sets the foreground color of comments to dark gray:
(Get-PSReadlineOption).CommentForegroundColor = 'DarkGray'
This is the equivalent of the following v1.x Set-PSReadLineOption call:
Set-PSReadLineOption -TokenKind Comment -ForegroundColor DarkGray
To get the names of all color-relevant properties, use the same approach as for v2.x:
(Get-PSReadLineOption).psobject.Properties.Name -like '*color'
This yields the following color property names:
ContinuationPromptForegroundColor
ContinuationPromptBackgroundColor
DefaultTokenForegroundColor
CommentForegroundColor
KeywordForegroundColor
StringForegroundColor
OperatorForegroundColor
VariableForegroundColor
CommandForegroundColor
ParameterForegroundColor
TypeForegroundColor
NumberForegroundColor
MemberForegroundColor
DefaultTokenBackgroundColor
CommentBackgroundColor
KeywordBackgroundColor
StringBackgroundColor
OperatorBackgroundColor
VariableBackgroundColor
CommandBackgroundColor
ParameterBackgroundColor
TypeBackgroundColor
NumberBackgroundColor
MemberBackgroundColor
EmphasisForegroundColor
EmphasisBackgroundColor
ErrorForegroundColor
ErrorBackgroundColor
This is sort of a common question, how to loop through properties Iterate over PSObject properties in PowerShell. You can also use get-member to get the property list. You can also pipe to "select *color".
Get-PSReadLineOption | get-member *color | % name
CommandColor
CommentColor
ContinuationPromptColor
DefaultTokenColor
EmphasisColor
ErrorColor
KeywordColor
MemberColor
NumberColor
OperatorColor
ParameterColor
PredictionColor
SelectionColor
StringColor
TypeColor
VariableColor

How do I sort an array of objects by one of their property values in Powershell?

For example, I have a variable, which returns the line with several arrays:
#{sourceDSAcn=B; LastSyncResult=0} #{sourceDSAcn=A; LastSyncResult=9} #{sourceDSAcn=C; LastSyncResult=0} #{sourceDSAcn=M; Last SyncResult=10}
I want to sort this line alphabetically by one of parameters. In this case - by sourceDSAcn, so result must be like that:
#{sourceDSAcn=A; LastSyncResult=9} #{sourceDSAcn=B; LastSyncResult=0} #{sourceDSAcn=C; LastSyncResult=0} #{sourceDSAcn=M; Last SyncResult=10}
How can I do that?
Your output format suggests two things:
The objects aren't arrays, but custom objects ([pscustomobject] instances).
You've used the Write-Host cmdet to print these objects to the host (display), which results in the hashtable-literal-like representation shown in your question (see this answer).
If, instead, you want the usual rich display formatting you get by default - while still sending the output to the host only rather than to the success output stream - you can use the Out-Host cmdlet.
Conversely, to produce data output to the pipeline, use either the Write-Output cmdlet or, preferably, PowerShell's implicit output feature, as shown below; for more information, see this answer.
In order to sort (custom) objects by a given property, simply pass the name of that property to
Sort-Object's (positionally implied) -Property parameter, as Mathias R. Jessen helpfully suggests:
# Using $variable by itself implicitly sends its value through the pipeline.
# It is equivalent to: Write-Output $variable | ...
$variable | Sort-Object sourceDSAcn # same as: ... | Sort-Object -Property sourceDSAcn

How to have a Powershell validatescript parameter pulling from an array?

I have a module with a lot of advanced functions.
I need to use a long list of ValidateSet parameters.
I would like to put the whole list of possible parameters in an array and then use that array in the functions themselves.
How can I pull the list of the whole set from an array?
New-Variable -Name vars3 -Option Constant -Value #("Banana","Apple","PineApple")
function TEST123 {
param ([ValidateScript({$vars3})]
$Fruit)
Write-Host "$Fruit"
}
The problem is that when I use the function it doesn't pull the content from the constant.
TEST123 -Fruit
If I specify the indexed value of the constant then it works.
TEST123 -Fruit $vars3[1]
It returns Apple.
You are misunderstanding how ValidateScript ...
ValidateScript Validation Attribute
The ValidateScript attribute specifies a script that is used to
validate a parameter or variable value. PowerShell pipes the value to
the script, and generates an error if the script returns $false or if
the script throws an exception.
When you use the ValidateScript attribute, the value that is being
validated is mapped to the $_ variable. You can use the $_ variable to refer to the value in the script.
... works. As the others have pointed out thus far. You are not using a script you are using a static variable.
To get what I believe you are after, you would do it, this way.
(Note, that Write- is also not needed, since output to the screen is the default in PowerShell. Even so, avoid using Write-Host, except for in targeted scenarios, like using color screen output. Yet, even then, you don't need it for that either. There are several cmdlets that can be used, and ways of getting color with more flexibility. See these listed MS powershelgallery.com modules)*
Find-Module -Name '*Color*'
Tweaking your code you posted, and incorporating what Ansgar Wiechers, is showing you.
$ValidateSet = #('Banana','Apple','PineApple') # (Get-Content -Path 'E:\Temp\FruitValidationSet.txt')
function Test-LongValidateSet
{
[CmdletBinding()]
[Alias('tlfvs')]
Param
(
[Validatescript({
if ($ValidateSet -contains $PSItem) {$true}
else { throw $ValidateSet}})]
[String]$Fruit
)
"The selected fruit was: $Fruit"
}
# Results - will provide intellisense for the target $ValidateSet
Test-LongValidateSet -Fruit Apple
Test-LongValidateSet -Fruit Dog
# Results
The selected fruit was: Apple
# and on failure, spot that list out. So, you'll want to decide how to handle that
Test-LongValidateSet -Fruit Dog
Test-LongValidateSet : Cannot validate argument on parameter 'Fruit'. Banana Apple PineApple
At line:1 char:29
Just add to the text array / file but this also means, that file has to be on every host you use this code on or at least be able to reach a UNC share to get to it.
Now, you can use the other documented "dynamic parameter validate set". the Lee_Daily points you to lookup, but that is a bit longer in the tooth to get going.
Example:
function Test-LongValidateSet
{
[CmdletBinding()]
[Alias('tlfvs')]
Param
(
# Any other parameters can go here
)
DynamicParam
{
# Set the dynamic parameters' name
$ParameterName = 'Fruit'
# Create the dictionary
$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
# Create the collection of attributes
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
# Create and set the parameters' attributes
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$ParameterAttribute.Position = 1
# Add the attributes to the attributes collection
$AttributeCollection.Add($ParameterAttribute)
# Generate and set the ValidateSet
$arrSet = Get-Content -Path 'E:\Temp\FruitValidationSet.txt'
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)
# Add the ValidateSet to the attributes collection
$AttributeCollection.Add($ValidateSetAttribute)
# Create and return the dynamic parameter
$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
$RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
return $RuntimeParameterDictionary
}
begin
{
# Bind the parameter to a friendly variable
$Fruit = $PsBoundParameters[$ParameterName]
}
process
{
# Your code goes here
$Fruit
}
}
# Results - provide intellisense for the target $arrSet
Test-LongValidateSet -Fruit Banana
Test-LongValidateSet -Fruit Cat
# Results
Test-LongValidateSet -Fruit Banana
Banana
Test-LongValidateSet -Fruit Cat
Test-LongValidateSet : Cannot validate argument on parameter 'Fruit'. The argument "Cat" does not belong to the set "Banana,Apple,PineApple"
specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
At line:1 char:29
Again, just add to the text to the file, and again, this also means, that file has to be on every host you use this code on or at least be able to reach a UNC share to get to it.
I am not sure exactly what your use case is, but another possibility if you're using PowerShell 5.x, or newer, is to create a class, or if you're using an older version you could embed a little C# in your code to create an Enum that you can use:
Add-Type -TypeDefinition #"
public enum Fruit
{
Strawberry,
Orange,
Apple,
Pineapple,
Kiwi,
Blueberry,
Raspberry
}
"#
Function TestMe {
Param(
[Fruit]$Fruit
)
Write-Output $Fruit
}

How to Create Text Files from an Array of Values in Powershell

I have a text file "list.txt" with a list of hundreds of URL's that I want to parse, along with some common-to-all config data, into individual xml files (config files) using each value in "list.txt", like so:
list.txt contains:
line_1
line_2
line_3
The boilerplate config data looks like (using line_1 as an example):
<?xml version="1.0"?>
<Website xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Url>line_1.mydomain.com</Url>
<Title>line_1</Title>
<Enabled>true</Enabled>
<PluginInName>Tumblr</PluginInName>
</Website>
So if "list.txt" contains 100 items, I want 100 config files written with the URL and Title elements individualized.
I have fumbled with several posts on reading the array and on creating text files, but I haven't been able to make any of it work.
What I tried, although it's munged at this point. I'm not sure where I started or how I got to here:
$FileName = "C:\temp\list.txt"
$FileOriginal = Get-Content $FileName
# create an empty array
Foreach ($Line in $FileOriginal)
{
$FileModified += $Line
if ($Line -match $pattern)
{
# Add Lines after the selected pattern
$FileModified += 'add text'
$FileModified += 'add second line text'
}
}
Set-Content $fileName $FileModified
This is way beyond my neophyte Powershell skills. Can anyone help out?
You're looking for a string-templating approach, where a string template that references a variable is instantiated on demand with the then-current variable value:
# Define the XML file content as a *template* string literal
# with - unexpanded - references to variable ${line}
# (The {...}, though not strictly necessary here,
# clearly delineates the variable name.)
$template = #'
<code>
<?xml version="1.0"?>
<Website xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Url>${line}.mydomain.com</Url>
<Title>${line}</Title>
<Enabled>true</Enabled>
<PluginInName>Tumblr</PluginInName>
</Website>
'#
# Loop over all input lines.
Get-Content C:\temp\list.txt | ForEach-Object {
$line = $_ # store the line at hand in $line.
# Expand the template based on the current $line value.
$configFileContent = $ExecutionContext.InvokeCommand.ExpandString($template)
# Save the expanded template to an XML file.
$configFileContent | Set-Content -Encoding Utf8 "$line.xml"
}
Notes:
I've chosen UTF-8 encoding for the output XML files, and to name them "$line.xml", i.e. to name them for each input line and to store them in the current location; adjust as needed.
The template expansion (interpolation) is performed via automatic variable $ExecutionContext, whose .InvokeCommand property provides access to the .ExpandString() method, which allows performing string expansion (interpolation) on demand, as if the input string were a double-quoted string - see this answer for a detailed example.
Surfacing the functionality of the $ExecutionContext.InvokeCommand.ExpandString() method in a more discoverable way via an Expand-String cmdlet is the subject of this GitHub feature request.
Ansgar Wiechers points out that a simpler alternative in this simple case - given that only a single piece of information is passed during template expansion - is to use PowerShell's string-formatting operator, -f to fill in the template:
# Define the XML file content as a *template* string literal
# with '{0}' as the placeholder for the line variable, to
# be instantiated via -f later.
$template = #'
<code>
<?xml version="1.0"?>
<Website xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Url>{0}.mydomain.com</Url>
<Title>{0}</Title>
<Enabled>true</Enabled>
<PluginInName>Tumblr</PluginInName>
</Website>
'#
# Loop over all input lines.
Get-Content C:\temp\list.txt | ForEach-Object {
# Expand the template based on the current $line value.
$configFileContent = $template -f $_
# Save the expanded template to an XML file.
$configFileContent | Set-Content -Encoding Utf8 "$line.xml"
}
Optional reading: choosing between -f and $ExecutionContext.InvokeCommand.ExpandString() for template expansion:
Tip of the hat to Ansgar for his help.
Using -f:
Advantages:
It is made explicit on invocation what values will be filled in.
Additionally, it's easier to include formatting instructions in placeholders (e.g., {0:N2} to format numbers with 2 decimal places).
Passing the values explicitly allows easy reuse of a template in different scopes.
An error will occur by default if you accidentally pass too few or too many values.
Disadvantages:
-f placeholders are invariably positional and abstract; e.g., {2} simply tells you that you're dealing with the 3rd placeholder, but tells you nothing about its purpose; in larger templates with multiple placeholders, this can become an issue.
Even if you pass the right number of values, they may be in the wrong order, which can lead to subtle bugs.
Using $ExecutionContext.InvokeCommand.ExpandString():
Advantages:
If your variables have descriptive names, your template will be more readable, because the placeholders - the variable names - will indicate their purpose.
No need to pass values explicitly on invocation - the expansion simply relies on the variables available in the current scope.
Disadvantages:
If you use a template in multiple functions (scopes), you need to make sure that the variables referenced in the template are set in each.
At least by default, $ExecutionContext.InvokeCommand.ExpandString() will quietly ignore nonexistent variables referenced in the template - which may or may not be desired.
However, you can use Set-StrictMode -Version 2 or higher to report an error instead; using Set-StrictMode is good practice in general, though note that its effect isn't lexically scoped and it can disable convenient functionality.
Generally, you manually need to keep your template in sync with the code that sets the variables referenced in the template, to ensure that the right values will be filled in (e.g., if the name of a referenced variable changes, the template string must be updated too).

Index is out of range powershell

I have a script that builds a GUI with a list of printers that will be selected by the user.
These printers are also on a CSV file built like this :
Computer (name of the printer); IP
xxxx;x.x.x.x
I want to collect all the selected values in an array named x
Then I want to take every entry in the CSV that corresponds to the selected item and put it in another array named y
Finally I export the y array into a new CSV that will be used to install the printers on the domain.
I tried to go straight from second step to last step but i couldn't.
Here is the part of the code :
$OKButton.Add_Click({
foreach ($objItem in $objListbox.SelectedItems)
{$x += $objItem}
y=#()
for ($i=0; $i -lt $x.length; $i++)
{
$y[$i]=Import-Csv C:\Users\Administrateur\Desktop\Classeur33.csv | Where-Object {$_.Computer -eq $x[$i]}
}
$y > C:\Users\Administrateur\Desktop\allezjoue.csv
I've tried to do it with a 3 values x array in another script and it worked fine, but I really need to keep the listbox that allows the user to select the printers he wants.
Powershell always returns me "Index out of range"
I tried to put "$y=$x" so they have the same range, but when I do this it returns that I can't index in an object which has "System.string" type.
This is PowerShell and very object oriented. Use the objects and collections at hand.
Decriptive variable names are your friend.
$objListbox.SelectedItems is already a collection of objects.
Put it in a variable and loop through it with Foreach-Object aka foreach.
Import-CSV returns a collection of objects.
$Selection = $ObjListbox.SelectedItems
$printers = Import-CSV 'C:\Users\Administrateur\Desktop\Classeur33.csv'
foreach ($chosen in $Selection) {
$printers = $printers | where-object { $_.Computer -eq $Chosen.Name }
}
$printers | Export-CSV 'C:\Users\Administrateur\Desktop\allezjoue.csv' -NoTypeInformation
$Chosen.Name should be edited to conform with whatever objects you get in $Selection. You can test this by $ObjListbox.SelectedItems | Get-Member and examining the members for a property with the name of the item selected, then assuming the names match what's in your CSV, you should be good.
(bonus note) Storing data in and running as local admin is bad practice, even on your home lab. Your mistakes will have the power of local admin, and your users will not be able to run the scripts since the source/results files are in admin's desktop.

Resources