Lee_Daily's helpful answer explains the problem with your attempt well and offers a working solution, including one using the unary form of -join.
Just to spell out for future readers why using just .Substring() is not an option for implementing extract-at most-N-chars. logic: attempting to extract a substring beyond the length of the input string causes an exception:
PS> 'abc'.Substring(0, 2) # OK
ab
PS> 'abc'.Substring(0, 4) # !! Exception, because at most 3 chars. can be extracted
Exception [...]: Index and length must refer to a location within the string. [...]
Your approach to solving that problem using a pipeline with Select-Object is a bit heavy-handed, though.
Below are better-performing alternatives that use expressions.
Esperento's helpful answer offers the best-performing solution, using .NET features only, though it is a bit "noisy" in that it requires a nested method call and a variable (rather than a literal) as input, because that variable must be referenced in the nested method call.
The approaches below are more PowerShell-idiomatic.
LotPings, in a comment, offers a concise solution based on the fact that strings can implicitly be treated as character arrays, so array slicing can be applied; note that indices are 0-based:
PS> -join 'abc'[0..1]
ab
PS> -join 'abc'[0..3] # equivalent of: 'abc'.Substring(0, 4), but by default without error
abc
Range expression 0..3 evaluates to array 0, 1, 2, 3, causing the characters at the specified indices to be returned as a character array which -join then reassembles into a string.
By default, PowerShell ignores indices outside the bounds of arrays, but the
caveat is that if Set-StrictMode -Version 3 or higher is in effect, the above too will cause an error.
A better-performing alternative that is not sensitive to Set-StrictMode is to use the -replace operator with a regex (regular expression).
That said, this solution is somewhat obscure.
PS> 'abc' -replace '(?<=.{2}).+' # equivalent of 'abc'.Substring(0, 2)
ab
PS> 'abc' -replace '(?<=.{4}).+' # always-safe equivalent of 'abc'.Substring(0, 4)
abc
.{4} matches exactly 4 characters (.) (implicitly) at the start of the string, without including those characters in the match ((?<=...), a look-behind assertion); .+ then matches all remaining characters (one or more).
The net effect is that all input strings with more than 4 characters have everything from the 5th character replaced with the empty string (due to absence of a replacement operand), leaving effectively just the first 4 characters.
Input strings with 4 or fewer characters are passed through as-is (no extraction needed).
For multi-line input strings, a little more work is needed (inline option (?s) to make . match newlines (`n) too):
PS> "a`nbc" -replace '(?s)(?<=.{3}).+' # extract first 3 chars as string: "a`n"
a
b
Also consider use of a simple helper function:
# Safe equivalent of:
# $String.Substring($Start)
# $String.Substring($Start, $Length)
function substr ([string] $String, [int] $Start = 0, [int] $Length = -1) {
if ($Length -eq -1 -or $Start + $Length -ge $String.Length) {
$String.Substring($Start)
}
else {
$String.Substring($Start, $Length)
}
}
Note that you need to call it with argument syntax (shell-like, whitespace-separated arguments), as with any function in PowerShell:
PS> substr 'abc' -Length 2 # same as: substr abc 0 2 / substr -String abc -Start 0 -Length 2
ab
PS> substr 'abc' -Length 4
abc
Finally, note that there's an RFC for introducing string-manipulation cmdlets (yet to be implemented as of PowerShell Core 6.2.0-preview.1), which proposes a Get-Substring cmdlet, among others, for efficient substring manipulation in the pipeline.