Casting in PowerShell. Weird syntax - powershell

Reading XML from a file into a variable can be done like this:
[xml]$x = Get-Content myxml.xml
But why not:
$x = [xml]Get-Content myxml.xml
Which gives:
Unexpected token 'Get-Content' in expression or statement.
At line:1 char:20
+ $x=[xml]get-content <<<< myxml.xml
+ CategoryInfo : ParserError: (get-content:String) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : UnexpectedToken
That is, why is the cast operation done on the left-hand side of the equals sign? Typically in programming languages the casting is done on the right-hand side like (say) in Java:
a = (String)myobject;

$x = [xml](Get-Content myxml.xml)

In PowerShell everything is an object. That includes the cmdlets. Casting is the process of converting one object type into another.
What this means for casting is that, like in math, you can add in unnecessary brackets to help you clarify for yourself what exactly is happening. In math, 1 + 2 + 3 can become ((1 + 2) + 3) without changing its meaning.
For PowerShell the casting is performed on the next object (to the right), so $x = [xml] get-content myxml.xml becomes $x = ([xml] (get-content)) myxml.xml. This shows that you are trying to cast the cmdlet into an xml object, which is not allowed.
Clearly this is not what you are trying to do, so you must first execute the cmdlet and then cast, AKA $x = [xml] (get-content myxml.xml). The other way to do it, [xml] $x = get-content myxml.xml, is declaring the variable to be of the type xml so that whatever gets assigned to it (AKA the entire right-hand side of the equal sign) will be cast to xml.

Related

Convert a string variable into an integer

I'm trying to convert my $file_data variable into an integer, the bit of code below grabs the number of a computer on my system held in a directory. However when run in PowerShell I get he below error even though the variable is a number.
The '++' operator works only on numbers. The operand is a 'System.String'.
At D:\pscript\Intune.ps1:7 char:26
+ For ($file_data -le 130; $file_data++ ){
+ ~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : OperatorRequiresNumber
I'm not sure where I'm going wrong on this any help would be amazing. :)
Get-Content D:\pscript\temp\Directory.txt
$file_data = Get-Content D:\pscript\temp\Directory.txt
For converting string to integer, you can typecast it or declare it at the first point.
[int] $file_data = Get-Content D:\pscript\temp\Directory.txt
However, if this needs to work, Directory.txt should have number which can fit into the category of an integer.

Unexpected Token in statement - PowerShell

I'm have a script which runs just fine at work but when I run the same script at home to build on it, the script fails with the following error:
Unexpected token '(' in expression or statement.
At C:\Users\MyAccount\Documents\Test.ps1:34 char:54
+ $Log = [Microsoft.VisualBasic.Interaction]::InputBox ('Enter the Even ...
Unexpected token '(' in expression or statement.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : UnexpectedToken
Here is the code which I am trying to run:
$log = [Microsoft.VisualBasic.Interaction]::InputBox ('Enter the Event ID you are looking for.', 'Event ID Query')
cd C:\temp\winevent_logs
$List = Get-ChildItem | Where-Object {$_.FullName -match "security"} | Select-Object
Get-WinEvent -FilterHashtable #{path=$List;id=$Log} | Measure-Object | Select-Object count
I'm guessing there's a configuration issue with my home system which is not interpreting Visual Basic operations, but I'm not sure where to start with this.
You have a syntax problem (which means that your problem is unrelated to which machine you run your code on):
When calling .NET methods in PowerShell, there must be no space between the method name and the opening parenthesis , (, unlike in C#, where this is allowed.
Using the [string] type's .ToUpper() instance method as a simple example:
# OK - no space between 'ToUpper' and '('
PS> 'foo'.ToUpper()
FOO
# BROKEN - space before '('
PS> 'foo'.ToUpper ()
ParserError:
Line |
1 | 'foo'.ToUpper ()
| ~
| Unexpected token '(' in expression or statement.
Note:
With the mistaken space, PowerShell interprets what follows the space ((), in this case) as a separate expression, which breaks the syntax, given that a space-separated list of expressions (that aren't connected with operators) is invalid; e.g., 1 2 triggers the same error.
As an aside: omitting () in what would otherwise be a method call - e.g., 'foo'.ToUpper - serves a useful purpose in PowerShell: it shows reflection information, namely the signatures (list of parameters and their types) of the method and its overloads; e.g.:
OverloadDefinitions
-------------------
string ToUpper()
string ToUpper(cultureinfo culture)
You put a space between InputBox and ('Enter. Remove the space so it gets executed as a method with arguments:
$log = [Microsoft.VisualBasic.Interaction]::InputBox('Enter the Event ID you are looking for.', 'Event ID Query')
Otherwise what you are trying to tell PowerShell to do is return the method object itself, and it doesn't know what to do with the resulting subexpression in parentheses.

How to assign and reference environment variables containing square brackets in Powershell

When the PSDrive is not specified, the following works:
${[foo]}="bar"
echo ${[foo]}
But the following does not work
$env:${[foo]}="bar"
At line:1 char:1
+ $env:${[foo]}="bar"
+ ~~~~~
Variable reference is not valid. ':' was not followed by a valid variable name character. Consider using ${} to delimit the name.
At line:1 char:6
+ $env:${[foo]}="bar"
+ ~~~~~~~~~~~~~~
Unexpected token '${[foo]}="bar"' in expression or statement.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : InvalidVariableReferenceWithDrive
${env:[foo]}="bar"
Cannot find path 'env:[foo]' because it does not exist.
At line:1 char:1
+ ${env:[foo]}="bar"
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (env:[foo]:String) [], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound
The following works, though I am curious if there's short hand syntax for it:
Set-Item -LiteralPath env:${[foo]} -Value "bar"
Get-Item -LiteralPath env:${[foo]} | % {$_.Value}
However the following does not work:
Set-Item -LiteralPath env:${[foo]2} -Value "bar"
Set-Item : Cannot process argument because the value of argument "name" is null. Change the value of argument "name" to a non-null value.
At line:1 char:1
+ Set-Item -LiteralPath env:${[foo]2} -Value "bar"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:String) [Set-Item], PSArgumentNullException
+ FullyQualifiedErrorId : SetItemNullName,Microsoft.PowerShell.Commands.SetItemCommand
Written as of PowerShell Core 6.2.0
The reason is that PowerShell treats the following:
${<drive>:<name>}
as if you had specified:
Get-Content -Path <drive>:<name> # or, with assignment, Set-Content -Path ...
This notation - though often used with the Env: drive (e.g., $env:Path) - is little-known as a general paradigm named namespace variable notation, which is explained in this answer.
The problem is the use of -Path rather than -LiteralPath, because -Path interprets its argument as a wildcard expression.
Therefore, the [foo] in ${env:[foo]} - rather than being used as-is - is interpreted as a wildcard expression that matches a single character that is either f or o ([foo] is a character set or range ([...]) that matches any one of the (distinct) characters inside - see about_Wildcards).
On assigning to ${env:[foo]}, the logic of Set-Content -Path requires that a wildcard-based path resolve to something existing, even though you're generally not required to explicitly create environment variables; e.g., ${env:NoSuchVarExistsYet} = 'new' works just fine.
Workaround:
Use double(!)-`-escaping of the wildcard metacharacters:
# Namespace variable notation only works with if you
# double(!)-backtick-escape the wildcard metacharacters:
# Assign to / implicitly create env. var '[foo]'
${env:``[foo``]} = 'bar'
# Get its value.
${env:``[foo``]}
Note:
Escaping shouldn't be required at all, because there is no good reason to treat paths that conceptually identify a given, known item as wildcard expressions - see GitHub issue #9225.
That double `-escaping is needed is an added quirk - see GitHub issue #7999.
Another workaround - one that doesn't involve escaping - is to use
Set-Content -LiteralPath env:[foo] bar and Get-Content -LiteralPath env:[foo], but that is both verbose and slow.
As for the other syntax variations you tried:
$env:${[foo]}="bar"
Since your variable reference isn't {...}-enclosed as a whole (except for the initial $), the token that follows the : is only allowed to contain characters that do not require escaping - and $, { and } all violate that rule.
{...}-enclosing the entire path - ${env:[foo]} - solves the syntax problem, but runs into the problem detailed above.
Set-Item -LiteralPath env:${[foo]} -Value "bar"
This does not work in general, because string expansion is applied beforehand here - it is as if you had passed "env:${[foo]}": the reference to a (regular) variable named ${[foo]} is expanded (replaced with its value) and in effect appended to literal env:, before handing the result to Set-Item.
If such a regular variable doesn't exist, what Set-Item sees is just env: (because non-existent variables default to $null, which becomes the empty string in a string context), which causes an error due to the lack of variable name.
By contrast, the following would set an environment variable named unrelated instead:
# Create a regular variable literally named '[foo]'.
${[foo]} = 'unrelated'
# !! The following sets env:unrelated, i.e., env. var 'unrelated',
# !! due to the string expansion that is performed on the -LiteralPath
# !! argument up front.
Set-Item -LiteralPath env:${[foo]} bar
$env:unrelated # -> 'bar'
The same applies to Get-Item -LiteralPath env:${[foo]} and
Set-Item -LiteralPath env:${[foo]2} -Value "bar".

Cannot find an overload for "ToString" and the argument count: "1"

i'm not understanding what i'm doing wrong here since i seem to do the same thing but only one works.
i have a text file with a number list that i want to process (round the values):
39.145049
40.258140
41.400803
42.540093
43.664530
and here my script:
$a = get-content "input.txt"
$b = $a -join ','
$b | % {$_.ToString("#.###")}
this results in the following error:
Cannot find an overload for "ToString" and the argument count: "1".
At D:\script.ps1:9 char:9
+ $b | % {$_.ToString("#.###")}
+ ~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodCountCouldNotFindBest
however if i take the result after joining which is:
39.145049,40.258140,41.400803,42.540093,43.664530
and create the following script:
$b = 39.145049,40.258140,41.400803,42.540093,43.664530
$b | % {$_.ToString("#.###")}
it works just fine and outputs:
39.145
40.258
41.401
42.54
43.665
where am i going wrong on this one?
This happens as the inputs are not of the same type.
$b1 = $a -join ','
$b2 = 39.145049,40.258140,....
$b1.GetType().Name
String
$b2.GetType().Name
Object[]
As the input in the first case is a single string, foreach loop doesn't process it as a collection of decimal values but a single string. Thus,
$b | % {$_.ToString("#.###")}
Is going to do (as pseudocode):
'39.145049,40.258140,41.400803,42.540093,43.664530'.ToString("#.###")
Whilst the array version is doing
39.145049.ToString("#.###")
40.258140.ToString("#.###")
41.400803.ToString("#.###")
Powershell's able to figure out in the later case that the values are numbers. In the first case, it's just a string and thus the automatic conversion doesn't work.
What actually works in the first case is to cast the values as nubmers. Like so,
$a | % {$([double]$_).ToString("#.###")}
39,145
40,258
41,401
42,54
43,665

Why doesn't $hash.key syntax work inside the ExpandString method?

The following Powershell script demonstrates the issue:
$hash = #{'a' = 1; 'b' = 2}
Write-Host $hash['a'] # => 1
Write-Host $hash.a # => 1
# Two ways of printing using quoted strings.
Write-Host "$($hash['a'])" # => 1
Write-Host "$($hash.a)" # => 1
# And the same two ways Expanding a single-quoted string.
$ExecutionContext.InvokeCommand.ExpandString('$($hash[''a''])') # => 1
$ExecutionContext.InvokeCommand.ExpandString('$($hash.a)') # => Oh no!
Exception calling "ExpandString" with "1" argument(s): "Object reference not set to an instance of an object."
At line:1 char:1
+ $ExecutionContext.InvokeCommand.ExpandString('$($hash.a)')
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : NullReferenceException
Anyone know why the $hash.key syntax works everywhere but inside explicit expansion? Can this be fixed, or do I have to suck it up and live with the $hash[''key''] syntax?
I use this method, since this bug exists in v4 (not in v5)
function render() {
[CmdletBinding()]
param ( [parameter(ValueFromPipeline = $true)] [string] $str)
#buggy
#$ExecutionContext.InvokeCommand.ExpandString($str)
"#`"`n$str`n`"#" | iex
}
Usage for your example:
'$($hash.a)' | render
The ExpandString api is not exactly meant for use from PowerShell scripts, it was added more for C# code. It's still a bug that your example doesn't work (and I think it's been fixed in V4), but it does mean there is a workaround - one that I recommend for general use.
Double quoted strings effectively (but not literally) call ExpandString. So the following should be equivalent:
$ExecutionContext.InvokeCommand.ExpandString('$($hash.a)')
"$($hash.a)"
I was trying to store text that prompts the user in a text file. I wanted to be able to have variables in the text file that are expanded from my script.
My settings are stored in a PSCustomObject called $profile and so in my text I was trying to do something like:
Hello $($profile.First) $($profile.Last)!!!
and then from my script I was trying to do:
$profile=GetProfile #Function returns PSCustomObject
$temp=Get-Content -Path "myFile.txt"
$myText=Join-String $temp
$myText=$ExecutionContext.InvokeCommand.ExpandString($myText)
which of course left me with the error
Exception calling "ExpandString" with "1" argument(s): "Object
reference not set to an instance of an object."
Finally I figured out I only needed to to store the PSCustomObject values I want in regular old variables, change the text file to use those instead of the object.property version and everything worked nicely:
$profile=GetProfile #Function returns PSCustomObject
$First=$profile.First
$Last=$profile.Last
$temp=Get-Content -Path "myFile.txt"
$myText=Join-String $temp
$myText=$ExecutionContext.InvokeCommand.ExpandString($myText)
And in the text I changed to
Hello $First $Last!!!