I've got a PowerShell function that accepts multiple values for a single argument via a comma separated list, like so:
test-function -Targets 127.0.0.1, 8.8.8.8, fake.fqdn.com -FileName "c:\results.csv"
Here is the param section:
function test-function {
param(
[string[]]$Targets
[string]$FileName
)
}
I'm trying to put an array in as multiple Target arguments, so I've put them into a single variable like so:
foreach ($target in $arrayTargets) { $temp = $temp + ", $Target" }
$Targets = $temp -replace "^, ", ""
This would result in the required "127.0.0.1, 8.8.8.8, fake.fqdn.com" as a string in the variable $Targets.
When I try to execute the function referencing the $Targets variable, the function interprets $Targets as if it's a single entry, not multiple. This is what I've tried with the same results:
test-function -Targets $Targets -FileName "c:\results.csv"
test-function -Targets "$Targets" -FileName "c:\results.csv"
I've also tried an ArgList several different ways:
$ArgList = "-Targets $Targets -FileName `"c:\results.csv`""
test-function $ArgList
Instead of taking each individual array entry as an argument, it takes the entire string as the argument.
How can I get this function to accept an array as multiple targets?
If you have an array of arguments, there should be no reason to pack them into a string to pass to your function. Simply pass the array.
test-function -Targets $arrayTargets
To assign an array of values to a variable in PowerShell, simply assign a comma-separated list. Note to make an array of strings, you should make sure to put the strings in quotes (single or double). All of the following are equivalent.
$arr = #("127.0.0.1", "8.8.8.8", "fake.fqdn.com")
$arr = ('127.0.0.1', '8.8.8.8', 'fake.fqdn.com')
$arr = "127.0.0.1", '8.8.8.8', "fake.fqdn.com"
If you need your array to convert to a comma-separated string for any reason, there's no need to build it with a foreach loop. Instead use the -join operator.
PS> $string = $arr -join ', '
PS> $string
127.0.0.1, 8.8.8.8, fake.fqdn.com
But you will probably only want your array in this form to display it. As shown above, it's best to keep it in array form to pass to a function. You can even get the array back from the string using the -split operator.
PS> $string -split ', '
127.0.0.1
8.8.8.8
fake.fqdn.com
Related
I've seen the # symbol used in PowerShell to initialise arrays.
What exactly does the # symbol denote and where can I read more about it?
In PowerShell V2, # is also the Splat operator.
PS> # First use it to create a hashtable of parameters:
PS> $params = #{path = "c:\temp"; Recurse= $true}
PS> # Then use it to SPLAT the parameters - which is to say to expand a hash table
PS> # into a set of command line parameters.
PS> dir #params
PS> # That was the equivalent of:
PS> dir -Path c:\temp -Recurse:$true
PowerShell will actually treat any comma-separated list as an array:
"server1","server2"
So the # is optional in those cases. However, for associative arrays, the # is required:
#{"Key"="Value";"Key2"="Value2"}
Officially, # is the "array operator." You can read more about it in the documentation that installed along with PowerShell, or in a book like "Windows PowerShell: TFM," which I co-authored.
While the above responses provide most of the answer it is useful--even this late to the question--to provide the full answer, to wit:
Array sub-expression (see about_arrays)
Forces the value to be an array, even if a singleton or a null, e.g. $a = #(ps | where name -like 'foo')
Hash initializer (see about_hash_tables)
Initializes a hash table with key-value pairs, e.g.
$HashArguments = #{ Path = "test.txt"; Destination = "test2.txt"; WhatIf = $true }
Splatting (see about_splatting)
Let's you invoke a cmdlet with parameters from an array or a hash-table rather than the more customary individually enumerated parameters, e.g. using the hash table just above, Copy-Item #HashArguments
Here strings (see about_quoting_rules)
Let's you create strings with easily embedded quotes, typically used for multi-line strings, e.g.:
$data = #"
line one
line two
something "quoted" here
"#
Because this type of question (what does 'x' notation mean in PowerShell?) is so common here on StackOverflow as well as in many reader comments, I put together a lexicon of PowerShell punctuation, just published on Simple-Talk.com. Read all about # as well as % and # and $_ and ? and more at The Complete Guide to PowerShell Punctuation. Attached to the article is this wallchart that gives you everything on a single sheet:
You can also wrap the output of a cmdlet (or pipeline) in #() to ensure that what you get back is an array rather than a single item.
For instance, dir usually returns a list, but depending on the options, it might return a single object. If you are planning on iterating through the results with a foreach-object, you need to make sure you get a list back. Here's a contrived example:
$results = #( dir c:\autoexec.bat)
One more thing... an empty array (like to initialize a variable) is denoted #().
The Splatting Operator
To create an array, we create a variable and assign the array. Arrays are noted by the "#" symbol. Let's take the discussion above and use an array to connect to multiple remote computers:
$strComputers = #("Server1", "Server2", "Server3")<enter>
They are used for arrays and hashes.
PowerShell Tutorial 7: Accumulate, Recall, and Modify Data
Array Literals In PowerShell
I hope this helps to understand it a bit better.
You can store "values" within a key and return that value to do something.
In this case I have just provided #{a="";b="";c="";} and if not in the options i.e "keys" (a, b or c) then don't return a value
$array = #{
a = "test1";
b = "test2";
c = "test3"
}
foreach($elem in $array.GetEnumerator()){
if ($elem.key -eq "a"){
$key = $elem.key
$value = $elem.value
}
elseif ($elem.key -eq "b"){
$key = $elem.key
$value = $elem.value
}
elseif ($elem.key -eq "c"){
$key = $elem.key
$value = $elem.value
}
else{
Write-Host "No other value"
}
Write-Host "Key: " $key "Value: " $value
}
I have a Powershell function in which I am trying to allow the user to add or remove items from a list by typing the word "add" or "remove" followed by a space-delimited list of items. I have an example below (slightly edited, so you can just drop the code into a powershell prompt to test it "live").
$Script:ServerList = #("Server01","Server02","Server03")
Function EditServerList (){
$Script:ServerList = $Script:ServerList |Sort -Unique
Write-host -ForegroundColor Green $Script:ServerList
$Inputs = $args
If ($Inputs[0] -eq "start"){
$Edits = Read-Host "Enter `"add`" or `"remove`" followed by a space-delimited list of server names"
#"# EditServerList $Edits
# EditServerList $Edits.split(' ')
EditServerList ($Edits.split(' ') |Where {$_ -NotLike "add","remove"})
EditServerList start
} Elseif ($Inputs[0] -eq "add"){
$Script:ServerList += $Inputs |where {$_ -NotLike $Inputs[0]}
EditServerList start
} Elseif ($Inputs[0] -eq "remove"){
$Script:ServerList = $Script:ServerList |Where {$_ -NotLike ($Inputs |Where {$_ -Notlike $Inputs[0]})}
EditServerList start
} Else {
Write-Host -ForegroundColor Red "ERROR!"
EditServerList start
}
}
EditServerList start
As you can see, the function takes in a list of arguments. The first argument is evaluated in the If/Then statements and then the rest of the arguments are treated as items to add or remove from the list.
I have tried a few different approaches to this, which you can see commented out in the first IF evaluation.
I have two problems.
When I put in something like "add Server05 Server06" (without quotes) it works, but it also drops in the word "add".
When I put in "remove Server02 Server03" (without quotes) it does not edit the array at all.
Can anybody point out where I'm going wrong, or suggest a better approach to this?
To address the title's generic question up front:
When you pass an array to a function (and nothing else), $Args receives a single argument containing the whole array, so you must use $Args[0] to access it.
There is a way to pass an array as individual arguments using splatting, but it requires an intermediate variable - see bottom.
To avoid confusion around such issues, formally declare your parameters.
Try the following:
$Script:ServerList = #("Server01", "Server02", "Server03")
Function EditServerList () {
# Split the arguments, which are all contained in $Args[0],
# into the command (1st token) and the remaining
# elements (as an array).
$Cmd, $Servers = $Args[0]
If ($Cmd -eq "start"){
While ($true) {
Write-host -ForegroundColor Green $Script:ServerList
$Edits = Read-Host "Enter `"add`" or `"remove`" followed by a space-delimited list of server names"
#"# Pass the array of whitespace-separated tokens to the recursive
# invocation to perform the requested edit operation.
EditServerList (-split $Edits)
}
} ElseIf ($Cmd -eq "add") {
# Append the $Servers array to the list, weeding out duplicates and
# keeping the list sorted.
$Script:ServerList = $Script:ServerList + $Servers | Sort-Object -Unique
} ElseIf ($Cmd -eq "remove") {
# Remove all specified $Servers from the list.
# Note that servers that don't exist in the list are quietly ignored.
$Script:ServerList = $Script:ServerList | Where-Object { $_ -notin $Servers }
} Else {
Write-Host -ForegroundColor Red "ERROR!"
}
}
EditServerList start
Note how a loop is used inside the "start" branch to avoid running out of stack space, which could happen if you keep recursing.
$Cmd, $Servers = $Args[0] destructures the array of arguments (contained in the one and only argument that was passed - see below) into the 1st token - (command string add or remove) and the array of the remaining arguments (server names).
Separating the arguments into command and server-name array up front simplifies the remaining code.
The $var1, $var2 = <array> technique to split the RHS into its first element - assigned as a scalar to $var1 - and the remaining elements - assigned as an array to $var2, is commonly called destructuring or unpacking; it is documented in Get-Help about_Assignment Operators, albeit without giving it such a name.
-split $Edits uses the convenient unary form of the -split operator to break the user input into an array of whitespace-separated token and passes that array to the recursive invocation.
Note that EditServerList (-split $Edits) passes a single argument that is an array - which is why $Args[0] must be used to access it.
Using PowerShell's -split operator (as opposed to .Split(' ')) has the added advantage of ignoring leading and trailing whitespace and ignoring multiple spaces between entries.
In general, operator -split is preferable to the [string] type's .Split() method - see this answer of mine.
Not how containment operator -notin, which accepts an array as the RHS, is used in Where-Object { $_ -notin $Servers } in order to filter out values from the server list contained in $Servers.
As for what you tried:
EditServerList ($Edits.split(' ') |Where {$_ -NotLike "add","remove"}) (a) mistakenly attempts to remove the command name from the argument array, even though the recursive invocations require it, but (b) actually fails to do so, because the RHS of -like doesn't support arrays. (As an aside: since you're looking for exact strings, -eq would have been the better choice.)
Since you're passing the arguments as an array as the first and only argument, $Inputs[0] actually refers to the entire array (command name + server names), not just to its first element (the command name).
You got away with ($Inputs[0] -eq "add") - even though the entire array was compared - because the -eq operator performs array filtering if its LHS is an array, returning a sub-array of matching elements. Since add was among the elements, a 1-element sub-array was returned, which, in a Boolean context, is "truthy".
However, your attempt to weed out the command name with where {$_ -NotLike $Inputs[0]} then failed, and add was not removed - you'd actually have to compare to $Inputs[0][0] (sic).
Where {$_ -NotLike ($Inputs |Where {$_ -Notlike $Inputs[0]})} doesn't filter anything out for the following reasons:
($Inputs |Where {$_ -Notlike $Inputs[0]}) always returns an empty array, because, the RHS of -Notlike is an array, which, as stated, doesn't work.
Therefore, the command is the equivalent of Where {$_ -NotLike #() } which returns $True for any scalar on the LHS.
Passing an array as individual arguments using splatting
Argument splatting (see Get-Help about_Splatting) works with arrays, too:
> function foo { $Args.Count } # function that outputs the argument count.
> foo #(1, 2) # pass array
1 # single parameter, containing array
> $arr = #(1, 2); foo #arr # splatting: array elements are passed as indiv. args.
2
Note how an intermediate variable is required, and how it must be prefixed with # rather than $ to perform the splatting.
I'd use parameters to modify the ServerList, this way you can use a single line to both add and remove:
Function EditServerList {
param(
[Parameter(Mandatory=$true)]
[string]$ServerList,
[array]$add,
[array]$remove
)
Write-Host -ForegroundColor Green "ServerList Contains: $ServerList"
$Servers = $ServerList.split(' ')
if ($add) {
$Servers += $add.split(' ')
}
if ($remove) {
$Servers = $Servers | Where-Object { $remove.split(' ') -notcontains $_ }
}
return $Servers
}
Then you can call the function like this:
EditServerList -ServerList "Server01 Server02 Server03" -remove "Server02 Server03" -add "Server09 Server10"
Which will return:
Server01
Server09
Server10
I have strings like
$value = "1;#Mohapatra,Mrutyunjaya (ADM) 10;#Sumit Upadhyay(ADM) 11;#Naidu,Ishan(ADM)"
I want to retrieve
"Mohapatra,Mrutyunjaya (ADM)", "Sumit Upadhyay(ADM)", "Naidu,Ishan(ADM)"
from $value.
I have tried $value.Split(";#")[0]. It is returning the first parameter only. But I want all the parameters
Split your string at sequences of \s*\d+;# (optional whitespace, followed by a number, a semicolon, and a hash character), and remove empty elements from the resulting list:
$value -split '\s*\d+;#' | Where-Object { $_ }
Just FYI, if you want to declare each as a variable you can say $a,$b,$c,$d = $Value -Split (";#") and each of $a, $b, $c and $d will retain those values.
I have the following ScriptBlock defined:
[ScriptBlock]$strSb = {
param(
[Parameter(Mandatory=$false,Position=0)]
[String[]]$Modules = #('String3','String4')
)
Write-Host "Passed in params:"
foreach($m in $Modules){
Write-Host $m
}
$defaultModules = #('String3','String4')
# Add Default Modules back if not present #
foreach($module in $defaultModules){
if($Modules -notcontains $module){
$Modules += $module
}
}
Write-Host "Final set:"
# Load Dependencies #
foreach($m in $Modules){
Write-Host $m
}
}
As the parameter states in the ScriptBlock, I want to be able to pass in an array of strings. When I call $strSb.Invoke(#('String11','String12')) I receive the following:
Passed in params:
String11
Final set:
String11
String3
String4
What I expect is:
Passed in params:
String11
String12
Final set:
String11
String12
String3
String4
Why is the invoke method truncating my array to the first item entered? And how would I go about fixing it so I can pass in an array of strings?
FWIW: I'm working in v2 and v3.
The problem is that the Invoke method takes an array of arguments (kind of like commands that have an -ArgumentList parameter), so each element in your array is parsed as a separate argument. The first argument, 'String11', is assigned to the first postitional parameter, $Modules, and any subsequent arguments are discarded, since there are no more positional parameters. It doesn't matter that $Modules is declared as a string array; since each element of the argument list is a separate argument, you're setting $Modules to an array of one element.
If you use the , operator to indicate that you're passing in a single array argument, it works as intended:
$strSb.Invoke((,#('String11','String12')))
BTW, you don't really need the #, because a comma-separated list of strings is interpreted as an array by default. Not just in this particular context, but in general. So just use this:
$strSb.Invoke((,('String11','String12')))
To prove out the explanation above, try this scriptblock, which is the same except that a second parameter (creatively named $SecondParameter) is declared, and then displayed after the loop that displays the value of the first parameter:
[ScriptBlock]$strSb = {
param(
[Parameter(Mandatory=$false,Position=0)]
[String[]]$Modules = #('String3','String4'),
[String]$SecondParameter
)
Write-Host "Passed in params:"
foreach($m in $Modules){
Write-Host $m
}
Write-Host "`nSecondParameter: $SecondParameter`n"
$defaultModules = #('String3','String4')
# Add Default Modules back if not present #
foreach($module in $defaultModules){
if($Modules -notcontains $module){
$Modules += $module
}
}
Write-Host "Final set:"
# Load Dependencies #
foreach($m in $Modules){
Write-Host $m
}
}
If you then pass in the arguments as you were, $strSb.Invoke(#('String11','String12')), you get these results:
11-26-13 19:02:12.55 D:\Scratch\soscratch» $strSb.Invoke(#('String11','String12'))
Passed in params:
String11
SecondParameter: String12
Final set:
String11
String3
String4
11-26-13 19:02:29.34 D:\Scratch\soscratch»
One last tip, not directly related to the question, is that you can compact the foreach loops by using a pipelines, which is are not only more succinct but generally more efficient. Here's a compacted version of your code:
[ScriptBlock]$strSb = {
param(
[Parameter(Mandatory=$false,Position=0)]
[String[]]$Modules = ('String3','String4')
)
Write-Host "Passed in params:"
$Modules | Write-Host
$defaultModules = 'String3','String4'
# Add Default Modules back if not present #
$defaultModules | ?{$Modules -notcontains $_} | %{$Modules += $_}
Write-Host "Final set:"
# Load Dependencies #
$Modules | Write-Host
}
If I understand what you're doing, you want to take 2 arrays, concatenate them, and ensure uniqueness...
First, Since you have a [Parameter...] on your parameter, you magically get [CmdletBinding()] on the method. This means that you are automatically going to get $Modules split into multiple calls.
Second, ScriptBlock.Invoke() takes a params style array and puts them into the method as separate arguments.
The first thing I would try is to add the attribute to gather all values:
[Parameter(ValueFromRemainingArguments=$true, Position=0, Mandatory=$true)]
[String[]]$Modules
However, for the Join, you can much more easily do something like:
($modules + $defaultModules) | Select -Unique
Not sure exactly why, but it doesn't seem to like that named parameter. Seems to like $args, tho :
[ScriptBlock]$strSb = {
$Modules = $args
Write-Host "Passed in params:"
foreach($m in $modules){
Write-Host $m
}
$defaultModules = #('String3','String4')
# Add Default Modules back if not present #
foreach($module in $defaultModules){
if($Modules -notcontains $module){
$Modules += $module
}
}
Write-Host "Final set:"
# Load Dependencies #
foreach($m in $Modules){
Write-Host $m
}
}
$strSb.Invoke('String11','String12')
Passed in params:
String11
String12
Final set:
String11
String12
String3
String4
I am trying to write a value to a REG_BINARY using Powershell. I can write to the REG_BINARY if I provide the data, but I need to be able to encode what I want to write in the file so I can use system and date variables. Below is my code and error.
If you uncomment the first $data it will work.
function Convert-ToCHexString
{
param ([String] $str)
$ans = ''
[System.Text.Encoding]::ASCII.GetBytes($str) | % { $ans += "0x{0:x2}," -f $_ }
return $ans.Trim(' ',',')
}
$Folder = Convert-ToCHexString Z:\
$Username = Convert-ToCHexString $env:USERNAME
$Filename = Convert-ToCHexString \archive.pst
$key = "HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles\Outlook\0a0d020000000000c000000000000046"
#$data = 0x5a,0x3a,0x5c,0x61,0x72,0x63,0x68,0x69,0x76,0x65,0x2e,0x70,0x73,0x74
$data = $Folder + "," + $Username + "," + $Filename + ",0x00,0x00,0x00"
Set-ItemProperty -path $key -name "001f0324" -value ([byte[]]($data))
Below is the error I get:
Cannot convert value "0x5a,0x3a,0x5c,0x6a,0x62,0x79,0x65,0x72,0x73,0x5c,0x61,0x72,0x63,0x68,0x69,0x76,0x65,0x2e,0x70,0x73,0x74,0x00,0x00,0x00" to type"System.Byte[]". Error: "Cannot convert value "0x5a,0x3a,0x5c,0x6a,0x62,0x79,0x65,0x72,0x73,0x5c,0x61,0x72,0x63,0x68,0x69,0x76,0x65,0x2e,0x70,0x73,0x74,0x00,0x00,0x00" to type "System.Byte". Error: "Additional non-parsable characters are at the end of the string.""
If the property is expecting a byte array use [Text.Encoding]::Unicode.GetBytes($data). Not sure what's up with the 0x00,0x00,0x00 at the end but if you need that, just append three 0's to the end of the byte array before passing it to Set-ItemProperty.
Your function Convert-ToCHexString returns a single string with your items separated by commas.
The cast ([byte[]]($data)) that you perform will only work if your string contains only a single hex value or is an array of values it can parse this way. It won't split the string for you.
However, the GetBytes function already returns a byte array, so there's no need to convert it to a string and then back again. Also, based on your comments, it looks like outlook wants these values to be encoded as Unicode (UTF-16), which is not surprising since that's the windows default. You'll also need to null terminate your byte array (the final two 0's you see from outlook). That makes your function look like this:
function Convert-ToCHexString
{
param ([String] $str)
$ans = ''
[System.Text.Encoding]::Unicode.GetBytes($str + "`0")
}