Code:
$text=Get-Content -Path "E:\1.txt"
$text.GetType() | Format-Table -AutoSize
For($i=0; $i -le 5 ;$i++)
{
$var=Write-host '$'text[$i]
$var
}
Actual Result:
$ text[0]
$ text[1]
$ text[2]
$ text[3]
$ text[4]
$ text[5]
I need below Result:
$text[0]
$text[1]
$text[2]
$text[3]
$text[4]
$text[5]
If you must use the quotes, using -separator also works:
$text=Get-Content -Path "E:\1.txt"
$text.GetType() | Format-Table -AutoSize
For($i=0; $i -le 5 ;$i++)
{
$var=Write-host '$'text[$i] -Separator ''
$var
}
Your code fundamentally doesn't do what you intend it to do, due to the mistaken use of Write-Host:
# !! Doesn't capture anything in $var, prints directly to the screen.
$var=Write-host '$'text[$i]
$var # !! is effectively $null and produces no output.
See the bottom section for details.
Instead, what you want is an expandable string (aka interpolating string, "..."-enclosed), with selective `-escaping of the $ character you want to be treated verbatim:
$var= "`$text[$i]" # Expandable string; ` escapes the $ char., $i is expanded
$var
There are other ways to construct the desired string:
$var = '$text[{0}]' -f $i, using -f, the format operator.
$var = '$' + "text[$i]", using string concatenation with +
but the above approach is simplest in your case.
As for what you tried:
Write-Host is typically - and definitely in your case - the wrong tool to use, unless the intent is to write to the display only, bypassing the success output stream and with it the ability to send output to other commands, capture it in a variable, or redirect it to a file. To output a value, use it by itself; e.g., $value instead of Write-Host $value (or use Write-Output $value, though that is rarely needed); see this answer.
What you thought of as a single argument, '$'text[$i], was actually passed as two arguments, verbatim $ and expanded text[$i] (e.g., text[0]) and because Write-Host simply space-concatenates multiple arguments, a space was effectively inserted in the (for-display) output.
That '$'text[$i] becomes two arguments is a perhaps surprising PowerShell idiosyncrasy; unlike in POSIX-compatible shells such as bash, composing a single string argument from a mix of unquoted and (potentially differently) quoted parts only works if the argument starts with an unquoted substring or (mere) variable reference; for instance:
Write-Output foo'bar none' does pass a single argument (passes foobar none), whereas
Write-Output 'bar none'foo does not (passes bar none and foo)
See this answer for more information.
Related
I am attemping to make the Destination from the Copy Item function be the $path and keep running into syntax error.
Function movefiles($dayhash){
foreach ($h in $dayhash.GetEnumerator() )
{
$path = "$formsfolderDaily Checklists\$today_($h.Value)"
Copy-Item $formsfolder$($h.Value) -Destination $formsfolder"Daily Checklists\"$today"_"$($h.Value)
editDate($path)
}
Desired outcome
Function movefiles($dayhash){
foreach ($h in $dayhash.GetEnumerator() )
{
$path = $formsfolder + "Daily Checklists\" + $today + "_" + ($h.Value)
Copy-Item $formsfolder$($h.Value) -Destination $path
editDate($path)
}
$path = "$formsfolderDaily Checklists\$today_($h.Value)"
This makes me think $FormsFolder is a path variable with a trailing backslash -- BUT THAT'S JUST A GUESS -- and one of the reasons Join-Path is so useful.
It's also hard to know what is a literal and what is part of a variblbe name when you start constructing complex expansion strings. I would recommend using the -f (Format operator) which nicely separates the literal and variable portions of your string. My best guess for the above would be:
$path = '{0}Daily Checklists\{1}_{2}' -f $formsfolder, $today, $h.Value
Your template string is on the the left-hand side of the operator, with zero-indexed placeholders in the format {0}, {1}, etc. The placeholders correspond to the variables/expressions/function calls found in the list on the right-hand side of the operator.
It sounds like you want to implement your solution using expandable (double-quoted) strings ("...").
To that end, you need to observe two fundamental rules:
In order to disambiguate variable names from subsequent characters, enclose their names in {...}, e.g. $[today} instead of just $today
Notably, _ is a legitimate character in a PowerShell variable name, so if a _ follows a variable reference {...} is needed too.
In order to embed expressions - such as $h.Value - inside "...", enclose them in $(...), the subexpression operator
You've done this in part in your question, but the first command is missing the $ before (.
For a complete overview of PowerShell's string-interpolation rules, see this answer.
Additionally:
You're using compound tokens composed of unquoted and quoted parts in order to form a single string argument, which is best avoided in PowerShell - see this answer.
Instead, use a single, "..."-enclosed string.
Therefore (the assumption is that the value of $formsfolder ends in \ (or /)):
Function movefiles($dayhash) {
foreach ($h in $dayhash.GetEnumerator() ) {
$path = "${formsfolder}Daily Checklists\$today_$($h.Value)"
Copy-Item "${formsfolder}$($h.Value)" -Destination "${formsfolder}Daily Checklists\${today}_$($h.Value)"
editDate $path
}
I have the following code:
$srv_range = 29..30+40+50..52
$srv_range.GetType()
$NewVMTemplate = New-Object psobject
$NewVMTemplate | Add-Member -MemberType NoteProperty -Name Name -Value $null
$srv_range | % {
$pod= $_
$servers = #()
1..2 | % {
$server = $NewVMTemplate | Select-Object *
$server.Name = "pod" + "{0:D2}" -f $pod + "-srv" + $_
$servers += $server
}
ForEach ( $server in $servers) {
write-host $server.Name
}
}
output:
PowerCLI C:\ .\eraseme.ps1
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
pod29-srv1
pod29-srv2
pod30-srv1
pod30-srv2
pod40-srv1
pod40-srv2
pod50-srv1
pod50-srv2
pod51-srv1
pod51-srv2
pod52-srv1
pod52-srv2
I want to input the range from CLI, but I get the following output with this code
param(
[Parameter(Mandatory=$False)] $srv_range
)
#$srv_range = 29..30+40+50..52
$srv_range.GetType()
$NewVMTemplate = New-Object psobject
$NewVMTemplate | Add-Member -MemberType NoteProperty -Name Name -Value $null
$srv_range | % {
$pod= $_
$servers = #()
1..2 | % {
$server = $NewVMTemplate | Select-Object *
$server.Name = "pod" + "{0:D2}" -f $pod + "-srv" + $_
$servers += $server
}
ForEach ( $server in $servers) {
write-host $server.Name
}
}
PowerCLI C:\ .\eraseme.ps1 29..30+40+50..52
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
pod29..30+40+50..52-srv1
pod29..30+40+50..52-srv2
How can I input the range from CLI and get the same result as the first code?
Your problem is that argument 29..30+40+50..52 is treated as a string literal in your .\eraseme.ps1 29..30+40+50..52 call - it is not recognized as an expression.
To force recognition as an expression, enclose the argument in (...), the grouping operator:
.\eraseme.ps1 (29..30+40+50..52)
The same applies if you want to use output from (another) command as a command argument; e.g.:
# Pass the lines read from file paths.txt as an array to Get-ChildItem
# (Parameter -Path is implied in both commands).
Get-ChildItem (Get-Content paths.txt)
Two asides:
• $(...), the subexpression operator, 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 embed a single command or expression in a statement (and even that isn't needed on the RHS of a variable assignment). While not likely, the unnecessary use of $(...) can have side effects - see this answer.
• You can make your script more robust by declaring your parameter with a more specific type, in which case an attempt to call it with a string would fail right away:
[Parameter(Mandatory=$False)] [int[]] $srv_range
(Other optimizations could be applied to your script as well.)
Optional background information
As for when an unquoted token is treated as an expression or nested command vs. an (expandable) string in argument mode (see also: about_Parsing):
(...), $(...), and #(...) by themselves or at the start of a token create a new parsing context, in which expressions or even nested commands can be used:
(...) is sufficient for a single expression or command. $(...) (the subexpression operator) can enclose multiple expressions / commands; so can #() (the array subexpression operator), and it additionally ensures that its output is always treated as an array.
Notably, the following expressions are not recognized without being enclosed in one of the above:
[...] (type literals) and access to their members, such as [Environment]::Version
.. (range expressions) such as 1..10
If, at the start of a token, (...), $(...), or #(...) are followed by additional characters, the first additional character is considered the start of a new, separate argument.
By contrast, if they're preceded by an unquoted literal or a variable-only reference, $(...) works like inside "..." (an expandable string), (...) starts a new argument that is an expression, and #(...) is taken as literal # with (...) again starting a new argument that is an expression.
A # followed by the name of a variable (e.g., #params) containing a collection or hashtable of parameter values initiates parameter splatting.
#{ ... } can be used to pass a hashtable literal (e.g., #{ key = 'value' }).
{ ... } creates a script block ([scriptblock]).
By themselves or at the start of a token, variable references, including member access (property access, method calls, indexing) can be used as-is:
Expressions such as $HOME, $PSVersionTable.PSVersion, $someArray[0], and $someString.ToUpper() are recognized, and returned as their inherent type.
Without member access, i.e., with a simple variable reference such as $HOME, subsequent characters are (potentially) considered part of the same argument that is then interpreted as an expandable string - see below.
With member access, the first of any additional characters is considered the start of a new argument (e.g., $foo.Length-more results in two arguments: the value of $foo.Length and string literal -more).
Everything else is treated as an expandable string, i.e., similar to the contents of a double-quoted string, except that metacharacters[1] still need escaping and certain tokens are interpreted as multiple arguments.
Expandable means that embedded simple variable references (e.g., $HOME\Desktop or $env:APPDATA\Test) are interpolated (replaced with their stringified values).
Note that this can result in a representation that differs from a given value's default output format as shown in the console, for instance (again, see this answer for more information).
Enclose a variable name in {...} to disambiguate it from subsequent characters, if necessary (e.g., ${HOME}).
To access a variable value's property or use an index or call a method or embed arbitrary commands, you must enclose the expression in $(...), e.g., v$($PSVersionTable.PSVersion)
Generally, it is safest to enclose tokens with embedded variable references / expressions in "...", because it avoids the following edge cases:
* $(...) at the start of an unquoted token is not interpreted as part of an expandable string, it is treated as a separate argument (e.g., Write-Output $('ab')c results in two arguments: the result of $('ab') and literal c).
* . at the start of a token immediately followed by a simple variable reference or subexpression results in separate arguments too.
(E.g., .$HOME results in two arguments: literal ., and the value of $HOME)
Note: Even though the result of the expansion is a string, it doesn't necessarily remain one: the final type is determined by the type of to the parameter of the command at hand to which the expanded value is bound.
Escaping / quoting:
PowerShell has many more metacharacters than cmd.exe, and a notable pitfall is that , must be escaped to be treated a literal, because , is PowerShell's array-construction operator.
To escape a single character, prefix it with ` (backtick).
To avoid the need for escaping metacharacters individually, enclose the value in "..." (double quotes) or '...' (single quotes):
Use double quotes if you want the string to be interpolated (expanded), i.e., if you want to be able to embed variable references and subexpressions.
Inside a double-quoted string, `-escape the following chars. to treat them as literals: ` " $
Use single quotes to treat the value as a literal.
Inside a single-quoted string, escape a ' as ''
Single- or double-quoting is usually the easiest way to escape spaces in a value.
Finally, note that --%, the so-called stop-parsing symbol (PSv3+), completely changes the interpretation of all remaining arguments: designed for use with legacy cmd.exe command lines, it stops interpreting the rest of the line except for expansion of cmd.exe-style %...% environment variables. See Get-Help about_Parsing
As for using quoted tokens:
'...' or "..." by themselves or at the start of a token:
These are parsed as as usual: as a literal ('...') or expandable ("...") string.
Any additional characters cause the first additional character to be considered the start of a new, separate argument.
'...' or "..." being preceded by an unquoted literal or variable-only reference:
They are evaluated as usual and the result (i.e., with quotes removed) is appended to what precedes them (evaluated to).
[1] The argument-mode metacharacters (characters with special syntactic meaning) are:
<space> ' " ` , ; ( ) { } | & < > # #.
Of these, < > # # are only special at the start of a token.
Examples
Write-Output 1..10 # STRING: -> '1..10'
Write-Output (1..10) # EXPRESSION: -> #(1, 2, ...)
# Write-Output $(1..10) would work too, but is only necessary if
# the enclosed expression comprises *multiple* statements.
Write-Output [Environment]::Version # STRING: -> '[Environment]::Ticks'
Write-Output ([Environment]::Version) # EXPRESSION: -> a [System.Version] instance.
Write-Output a,b # !! ARRAY #(1, 2), because "," is not escaped.
Write-Output a`,b #`# STRING 'ab'
Write-Output "a,b" # ditto
Write-Output 'a,b' # ditto
Write-Output $HOME\Desktop # EXPANDED string (e.g.) 'C:\Users\jdoe\Desktop'
Write-Output "$HOME\Desktop" # ditto
Write-Output '$HOME\Desktop' # LITERAL string '$HOME\Desktop'
Write-Output dir=$HOME # EXPANDED string (e.g.) 'dir=C:\Users\jdoe\Desktop'
Write-Output $PSVersionTable.PSVersion # a [System.Version] instance
Write-Output "$($PSVersionTable.PSVersion)/more" # a [string]; e.g., '5.1.14393.576/more'
Write-Output "v$($PSVersionTable.PSVersion)" # ditto; e.g., 'v5.1.14393.576'
# !!! These DO NOT WORK as intended.
Write-Output $($PSVersionTable.PSVersion)/more # $(...) at the *start*
Write-Output $PSVersionTable.PSVersion/more # $(...) missing
Write-Output "$PSVersionTable.PSVersion/more" # $(...) missing
Write-Output .$HOME # Specifically, .$ at the beginning is the problem; escaping . works
In PowerShell, when entering a command, I can refer to the value of the first and last word of the most recently entered command with $^ and $$. I am wondering if there is a shortcut to refer to the second last, nth last or nth word also.
There's no direct equivalent to the automatic variables you mention, but you can combine Get-History with PowerShell's language parser (System.Management.Automation.Language.Parser) to achieve your intent:
function Get-PrevCmdLineTokens {
# Get the previous command line's text.
$prevCmdLine = (Get-History)[-1].CommandLine
# Use the language parser to break it into syntactic elements.
$tokens = $null
$null = [System.Management.Automation.Language.Parser]::ParseInput(
$prevCmdLine,
[ref] $tokens,
[ref] $null
)
# Get and output an array of the text representations of the syntactic elements,
# (excluding the final `EndOfInput` element).
$tokens[0..($tokens.Count - 2)].Text
}
Example:
PS> $null = Write-Output Honey "I'm $HOME"
PS> Get-PrevCmdLineTokens
The above yields:
$null
=
Write-Output
Honey
"I'm $HOME"
Note:
As with $^ and $$, the tokens that make up the command are unexpanded, meaning that they are represented as typed rather than by their interpolated value.
However, unlike with $^ and $$, any syntactic quoting is retained (e.g., "I'm $HOME" instead of I'm $HOME).
While you could use .Value instead of .Text in the function above in order to strip the syntactic quoting, you would then miss out on tokens such as $null and =.
I am porting a script from bash to PowerShell, and I would like to keep the same support for argument parsing in both. In the bash, one of the possible arguments is -- and I want to also detect that argument in PowerShell. However, nothing I've tried so far has worked. I cannot define it as an argument like param($-) as that causes a compile error. Also, if I decide to completely forego PowerShell argument processing, and just use $args everything appears good, but when I run the function, the -- argument is missing.
Function Test-Function {
Write-Host $args
}
Test-Function -- -args go -here # Prints "-args go -here"
I know about $PSBoundParameters as well, but the value isn't there, because I can't bind a parameter named $-. Are there any other mechanisms here that I can try, or any solution?
For a bit more context, note that me using PowerShell is a side effect. This isn't expected to be used as a normal PowerShell command, I have also written a batch wrapper around this, but the logic of the wrapper is more complex than I wanted to write in batch, so the batch wrapper just calls the PowerShell function, which then does the more complex processing.
I found a way to do so, but instead of double-hyphen you have to pass 3 of them.
This is a simple function, you can change the code as you want:
function Test-Hyphen {
param(
${-}
)
if (${-}) {
write-host "You used triple-hyphen"
} else {
write-host "You didn't use triple-hyphen"
}
}
Sample 1
Test-Hyphen
Output
You didn't use triple-hyphen
Sample 2
Test-Hyphen ---
Output
You used triple-hyphen
As an aside: PowerShell allows a surprising range of variable names, but you have to enclose them in {...} in order for them to be recognized; that is, ${-} technically works, but it doesn't solve your problem.
The challenge is that PowerShell quietly strips -- from the list of arguments - and the only way to preserve that token is you precede it with the PSv3+ stop-parsing symbol, --%, which, however, fundamentally changes how the arguments are passed and is obviously an extra requirement, which is what you're trying to avoid.
Your best bet is to try - suboptimal - workarounds:
Option A: In your batch-file wrapper, translate -- to a special argument that PowerShell does preserve and pass it instead; the PowerShell script will then have to re-translate that special argument to --.
Option B: Perform custom argument parsing in PowerShell:
You can analyze $MyInvocation.Line, which contains the raw command line that invoked your script, and look for the presence of -- there.
Getting this right and making it robust is nontrivial, however.
Here's a reasonably robust approach:
# Don't use `param()` or `$args` - instead, do your own argument parsing:
# Extract the argument list from the invocation command line.
$argList = ($MyInvocation.Line -replace ('^.*' + [regex]::Escape($MyInvocation.InvocationName)) -split '[;|]')[0].Trim()
# Use Invoke-Expression with a Write-Output call to parse the raw argument list,
# performing evaluation and splitting it into an array:
$customArgs = if ($argList) { #(Invoke-Expression "Write-Output -- $argList") } else { #() }
# Print the resulting arguments array for verification:
$i = 0
$customArgs | % { "Arg #$((++$i)): [$_]" }
Note:
There are undoubtedly edge cases where the argument list may not be correctly extracted or where the re-evaluation of the raw arguments causes side effect, but for the majority of cases - especially when called from outside PowerShell - this should do.
While useful here, Invoke-Expression should generally be avoided.
If your script is named foo.ps1 and you invoked it as ./foo.ps1 -- -args go -here, you'd see the following output:
Arg #1: [--]
Arg #2: [-args]
Arg #3: [go]
Arg #4: [-here]
I came up with the following solution, which works well also inside pipelines multi-line expressions. I am using the PowerShell Parser to parse the invocation expression string (while ignoring any incomplete tokens, which might be present at the end of $MyInfocation.Line value) and then Invoke-Expression with Write-Output to get the actual argument values:
# Parse the whole invocation line
$code = [System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.Line.Substring($MyInvocation.OffsetInLine - 1), [ref]$null, [ref]$null)
# Find our invocation expression without redirections
$myline = $code.Find({$args[0].CommandElements}, $true).CommandElements | % { $_.ToString() } | Join-String -Separator ' '
# Get the argument values
$command, $arguments = Invoke-Expression ('Write-Output -- ' + $myline)
# Fine-tune arguments to be always an array
if ( $arguments -is [string] ) { $arguments = #($arguments) }
if ( $arguments -eq $null ) { $arguments = #() }
Please be aware that the original values in the function call are reevaluated in Invoke-Expression, so any local variables might shadow values of the actual arguments. Because of that, you can also use this (almost) one-liner at the top of your function, which prevents the pollution of local variables:
# Parse arguments
$command, $arguments = Invoke-Expression ('Write-Output -- ' + ([System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.Line.Substring($MyInvocation.OffsetInLine - 1), [ref]$null, [ref]$null).Find({$args[0].CommandElements}, $true).CommandElements | % { $_.ToString() } | Join-String -Separator ' '))
# Fine-tune arguments to be always an array
if ( $arguments -is [string] ) { $arguments = #($arguments) }
if ( $arguments -eq $null ) { $arguments = #() }
My powershell command below
$BUILD_SOURCEVERSIONMESSAGE= (Get-Item Env:\BUILD_SOURCEVERSIONMESSAGE)
returns output in this format
2018-10-26T01:08:44.7409834Z BUILD_SOURCEVERSIONMESSAGE Merge 569594f057e2c4bd0320159855e81e14216ca66f into 41107d0f0db5ef2986831db2182280e0c...
I am trying to parse the string 569594f057e2c4bd0320159855e81e14216ca66f from the output above.
I tried converting the output to a string, splitting it on whitespace, and accessing the second element of the array as follows. However, I get empty string. How can I access the required string?
echo $BUILD_SOURCEVERSIONMESSAGE
$out = $BUILD_SOURCEVERSIONMESSAGE | Out-String
$out1 = $out.split()
echo $out1[1]
The concise equivalent of command Get-Item Env:\BUILD_SOURCEVERSIONMESSAGE - i.e., retrieving the value of environment variable BUILD_SOURCEVERSIONMESSAGE - is the expression $env:BUILD_SOURCEVERSIONMESSAGE.
Using the unary form of Powershell's -split operator, which splits the input by any nonempty run of whitespace (while stripping leading and trailing whitespace), you can get the desired output as follows:
PS> (-split $env:BUILD_SOURCEVERSIONMESSAGE)[3]
569594f057e2c4bd0320159855e81e14216ca66f
Index 3 extracts the 4th token resulting from the tokenization via -split.
If you want to use string interpolation with the result:
$prefix = 'before<'; $postfix = '>after'
$val = (-split $env:BUILD_SOURCEVERSIONMESSAGE)[3]
# Output a synthesized string that applies a pre- and postfix, using
# {...} to enclose variable names to avoid ambiguity.
"${prefix}${val}${postfix}"
The above yields:
before<569594f057e2c4bd0320159855e81e14216ca66f>after