Why does Powershell replace commas with whitespaces? - string

I'm doing some script in Powershell to automate a task. This script is going to get arguments, such as:
PS > myscript.ps1 par=a,1,2,0.1 par=b,3,4,0.1 par=c,5,6,0.1 bin=file.exe exeargs="fixed args for file.exe"
In short, file.exe is an executable which accept parameters (including a, b and c) and this ps1 script is going to execute file.exe passing args a, b and c within the specified range, varying 'em by the specified precision.
The question is, I first split each $arg in $args by the character "=", and then I should split them by "," to get the specified values.
The thing is, when I do:
foreach ($arg in $args)
{
$parts = ([string] $arg).split("=")
Write-Host $parts[1]
}
The output is
a 1 2 0.1
b 3 4 0.1
c 5 6 0.1
file.exe
fixed args for file.exe
I.e., it already substituted the "," character with a whitespace, so my second split should be with white space, not with comma.
Any guess on why does it happen?
Thanks in advance!

First of all why are you writing it like a C program or something? You don't have to pass arguments like that, use $args and split on = etc. when Powershell has a more powerful concept of parameters, whereby you can pass the named paramters and arguments rather than doing the parsing that you are doing. ( More on parameters here: http://technet.microsoft.com/en-us/library/dd315296.aspx)
With that said, let me answer your question:
What you are doing is when you pass in arguments like:
par=a,1,2,0.1 par=b,3,4,0.1 par=c,5,6,0.1 bin=file.exe exeargs="fixed args for file.exe"
you are passing in array of arrays. The first element is the array with elements:
par=a
1
2
0.1
Ok coming to the split:
When you do [string] $a, you are converting the array into a string. By default this means an array with elements 1,2,3 will become 1 2 3.
So your first argument there par=a,1,2,0.1, becomes par=a 1 2 0.1 and the split on = means parts[1] becomes a 1 2 0.1, which is what you see.
So how can you retain the comma?
Just make an array to be converted into a string with , inbetween than space, right?
Here are some ways to do that:
Using -join operator:
foreach ($arg in $args)
{
$parts = ($arg -join ",").split("=")
Write-Host $parts[1]
}
now you will see the output with the commas that you want.
Another way, using $ofs special variable:
foreach ($arg in $args)
{
$ofs =","
$parts = ([string] $arg).split("=")
Write-Host $parts[1]
}
(more on $ofs here: http://blogs.msdn.com/b/powershell/archive/2006/07/15/what-is-ofs.aspx )
Disclaimer - All this explanation to make you understand what is happening. Please do not continue this and use paramters.

This is happening in the parsing of the command line for your script and not during the split() method. To see this, try putting a "Write-Host $args" at the beginning, like so:
Write-Host $args
foreach ($arg in $args)
{
$parts = ([string] $arg).split("=")
Write-Host $parts[1]
}
This is because the ',' character is used to separate elements in an array. For example:
PS C:\temp> $a = 1,2,3
PS C:\temp> Write-Host $a
1 2 3
PS C:\temp> $a.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Try this command line instead:
.\myscript.ps1 "par=a,1,2,0.1" "par=b,3,4,0.1" "par=c,5,6,0.1" "bin=file.exe" "exeargs=fixed args for file.exe"

Related

PowerShell, getting string syntax correct in 7-zip script

I can't seem to solve this, been struggling with it for a while (maybe it's simple; I just can't see it as I've been looking at it for so long).
I can get the 7z.exe syntax to work, but when I try and put it together into a simple script, it fails.
e.g., if I run .\zip.ps1 "C:\test\test.zip" "C:\test\test1.txt" "C:\test\test2.txt*
Instead of zipping up the 2 files required, it zips up everything in the C:\test folder, completely ignoring my arguments.
How can I adjust the below string syntax so that 7z.exe will correctly respect the input arguments and compress them from within a PowerShell script?
"Received $($args.Count) files:"
$parent = Split-Path (Split-Path $args[0]) -Leaf
$sevenzip = "C:\Program Files\7-Zip\7z.exe"
$zipname = "$($parent) $(Get-Date -Format 'yyyy_MM_dd HH_mm_ss').zip"
$args_line = $args | foreach-object { "`"$_`"" }
# $args_line = '"' + $($args -join """ """) + '"' # Want to use """ here so that it can capture not only files with spaces, but files with ' in the filename
''
"Zip Name : $zipname"
''
"Arguments : $args_line"
''
if (Test-Path $sevenzip) {
if (Test-Path "C:\0\$zipname") { rm "C:\0\$zipname" -Force }
''
'String output of the line to run:'
"& ""$sevenzip"" a -r -tzip ""C:\0\$zipname"" $args_line" # Taking this output and pasting onto console works.
''
& "$sevenzip" a -r -tzip "C:\0\$zipname" "$args_line" # This does not work
} else {
"7z.exe was not found at '$sevenzip', please check and try again"
}
The error that I get is:
Files read from disk: 0
Archive size: 22 bytes (1 KiB)
Scan WARNINGS for files and folders:
1 : The system cannot find the file specified.
----------------
Pass $args directly to your & "$sevenzip" call:
& "$sevenzip" a -r -tzip "C:\0\$zipname" $args
This makes PowerShell pass the array elements as individual arguments, automatically enclosing them in "..." if needed (based on whether they contain spaces).
Using arrays as arguments for external programs is in effect an implicit form of array-based splatting; thus, you could alternatively pass #args.
Generally, note that in direct invocation[1] you cannot pass multiple arguments to an external program via a single string; that is, something like "$args_line" cannot be expected to work, because it is passed as a single argument to the target program.
If you want to emulate the resulting part of the command line, for display purposes:
($argListForDisplay = $args.ForEach({ ($_, "`"$_`"")[$_ -match ' '] })) -join ' '
Note:
Each argument is conditionally enclosed in "..." - namely based on whether it contains at least one space - and the resulting tokens are joined to form a single, space-separated list (string).
The assumption is that no argument has embedded " chars.
A simplified example:
& { # an ad-hoc script block that functions like a script or function
# Construct the argument list *for display*
$argListForDisplay = $args.ForEach({ ($_, "`"$_`"")[$_ -match ' '] }) -join ' '
#"
The following is the equivalent of:
Write-Output $argListForDisplay
"#
# Pass $args *directly*
# Simply prints the argument received one by one, each on its own line.
# Note: With PowerShell-native commands, generally use #args instead (see below).
Write-Output $args
} firstArg 'another Arg' lastArg # sample pass-through arguments
Output:
The following is the equivalent of:
Write-Output firstArg "another Arg" lastArg
firstArg
another Arg
lastArg
Note: Write-Output is used in lieu of an external program for convenience. Technically, you'd have to use #args instead of $args in order to pass the array elements as individual, positional arguments, but, as stated, this is the default behavior with externals programs. Write-Output, as a PowerShell-native command, receives the array as a whole, as a single argument when $args is used; it just so happens to process that array the same way as if its elements had been passed as individual arguments.
[1] You can use a single string as an -ArgumentList value for Start-Process. However, Start-Process is usually the wrong tool for invoking console applications such as 7z.exe - see this answer.

Construct a string parameter to function - powershell

Im wondering how you can construct a string on the fly as a parameter to a function?
Example
say I have a function like
function MyFunc
{
Param
(
[Parameter(mandatory=$true)] [string] $myString,
[Parameter(mandatory=$true)] [int] $myInt
)
Write-Host ("Param 1 is {0}" -f $myString)
Write-Host ("Param 2 is {0}" -f $myInt)
}
How can I call it whilst constructing the first string param on the fly e.g.
$myName = "Casper"
$myInt=7
MyFunc "Name is " + $myName $myInt
Ive tried putting {} around the first "bit" like
MyFunc {Name is " + $myName} $myInt
This then incorrectly prints out
Param 1 is "Name is "+$myName
Param 2 is 7
what I want it to print is
Param 1 is "Name is Casper"
Param 2 is 7
I know a better way of doing this would just be to set up the string first,
$pm1 = "Name is " + $myName
and call function
MyFunc $pm1 $myInt
but I am just interested to know how it can be done on the fly as it were. How can I construc the string and pass as first parameter on the function call? Hope thats clear.
Thanks
As a general rule of thumb, you can always nest any complex expression in a separate pipeline using the subexpression operator $(...) or grouping operator (...):
MyCommand $("complex",(Get-Something),"argument","expression" -join '-')
But in your particular case we don't need that - you just need to place the variable expression $myName inside the string literal and PowerShell will automatically evaluate and expand its value:
MyFunc "Name is $myName" $myInt
If the variable expression is to be followed by some characters that would otherwise make up a valid part of the variable path, use curly brackets {} as qualifiers:
MyFunc "Name is ${myName}" $myInt

Powershell: difference between assignment and parameter for cmdlet (parsing)

I am new to powershell language and I have problems understanding some basic concepts regarding string concatenation.
I tried to concat a string with the + char as I knew it from other programming languages i. e. Java.
line 1: $result = 7
line 2: Write-Host "Result: " + $result + "!" # Result: + 7 + !
I then realized (i. e. in this question How do I concatenate strings and variables in PowerShell?) that I need to do it (in one of) the powershell way(s); for example like this.
line 3: Write-Host "Result: $result!" # Result: 7!
As I experimented a little I found out that if I assign the expression in line 2 to a variable it somehow works as I anticipated it in the first place.
line 4: $str = "Result: " + $result + "!"
line 5: Write-Host $str # Result: 7!
So my question is, why is there a difference if I pass a Java-style concatenated string to Write-Output cmdlet or if I assign the same string to a variable?
String concatenation and expansion is a bit different in PowerShell, here are several ways to accomplish it-
Format operator:
PS C:\> 'This exhibits {0} string expansions! {1} {0}!' -f #(2,'Wow!')
This exhibits 2 string expansions! Wow! 2!
Each item in the array is accessed by the number in the braces.
Subexpression:
PS C:\> "Sometimes you need calculations in a string. 5 + 3 = $(5 + 3)"
Sometimes you need calculations in a string. 5 + 3 = 8
An explanation here: this will not work with string literals, i.e., ''.
Everything contained in the subexpression will be evaluated and converted to a string if possible utilizing an object's ToString() method.
String Expansion:
PS C:\> $Var = 'This string'
PS C:\> "$Var is amazing!"
This string is amazing!
This will also not work with string literals, i.e., ''. If you need to concatenate a variable-qualifying character next to the variable call, you can use curly braces to avoid a null value, i.e., ${Var}_notattached
String Concatenation:
Tried and true:
PS C:\> 'Sometimes you just ' + 'need to add. 8 = ' + 8
Sometimes you just need to add. 8 = 8

Combining arrays in powershell gives System.Object[] in output?

I'm looping through some files and want to prepend the filename to an existing file.
I thought I'd got stuff going ok, but by the second time around the loop, I lose the existing end of the file, and get System.Object[] instead of the former contents.
A simple example here shows roughly what I'm doing to prepend the information, and replicates the outcome (which I don't want!).
$a = "1","2","3"
$b = "test"
$c = -join $b,$a
# At this point, $c is as I expect; it has added $b to the start of the list.
$a = $c
$c = -join $b,$a
# This time, when I try to add $b to the front of $a, it correctly adds the new line, and the second line is also correct, but then it dumps the object properties of the remaining lines (or something?)
The output is shown below
test
test
Length : 3
LongLength : 3
Rank : 1
SyncRoot : {1, 2, 3}
IsReadOnly : False
IsFixedSize : True
IsSynchronized : False
Count : 3
If you're writing this to a file using set-content, then it writes the object properties as System.Object[], which isn't what I want!
Thanks
Update:
As per the comment on this original post, the solution I'm looking for is to use $a = (,$b)+$a.
This has the effect of prepending the string in $b to the start of the array of strings stored in $a.
I think, you need some clarification here:
$a = "1","2","3"
$b = "test"
$c = -join $b,$a
does not create an array $c containing
'test', '1', '2', '3'
instead $clooks like this:
'test', ('1', '2', '3')
What I think you wanted to do is:
$c = $b , $a
which results in
'test', '1', '2', '3'

How do I delay expansion of variables in PowerShell strings?

Whatever you want to call it, I'm trying to figure out a way to take the contents of an existing string and evaluate them as a double-quoted string. For example, if I create the following strings:
$string = 'The $animal says "meow"'
$animal = 'cat'
Then, Write-Host $string would produce The $animal says "meow". How can I have $string re-evaluated, to output (or assign to a new variable) The cat says "meow"?
How annoying...the limitations on comments makes it very difficult (if it's even possible) to include code with backticks. Here's an unmangled version of the last two comments I made in response to zdan below:
----------
Actually, after thinking about it, I realized that it's not reasonable to expect The $animal says "meow" to be interpolated without escaping the double quotes, because if it were a double-quoted string to begin with, the evaluation would break if the double quotes weren't escaped. So I suppose the answer would be that it's a two step process:
$newstring = $string -replace '"', '`"'
iex "`"$string`""
One final comment for posterity: I experimented with ways of getting that all on one line, and almost anything that you'd think works breaks once you feed it to iex, but this one works:
iex ('"' + ($string -replace '"', '`"') + '"')
Probably the simplest way is
$ExecutionContext.InvokeCommand.ExpandString($var)
You could use Invoke-Expression to have your string reparsed - something like this:
$string = 'The $animal says `"meow`"'
$animal = 'cat'
Invoke-Expression "Write-Host `"$string`""
Note how you have to escape the double quotes (using a backtick) inside your string to avoid confusing the parser. This includes any double quotes in the original string.
Also note that the first command should be a command, if you need to use the resulting string, just pipe the output using write-output and assign that to a variable you can use later:
$result = Invoke-Expression "write-output `"$string`""
As noted in your comments, if you can't modify the creation of the string to escape the double quotes, you will have to do this yourself. You can also wrap this in a function to make it look a little clearer:
function Invoke-String($str) {
$escapedString = $str -replace '"', '`"'
Invoke-Expression "Write-Output `"$escapedString`""
}
So now it would look like this:
# ~> $string = 'The $animal says "meow"'
# ~> $animal = 'cat'
# ~> Invoke-String $string
The cat says "meow"
You can use the -f operator. This is the same as calling [String]::Format as far as I can determine.
PS C:\> $string = 'The {0} says "meow"'
PS C:\> $animal = 'cat'
PS C:\> Write-Host ($string -f $animal)
The cat says "meow"
This avoids the pitfalls associated with quote stripping (faced by ExpandString and Invoke-Expression) and arbitrary code execution (faced by Invoke-Expression).
I've tested that it is supported in version 2 and up; I am not completely certain it's present in PowerShell 1.
Edit: It turns out that string interpolation behavior is different depending on the version of PowerShell. I wrote a better version of the xs (Expand-String) cmdlet with unit tests to deal with that behavior over here on GitHub.
This solution is inspired by this answer about shortening calls to object methods while retaining context. You can put the following function in a utility module somewhere, and it still works when you call it from another module:
function xs
{
[CmdletBinding()]
param
(
# The string containing variables that will be expanded.
[parameter(ValueFromPipeline=$true,
Position=0,
Mandatory=$true)]
[string]
$String
)
process
{
$escapedString = $String -replace '"','`"'
$code = "`$ExecutionContext.InvokeCommand.ExpandString(`"$escapedString`")"
[scriptblock]::create($code)
}
}
Then when you need to do delayed variable expansion, you use it like this:
$MyString = 'The $animal says $sound.'
...
$animal = 'fox'
...
$sound = 'simper'
&($MyString | xs)
&(xs $MyString)
PS> The fox says simper.
PS> The fox says simper.
$animal and $sound aren't expanded until the last two lines. This allows you to set up a $MyString up front and delay expansion until the variables have the values you want.
Invoke-Expression "`"$string`""

Resources