Create an incrementing variable from 2 variables in PowerShell - powershell

OK, First I consider myself a newbie and have much to learn about PowerShell and this is my first post ever.
I am trying to loop through some data and put it into a custom object and put them into separate arrays for later use. The issue is that I want to create a variable representing $week_data1 by using a counter $i so I can reduce the amount of code required. I do have a concatenated variable being written out: write-host '$week++ ='$week$i But I think it is being represented as a string?
How can I get $week_data$i to represent the array to insert the data?
Input data. Each week ends on Saturday.
$week1=#('2021-05-01')
$week2=#('2021-05-02', '2021-05-03', '2021-05-04', '2021-05-05', '2021-05-06', '2021-05-07', '2021-05-08')
$week3=#('2021-05-09', '2021-05-10', '2021-05-11', '2021-05-12', '2021-05-13', '2021-05-14', '2021-05-15')
$week4=#('2021-05-16', '2021-05-17', '2021-05-18', '2021-05-19', '2021-05-20', '2021-05-21', '2021-05-22')
$week5=#('2021-05-23', '2021-05-24', '2021-05-25', '2021-05-26', '2021-05-27', '2021-05-28', '2021-05-29')
$week6=#('2021-05-30', '2021-05-31')
$month =#($week1, $week2, $week3, $week4, $week5, $week6)
Create the output structures to be populated.
$week_data1=#()
$week_data2=#()
$week_data3=#()
$week_data4=#()
$week_data5=#()
$week_data6=#()
$month_data =#($week_data1, $week_data2, $week_data3, $week_data4, $week_data5, $week_data6)
Loop through the array and count the week number that is being processed.
$i = 0
foreach($week in $month)
{ $i++
$n=0
Here I can write out a Variable and it concatenates properly.
**write-host '$week++ ='$week$i**
foreach($day in $week)
{$n++
write-host '$day ='$day
Pull in data from a .csv file to populate the custom object.
foreach($line in $csv)
{
if($line -match $day)
Match the line in the CSV file that has the correct Date in it. One line in the file per date in the month.
{ #write-host '$line.Day = ' $line.Day
# custom object to be used later
$date_data = [PSCustomObject] #{
week_numb = $i
date = $line.Day
attempts = $line.Attempts
connects = $line.Connects
}
I have tried different syntax versions but it does not work here? I want to put the custom object data into the new array for the week being processed.
#write-host '$week_data[$i]='$week_data[$i]
$week_data$i += $date_data # Add data from csv file into a
#$week_data[$i] += $date_data
}
}
}
}
Issue using $week_data$i as a variable I get an error:
At line:38 char:17
$week_data$i += $date_data # Add data from csv file into a
~~
Unexpected token '$i' in expression or statement.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : UnexpectedToken

You're looking for variable indirection, i.e. the ability to refer to a variable indirectly, by a name stored in another variable or returned from an expression.
Note, however, that there are usually superior alternatives, such as using arrays or hashtables as multi-value containers - see this answer for an example.
If you do need to use variable indirection, use Get-Variable and Set-Variable:
$week_data1 = 'foo', 'bar'
$i = 1
# Same as: $week_data1
# Note that "$" must NOT be specified as part of the name.
Get-Variable "week_data$i" -ValueOnly
# Same as: $week_data1 = 'baz', 'quux'
Set-Variable "week_data$i" baz, quux
# Updating an existing value requires nesting the two calls:
# Same as: $week_data1 += 'quuz'
Set-Variable "week_data$i" ((Get-Variable "week_data$i" -ValueOnly) + 'quuz')
As an aside: "extending" an array with += is convenient, but inefficient: a new array must be created behind the scenes every time - see this answer.
Similarly, calling cmdlets to set and get variables performs poorly compared to direct assignments and variable references.
See this answer for applying the indirection technique analogously to environment variables, using Get-Content / Set-Content and the Env: drive.
As for what you tried:
$week_data$i = ... is an assignment expression, which is interpreted as directly juxtaposing two variables, $week_data and $i, which causes the syntax error you saw.
By contrast, something like Write-Output $week_data$i is a command, and while $week_data$i is also interpreted as two variable references, as a command argument it is syntactically valid, and would simply pass the (stringified) concatenation of the two variable values; in other words: $week_data$i acts as if it were double-quoted, i.e. an expandable string, and the command is therefore equivalent to Write-Output "$week_data$i"

Unrelated to the answer, but likely helpful for you, I have a function that will determine what week in a month a given date is.
Function Get-Week{
[cmdletbinding()]
param([parameter(ValueFromPipeline)][string[]]$Date)
process{
ForEach($Day in $Date){
$DTDay=[datetime]$Day
$Buffer = ([datetime]("{0}-01-{1}" -f $DTDay.Month,$DTDay.Year)).dayofweek.value__ -1
[math]::Truncate(($DTDay.Day+$Buffer)/7)+1
}
}
}
So you feed that a string that can be converted to a date like:
'5-13-2021' | Get-Week
or
Get-Week '5-13-2021'
and you get back a number indicating what week (ending on Saturday) of the month that day falls in.

Related

Getting error when adding nested hashtable to array in Powershell

I have a nested hashtable with an array and I want to loop through the contents of another array and add that to the nested hashtable. I'm trying to build a Slack message block.
Here's the nested hashtable I want to add to:
$msgdata = #{
blocks = #(
#{
type = 'section'
text = #{
type = 'mrkdwn'
text = '*Services Being Used This Month*'
}
}
#{
type = 'divider'
}
)
}
$rows = [ ['azure vm', 'centralus'], ['azure sql', 'eastus'], ['azure functions', 'centralus'], ['azure monitor', 'eastus2'] ]
$serviceitems = #()
foreach ($r in $rows) {
$servicetext = "*{0}* - {1}" -f $r[1], $r[0]
$serviceitems += #{'type'='section'}
$serviceitems += #{'text'= ''}
$serviceitems.text.Add('type'='mrkdwn')
$serviceitems.text.Add('text'=$servicetext)
$serviceitems += #{'type'='divider'}
}
$msgdata.blocks += $serviceitems
The code is partially working. The hashtables #{'type'='section'} and #{'type'='divider'} get added successfully. Trying to add the nested hashtable of #{'text' = #{ 'type'='mrkdwn' 'text'=$servicetext }} fails with this error:
Line |
24 | $serviceitems.text.Add('type'='mrkdwn')
| ~
| Missing ')' in method call.
I tried looking through various Powershell posts and couldn't find one that applies to my specific situation. I'm brand new to using hashtables in Powershell.
Complementing mklement0's helpful answer, which solves the problem with your existing code, I suggest the following refactoring, using inline hashtables:
$serviceitems = foreach ($r in $rows) {
#{
type = 'section'
text = #{
type = 'mrkdwn'
text = "*{0}* - {1}" -f $r[1], $r[0]
}
}
#{
type = 'divider'
}
}
$msgdata.blocks += $serviceitems
This looks much cleaner and thus easier to maintain in my opinion.
Explanations:
$serviceitems = foreach ... captures all output (to the success stream) of the foreach loop in variable $serviceitems. PowerShell automatically creates an array from the output, which is more efficient than manually adding to an array using the += operator. Using += PowerShell has to recreate an array of the new size for each addition, because arrays are actually of fixed size. When PowerShell automatically creates an array, it uses a more efficient data structure internally.
By writing out an inline hash table, without assigning it to a variable, PowerShell implicitly outputs the data, in effect adding it to the $serviceitems array.
We output two hash tables per loop iteration, so PowerShells adds two array elements to $serviceitems per loop iteration.
Note:
This answer addresses your question as asked, specifically its syntax problems.
For a superior solution that bypasses the original problems in favor of streamlined code, see zett42's helpful answer.
$serviceitems.text.Add('type'='mrkdwn') causes a syntax error.
Generally speaking, IF $serviceitems.text referred to a hashtable (dictionary), you need either:
method syntax with distinct, ,-separated arguments:
$serviceitems.text.Add('type', 'mrkdwn')
or index syntax (which would quietly overwrite an existing entry, if present):
$serviceitems.text['type'] = 'mrkdwn'
PowerShell even lets you access hashtable (dictionary) entries with member-access syntax (dot notation):
$serviceitems.text.type = 'mrkdwn'
In your specific case, additional considerations come into play:
You're accessing a hashtable via an array, instead of directly.
The text entry you're trying to target isn't originally a nested hashtable, so you cannot call .Add() on it; instead, you must assign a new hashtable to it.
Therefore:
# Define an empty array
$serviceItems = #()
# "Extend" the array by adding a hashtable.
# Note: Except with small arrays, growing them with +=
# should be avoided, because a *new* array must be allocated
# every time.
$serviceItems += #{ text = '' }
# Refer to the hashtable via the array's last element (-1),
# and assign a nested hashtable to it.
$serviceItems[-1].text = #{ 'type' = 'mrkdwn' }
# Output the result.
$serviceItems

PowerShell create array failed in a loop

Thought I have read enough examples here and elsewhere. Still I fail creating arrays in Power Shell.
With that code I hoped to create slices of pair values from an array.
$values = #('hello','world','bonjour','moon','ola','mars')
function slice_array {
param (
[String[]]$Items
)
[int16] $size = 2
$pair = [string[]]::new($size) # size is 2
$returns = [System.Collections.ArrayList]#()
[int16] $_i = 0
foreach($item in $Items){
$pair[$_i] = $Item
$_i++;
if($_i -gt $size - 1){
$_i = 0
[void]$returns.Add($pair)
}
}
return $returns
}
slice_array($values)
the output is
ola
mars
ola
mars
ola
mars
I would hope for
'hello','world'
'bonjour','moon'
'ola','mars'
Is possible to slice that array to an array of arrays with length 2 ?
Any explenation why it doesn't work as expected ?
How should the code be changed ?
Thanks for any hint to properly understand Arrays in PowerShell !
Here's a PowerShell-idiomatic solution (the fix required for your code is in the bottom section):
The function is named Get-Slices to adhere to PowerShell's verb-noun naming convention (see the docs for more information).
Note: Often, the singular form of the noun is used, e.g. Get-Item rather than Get-Items, given that you situationally may get one or multiple output values; however, since the express purpose here is to slice a single object into multiple parts, I've chosen the plural.
The slice size (count of elements per slice) is passed as a parameter.
The function uses .., the range operator, to extract a single slice from an array.
It uses PowerShell's implicit output behavior (no need for return, no need to build up a list of return values explicitly; see this answer for more information).
It shows how to output an array as a whole from a function, which requires wrapping it in an auxiliary single-element array using the unary form of ,, the array constructor operator. Without this auxiliary array, the array's elements would be output individually to the pipeline (which is also used for function / script output; see this answer for more information.
# Note: For brevity, argument validation, pipeline support, error handling, ...
# have been omitted.
function Get-Slices {
param (
[String[]] $Items
,
[int] $Size # The slice size (element count)
)
$sliceCount = [Math]::Ceiling($Items.Count / $Size)
if ($sliceCount -le 1) {
# array is empty or as large as or smaller than a slice? ->
# wrap it *twice* to ensure that the output is *always* an
# *array of arrays*, in this case containing just *one* element
# containing the original array.
,, $Items
}
else {
foreach ($offset in 0..($sliceCount-1)) {
, $Items[($offset * $Size)..(($offset+1) * $Size - 1)] # output this slice
}
}
}
To slice an array into pairs and collect the output in an array of arrays (jagged array):
$arrayOfPairs =
Get-Slices -Items 'hello','world','bonjour','moon','ola','mars' -Size 2
Note:
Shell-like syntax is required when you call functions (commands in general) in PowerShell: arguments are whitespace-separated and not enclosed in (...) (see this answer for more information)
Since a function's declared parameters are positional by default, naming the arguments as I've done above (-Item ..., -Size ...) isn't strictly necessary, but helps readability.
Two sample calls:
"`n-- Get pairs (slice count 2):"
Get-Slices -Items 'hello','world','bonjour','moon','ola','mars' -Size 2 |
ForEach-Object { $_ -join ', ' }
"`n-- Get slices of 3:"
Get-Slices -Items 'hello','world','bonjour','moon','ola','mars' -Size 3 |
ForEach-Object { $_ -join ', ' }
The above yields:
-- Get pairs (slice count 2):
hello, world
bonjour, moon
ola, mars
-- Get slices of 3:
hello, world, bonjour
moon, ola, mars
As for what you tried:
The only problem with your code was that you kept reusing the very same auxiliary array for collecting a pair of elements, so that subsequent iterations replaced the elements of the previous ones, so that, in the end, your array list contained multiple references to the same pair array, reflecting the last pair only.
This behavior occurs, because arrays are instance of reference types rather than value types - see this answer for background information.
The simplest solution is to add a (shallow) clone of your $pair array to your list, which ensures that each list entry is a distinct array:
[void]$returns.Add($pair.Clone())
Why you got 3 equal pairs instead of different pairs:
.Net (powershell based on it) is object-oriented language and it has consept of reference types and value types. Almost all types are reference types.
What happens in your code:
You create $pair = [string[]] object. $pair variable actually stores memory address of (reference to) [string[]] object, because arrays are reference types
You fill $pair array with values
You add (!) $pair to $returns. Remember that $pair is reference to memory block. And when you add it to $returns, it adds memory address of [string[]] you wrote values to.
You repeat step2: You fill $pair array with different values, but address of this array in memory keeps the same. Doing this you actually replace values from step2 with new values in the same $pair object.
= // = step3
= // = step4
= // = step3
As a result: in $returns there are three same memory addresses: [[reference to $pair], [reference to $pair], [reference to $pair]]. And $pair values were overwritten by code with last pair values.
On output it works like this:
Powershell looks at $results which is array.
Powershell looks to $results[0] which reference to $pair
Powershell outputs reference to $pair[0]
Powershell outputs reference to $pair[1]
Powershell looks to $results[1] which reference to $pair
Powershell outputs reference to $pair[0]
Powershell outputs reference to $pair[1]
Powershell looks to $results[1] which reference to $pair
Powershell outputs reference to $pair[0]
Powershell outputs reference to $pair[1]
So you see, you triple output the object from the same memory address. You overwritten it 3 times in slice_array and now it stores only last pair values.
To fix it in your code, you should create a new $pair in memory: add $pair = [string[]]::new($size) just after $returns.Add($pair)

Powershell color formatting with format operator

I'm using a format operator inside a script with a following example:
$current = 1
$total = 1250
$userCN = "contoso.com/CONTOSO/Users/Adam Smith"
"{0, -35}: {1}" -f "SUCCESS: Updated user $current/$total", $userCN
This excpectedly shows the following output:
SUCCESS: Updated user 1/1250 : contoso.com/CONTOSO/Users/Adam Smith
The format operator is there to keep the targeted output text in place with current / total running numbers varying between 1-99999. Without the format operator I could highlight the success line like this:
Write-Host -BackgroundColor Black -ForegroundColor Green "SUCCESS: Updated user $current/$total: $userCN"
But the question is how could I use the highlight-colors combined with the format operator? There's only the -f parameter and it doesn't allow the color parameters because, well, it's not the same thing as Write-Host in the first place.
Unlike other shells, PowerShell allows you to pass commands and expressions as command arguments simply by enclosing them in parentheses, i.e by using (...), the grouping operator.
When calling PowerShell commands (cmdlets, scripts, functions), the output is passed as-is as an argument, as its original output type.
Therefore, Theo's solution from a comment is correct:
Write-Host -BackgroundColor Black -ForegroundColor Green `
("{0, -35}: {1}" -f "SUCCESS: Updated user $current/$total", $userCN)
That is, the -f expression inside (...) is executed and its output - a single string in this case - is passed as a positional argument to Write-Host (implicitly binds to the -Object parameter).
Note that you do not need, $(...), the subexpression operator, in this case:
(...) is sufficient to enclose an expression or command.
In fact, in certain cases $(...) can inadvertently modify your argument, because it acts like the pipeline; that is, it enumerates and rebuilds array expressions as regular PowerShell arrays (potentially losing strong typing) and unwraps single-element arrays:
# Pass a single-element array to a script block (which acts like a function).
# (...) preserves the array as-is.
PS> & { param($array) $array.GetType().Name } -array ([array] 1)
Object[] # OK - single-element array was passed as-is
# $(...) unwraps it.
PS> & { param($array) $array.GetType().Name } -array $([array] 1)
Int32 # !! Single-element array was unwrapped.
# Strongly-typed array example:
PS> & { param($array) $array.GetType().Name } ([int[]] (1..10))
Int32[] # OK - strongly typed array was passed as-is.
# Strongly-typed array example:
PS> & { param($array) $array.GetType().Name } $([int[]] (1..10))
Object[] # !! Array was *enumerated and *rebuilt* as regular PowerShell array.
The primary use of $(...) is:
expanding the output from expressions or commands inside expandable strings (string interpolation)
To send the output from compound statements such as foreach (...) { ... } and if (...) { ... } or multiple statements directly through the pipeline, after collecting the output up front (which (...) does as well); however, you can alternatively wrap such statements & { ... } (or . { ... } in order to execute directly in the caller's scope rather than a child scope) in order to get the usual streaming behavior (one-by-one passing of output) in the pipeline.
Taking a step back: Given that you already can use compound statements as expressions in variable assignments - e.g., $evenNums = foreach ($num in 1..3) { $num * 2 } - and expressions generally are accepted as the first segment of a pipeline - e.g., 'hi' | Write-Host -Fore Yellow - it is surprising that that currently doesn't work with compound statements; this GitHub issue asks if this limitation can be lifted.
In the context of passing arguments to commands:
Use $(...), the subexpression operator only if you want to pass the output from multiple commands or (one or more) compound statements as an argument and/or, if the output happens to be a single object, you want that object to be used as-is or, if it happens to be a single-element array (enumerable), you want it to be unwrapped (pass the element itself, not the array.
Of course, if you're constructing a string argument, $(...) can be useful inside that string, for string interpolation - e.g., Write-Host "1 + 1 = $(1 + 1)"
Use #(...), the array subexpression operator only if you want to pass the output from multiple commands as an argument and/or you want to ensure that the output becomes a array; that is, the output is returned as a (regular PowerShell) array, of type [object[]], even if it happens to comprise just one object. In a manner of speaking it is the inverse of $(...)'s behavior in the single-object output case: it ensures that single-object output too becomes an array.

Convert Byte Array (from legacy program data file) to Powershell object

I have a "structured" file (logical fixed-length records) from a legacy program on a legacy (non-MS) operating system. I know how the records were structured in the original program, but the original O/S handled structured data as a sequence of bytes for file I/O, so a hex dump won't show you anything more than what the record length is (there are marker bytes and other record overhead imposed by the access method API used to generate the file originally).
Once I have the sequence of bytes in a Powershell variable, with the overhead bytes "cut away", how can I convert this into a structured object? Some of the "fields" are 16-bit integers, some are strings of the form [s]data (where [s] is a byte giving the length of the "real" data in that field), some are BCD coded fixed-point numbers, some are IEEE floats.
(I haven't been specific about the structure, either on the Powershell side or on the legacy side, because I am seeking a more-or-less 'generic' solution/technique, as I actually have several different files with different record structures to process.)
Initially, I tried to do it by creating a type that could take the buffer and overwrite a struct so that all the fields were nicely filled in. However, certain issues arose (regarding struct layout, fixed buffers and mixing fixed and managed members) and I also realised that there was no guarantee that the data in the buffer would be properly (or even legally) aligned. Decided to try a more programmatic path.
"Manual" parsing is out, so how about automatic parsing? You're going to need to define the members of your PSobject at some point, why not do it in a way that can help programmatically parse the data. This method does not require the data in the buffer to be correctly aligned or even contiguous. You can also have fields overlap to separate raw unions into the individual members (though, typically, only one will contain a "correct" value).
First step, build a hash table to identify the members, the offset in the buffer, their data types and, if an array, the number of elements :
$struct = #{
field1 = 0,[int],0; # 0 means not an array
field2 = 4,[byte],16; # a C string maybe
field3 = 24,[char],32; # wchar_t[32] ? note: skipped over bytes 20-23
field4 = 56,[double],0
}
# the names field1/2/3/4 are arbitrary, any valid member name may be used (but not
# necessarily any valid hash key if you want a PSObject as the end result).
# also, the values could be hash tables instead of arrays. that would allow
# descriptive names for the values but doesn't affect the end result.
Next, use [BitConverter] to extract the required data. The problem here is that we need to call the correct method for all the varying types. Just use a (big) switch statement. The basic principle is the same for most values, get the type indicator and initial offset from the $struct definition then call the correct [BitConverter] method and supply the buffer and initial offset, update the offset to where the next element of an array would be and then repeat for as many array elements as are required. The only trap here is that the data in the buffer must have the same format as expected by [BitConverter], so for the [double] example, the bytes in the buffer must conform to IEEE-754 floating point format (assuming that [BitConverter]::ToDouble() is used). Thus, for example, raw data from a Paradox database will need some tweeking because it flips the high bit to simplify sorting.
$struct.keys | foreach {
# key order is undefined but that won't affect the final object's members
$hashobject = #{}
} {
$fieldoffs = $struct[$_][0]
$fieldtype = $struct[$_][1]
if (($arraysize = $struct[$_][2]) -ne 0) { # yes, I'm a C programmer from way back
$array = #()
} else {
$array = $null
}
:w while ($arraysize-- -ge 0) {
switch($fieldtype) {
([int]) {
$value = [bitconverter]::toint32($buffer, $fieldoffs)
$fieldoffs += 4
}
([byte]) {
$value = $buffer[$fieldoffs++]
}
([char]) {
$value = [bitconverter]::tochar($buffer, $fieldoffs)
$fieldoffs += 2
}
([string]) { # ANSI string, 1 byte per character
$array = new-object string (,[char[]]$buffer[$fieldoffs..($fieldoffs+$arraysize)])
# $arraysize has already been decremented so don't need to subtract 1
break w # "array size" was actually string length so don't loop
#
# description:
# first, get a slice of the buffer as a byte[] (assume single byte characters)
# next, convert each byte to a char in a char[]
# then, invoke the constructor String(Char[])
# finally, put the String into $array ready for insertion into $hashobject
#
# Note the convoluted syntax - New-Object expects the second argument to be
# an array of the constructor parameters but String(Char[]) requires only
# one argument that is itself an array. By itself,
# [char[]]$buffer[$fieldoffs..($fieldoffs+$arraysize)]
# is treated by PowerShell as an argument list of individual chars, corrupting the
# constructor call. The normal trick is to prepend a single comma to create an array
# of one element which is itself an array
# ,[char[]]$buffer[$fieldoffs..($fieldoffs+$arraysize)]
# but this won't work because of the way PowerShell parses the command line. The
# space before the comma is ignored so that instead of getting 2 arguments (a string
# "String" and the array of an array of char), there is only one argument, an array
# of 2 elements ("String" and array of array of char) thereby totally confusing
# New-Object. To make it work you need to ALSO isolate the single element array into
# its own expression. Hence the parentheses
# (,[char[]]$buffer[$fieldoffs..($fieldoffs+$arraysize)])
#
}
}
if ($null -ne $array) {
# must be in this order* to stop the -ne from enumerating $array to compare against
# $null. this would result in the condition being considered false if $array were
# empty ( (#() -ne $null) -> $null -> $false ) or contained only one element with
# the value 0 ( (#(0) -ne $null) -> (scalar) 0 -> $false ).
$array += $value
# $array is not $null so must be an array to which $value is appended
} else {
# $array is $null only if $arraysize -eq 0 before the loop (and is now -1)
$array = $value
# so the loop won't repeat thus leaving this one scalar in $array
}
}
$hashobject[$_] = $array
}
#*could have reversed it as
# if ($array -eq $null) { scalar } else { collect array }
# since the condition will only be true if $array is actually $null or contains at
# least 2 $null elements (but no valid conversion will produce $null)
At this point there is a hash table, $hashobject, with keys equal to the field names and values containing the bytes from the buffer arranged into single (or arrays of) numeric (inc. char/boolean) values or (ANSI) strings. To create a (proper) object, just invoke New-Object -TypeName PSObject -Property $hashobject or use [PSCustomObject]$hashobject.
Of course, if the buffer actually contained structured data then the process would be more complicated but the basic procedure would be the same. Note also that the "types" used in the $struct hash table have no direct effect on the resultant types of the object members, they are only convenient selectors for the switch statement. It would work just as well with strings or numbers. In fact, the parentheses around the case labels are because switch parses them the same as command arguments. Without the parentheses, the labels would be treated as literal strings. With them, the labels are evaluated as a type object. Both the label and the switch value are then converted to strings (that's what switch does for values other than script blocks or $null) but each type has a distinct string representation so the case labels will still match up correctly. (Not really on point but still interesting, I think.)
Several optimisations are possible but increase the complexity slightly. E.g.
([byte]) { # already have a byte[] so why collect bytes one at a time
if ($arraysize -ge 0) { # was originally -gt 0 so want a byte[]
$array = [byte[]]$buffer[$fieldoffs..($fieldoffs+$arraysize)]
# slicing the byte array produces an object array (of bytes) so cast it back
} else { # $arraysize was 0 so just a single byte
$array = $buffer[$fieldoffs]
}
break w # $array ready for insertion into $hashobject, don't need to loop
}
But what if my strings are actually Unicode?, you say. Easy, just use existing methods from the [Text.Encoding] class,
[string] { # Unicode string, 2 (LE) bytes per character
$array = [text.encoding]::unicode.getstring([byte[]]$buffer[$fieldoffs..($fieldoffs+$arraysize*2+1)])
# $arraysize should be the string length so, initially, $arraysize*2 is the byte
# count and $arraysize*2-1 is the end index (relative to $fieldoffs) but $arraysize
# was decremented so the end index is now $arraysize*2+1, i.e. length*2-1 = (length-1)*2+1
break w # got $array, no loop
}
You could also have both ANSI and Unicode by utilising a different type indicator for the ANSI string, maybe [char[]]. Remember, the type indicators do not affect the result, they just have to be distinct (and hopefully meaningful) identifiers.
I realise that this is not quite the "just dump the bytes into a union or variant record" solution mentioned in the OPs comment but PowerShell is based in .NET and uses managed objects where this sort of thing is largely prohibited (or difficult to get working, as I found). For example, assuming you could just dump raw chars (not bytes) into a String, how would the Length property get updated? This method also allows some useful preprocessing such as splitting up unions as noted above or converting raw byte or char arrays into the Strings they represent.

Powershell: unexpected return value from function, use of $args to access parameters

Ok, I have coded for quite a while in different, but I am not getting Powershells concept of a function return?....
I am very new to Powershell, so I am sure I am missing something very basic.
I have the function below:
function plGetKeyValue ([string] $FileName, [string] $SectionName, [string] $Key)
{
if ($PSBoundParameters.Count -lt 2 -or $PSBoundParameters.Count -gt 3 )
{
"Invalid call to {0} in {1}" -f $MyInvocation.MyCommand.Name,
$MyInvocation.MyCommand.ModuleName
return
}
# Declaration
$lFileContents = ""
$lSections = ""
$lDataStart = ""
$lStart = -1
$lEnd = -1
$lFoundSection = ""
$lNextSection = ""
$lResults = ""
$lRetValue = ""
# Handle the optional parameter.
if ( $PSBoundParameters.Count -eq 2 ) {
$PSBoundParameters.Add('Key', $SectionName)
$PSBoundParameters.Remove('SectionName')
$Key = $SectionName
$SectionName = $null
}
# Read the file in
$lFileContents = Get-Content $FileName | Select-String -Pattern .*
# Get the sections.
$lSections = $lFileContents -match '\['
$lSections = $lSections -notmatch '#'
# Start of the data.
$lDataStart = $lFileContents | Select-String -Pattern "^#", "^$" -NotMatch `
| select-object -First 1
# We have a section.
if ( $PSBoundParameters.ContainsKey( 'SectionName' ) ) {
# Find the section.
$lFoundSection = $lSections | Select-String -Pattern "$lSectionName\b"
# If none found we are out.
if ( -Not $lFoundSection) { return $lRetValue }
# Starting point for the key search is the line following
# the found section.
$lStart = $lFoundSection[0].LineNumber
# Loop through the sections and find the one after the found one.
$lNextSection = $lSections | ForEach-Object {
# If we hit it, break.
if ($_.LineNumber -gt $lStart) {
break;
}
}
# Set the ending line for the search to the end of the section
# or end of file. Which ever we have.
if ($lNextSection) {
$lEnd = $lNextSection[0].LineNumber
} else {
$lEnd = $lFileContents[-1]
}
} else {
# No section.
$lStart = $lDataStart.LineNumber
# Set the ending line for the search to the end of the section
# or end of file. Which ever we have.
if ($lSections) {
$lEnd = $lSections[0].LineNumber
} else {
$lEnd = $lFileContents[-1]
}
}
# Extract the lines starting with the key.
$lResults = $lFileContents[$lStart..$lEnd] -match "$Key\b"
# We got results.
# Split the value off.
return $lRetValue = $lResults[0] | Select -ExpandProperty "Line"
}
The process of creating this function has sparked several questions that I have researched and become confused with
1) The documentation indicates that $args should be used to determine arguments. It never seems to populate for me? I am using version 4? As a alternative I used $PSBoundParameters. Is this advisable?
2) Based on a lot of reading and head scratching, I have found that return values from functions rturn all uncaptured output to the pipeline. Can someone, please clarify uncaptured?
As an example, I would like the function below to return a string in the variable $lRetValue. Currently, it is returning True. Based on that I believe I have something uncaptured? But everything I am executing is captured in a variable. What am I missing?
The calling routine is calling the code in the following form:
$FileName = "S:\PS\Home\GlobalConfig\jobs.cfg"
$key = "Help"
$section = "Section"
$r = plGetKeyValue $FileName $Key
write-host "r is: $r"
The output shows as follows:
PS C:> S:\PS\Home\Job\Test.ps1
r is: True
Any assistance would be very much appreciated.
Terminology note: somewhat arbitrarily, I'll distinguish between parameters and arguments below:
- parameters as the placeholders that are defined as part of a function declaration,
- as distinct from arguments as the values that are bound to the placeholders in a given invocation.
Conceptual information:
1) The documentation indicates that $args should be used to determine arguments.
$args is a fallback mechanism for examining unbound arguments in non-advanced (non-cmdlet) functions.
$args is populated:
ONLY if your function is not an advanced function (a function is marked as an advanced function by the presence of a param(...) parameter-declaration statement - as opposed to declaring the parameters inside function someFunc(...)) - if decorated with a [CmdletBinding()] attribute).
even then it only contains the unbound arguments (those not mapped to declared parameters).
In other words: only if you declare your function without any parameters at all does $args contain all arguments passed.
Conversely, in an advanced function there mustn't be unbound arguments, and invoking an advanced function with arguments that cannot be bound to parameters simply fails (generates an error).
Since defining advanced functions is advisable in general, because they are best integrated with the PowerShell infrastructure as a whole, it's best to make do without $args altogether.
Instead, use a combination of multiple parameter sets and/or array parameters to cover all possible valid input argument scenarios.
$PSBoundArguments contains the arguments bound to declared parameters, and is normally not needed, because the variable names corresponding to the parameters names (e.g., $SectionName) can be used directly. (It has specialized uses, such as passing all bound parameters on to another cmdlet/function via splat #PSBoundArguments).
2) Based on a lot of reading and head scratching, I have found that return values from functions return all uncaptured output to the pipeline. Can someone, please clarify "uncaptured"?
Generally, any PowerShell statement or expression that generates output is sent to the success stream (loosely comparable to stdout in Unix shells) by default, UNLESS output is captured (e.g., by assigning to a variable) or redirected (e.g., by sending output to a file).
Thus, in a reversal of how most programming languages behave, you must take action if you do NOT want a statement to produce output.
If you're not interested in a statement's output (as opposed to capturing / redirecting it for later use), you can redirect to $null (the equivalent of /dev/null), pipe to cmdlet Out-Null, or assign to dummy variable $null ($null = ...).
Therefore, in a manner of speaking, you can call output that is sent to the success stream uncaptured.
That, however is unrelated to the return statement:
The return statement does not work the same way as in other languages; its primary purpose in PowerShell is as a control-flow mechanism - to exit a function or script block - rather than a mechanism to output data (even though it can also be used for that: with an argument, is another way to send output to the success stream).
Diagnosing your specific problem:
There are many ways in which your function could be made a better PowerShell citizen[1]
, but your immediate problem is this:
$PSBoundParameters.Remove('SectionName')
returns a Boolean value that is sent to the output stream, because you neither suppress, capture nor redirect it. In your case, since the $SectionName parameter is bound, it does have an entry in $PSBoundParameters, so $PSBoundParameters.Remove('SectionName') returns $true.
To suppress this unwanted output, use something like this:
$null = $PSBoundParameters.Remove('SectionName')
Generally speaking, unless you know that a statement does not generate output, it's better to be safe and prepend $null = (or use an equivalent mechanism to suppress output).
Especially with direct method calls on objects, it's often not clear whether a value - which turns into output (is sent to the success stream) - will be returned.
[1] The following help topics provide further information:
- USE of parameters, including how to inspect them with help -Full / -Detailed ...:
help about_Parameters
- DEFINING simple functions and their parameters:
help about_functions,
from which you can progress to advanced functions:
help about_functions_advanced
and their parameter definitions:
help about_Functions_Advanced_Parameters