Powershell get path value from xml file - powershell

I'm a beginner of powershell and I'm having trouble reading the path value of an xml file.
Settings.xml
<?xml version="1.0"?>
<Configuration>
<EmailSettings>
<SMTPServer>blabla</SMTPServer>
<SMTPPort>blabla</SMTPPort>
<Path>Repository\Excel</Path>
</EmailSettings>
</Configuration>
To read the data in the XML file I do this way
$ScriptPath = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
[xml]$ConfigFile = Get-Content "$ScriptPath\Settings.xml"
The problem is that if I display the extracted value it is shown correctly, while if I concatenate it with another value, for example to obtain a complete path, I get this
Write-Host $ScriptPath
--> c:\script
write-host $ConfigFile.Configuration.EmailSettings.Path
--> Repository\Excel
write-host $ScriptPath\$ConfigFile.Configuration.EmailSettings.Path
--> c:\script\System.Xml.XmlDocument.Configuration.EmailSettings.Path
How do we do to get the value (in string format??) to be able to concatenate to other variables?
Thank you

You need $(...), the subexpression operator, in order to evaluate an expression such as $ConfigFile.Configuration.EmailSettings.Path inside an expandable string.
By contrast, a simple variable reference such as $ScriptPath is fine without the $(...):
Write-Host "$ScriptPath\$($ConfigFile.Configuration.EmailSettings.Path)"
Note that I've additionally added explicit double-quoting, to make the command more robust - see below for details.
As an aside: Write-Host is generally the wrong tool to use, unless the intent is explicitly to write to the display only, bypassing PowerShell's output streams.
Generally, unquoted tokens that aren't single expressions are implicitly treated like expandable strings, i.e., as if they had been passed in double quotes, and by and large the same rules apply.
However, in the absence of explicit quoting there are additional pitfalls, such as tokens that start with - or the need to escape additional characters, so always double-quoting expandable tokens is a good habit to form.
How PowerShell parses unquoted tokens in argument mode in general is a complex topic - see this answer.

Related

Powershell Get-Content - variable File

I need to change the encoding of a file from UTF8-BOM to UTF8 different files where the date is different. I have a variable with the Data that I need but I have problems passing that variable to the Get-Content param.
$Today = Get-Date -Format "yyyyMMdd"
Get-Content 'D:\Folder_1\Test_file.'+$(${Today})+'.csv' | Set-Content -Encoding Ascii 'D:\Folder_2\Test_file.'+$(${Today})+'.csv'
The file names are "Test_file.20220923.csv" for example.
The error:
Get-Content : A positional parameter cannot be found that accepts argument '+20220923+.
'D:\Folder_1\Test_file.'+$(${Today})+'.csv' is an expression (a string concatenation via the + operator).
In order to pass an expression as an argument to a command, you must enclose it in (...), the grouping operator.[1]
Therefore (applies analogously to your Set-Content call):
Get-Content ('D:\Folder_1\Test_file.'+${Today}+'.csv')
Note that I've omitted the unnecessary use of $(...), the subexpression operator, which is typically only needed inside expandable (double-quoted) string ("..."), and then only for embedding expressions or commands (a variable reference such as $Today by itself can be embedded as-is).
The equivalent command using an expandable string:
Get-Content "D:\Folder_1\Test_file.${Today}.csv"
As for what you tried:
# WRONG: Passes *two* arguments.
Get-Content 'D:\Folder_1\Test_file.'+$(${Today})+'.csv'
Even though 'D:\Folder_1\Test_file.'+$(${Today})+'.csv' contains no spaces, PowerShell breaks this compound token into two arguments, verbatim D:\Folder_1\Test_file. and (for instance) verbatim +20220923+.csv.[2]
It is the latter, i.e. the extra positional argument causes Get-Content to report an error, because it only supports one positional argument (which binds to its -Path parameter).
[1] $(...) is often seen in practice instead, but $(...) is only ever needed in two cases: (a) to embed entire statement(s), notably loops and conditionals, in another statement, and (b) to embed an expression, command, or statement(s) inside "...", an expandable (interpolating) string. Just (...) is enough to pass a single command or expression as a command argument. While not likely, the unnecessary use of $(...) can have side effects - see this answer.
[2] In case you're wondering how a compound token such as 'D:\Folder_1\Test_file.'+${Today}+'.csv' is parsed and results in these two arguments, see this answer.

New-MailboxExportRequest quote issue

I'm trying to get the following command into a program I am developing in PowerShell studio, I'm having some issues figuring out the escaping sequence here, and testing takes forever since I have to import the PST just to view the data :(
Consider the following command which works correctly from the shell...
New-MailboxExportRequest -Mailbox user#domain.com -Name JobName -IncludeFolders "#Inbox#/*","#SentItems#" -ContentFilter {(Received -gt "06/10/2022") -and (Sent -gt "06/10/2022")} -FilePath "\\Server\Folder\MyPST.pst"
I am using variables inside program designer, so my actual code looks like this...
New-MailboxExportRequest -Mailbox $mailbox -Name $jobname -IncludeFolders $IncludeFolders -ContentFilter {(Received -gt "$RecDate") -and (Sent -gt "$SentDate")} -FilePath "$FilePath"
Basically I need to get the first code sample working using variables, however the -IncludeFolders and -ContentFilter parameters require some escaping I can't seem to figure out. Any and all help is much appreciated.
tl;dr:
You need to enclose your -ContentFilter argument in "..." overall in order to support embedding variable values, which you can themselves enclose in embedded '...' quoting:
-ContentFilter "(Received -gt '$RecDate') -and (Sent -gt '$SentDate')"
No special syntax considerations apply with respect to using a variable with -IncludeFolders: Just make sure that variable $IncludeFolders contains an array of strings identifying the target folders; applied to your example:
$IncludeFolders = '#Inbox#/*', '#SentItems#'
If the list of folders must be parsed from user input provided as a single string ($textbox5.text in this example) do the following to convert this single string to an array of names:
# If $textbox5.text contains string '#Inbox#/*, #SentItems#',
# the result is the same as above.
$IncludeFolders = ($textbox5.text -split ',').Trim()
Read on for background information and caveats re -ContentFilter.
Note:
The -ContentFilter information below also applies to other Exchange cmdlets that accept filters, such as Get-Recipient with its -Filter parameter.
It also applies analogously to the -Filter parameter used with AD (Active Directory) cmdlets such as Get-ADUser, although the situation there is complicated by the fact that the AD provider does support evaluation of PowerShell variables, but only in simple cases - see this answer.
New-MailboxExportRequest's -ContentFilter expects a string argument, not a script block ({ ... }), which you've used in your question.
While you can situationally get away with a script block, it is best avoided for two reasons:
When a script block is used as a string (converted to one), string interpolation (embedding the values of variables in the string) is not supported - a script block stringifies to its verbatim content (sans { and }).
Conceptually, use of a script block can give the mistaken impression that an arbitrary piece of PowerShell code may be passed, which is not the case: -ContentFilter supports a domain-specific syntax only that only emulates a limited set of PowerShell features, called OPath filter syntax.
Notably, evaluation of PowerShell variables is not supported - their values must be embedded in the string up front, by PowerShell, using string interpolation.
Therefore:
It's best to use a string argument with -ContentFilter to begin with.
Since you do need to embed variable values, you need string interpolation, via an expandable (double-quoted) string ("..."), inside of which you may quote values with '...' for syntactical convenience.
In case there is no need to embed variable values, use a verbatim (single-quoted) string ('...'), inside of which you may quote values with "...", as in your question. (In effect, this is the equivalent of the (ill-advised) use of { ... }).
Applied to your example: The following embeds the (stringified) values of variables $RecDate and $SentDate in your -ContentFilter argument:
# Note the use of "..." for the outer quoting, and
# '...' for the embedded quoting.
-ContentFilter "(Received -gt '$RecDate') -and (Sent -gt '$SentDate')"
Caveats:
If the values of the variables to embed themselves contain ', use escaped embedded "..." quoting instead; e.g., `"$var`"
If a value could contain ' and/or " and you don't know which, you'd have to use an extra layer of up-front string interpolation on the PowerShell side to escape one of them, depending on the embedded quoting character chosen, using $(...), the subexpression operator, using a -replace operation:
In this escaping you must technically satisfy the OPath filter syntax requirements; the linked help OPath topic suggests that, like in PowerShell, '' can be used to escape ' inside a '...' string; e.g.: -ContentFilter "Body -like '$($var -replace "'", "''")*'"
While, as stated, OPath filters do not support evaluating PowerShell variable references in general, the following automatic variables - which are conceptually constants - are recognized (the linked help topic calls them system values):
$true, $false
$null
Therefore, if you use "..." for the outer quoting (for string interpolation), the $ in these variables must be escaped with ` (the so-called backtick, PowerShell's escape character), to prevent PowerShell from replacing these variable references with their values up front:
`$true, `$false
`$null

How to list contents of a specific directory in powershell?

I'm rather baffled by Powershell in general. Very weird. Anyway, I've read:
Powershell's equivalent to Linux's: ls -al
so I now know how to list the contents of the current directory. But how can I list the contents of an arbitrary directory?
Specifically,
How do I type in the path I want to check?
in Windows, \ is the directory separator; but it's also an escape char in most languages. What do I do with it?
Do I need single-quotes? Double-quotes? No-quotes?
Where do I place the argument relative to the switches? After, like I'm used to, or rather before?
If I want to use an environment variable, or a powershell variable, as part of the path to list - how do I do that?
General PowerShell information and examples:
PowerShell-native commands, including user-authored ones that opt in, have standardized parameter syntax and binding rules, so the following, focused on Get-ChildItem, applies generally:
See the help topic for Get-ChildItem, which describes the command's purpose, syntax, and individual parameters, along with showing examples.
If you have offline help installed (true by default in Windows PowerShell; installable on demand via Update-Help in PowerShell (Core) 7+), you can also print the examples with Get-Help -Example Get-ChildItem | more
As for how to generally read the notation describing the supported parameters listed under the SYNTAX heading of cmdlet help topics, which PowerShell calls syntax diagrams, see the conceptual about_Command_Syntax help topic.
Offline, you can print the syntax diagrams for PowerShell-native commands with standard switch -? or by passing the command name to Get-Command -Syntax (Get-ChildItem -? or Get-Command -Syntax Get-ChildItem). To also access the help text offline, you may have to install local help first, as described above.
Examples:
# The following commands all pass "\" (the [current drive's] root dir)
# to the -Path parameter.
# Note:
# * "\" is NOT special in PowerShell, so it needs no escaping.
# PowerShell allows use of "/" instead even on Windows.
# * -Path arguments are interpreted as wildcard patterns, whether
# quoted or not. You may pass *multiple* paths / patterns.
# * Switch -Force - which, as all switches - can be placed anywhere
# among the arguments, requests that *hidden* items be listed too.
Get-ChildItem -Path \ -Force
Get-ChildItem \ -Force # ditto, with *positional* param. binding
'\' | Get-ChildItem # ditto, via the pipeline.
Get-ChildItem / -Force # ditto, with "/" as alternative to "\"
# To force interpretation as a *literal* path - which matters for
# paths that contain "[" chars. - use -LiteralPath.
# Here too you may pass *multiple* paths.
Get-ChildItem -LiteralPath \ -Force
# Quoting is needed for paths with spaces, for instance.
# Single-quoting treats the string *verbatim*:
Get-ChildItem 'C:\path\to\dir with spaces'
# Double-quoting *expands* (interpolates) variable references
# and subexpressions.
Get-ChildItem "$HOME\dir with spaces"
# A variable alone can be used as-is, without double-quoting, even
# if its value contains spaces.
Get-ChildItem $HOME
To answer your specific questions, for readers who come from POSIX-compatible shells such as Bash:
How do I type in the path I want to check?
Get-ChildItem offers two ways to specify one or more input paths, as most file-processing cmdlets in PowerShell do:
-Path accepts one or more names or paths that are interpreted as a wildcard pattern.
Note that - unlike in POSIX-compatible shells such as Bash - it does not matter whether the path is unquoted or not; e.g., Get-ChildItem -Path *.txt, Get-ChildItem "*.txt", and Get-ChildItem '*.txt' are all equivalent; more on that below (note the incidental omission of -Path in the latter two calls, which makes "*.txt" and '*.txt' bind positionally to the first positional parameter, -Path).
-LiteralPath accepts one or more names or paths that are assumed to refer to existing file-system items literally (verbatim).
Given that literal paths on Unix-like platforms usually do not contain * and ? characters and on Windows cannot, use of -LiteralPath for disambiguation is usually only necessary for paths that contain [ (and possibly also ]), given that PowerShell's wildcard pattern language also supports character sets and ranges (e.g. [ab] and [0-9]).
Binding to -LiteralPath via an argument requires explicit use of -LiteralPath, i.e. use of a named argument; e.g., Get-ChildItem -LiteralPath foo
However, supplying System.IO.FileInfo and/or System.IO.DirectoryInfo instances (such as output by (another) Get-ChildItem or Get-Item call) via the pipeline DOES bind to -LiteralPath, as explained in this answer.
in Windows, \ is the directory separator; but it's also an escape char in most languages. What do I do with it?
In PowerShell \ is not an escape character, so \ instances are treated as literals and do not require escaping; it is `, the so-called backtick that serves as the escape character in PowerShell - see the conceptual about_Special_Characters help topic.
Note, however, that PowerShell generally allows you to use \ and / in paths interchangeably, so that, e.g., Get-ChildItem C:/Windows works just fine.
Where do I place the argument relative to the switches? After, like I'm used to, or rather before?
Note:
All parameters have names in PowerShell - that is, there is no POSIX-like distinction between options (e.g. -l and operands (value-only arguments, such as the / in ls -l /).
A command may declare parameters that may also be passed by value only, positionally, meaning that prefixing the value with the parameter name is then optional (e.g., Get-Path / as shorthand for Get-Path -Path /).
Only parameters that require a value (argument) can potentially be passed as values only - depending on the parameter declaration of the target command - in which case their order matters:
Value-only arguments are called positional arguments, and they bind in order to those parameters of the target command that are declared as positional, if any - see this answer for how to discover which of a given command's parameters are positional ones.
Prefixing a value by its target parameter (e.g., -LiteralPath /some/path) makes it a named argument.
A switch (flag) in PowerShell, such as -Force, is a special parameter type - Boolean in nature - that by definition requires passing it as a named argument, typically without a value (though you can attach a value, e.g. -Force:$true or -Force:$false - note that : is then required to separate the parameter name from its value; see this answer for details).
As an aside: Unlike POSIX-compliant utilities, PowerShell does not support parameters with optional values of any other type - see this answer.
Since PowerShell allows named arguments to be specified in any order, the implication is that you're free to place by-definition-named switch arguments such as -Force anywhere on the command line.
In short: Order only matters among positional (unnamed) arguments in PowerShell.
See the conceptual about_Parameters help topic for more information.
Do I need single-quotes? Double-quotes? No-quotes?
A path (parameter value in general) needs quoting:
if it contains PowerShell metacharacters, notably spaces; e.g. C:\path\to\foo needs no quoting, whereas C:\path with spaces\to\foo does.
if it starts with a subexpression ($(...)), in which case you need double-quoting, i.e. "..." (see below) - though you may choose to always use "..."-quoting for paths involving subexpressions or variable references.
Note that PowerShell has no concept that is the equivalent of the so-called shell expansions in POSIX-compatible shells such as Bash; therefore, whether a given argument is quoted or not makes no semantic difference (assuming it doesn't require quoting); as noted above, *.txt, '*.txt' and "*.txt" are all equivalent, and it is the target command, not PowerShell itself, that interprets the pattern - see this answer for more information.
If quoting is needed:
Verbatim (single-quoted) strings ('...') treat their content verbatim
Expandable (double-quoted) strings ("...") perform string interpolation ("expand" embedded variables and subexpressions, i.e replace them with their values).
If I want to use an environment variable, or a powershell variable, as part of the path to list - how do I do that?
To use such variables as-is, no quoting is needed (even if the values contain spaces):
# PowerShell variable:
Get-ChildItem -LiteralPath $HOME
# Environment variable, e.g. on Windows:
Get-ChildItem -LiteralPath $env:USERPROFILE
To make a variable (or expression) part of a larger string, embed it in an expandable (double-quoted) string ("..."); e.g.:
Get-ChildItem -LiteralPath "$HOME/Desktop"
Note:
Embedding the output from expressions or commands requires use of $(...), the subexpression operator; e.g. Get-ChildItem "$(Get-Variable -ValueOnly Home)/Desktop"; for a complete overview of PowerShell's expandable strings (string interpolation), see this answer.
Situationally, such as in the example above, omitting the "..." quoting would work too - see this answer for more information.
Additionally and alternatively, PowerShell allows you to use (...), the grouping operator to pass the result of arbitrary expressions and commands as arguments; the following is the equivalent of the command above:
Get-ChildItem -LiteralPath ($HOME + '/Desktop')
Alternatively, using the Join-Path cmdlet:
Get-ChildItem -LiteralPath (Join-Path $HOME Desktop)

PowerShell safe expansion of non printing characters in arbitrary strings

I have data in an XML file, that will eventually be used as a Registry path, which MAY contain non printing characters (for example when copying the path from a web site into the XML). I want to validate the data and throw a specific error if non printing characters are found.
In Powershell, if I define a variable with non printing characters in single quotes and then test-Path it tests as a valid path as the non printing character is handled as a literal.
Test-Path 'HKEY_LOCAL_MACHINE\SOFTWARE\Test\`n#microsoft.com/GENUINE\#microsoft.com/GENUINE' -isValid
The same thing with double quotes will "expand" the non printing characters and return false, which is what I need.
Test-Path "HKEY_LOCAL_MACHINE\SOFTWARE\Test\`n#microsoft.com/GENUINE\#microsoft.com/GENUINE" -isValid
I have found reference to [string]::Format(() being used to expand the non printing characters, but
$invalidPath = 'HKEY_LOCAL_MACHINE\SOFTWARE\Test\`n#microsoft.com/GENUINE\#microsoft.com/GENUINE'
[string]::Format("{0}",$invalidPath)
does not expand the non printing character as expected.
I have also seen reference to using Invoke-Expression but that is NOT safe, and not an option.
Finally I found $ExecutionContext.InvokeCommand.ExpandString(), which seems to work,
$ExecutionContext.InvokeCommand.ExpandString('HKEY_LOCAL_MACHINE\SOFTWARE\Test\`n#microsoft.com/GENUINE\#microsoft.com/GENUINE')
returns a multiline string to the console, while
$ExecutionContext.InvokeCommand.ExpandString('Write-Host "Screwed"') returns the actual string to the console, rather than actually executing the Write-Host and only returning Screwed to the console.
Finally,
$invalidPath = 'HKEY_LOCAL_MACHINE\SOFTWARE\Test\`n#microsoft.com/GENUINE\#microsoft.com/GENUINE'
Test-Path ($ExecutionContext.InvokeCommand.ExpandString($invalidPath)) -isValid
returns false as expected. Which has me thinking this is the correct approach to pursue, but given all the gotchas elsewhere, I want to be 100% sure there is no way for this approach to be used as a security weak point. Am I on the right track, or are there gotchas my Google-Fu hasn't turned up yet?
Like Invoke-Expression, $ExecutionContext.InvokeCommand.ExpandString() is vulnerable to injection of unwanted commands, except that in the latter case such commands are only recognized if enclosed in $(...), the subexpression operator, as that is the only way to embed commands in expandable strings (which the method's argument is interpreted as).
For instance:
$ExecutionContext.InvokeCommand.ExpandString('a $(Write-Host -Fore Red Injected!) b')
A simple way to prevent this is to categorically treat all embedded $ chars. verbatim, by escaping them with `:
'a $(Write-Host -Fore Red Injected!) b',
'There''s no place like $HOME',
'Too `$(Get-Date) clever by half' |
ForEach-Object {
$ExecutionContext.InvokeCommand.ExpandString(($_ -replace '(`*)\$', '$1$1`$$'))
}
Note: It is sufficient to escape verbatim $ in the input string. A Unicode escape-sequence representation of $ (or ( / )) (`u{24} (or `u{28} / `u{29}), supported in PowerShell (Core) v6+ only), is not a concern, because PowerShell treats the resulting characters verbatim.
Of course, you may choose to report an error if there's a risk of command (or variable-value) injection, which can be as simple as:
$path = 'There''s no place like $HOME'
if ($path -match '\$') { Throw 'Unsupported characters in path.' }
However, this also prevents legitimate use of a verbatim $ in paths.
Taking a step back:
You state that the paths may have been copy-pasted from a web site.
Such a pasted string may indeed contain (perhaps hidden) control characters, but they would be contained verbatim rather than as PowerShell_escape sequences.
As such, it may be sufficient to test for / quietly remove control characters from the string's literal content (before calling Test-Path -IsValid):
# Test for control characters.
if ($path -match '\p{C}') { throw 'Path contains control characters.' }
# Quietly remove them.
$sanitizedPath = $path -replace '\p{C}'
I was initially running PS Version 7.1.3, in which all of these methods bore the same results.
When I ran:
$invalidPath = 'HKEY_LOCAL_MACHINE\SOFTWARE\Test\`n#microsoft.com/GENUINE\#microsoft.com/GENUINE'
Test-Path ($ExecutionContext.InvokeCommand.ExpandString($invalidPath)) -isValid
True is returned
Same as:
Test-Path ([regex]::Escape($invalidPath)) -IsValid
As well as the other methods you mentioned.
In testing in 5.1 I saw the same results as you.
In your XML, would you be seeing something like `n or \n or the actual non-printing characters? IE
'HKEY_LOCAL_MACHINE\SOFTWARE\Test\`n#microsoft.com/GENUINE\#microsoft.com/GENUINE'
OR
'HKEY_LOCAL_MACHINE\SOFTWARE\Test\
#microsoft.com/GENUINE\#microsoft.com/GENUINE'
If the latter is true you should be able to just pass the string to test-path in a variable like Test-Path "$invalidPath" -IsValid to get what you're looking for. In 7.1.3 (maybe earlier) it seems that PS is smart enough to parse those escape sequences and such -- or there is no simple way of doing what you're looking for that I can find.

Using Powershell environmental variables as strings when outputting files

I am using Get-WindowsAutopilotInfo to generate a computer's serial number and a hash code and export that info as a CSV.
Here is the code I usually use:
new-item "c:\Autopilot_Export" -type directory -force
Set-Location "C:\Autopilot_Export"
Get-WindowsAutopilotInfo.ps1 -OutputFile Autopilot_CSV.csv
Robocopy C:\Autopilot_Export \\Zapp\pc\Hash_Exports /copyall
This outputs a CSV file named "Autopilot_CSV.csv" to the C:\Autopilot_Export directory and then Robocopy copies it to the Network Share drive inside of the Hash_Exports folder listed above. In the above code, if I manually type in "test", "123", "ABC", etc. and replace "Autopilot_CSV" it will output a CSV under all of those names as well. So it looks like Get-WindowsAutopilotInfo will create a CSV file and save the file name with whatever string you pass into it. Great.
However, I am running this script on multiple different computers and I would like the exported CSV to be named something unique to the machine it is running on so I can easily identify it once it's copied. I am trying to pass the value of the environmental variable $env:computername as a string input for the CSV and it isn't working.
Here's the code I'm trying to use:
new-item "c:\Autopilot_Export" -type directory -force
Set-Location "C:\Autopilot_Export"
Get-WindowsAutopilotInfo.ps1 -OutputFile $env:computername.csv
Robocopy C:\Autopilot_Export C:\Users\danieln\Desktop /copyall
This code does not generate the CSV file at all. Why not?
Do I just have a basic misunderstanding of how environmental variables are used in Powershell? $env:computername appears to return a string of the computer's name, but I cannot pass it into Get-WindowsAutopilotInfo and have it save, but it will work if I manually type a string in as the input.
I have also tried setting it to a variable $computername = [string]$env:computername and just passing $computername in before the .CSV and that doesn't appear to work either. And per the docmentation, environmental variables are apparently already strings.
Am I missing something?
Thanks!
js2010's answer shows the effective solution: use "..."-quoting, i.e. an expandable string explicitly.
It is a good habit to form to use "..." explicitly around command arguments that are strings containing variable references (e.g. "$HOME/projects") or subexpressions (e.g., "./folder/$(Get-Date -Format yyyy-MM)")
While such compound string arguments generally do not require double-quoting[1] - because they are implicitly treated as if they were "..."-enclosed - in certain cases they do, and when they do is not obvious and therefore hard to remember:
This answer details the surprisingly complex rules, but here are two notable pitfalls if you do not use "...":
If a compound argument starts with a subexpression ($(...)), its output is passed as a separate argument; e.g. Write-Output $(Get-Location)/folder passes two arguments to Write-Output: the result of $(Get-Location) and literal /folder
If a compound argument starts with a variable reference and is followed by what syntactically looks like either (a) a property access (e.g., $PSVersionTable.PsVersion) or (b) a method call (e.g., $PSHome.Tolower()) it is executed as just that, i.e. as an expression (rather than being considered a variable reference followed by a literal part).
Aside #1: Such an argument then isn't necessarily a string, but whatever data type the property value or method-call return value happens to be.
Aside #2: A compound string that starts with a quoted string (whether single- or double-quoted) does not work, because the quoted string at the start is also considered an expression, like property access and method calls, so that what comes after is again passed as separate argument(s). Therefore, you can only have a compound strings consisting of a mix of quoted and unquoted substrings if the compound string starts with an unquoted substring or a non-expression variable reference.[2]
The latter is what happened in this case:
Unquoted $env:computername.csv was interpreted as an attempt to access a property named csv on the object stored in (environment) variable $env:computername, and since the [string] instance stored there has no csv property, the expression evaluated to $null.
By forcing PowerShell to interpret this value as an expandable string via "...", the usual interpolation rules apply, which means that the .csv is indeed treated as a literal (because property access requires use of $(...) in expandable strings).
[1] Quoting is required for strings that contain metacharacters such as spaces.
For values to be treated verbatim, '...' quoting is the better choice (see the bottom section of this answer for an overview of PowerShell string literals).
Also, using neither double- nor single-quoting and individually `-escaping metacharacters is another option (e.g. Write-Output C:\Program` Files.
[2] For instance, Write-Output a"b`t"'`c' and Write-Output $HOME"b`t"'`c' work as intended, whereas Write-Output "b`t"'`c' and Write-Output $HOME.Length"b`t"'`c' do not (the latter two each pass 3 arguments). The workaround is to either use a single "..." string with internal `-escaping as necessary (e.g. Write-Output "${HOME}b`t``c") or to use string-concatenation expression with + enclosed in (...) (e.g. Write-Output ($HOME + "b`t" + '`c'))
Doublequoting seems to work. The colon is special in powershell parameters.
echo hi | set-content "$env:computername.csv"
dir
Directory: C:\users\me\foo
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2/11/2021 1:02 PM 4 COMP001.csv
The colon is special. For example in switch parameters:
get-childitem -force:$true
Actually, it's trying to find a property named csv:
echo hi | Set-Content $env:COMPUTERNAME.length
dir
Directory: C:\Users\me\foo
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2/11/2021 3:04 PM 4 8
Basically pass it a string rather than the variable:
write-host $env:computername.csv
# output: (no output - looking for a variable called "computername.csv" instead of "computername")
Try the following syntax:
$filename = "$($env:computername).csv"
write-host $filename
# output: MYPCNAME.csv