strange script issue in Powershell - arrays

I am new to PowerShell, but not scripting.
Why does this script:
$usr = "john.doe"
$usrname = $usr -split ".", 0, "simplematch"
$fullname = upperInitial($usrname[0]) + upperInitial($usrname[1])
write-host "Hello $fullname"
function upperInitial($upperInitialString) {
return $upperInitialString.substring(0, 1).toupper() + $upperInitialString.substring(1).tolower()
}
return me just 'Hello John' and not 'Hello John Doe'?

It's not treating the second call of the upperInitial function as a function, it's treating it as a paramter of the first call to the function I think.
Either of these work:
$fullname = "$(upperInitial($usrname[0])) $(upperInitial($usrname[1]))"
write-host "Hello $fullname"
The above uses the subexpression operator $() to execute the functions within a double quoted string.
$fullname = (upperInitial($usrname[0])) + ' ' + (upperInitial($usrname[1]))
write-host "Hello $fullname"
This one combines the result of the two functions as you had intended, although I also added a space character because otherwise it was JohnDoe.

Related

Powershell reevaluate string to array

i have a number of strings like this this that i would like powershell to reevaluate\convert to an array (like what would happen if you just wrote the same code in ISE without the single quotes).
$String = '#("a value","b value","c value")'
Is there a easier way to do this than stripping the '#' & '()' out of the string and using -split?
Thanks for the help in advance.
As long as the string contains a valid expression, you can use the [scriptblock]::Create(..) method:
$String = '#("a value","b value","c value")'
& ([scriptblock]::Create($String))
Invoke-Expression would also work and in this case would be mostly the same thing.
However, the nice feature about Script Blocks, as zett42 pointed out in a comment, is that with them we can validate that arbitrary code execution is forbidden with it's CheckRestrictedLanguage method.
In example below, Write-Host is an allowed command and a string containing only 'Write-Host "Hello world!"' would not throw an exception however, assignment statements or any other command not listed in $allowedCommands will throw and the script block will not be executed.
$String = #'
Write-Host "Hello world!"
$stream = [System.Net.Sockets.TcpClient]::new('google.com', 80).GetStream()
'#
[string[]] $allowedCommands = 'Write-Host'
[string[]] $allowedVaribales = ''
try {
$scriptblock = [scriptblock]::Create($String)
$scriptblock.CheckRestrictedLanguage(
$allowedCommands,
$allowedVaribales,
$false # Argument to allow Environmental Variables
)
& $scriptblock
}
catch {
Write-Warning $_.Exception.Message
}
Another alternative is to run the expression in a Runspace with ConstrainedLanguage Mode. This function can make it really easy.
using namespace System.Management.Automation.Language
using namespace System.Collections.Generic
using namespace System.Management.Automation.Runspaces
function Invoke-ConstrainedExpression {
[CmdletBinding(DefaultParameterSetName = 'ScriptBlock')]
param(
[Parameter(ParameterSetName = 'Command', Mandatory, ValueFromPipeline, Position = 0)]
[string] $Command,
[Parameter(ParameterSetName = 'ScriptBlock', Mandatory, Position = 0)]
[scriptblock] $ScriptBlock,
[Parameter()]
[Management.Automation.PSLanguageMode] $LanguageMode = 'ConstrainedLanguage',
# When using this switch, the function inspects the AST to find any variable
# not being an assigned one in the expression, queries the local state to find
# it's value and injects that variable to the Initial Session State of the Runspace.
[Parameter()]
[switch] $InjectLocalVariables
)
process {
try {
$Expression = $ScriptBlock
if($PSBoundParameters.ContainsKey('Command')) {
$Expression = [scriptblock]::Create($Command)
}
# bare minimum for the session state
$iss = [initialsessionstate]::CreateDefault2()
# set `ContrainedLanguage` for this session
$iss.LanguageMode = $LanguageMode
if($InjectLocalVariables.IsPresent) {
$ast = $Expression.Ast
$map = [HashSet[string]]::new([StringComparer]::InvariantCultureIgnoreCase)
$ast.FindAll({ $args[0] -is [AssignmentStatementAst] }, $true).Left.Extent.Text |
ForEach-Object { $null = $map.Add($_) }
$variablesToInject = $ast.FindAll({
$args[0] -is [VariableExpressionAst] -and
-not $map.Contains($args[0].Extent.Text)
}, $true).VariablePath.UserPath
foreach($var in $variablesToInject) {
$value = $ExecutionContext.SessionState.PSVariable.GetValue($var)
$entry = [SessionStateVariableEntry]::new($var, $value, '')
$iss.Variables.Add($entry)
}
}
# create the PS Instance and add the expression to invoke
$ps = [powershell]::Create($iss).AddScript($Expression)
# invoke the expression
[Collections.Generic.List[object]] $stdout = $ps.Invoke()
$streams = $ps.Streams
$streams.PSObject.Properties.Add([psnoteproperty]::new('Success', $stdout))
$streams
}
finally {
if($ps) { $ps.Dispose() }
}
}
}
Now we can test the expression using Constrained Language:
Invoke-ConstrainedExpression {
Write-Host 'Starting script'
[System.Net.WebClient]::new().DownloadString($uri) | iex
'Hello world!'
}
The output would look like this:
Success : {Hello world!}
Error : {Cannot create type. Only core types are supported in this language mode.}
Progress : {}
Verbose : {}
Debug : {}
Warning : {}
Information : {Starting script}
DevBlogs Article: PowerShell Constrained Language Mode has some nice information and is definitely worth a read.

Adding element to array in powershell scriptblock converts array to string

I noticed odd behaviour using arrays in scriptblocks. The following code shows the problem:
$array = #("x", "y")
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$bad = {
$array += "z"
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$array
}
$good = {
$array = $array.Clone()
$array += "z"
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$array
}
& $good
& $bad
Executing the script will produce the following output:
Object[]
array
Object[]
array
x
y
z
String
System.Object
z
The scriptblock $bad does not work as I would expect. It converts the array to string, but it should simply add the element z to the array. If there is no element added, the array can be used as expected.
I noticed this behaviour in powershell 5.0 and 5.1 but not in the ISE. Is it a bug or can anyone explain this?
It's a scope issue. The variable on the left side of the assignment operation in the scriptblocks is defined in the local scope.
This statement
$array = $array.Clone()
clones the value of the global variable $array and assigns it to the local variable $array (same name, but different variable due to different scope). The local variable $array then contains a copy of the original array, so the next statement
$array += "z"
appends a new element to that array.
In your other scriptblock you immediately append a string to the (local) variable $array. In that context the local variable is empty, so $array += "z" has the same effect as $array = "z", leaving you with a variable containing just the string "z".
Specify the correct scope and you'll get the behavior you expect:
$array = #("x", "y")
$not_bad = {
$script:array += "z"
Write-Host "$($script:array.GetType().Name)"
Write-Host "$($script:array.GetType().BaseType)"
$script:array
}
& $not_bad
Beware, however, that this will actually modify the original array in the global/script scope (your $good example leaves the original array unchanged).
I'm not sure if I would consider this behavior a bug, but it's definitely a gotcha.
I would like to post my preferred solution which bases on Ansgars explanation:
$array = #("x", "y")
$not_bad = {
$array = $array + "z"
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$array
}
& $not_bad
Important is the assignment to the local variable (or better to create a local variable) before adding further elements. A simple
$array = $array
would do, but this line may be confusing.

using variable in PowerShell array element

How to I add variables data inside the string element of an array? If I do $s.Length, the output is 1 instead of 2.
$IPAddress = '192.168.1.1'
[string[]]$s = (
'https://google.com/' + $IPAddress + '/hostname',
'https://google.com/' + $IPAddress + '/DNS'
)
foreach ($element in $s) {
Write-Host $element
}
The simplest way to accomplish what you are trying (string expansion) is:
$s = "https://google.com/$IPAddress/hostname",
"https://google.com/$IPAddress/DNS"
By using double quotes it will automatically expand $IPAddress within the strings. This works best when the variable is a string, as more complex objects may not perform as expected. If you need to reference a property of an object in this manner you will need to wrap it in $(), for example "Hello $($User.Name)!" to expand the Name property of the $User object.
$s contains a single string because of the way you define the array. The concatenation operator (+) has a weaker precedence than the array construction operator (,). Because of that a statement
'foo' + $v + 'bar', 'foo' + $v + 'baz'
actually works like this:
'foo' + $v + #('bar', 'foo') + $v + 'baz'
Due to the string concatenation operation the array is mangled into a space-separated string (the separator is defined in the automatic variable $OFS), resulting in this:
'foo' + $v + 'bar foo' + $v + 'baz'
To avoid this behavior you need to either put the concatenation operations in grouping expressions:
$s = ('https://google.com/' + $IPAddress + '/hostname'),
('https://google.com/' + $IPAddress + '/DNS')
or inline the variables (requires double-quoted strings):
$s = "https://google.com/${IPAddress}/hostname",
"https://google.com/${IPAddress}/DNS"
You could also use the format operator, but that requires grouping expressions as well:
$s = ('https://google.com/{0}/hostname' -f $IPAddress),
('https://google.com/{0}/DNS' -f $IPAddress)
Side note: Casting the variable to [string[]] is optional. Using the comma operator will give you an array even without an explicit cast.
TheMadTechnician beat me to it by a few seconds, but if you prefer to construct the string expressions explicitly, wrap them in parens:
$IPAddress = '192.168.1.1'
[string[]]$s = (
('https://google.com/'+$IPAddress+'/hostname'),
('https://google.com/'+$IPAddress+'/DNS'))
foreach ($element in $s)
{
Write-Host $element
}
The parens force the expressions inside to be evaluated first.

Add new set of values to ArrayList

So I have the following ArrayList stored in $var:
ip_prefix region string
0.0.0.0/24 GLOBAL Something
0.0.0.0/24 GLOBAL Something
0.0.0.0/24 GLOBAL Something
0.0.0.0/24 GLOBAL Something
I need to add a row to this however the following code returns an error:
$var.add("127.0.0.1/32", "GLOBAL", "something")
Error:
Cannot find an overload for "Add" and the argument count: "3".
At line:1 char:1
+ $awsips.add("127.0.0.1/32", "GLOBAL", "SOMETHING")
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodCountCouldNotFindBest
I'm sure it's something simple I have to adjust, however Google searches had me going around in circles.
$var = New-Object System.Collections.ArrayList
$var.Add(#{"ip_prefix" = "0.0.0.0/24"; "region" = "GLOBAL"; string = "Something"})
$var.Add(#{"ip_prefix" = "127.0.0.1/32"; "region" = "GLOBAL"; string = "SOMETHING"})
$var
$var | %{ Write-Output "$($_.ip_prefix), $($_.region), $($_.string)" }
Or:
$var = #()
$var += #{"ip_prefix" = "0.0.0.0/24"; "region" = "GLOBAL"; string = "Something"}
$var += #{"ip_prefix" = "127.0.0.1/32"; "region" = "GLOBAL"; string = "SOMETHING"}
Should do the job
$obj = New-Object PSObject -Property #{
ip_prefix = "0.0.0.0/24"
region = "GLOBAL"
string = "Something"
}
$var+= $obj
Your output suggests that your array list contains custom objects with properties ip_prefix, region, and string.
You therefore need to add a single object with the desired property values to your array list.
By contrast, you attempted to add 3 indvividual elements to the array list, which is not only conceptually wrong, but also fails syntactically, given that the .Add() method only accepts a single argument (technically, there is a method for adding multiple items, .AddRange()).
In PSv3+, syntax [pscustomobject]#{...} constructs a custom object from a hashtable literal with the definition order of the entries preserved.
$null = $var.Add(
[pscustomobject] #{ ip_prefix="127.0.0.1/32"; region="GLOBAL"; string="something" }
)
Note how $null = ... is used to suppress the .Add() method's output (the index at which the item was inserted).
SQLAndOtherStuffGuy's answer is on the right track, but beware that $var += ... silently replaces the array list stored in $var with a regular PowerShell array ([System.Object[]]).

How can I get the value from a multidimentional array in Powershell

I am trying to pull some file paths from an array and use them to test if a folder is located on serval machines.
I seem to get an error pulling the value from the 2-dimentional array and putting it in a variable.
Here is a striped down, very basic version of my script.
$PCS = "PC1","PC2","PC3"
$locations= #("System32","Windows\System32"),
("Public","Users\Public")
ForEach($PC in $PCS){
$i=0
Do
{
$fullpath = "\\" + $PC + "\C$\" + "$locations[$i][1]"
test-path $fullpath
$i++
}
While ($i -le ($locations.length-1) )
}
However when I use $fullpath to test if the folder is there, I get false, and upon further investigation, the actual value of $fullpath is:
\\PC1\C$\System.Object[] System.Object[] System.Object[] System.Object[]
How can I get the value from the array, so I can use it as a filepath location?
$fullpath = "\\" + $PC + "\C$\" + "$($locations[$i][1])"
or
$fullpath = "\\" + $PC + "\C$\" + $locations[$i][1]
As arco444 points out, your code as posted seems incomplete, but I think your issue will be fixed by above.
Explanation
When you use "$locations[$i][1]" it only interprets $locations as a variable within the string. Using . to access properties or [] to access elements is interpreted as literal characters.
So in this case, you can use the second option (don't surround it in quotes) if the result of the lookup is already a string or can be coerced into one.
In the general sense, the first option uses $(...) which is a sub-expression. The ... can be any code, including entire pipelines and function calls and such.

Resources