How to escape the following regex, so its usable in PowerShell? - powershell

As far as I know, there is no way in PowerShell to execute an exe with parameters specified in a variable and direct the return of the exe into a variable. Therefore I am currently writing a small function to make this possible. But now I am stuck at the point that the parameters have to be passed individually when calling with &. (This is not necessary for all programs, but some programs cause problems if you pass all parameters as a string in a variable) Therefore I want to use a split to write the parameters passed to my function into an array. And then pass the array with the parameters in my exe call.
For this I have the following regex:
[^\s"']+|"([^"]*)"|'([^']*)'
This regex allows that single and double quotes are taken into account when passing parameters and that a text with spaces inside them is not split.
But unfortunately I don't have the slightest idea how to best escape this regex so that it doesn't cause any problems in the PowerShell script.
Here then still my function to make it a little easier to understand:
The function executes the file passed in the $Path parameter with the parameters from the $Arguments. Before the execution i try to split the $Arguments with the regex. As return of the function, you get an object with the ExitCode and the output of the executed file. Here you can see my attempt, but the quotes cause problems with the following code.
function Invoke-Process ($Path,$Arguments){
[PsObject[]] $ReturnValue = #()
$Params=$Arguments -split([regex]::escape([^\s"']+|"([^"]*)"|'([^']*)'))
$ExCode = 0
try {
$ProcOutput = & $Path $Params | out-string
}catch{
$ProcOutput = "Failed: $($_.Exception.Message)"
$ExCode = 1
}
$ReturnValue += [PsObject]#{ ExitCode = $ExCode; Output = $ProcOutput}
Return $ReturnValue
}
The function is called as follows:
$ExePath = "C:\arcconf\arcconf.exe"
$ExeParams = "getconfig 1"
$Output = Invoke-Process $ExePath $ExeParams
I hope you can understand my problem. I am also open to other ways of writing the function.
Greetings

There's nothing you need to escape - the pattern is perfectly valid.
All you need is a string literal type that won't treat the quotation marks as special. For this, I would suggest a verbatim here-string:
#'
This is a single-line string ' that " can have all sorts of verbatim quotation marks
'#
The qualifiers for a here-string is #' as the last token on the preceding line and '# as the first token on the following line (for an expandable here-string, use #" and "#).
Try running it with valid sample input:
$pattern = #'
[^\s"']+|"([^"]*)"|'([^']*)'
'#
'getconfig 1 "some string" unescaped stuff 123' |Select-String $pattern -AllMatches |ForEach-Object {$_.Matches.Value}
Which, as expected, returns:
gesture
1
"some string"
unescaped
stuff
123
As an alternative to a here-string, the best alternative is a regular verbatim string literal. The only character you need to escape is ', and the escape sequence is simply two in a row '', so your source code becomes:
$pattern = '[^\s"'']+|"([^"]*)"|''([^'']*)'''

Mathias R. Jessen's helpful answer answers your immediate question well.
Taking a step back:
there is no way in PowerShell to execute an exe with parameters specified in a variable and direct the return of the exe into a variable
No: PowerShell does support both of those things:
$output = someExternalProgram ... captures the stdout output from an external program in variable $output; use redirection 2>&1 to also capture stderr output; use >$null or 2>$null to selectively discard stdout or stderr output - see this answer for details.
someExternalProgram $someVariable ... passes the value of variable $someVariable as an argument to an external program; if $someVariable is an array (collection) of values, each element is passed as a separate argument. Instead of a variable reference, you may also use the output from a command or expression, via (...); e.g., someExternalProgram (1 + 2) passes 3 as the argument - see this answer for details.
Note: An array's elements getting passed as individual arguments only happens for external programs by default; to PowerShell-native commands, arrays are passed as a whole, as a single argument - unless you explicitly use splatting, in which case you must pass #someVariable rather than $someVariable. For external programs, #someVariable is effectively the same as $someVariable. While array-based splatting also works for PowerShell-native commands, for passing positional arguments only, the typical and more robust and complete approach is to use hashtable-based splatting, where the target parameters are explicitly identified.
A general caveat that applies whether or not you use variables or literal values as arguments: Up to at least PowerShell 7.2.x, passing empty-string arguments or arguments with embedded " chars. to external programs is broken - see this answer.
With the above in mind you can rewrite your function as follows, which obviates the need for - ultimately brittle - regex parsing:
function Invoke-Process {
# Split the arguments into the executable name / path and all
# remaining arguments.
$exe, $passThruArgs = $args
try {
# Call the executable with the pass-through arguments.
# and capture its *stdout* output.
$output = & $exe #passThruArgs
$exitCode = $LASTEXITCODE # Save the process' exit code.
} catch {
# Note: Only If $exe is empty or isn't a valid executable name or path
# does a trappable statement-terminating error occur.
# By contrast, if the executable can be called in principle, NO
# trappable error occurs if the process exit code is nonzero.
$exitCode = 127 # Use a special exit code
$output = $_.ToString() # Use the exception message
}
# Construct and output a custom object containing
# the captured stdout output and the process' exit code.
[pscustomobject] #{
ExitCode = $exitCode
Output = $output
}
}
Sample calls:
Invoke-Process cmd /c 'echo hi!'
# Equivalent, using an array variable for the pass-through arguments.
$argsForCmd = '/c', 'echo hi!'
Invoke-Process cmd #argsForCmd
Note:
Since Invoke-Process is a PowerShell command,
splatting (#argsForCmd) is necessary here in order to pass the array
elements as individual arguments, which inside the function are then reflected in the automatic $args variable variable.
The automatic $args variable is only available in simple functions, as opposed to advanced ones, which behave like cmdlets and therefore automatically support additional features, such as common parameters; to make your function and advanced one, replace the line $exe, $passThruArgs = $args at the top of the function with the following:
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $exe,
[Parameter(ValueFromRemainingArguments)]
[string[]] $passThruArgs
)

Related

start powershell wont accept variable as parameter

issue
the called powershell script will accept parameters but not all of them:
Current Set-Up and code:
I have a common folder where two .ps1 scripts are located:
DoWork.ps1
Workmanager.ps1
Workmanager.ps1 calls the Dowork.ps1:
$targetPath="M:\target"
echo "target path: $targetPath"
start powershell {.\DoWork.ps1 -target $targetPath -tempdrive D:\}
output (as expected):
target path: M:\target
DoWork.ps1 contains some start code:
param
(
[string]$tempdrive,
[string]$target,
[int] $threads = 8,
[int] $queuelength = -1
)
echo "variables:"
echo "temp drive: $tempdrive"
echo "target path: $target"
Unexpectedly, the $target is not beeing assigned. Previously I had the variable named $targetpath, which did not work either.
variables:
temp drive: D:\
target path:
Findings
It appears that the issue relies in Workmanager.ps1. Spcifying the parameter as fixed string rather than as variable will load the parameter. Any solution for this?
start powershell {.\DoWork.ps1 -target "foo" -tempdrive D:\}
When you use a ScriptBlock as an argument to powershell.exe, variables aren't going to be evaluated until after the new session starts. $targetPath has not been set in the child PowerShell process called by Workmanager.ps1 and so it has no value. This is actually an expected behavior of a ScriptBlock in general and behaves this way in other contexts too.
The solution is mentioned in the help text for powershell -?:
[-Command { - | <script-block> [-args <arg-array>] <========== THIS GUY
| <string> [<CommandParameters>] } ]
You must provide the -args parameter which will be passed to the ScriptBlock on execution (separate multiple arguments with a ,). Passed arguments are passed positionally, and must be referenced as though you were processing the arguments to a function manually using the $args array. For example:
$name = 'Bender'
& powershell { Write-Output "Hello, $($args[0])" } -args $name
However, especially with more complicated ScriptBlock bodies, having to remember which index of $args[i] contains the value you want at a given time is a pain in the butt. Luckily, we can use a little trick with defining parameters within the ScriptBlock to help:
$name = 'Bender'
& powershell { param($name) Write-Output "Hello, $name" } -args $name
This will print Hello, Bender as expected.
Some additional pointers:
The ScriptBlock can be multiline as though you were defining a function. way. The examples above are single line due to their simplicity.
A ScriptBlock is just an unnamed function, which is why defining parameters and referencing arguments within one works the same way.
To exemplify this behavior outside of powershell.exe -Command, Invoke-Command requires you to pass variables to its ScriptBlock in a similar fashion. Note however that answer uses an already-defined function body as the ScriptBlock (which is totally valid to do)
You don't need to use Start-Process here (start is its alias), at least as demonstrated in your example. You can simply use the call operator & unless you need to do something more complex than "run the program and wait for it to finish". See this answer of mine for more information.
If you opt to pass a string to powershell.exe instead, you don't need to provide arguments and your variables will get rendered in the current PowerShell process. However, so will any other unescaped variables that might be intended to set within the child process, so be careful with this approach. Personally, I prefer using ScriptBlock regardless, and just deal with the extra parameter definition and arguments.
Using the call & operator is optional when you are not executing a path rendered as a string. It can be omitted in the examples above, but is more useful like so:
& "C:\The\Program Path\Contains\spaces.exe"
& $programPathAsAVariable

powershell cannot use methods on backreference matches

i tried harder to convert the case on the fly but it seems that powershell methods does not run for backrefence matches example below:
$a="string"
[regex]::replace( "$a",'(.)(.)',"$('$1'.toupper())$('$2'.tolower())" )
> string
$a -replace '(.)(.)',"$('${1}'.toupper())$('${2}'.tolower())"
> string
expected result
> StRiNg
don't know if it's possible or not
You need a script block to call the String class methods. You can effectively do what you want. For Windows PowerShell, you cannot do script block substitutions with the -replace operator. You can do that in PowerShell Core (v6+) though:
# Windows PowerShell
$a="string"
[regex]::Replace($a,'(.)(.)',{$args[0].Groups[1].Value.ToUpper()+$args[0].Groups[2].Value.ToLower()})
# PowerShell Core
$a="string"
$a -replace '(.)(.)',{$_.Groups[1].Value.ToUpper()+$_.Groups[2].Value.ToLower()}
Note that the script block substitution recognizes current MatchInfo object ($_). Using the Replace() method, the script block is passed in the MatchInfo object as an argument in the automatic variable $args unless you specify a param() block.

Powershell indexing for creating user logon name

I'm trying to make a user creation script for my company to make things more automated.
I want the script to take the Firstname + Lastname[0] to make the users logon name, but i can't get the syntax right,
I have tried writing {} and () but no luck there.
that's the original peace from my script
New-ADuser...........-UserPrincipalName $fname+$lname[0]
any tips?
Gabriel Luci's helpful answer provides an effective solution and helpful pointers, but it's worth digging deeper:
Your problem is that you're trying to pass expression $fname+$lname[0] as an argument, which requires enclosing (...):
New-ADuser ... -UserPrincipalName ($fname+$lname[0])
PowerShell has two distinct parsing modes, and when a command (such as New-ADUser) is called, PowerShell operates in argument mode, as opposed to expression mode.
Enclosing an argument in (...) forces a new parsing context, which in the case of $fname+$lname[0] causes it to be parsed in expression mode, which performs the desired string concatenation.
In argument mode, unquoted arguments are implicitly treated as if they were enclosed in "...", i.e., as expandable strings under the following circumstances:
If they don't start with (, #, $( or #(.
If they either do not start with a variable reference (e.g., $var) or do start with one, but are followed by other characters that are considered part of the same argument (e.g., $var+$otherVar).
Therefore, $fname+$lname[0] is evaluated as if "$fname+$lname[0]" had been passed:
The + become part of the resulting string.
Additionally, given that inside "..." you can only use variable references by themselves (e.g., $fname), not expressions (e.g., $lname[0]), $lname[0] won't work as intended either, because the [0] part is simply treated as a literal.
Embedding an expression (or a command or even multiple expressions or commands) in "..." requires enclosing it in $(...), the subexpression operator, as in Gabriel's answer.
For an overview of PowerShell's string expansion rules, see this answer.
The following examples use the Write-Output cmdlet to illustrate the different behaviors:
$fname = 'Jane'
$lname = 'Doe', 'Smith'
# WRONG: Same as: "$fname+$lname[0]", which
# * retains the "+"
# * expands array $lname to a space-separated list
# * treats "[0]" as a literal
PS> Write-Output -InputObject $fname+$lname[0]
Jane+Doe Smith[0]
# OK: Use an expression via (...)
PS> Write-Output -InputObject ($fname+$lname[0])
JaneDoe
# OK: Use an (implicit or explicit) expandable string.
PS> Write-Output -InputObject $fname$($lname[0]) # or: "$fname$($lname[0])"
JaneDoe
# OK: Use an intermediate variable:
PS> $userName = $fname + $lname[0]; Write-Output -InputObject $userName
Use a string for the UserPrincipalName, with the variables in the string:
New-ADuser -UserPrincipalName "$fname$($lname[0])"
PowerShell can usually figure out when you put a variable inside a string. When it can't, like in the case of $lname[0], you enclose it in $().
This is called "variable expansion" (other languages, like C#, call it "string interpolation"). Here's a good article that describes it in more detail: https://powershellexplained.com/2017-01-13-powershell-variable-substitution-in-strings/
i just saw the answers and a minute before i realized that i should actually set it up as another variable, $logon = $fname+lname[0]
and pass it as -userPrincipalName $logon.
Thanks for the help, you guy are the best!

When can I use newlines (line breaks) in my Powershell commands to improve readability

How do I know when I can use a New Lines/Carriage returns in my Powershell scripts? All of the search results when searching for this answer all point to the Output. I don't care about the output in this case. I am more interested in my ability to format my Powershell scripts for readability.
For Example. Two versions of a Powershell command line below. One works and one doesn't. What the command does is unimportant in this case. The point is I am needing to know when I'm allowed to create a new line and when I am not.
This command line Works as it's just one long single line:
& 'C:\Program Files\ArangoDB3 3.3.3\usr\bin\arangoimp.exe' --file 'C:\Program Files\ArangoDB3 3.3.3\usr\bin\tstImportJSON.json' --type json --collection users --progress true --overwrite true --server.username root --server.password password
This command line does NOT work due to the fact that there is a New Line in the middle of the script.
& 'C:\Program Files\ArangoDB3 3.3.3\usr\bin\arangoimp.exe' --file
'C:\Program Files\ArangoDB3 3.3.3\usr\bin\tstImportJSON.json'
--type json --collection users --progress true --overwrite true
--server.username root --server.password password
In my case I'm just running different versions of the same command line after adding line breaks to see if they work or not. I know that I can begin a New Line when using an IF statement. I can also use New Lines when piping an object |. My assumption is that somewhere there is a list of Powershell scripting rules. I thought I had seen them somewhere once upon a time when I originally began getting into Powershell but no clue where it is now.
You can use ` as the line-continuation character[1]
at the very end of a line (not even whitespace is allowed after it) in order to spread it across multiple lines.
Here's a simplified example:
& cmd.exe /c echo `
hi `
there
This is the equivalent of & cmd.exe /c echo hi there and yields hi there.
Note:
Since the ` character is both visually subtle and the syntax is easy to break by accidentally placing characters after it, consider using an array as an alternative - see below.
Individual commands must be on a single line and therefore need ` if you want to spread them across multiple lines, as shown above.
However, in a pipeline you may end the line with | and continue on the next line, without needing the `; e.g.:
Get-Date | # Because the line ends with |, parsing continues on the next line.
Select-Object Day
In PowerShell [Core] v7+, you may alternatively place the | at the start of the (very) next line:
Get-Date # PS v7+ only
| Select-Object Day
Additionally, if individual arguments create a new parsing context in expression mode - such as an inherently multi-line capable (...) expression or a script block ({...}) passed to a cmdlet - you're also free to spread the expression across multiple lines; e.g.:
1, 2, 3 | ForEach-Object { # { starts a multiline-aware context
$_ + 1
}
A hybrid case is an array-literal argument, which allows you to break a command after an interior element's , separator:
Get-Date | Select-Object -Property Day,
Year
Statements that start in expression mode always allow spreading across multiple lines (though embedded command-mode statements are subject to the usual limitations):
$foo = # an assignment is parsed in expression mode
'bar'
Alternatively, consider the use of an array[2] to pass the arguments, which allows you to use multiline expression-mode syntax to define the arguments as individual array elements beforehand:
# Construct the array of arguments (using multiline expression syntax)...
$arguments = '/c',
'echo',
'hi there'
# ... and pass it to cmd.exe
& cmd.exe $arguments
Note: Array element hi there is passed as "hi there" by PowerShell: it employs automatic double-quoting to ensure that the argument is recognized as a single argument by the target program.
As an aside: for calling PowerShell commands (as opposed to external programs, as in the case at hand), consider constructing the arguments in a hashtable for use with splatting, where each entry key specifies a target parameter name and the corresponding value the parameter value (argument); e.g.:
# Define the hashtable representing the named arguments.
$argsHash = #{
Filter = 'T*'
File = $true
}
# Note the use of "#" instead of "$"
# Equivalent of:
# Get-ChildItem -Filter T* -File
Get-ChildItem #argsHash
[1] ` is PowerShell's general-purpose escape character. Placed at the very end of a line, its function is subtly different: instead of escaping the newline that follows (which would mean retaining it as a literal), it effectively tells PowerShell to remove it and treat the next line as the continuation of the current one.
[2] An earlier form of this answer recommended array-based splatting, before PetSerAl pointed out that for invoking external programs it's sufficient to use an array as-is.
While splatting can be used too, its semantics are subtly different if one of the array element is --%, the stop-parsing symbol (in short: only when splatting does --% have its special meaning).
Splatting is a useful technique when calling PowerShell commands, however, primarily in its hash-table form (see previous link).
Break down the script a little bit more for additional readability.
$1 = "--file 'C:\Program Files\ArangoDB3 3.3.3\usr\bin\tstImportJSON.json'"
$2 = "--type json"
$3 = "--collection users"
$4 = "--progress true"
$5 = "--overwrite true"
$6 = "--server.username root"
$7 = "--server.password password"
$Arguments = "$1 $2 $3 $4 $5 $6 $7"
& 'C:\Program Files\ArangoDB3 3.3.3\usr\bin\arangoimp.exe' $Arguments
Your output will look like this.
C:\Program Files\ArangoDB3 3.3.3\usr\bin\arangoimp.exe --file 'C:\Program Files\ArangoDB3 3.3.3\usr\bin\tstImportJSON.json' --type json --collection users --progress true --overwrite true --server.username root --server.password password
typically I would normally move an uglier looking beast like this into its own function.
The standard way of formatting PowerShell arguments for readability is through splatting, which consists of assigning a hash table to a named splatting variable carrying the options, and using the At symbol to dereference the arguments when needed.
Your specific example of running an application with arguments would be written like below, in idiomatic PowerShell.
$arangoImpOptions = #{
FilePath = "$env:ProgramFiles\ArangoDB3 3.3.3\usr\bin\arangoimp.exe"
Arguments = #{
file = "$env:ProgramFiles\ArangoDB3 3.3.3\usr\bin\tstImportJSON.json"
type = 'json'
collection = 'users'
progress = 'true'
overwrite = 'true'
'server.username' = 'root'
'server.password' = 'password'
}.GetEnumerator().ForEach({ '--{0} "{1}"' -f #($_.Key, $_.Value) }) -join ' '
}
Start-Process #arangoImpOptions

Run PowerShell custom function in new window

Function Check-PC
{
$PC = Read-Host "PC Name"
If($PC -eq "exit"){EXIT}
Else{
Write-Host "Pinging $PC to confirm status..."
PING -n 1 $PC
}
This is a snippet of a function I have written into a PowerShell script. I would like the function to run in a new instance of PowerShell, not in the main window.
Is it possible to run this in a separate process of PowerShell without writing it as a separate script and calling the script? Something like this:
$x= Start-Process powershell | Check-PC
I need to keep everything in one script if possible.
Note: It is the involvement of Start-Process that complicates the solution significantly - see below. If powershell were invoked directly from PowerShell, you could safely pass a script block as follows:
powershell ${function:Check-PC} # !! Does NOT work with Start-Process
${function:Check-PC} is an instance of variable namespace notation: it returns the function's body as a script block ([scriptblock] instance); it is the more concise and faster equivalent of Get-Content Function:Check-PC.
If you needed to pass (positional-only) arguments to the script block, you'd have to append -Args, followed by the arguments as an array (,-separated).
Start-Process solution with an auxiliary self-deleting temporary file:
See the bottom half of this answer to a related question.
Start-Process solution with -EncodedCommand and Base64 encoding:
Start-Process powershell -args '-noprofile', '-noexit', '-EncodedCommand',
([Convert]::ToBase64String(
[Text.Encoding]::Unicode.GetBytes(
(Get-Command -Type Function Check-PC).Definition
)
))
The new powershell instance will not see your current session's definitions (unless they're defined in your profiles), so you must pass the body of your function to it (the source code to execute).
(Get-Command -Type Function Check-PC).Definition returns the body of your function definition as a string.
The string needs escaping, however, in order to be passed to the new Powershell process unmodified.
" instances embedded in the string are stripped, unless they are either represented as \" or tripled (""").
(\" rather than the usual `" is needed to escape double quotes in this case, because PowerShell expects \" when passing a command to the powershell.exe executable.)
Similarly, if the string as a whole or a double-quoted string inside the function body ends in (a non-empty run of) \, that \ would be interpreted as an escape character, so the \ must be doubled.Thanks, PetSerAl.
The most robust way to bypass these quoting (escaping) headaches is to use a Base64-encoded string in combination with the -EncodedCommand parameter:
[Convert]::ToBase64String() creates a Base64-encoded string from a [byte[]] array.
[Text.Encoding]::Unicode.GetBytes() converts the (internally UTF-16 -
"Unicode") string to a [byte[]] array.
Note: To also pass arguments, you have two options:
You can "bake" them into the -EncodedCommand argument, assuming you can call a command to pass them to there - see below, which shows how to define your function as such in the new session, so you can call it by name with arguments.Thanks, Abraham Zinala
The advantage of this approach is that you can pass named arguments this way. The disadvantage is that you are limited to arguments that have string-literal representations.
You can use the (currently undocumented) -EncodedArguments parameter, to which you must similarly pass a Base64-encoded string, albeit based on the CLIXML representation of the array of arguments to pass
The advantage of this approach is that you can pass a wider array of data types, within the limits of the type fidelity that CLIXML serialization can provide - see this answer; the disadvantage is that only positional arguments are supported this way (although you could work around that by passing a hashtable that the target code then uses for splatting with the ultimate target command).
Here's a simplified, self-contained example, which uses Write-Output to echo the (invariably positional) arguments received:
$command = 'Write-Output $args'
$argList = 'foo', 42
Start-Process powershell -args '-noprofile', '-noexit',
'-EncodedCommand',
([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command))),
'-EncodedArguments',
([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes(
[System.Management.Automation.PSSerializer]::Serialize($argList)
)))
In case you want to pass the complete function, so it can be called by name in order to pass arguments as part of the command string, a little more work is needed.
# Simply wrapping the body in `function <name> { ... }` is enough.
$func = (Get-Command -Type Function Check-PC)
$wholeFuncDef = 'Function ' + $func.Name + " {`n" + $func.Definition + "`n}"
Start-Process powershell -args '-noprofile', '-noexit', '-EncodedCommand', `
([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes("$wholeFuncDef; Check-PC")))
As stated above, you can "bake" any arguments to pass to your function - assuming they can be represented as string literals - into the -EncodedCommand argument, simply by appending them inside the "$wholeFuncDef; Check-PC" string above; e.g.,
"$wholeFuncDef; Check-PC -Foo Bar -Baz Quux"
Start-Process solution with regex-based escaping of the source code to pass:
PetSerAl suggests the following alternative, which uses a regex to perform the escaping.
The solution is more concise, but somewhat mind-bending:
Start-Process powershell -args '-noprofile', '-noexit', '-Command', `
('"' +
((Get-Command -Type Function Check-PC).Definition -replace '"|\\(?=\\*("|$))', '\$&') +
'"')
"|\\(?=\\*("|$)) matches every " instance and every nonempty run of \ chars. - character by character - that directly precedes a " char. or the very end of the string.
\\ is needed in the context of a regex to escape a single, literal \.
(?=\\*("|$)) is a positive look-ahead assertion that matches \ only if followed by " or the end of the string ($), either directly, or with further \ instances in between (\\*). Note that since assertions do not contribute to the match, the \ chars., if there are multiple ones, are still matched one by one.
\$& replaces each matched character with a \ followed by the character itself ($&) - see this answer for the constructs you can use in the replacement string of a -replace expression.
Enclosing the result in "..." ('"' + ... + '"') is needed to prevent whitespace normalization; without it, any run of more than one space char. and/or tab char. would be normalized to a single space, because the entire string wouldn't be recognized as a single argument.
Note that if you were to invoke powershell directly, PowerShell would generally automatically enclose the string in "..." behind the scenes, because it does so for arguments that contain whitespace when calling an external utility (a native command-line application), which is what powershell.exe is - unlike the Start-Process cmdlet.
PetSerAl points out that the automatic double-quoting mechanism is not quite that simple, however (the specific content of the string matters with respect to whether automatic double-quoting is applied), and that the specific behavior changed in v5, and again (slightly) in v6.