简体   繁体   中英

Why + operator adding space in powershell

I encounter strange issue. I have a file with content like

-----MyData-----
aaa
bbb
ccc
ddd
-----YourData-----

and I wrote a function to get all data of file in a single string except first and last line. The function is as follows

function readFile([string] $datapath){
$returningData = $null
foreach($line in [System.IO.File]::ReadLines($datapath)) {
    if(-not ($line -like '*-MyData-*') -and -not ($line -like '*-YourData-*')){
        $returningData+=$line
    }
}
return $returningData
}

Now, this method is returning " aaabbbcccddd " as expected but in IF block if I use $returningData+$line instead of what I am using write now then it returns " aaa bbb ccc ddd ".

I looked for it but couldn't find any answer. Why simple concatenation adding an extra space?

There are two things in play here:

Displaying data

The way PowerShell displays and/or converts objects (including arrays) into a string:

$MyData = 'aaa', 'bbb', 'ccc', 'ddd'

PS C:\> "$MyData"
aaa bbb ccc ddd

PS C:\> Write-Host $MyData
aaa bbb ccc ddd

Or output (unroll) from the pipeline:

PS C:\> $MyData # Simular to: $MyData | ForEach-Object { $_ }
aaa
bbb
ccc
ddd

Also be aware that the arrays are flattened in a string or output (see eg: Why does PowerShell flatten arrays automatically? )

To better display the object and its structure, you might consider to use an serializer , like ConvertTo-Json :

PS C:\> ConvertTo-Json $MyData
[
  "aaa",
  "bbb",
  "ccc",
  "ddd"
]

Or ConvertTo-Expression for even more complex objects like:

$MyObject = 'aaa', @([datetime]'1963-10-07', [version]'1.2.3'), 'bbb'

PS C:\> Write-Host $MyObject
aaa 10/7/1963 12:00:00 AM 1.2.3 bbb

PS C:\> ConvertTo-Expression $MyObject
'aaa',
(
        [datetime]'1963-10-07T00:00:00.0000000',
        [version]'1.2.3'
),
'bbb'

Type Casting

The other thing in play here is the automatic type casting where objects are implicitly converted to the same type when used as operands with an operator. Meaning if the operands are of a different type, the type of the first operand will be used for the operation and the second operand will be converted to the same type (if possible) as the first one.

This counts for comparison operators :

PS C:\> 10 -gt '9' # 10 > 9
True

PS C:\> '10' -gt 9 # '10' < '9'
False

And most other operators including arithmetic operators :

PS C:\> 10 + '9' # Results in a integer: 10 + 9
19

PS C:\> '10' + 9 # Results in a string: '10' + '9'
109

And also in case of an array :

PS C:\> 'aaa' + 'bbb', 'ccc' # Results in a string: 'aaa' + 'bbb ccc'
aaabbb ccc

PS C:\> 'aaa', 'bbb' + 'ccc' # Results in an array: 'aaa', 'bbb' + @('ccc')
aaa
bbb
ccc

In case the first operand is $Null (which isn't part of any data type), the type of the second operand is used:

PS C:\> $a = $Null + 'aaa'
PS C:\> $a.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object

PS C:\> $a = $Null + @('aaa')
PS C:\> $a.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

Conclusion

Changing your line:

$returningData = $null

to:

$returningData = @()

or:

$returningData = ''

Will likely give you different results in this matter.

Note: as with using the increase assignment operator ( += ) to create an object collection , you should generally avoid using the increase assignment operator ( += ) for building strings as it is exponential expensive.

Instead, I recommend you to use the pipeline and the -Join operator for building strings:

PS C:\> $MyData -Join ''
aaabbbcccddd

Or in your script:

$returningData = @(
    foreach($line in $MyData) {
        if(-not ($line -like '*a*') -and -not ($line -like '*b*')){ $line }
    }
) -Join ''

@iron's answer has a lot of great information, but let's not ignore how concise PowerShell can be. I managed to bring this down to a few lines:

$File = 'C:\temp\MyData.txt'
$Patterns = @('.-MyData-.','.-YourData-.','^$')
$String = (Select-String -Pattern $Patterns -Path $File -NotMatch ).line -join ''

Assuming the use case is as simple as the question indicates.

I'd also point out that if other comments are correct and there are rouge spaces somewhere. you could pipe this to ForEach-Object to trim each line.

$File = 'C:\temp\MyData.txt'
$Patterns = @('.-MyData-.','.-YourData-.','^$')
$String = ( (Select-String -Pattern $Patterns -Path $File -NotMatch ).line | ForEach-Object{ $_.Trim() } ) -join ''

.Trim() will only take spaces from the beginning and end of the line. you can use .Replace( ' ', '' ) as suggested by one of the comments if you want to remove all spaces per line.

I'm sure my RegEx's could be a little better, but I gotta head out, if any one has some pointers, either edit or add to the comments. Thanks.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM