Using Powershell environmental variables as strings when outputting files - powershell

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

Related

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)

Impossible to remove a variable in Powershell

I have come across the strangest behaviour that has been driving me nuts when writing scripts. It is impossible sometimes to remove the value of a variable in Powershell. I have tried:
Remove-Variable -Force
Also tried making it equal to an empty string or making it $null but the variable value and type remains.
Anyone have an idea how this can happen?
I am using Powershell version 5 on Windows Server 2016.
Here some screenshots:
To remove a variable, pass its name without the $ sigil to the Remove-Variable cmdlet's
-Name parameter (which is positionally implied); using the example of a variable $date:
Using an argument:
# Note the required absence of $ in the name; quoting the var. name is
# optional in this case.
Remove-Variable -Force -Name date
Using the pipeline would require you to specify objects whose .Name property contains the name of the variable to delete, because these property values implicitly bind to Remove-Variable's -Name parameter; the simplest way to achieve that is to use the Get-Variable cmdlet, which too requires specifying the name without the $:
# Works, but is inefficient.
Get-Variable -Name date | Remove-Variable -Force
However, this is both more verbose and less efficient than directly passing the name(s) as an argument.
As for what you tried:
You variable-removal command is conceptually flawed:
$date | Remove-Variable -Force
Except as the LHS of an assignment ($date = ...), referring to a variable with the $ sigil returns its value, not the variable itself.
That is, since your $date variable contains a [datetime] instance, it is that instance that is sent through the pipeline, and since only strings are supported as input - that is, variable names - the command fails.
In effect, your call is equivalent to the following, which predictably fails:
PS> Get-Date | Remove-Variable -Force
Remove-Variable : The input object cannot be bound to any parameters for the command
either because the command does not take pipeline input
or the input and its properties do not match any of the parameters that take pipeline input.
What the somewhat verbose, general error message is implying in this case is that the input object was of the wrong type (because only objects with a .Name property are accepted, which [datetime] doesn't have).
Contexts in which you need refer to a variable itself rather than to its value:
What these contexts have in common is that you need to specify the variable name without the $ sigil.
Two notable examples:
All *-Variable cmdlets expect the names of variables to operate on, such as the Get-Variable cmdlet that returns objects representing variables, of type System.Management.Automation.PSVariable; these objects include the name, value, and other attributes of a PowerShell variable.
# Gets an object describing variable $date
$varObject = Get-Variable date # -Name parameter implied
When you pass the name of an output variable to a -*Variable common parameter
# Prints Get-Date's output while also capturing the output
# in variable $date.
Get-Date -OutVariable date
As implied, above, assigning to a variable with = is the only exception: there you do use the $ sigil, e.g. $date = Get-Date.
Note that this differs from POSIX-compatible shells such as bash, where you do not use $ in assignments (and must not have whitespace around =); e.g., date=$(date).

How can I create an array of directories and files in PowerShell?

I know that in PowerShell an array can be created as follows.
$myArray = "item1", "item2", "item3"
or using another syntax.
$myfruits = #('Pineapple','Oranges','Bananas')
I need to create an array that has names of directories and a file found in the script root.
$myArray = folder1, folder2, folder3, myprogram.exe
The items in the array are actual names of folders and an executable without quotes as they are found in the current directory (script root). This does not work with PowerShell giving error Missing argument in Parameter list.
What am I doing wrong? I'm on Windows 10 x64 using PowerShell 7.0.
tl;dr
To define an array literal with strings, you must quote its elements:
$myArray = 'folder1', 'folder2', 'folder3', 'myprogram.exe'
Syntax #(...) for array literals should be avoided. Not only is #(...) unnecessary, it is inefficient in PowerShell versions up to 5.0, but - more importantly - it invites conceptual confusion by falsely suggesting that it constructs arrays. For more information, see the bottom section of this answer.
PowerShell has two fundamental parsing modes:
argument mode, which works like traditional shells
In argument mode, the first token is interpreted as a command name (such as cmdlet name, function name, or filename of an executable), followed by a whitespace-separated list of arguments, which may be unquoted, if they contain neither whitespace nor shell metacharacters.
expression mode, which works like traditional programming languages, where strings must be quoted
It is the first token (excluding the LHS of a variable assignment and the =) that determines which mode is applied - see this answer for more information.
In the following statement:
$myArray = folder1, folder2, folder3, myprogram.exe
folder1, because it is unquoted and:
doesn't start with $ or ( or #
isn't a number literal,
triggers argument mode; that is, folder1 is interpreted as the name of a command, which results in the - somewhat unhelpful - error message you saw (you can trigger it with something like Get-Date, too).
Therefore, to define an array literal with strings, you must quote its elements:
$myArray = 'folder1', 'folder2', 'folder3', 'myprogram.exe'
Note that a - convenient, but inefficient - argument-mode alternative that spares you the effort of quoting would be the following:
$myArray = Write-Output folder1, folder2, folder3, myprogram.exe

Powershell appcmd.exe trying to execute

I hava to add some environment to appPools? and i tried this code:
$Appcmd = [System.Environment]::SystemDirectory + "\inetsrv\appcmd.exe"
& $appcmd --% set config -section:system.applicationHost/applicationPools /+""[name='$Task.eProto_Pool'].environmentVariables.[name='PRODUCT_NAME',value='eProto']"" /commit:apphost"
but $Task in second line does not work, How can I past a variable to this string? I also tried %Task%
.eProto_Pool is a property of $Task. If you want to dereference (that is, retrieve one single property of an object) within a string, you need to wrap the string with $(), the subexpression operator in PowerShell.
For example, I'll make a new hashtable called $MyString that has two properties.
$MyString = #{Name = "Stephen";Value="CoolDude"}
>$MyString
Name Value
---- -----
Value CoolDude
Name Stephen
Look what happens if I try to reference it inside a string with regular string expansion. This is basically what you were doing in your example above. See how it fails to work as you would expect?
write-host " The user $MyString.Name is a $MyString.Value"
The user System.Collections.Hashtable.Name is a System.Collections.Hashtable.Value
Time to use the subexpression operator to save the day.
write-host " The user $($MyString.Name) is a $($MyString.Value)"
The user Stephen is a CoolDude
When in doubt, subexpression it out.
On second glance
I think it might be the percentage sign % which is causing you grief. This is a shorthand for the ForEach-Object command in PowerShell. Try this instead:
Invoke-expression "$appcmd --% set config -section:system.applicationHost/applicationPools /+`"`"[name='$($Task.eProto_Pool)'].environmentVariables.[name='PRODUCT_NAME',value='eProto']`"`" /commit:apphost`""
This should escape the strings like you need, and also pass the parameters in, like the eProto_Pool property of $Task.

Meaning of Powershell statement

I am completely new to Powershell and I am trying to understand what this fragment of code does:
$OwnFunctionsDir = "$env:USERPROFILE\Documents\WindowsPowerShell\Functions"
Write-Host "Loading own PowerShell functions from:" -ForegroundColor Green
Write-Host "$OwnFunctionsDir" -ForegroundColor Yellow
Get-ChildItem "$OwnFunctionsDir\*.ps1" | %{.$_}
Write-Host ''
In particular I cannot interpret what the line Get-Children … does. This code is intended to be added to your Powershell profile to load commonly used functions at Powershell startup.
Short Answer
This command loads the contents of all ".ps1" files in "<yourHomeDirecotry>\Documents\WindowsPowerShell\Functions" into your working session.
Long Answer
First, $env:USERPROFILE is an environment variable that corresponds to your home directory. So in my case it is "c:\users\jboyd"
The first interesting bit of code is:
$OwnFunctionsDir = "$env:USERPROFILE\Documents\WindowsPowerShell\Functions"
This assigns a string to a new variable named OwnFunctionsDir. What's interesting about this string is that it is double quoted and it contains the variable $env:USERPROFILE. PowerShell expands variables in double quoted strings (this is not the case for single quoted strings). So if this were running on my machine the value of $OwnFunctionsDir would be "c:\users\jboyd\Documents\WindowsPowerShell\Functions".
Skipping the Write-host functions (because I think they are pretty self explanatory) takes us to:
Get-ChildItem "$OwnFunctionsDir\*.ps1" | %{.$_}
Get-ChildItem is interesting because its behavior depends on the PowerShell provider (don't worry about what that is) but in this case Get-ChildItem is the equivalent of dir or ls. $OwnFunctionsDir\*.ps1 is the path being enumerated. Using my machine as an example, this is equivalent to listing all files with names matching the wildcard pattern "*.ps1" (essentially all PowerShell files) in the directory "c:\users\jboyd\Documents\WindowsPowerShell\Functions".
The | character pipes the results of the command on the left to the command on the right.
The % character is an alias for the ForEach-Object command. The left and right curly braces are a scriptblock, this is the body of the foreach command. So each item from Get-ChildItem is piped into the scriptblock.
In the scriptblock of the ForEach-Object command the $_ variable represents the current item being processed. In this case $_ will be a PowerShell file with an extension ".ps1". When we invoke a PowerShell file with a period in front of it that is called dot sourcing. Dot sourcing loads the content of a file into your working session. So any variables or functions in the file are loaded.