Powershell call cmd.exe command like copy /b - powershell

I saw this already Fast and simple binary concatenate files in Powershell
I'm not interested by the answer above I'm interested about what's wrong with syntax below :
when I call a cmd.exe command like copy /b:
function join-file {
copy /b $($args[0])+$($args[1]) $($args[2])
}
I get an error Copy-Item : A positional parameter cannot be found

As the error alludes to, copy is actually just an alias for Copy-Item and it does not have a /b parameter. You can call cmd to use its copy command.
function join-file {
cmd /c copy /b $($args[0])+$($args[1]) $($args[2])
}

Note: This answer complements Doug Maurer's helpful answer, which provides an effective solution (for file names without spaces).
There's a subtlety in how PowerShell parses unquoted compound tokens such as $($args[0])+$($args[1]) (by compound token I mean directly concatenated distinct syntax constructs):
$($args[0])+$($args[1]) results in two arguments[1] - although with the specific command at hand (cmd.exe's internal copy command) that happens not to be a problem:
Argument 1: The value of $($args[0])
Argument 2: A verbatim + directly followed by the value of $($args[1])
To avoid this problem, enclose the whole compound token in "...", so as to predictably treat it as an expandable string.
The upshot:
To be safe, use double-quoting ("...") explicitly to enclose compound tokens that involve variable references or subexpressions.
By contrast, to reference a variable or even method call in isolation, neither quoting nor enclosing in $(...), the subexpression operator, are needed.
Applied naively to your command (see the better solution below):
# Note: See better solution below.
function join-file {
# Note the "..." around the first argument, and the absence of quoting
# and $(...) around the second.
cmd /c copy /b "$($args[0])+$($args[1])" $args[2]
}
However, if $args[0] or $($args[1]) contained spaces, the copy command would malfunction; it is therefore more robust to pass the file names and the + as separate arguments, which copy also supports:
function join-file {
# Pass the arguments individually, which obviates the need for quoting
# and $(...) altogether:
cmd /c copy /b $args[0] + $args[1] $args[2]
}
[1] You can verify this as follows: $arr='foo', 'bar'; cmd /c echo $($arr[0])+$($arr[1]), which yields: foo +bar (note the space).

Related

Powershell 7: using ampersand (&) in string literals

I'm trying to execute the following command in PowerShell, but I have no idea how to escape the ampersand character which is part of the URL
az rest `
--method GET `
--uri ("https://graph.microsoft.com/v1.0/groups?`$count=true&`$filter=startsWith(displayName,'some+filter+text')&`$select=id,displayName") `
--headers 'Content-Type=application/json'
As the & character is used to start a new command, it breaks the url and want to execute the remainder.
Is there a way to tell powershell not to do that?
Olaf's answer provides an effective solution; let me add an explanation:
The source of the problem is a confluence of two behaviors:
When calling external programs, PowerShell performs on-demand double-quoting of each argument solely based on whether a given argument value contains spaces - otherwise, the argument is passed unquoted - irrespective of whether or not the value was originally quoted in the PowerShell command (e.g., cmd /c echo ab, cmd /c echo 'ab', and cmd /c echo "ab" all result in unquoted ab getting passed as the last token on the command line PowerShell rebuilds behind the scenes to ultimately use for execution).
The Azure az CLI is implemented as a batch file (az.cmd) and when a batch file is called, it is cmd.exe that parses the arguments given; surprisingly - and arguably inappropriately - it parses them as if the command had been submitted from inside a cmd.exe session.
As a result, if an argument is passed from PowerShell to a batch file that (a) contains no spaces, yet (b) contains cmd.exe metacharacters such as &, the call breaks.
A simple demonstration, using a cmd /c echo call as a stand-in for a call to a batch file:
# !! Breaks, because PowerShell (justifiably) passes *unquoted* a&b
# !! when it rebuilds the command line to invoke behind the scenes.
PS> cmd /c echo 'a&b'
a
'b' is not recognized as an internal or external command,
operable program or batch file.
There are three workarounds:
Use embedded "..." quoting:
# OK, but with a CAVEAT:
# Works as of PowerShell 7.2, but arguably *shouldn't*, because
# PowerShell should automatically *escape* the embedded " chars. as ""
PS> cmd /c echo '"a&b"'
"a&b"
# Ditto, using an *expandable* (interpolating) PowerShell string:
PS> cmd /c echo "`"$HOME & Family; can't put a `$ value on that.`""
"C:\Users\jdoe & Family; can't put a $ value on that." # e.g.
Use --%, the stop-parsing token - but see the bottom section of this answer for the limitations of --% and its associated pitfalls.
# OK, but with a CAVEAT:
# Requires "..." quoting, but doesn't recognize *PowerShell* variables,
# also doesn't support single-quoting and line continuation.
PS> cmd /c echo --% "a&b"
"a&b"
Call via cmd /c and pass a single string encompassing the batch-file call and all its arguments, (ultimately) using cmd.exe's syntax.
# OK (remember, cmd /c echo stands for a call to a batch file, such as az.cmd)
# Inside the single string passed to the outer cmd /c call,
# be sure to use "...", as that is the only quoting cmd.exe understands.
PS> cmd /c 'cmd /c echo "a&b"'
"a&b"
# Ditto, using an *expandable* (interpolating) PowerShell string:
PS> cmd /c "cmd /c echo `"$HOME & Family; can't put a `$ value on that.`""
"C:\Users\jdoe & Family; can't put a $ value on that." # e.g.
Taking a step back:
Now, wouldn't it be nice if you didn't have to worry about all these things?
Especially since you may not know or care if a given CLI - such as az - just so happens to be implemented as a batch file?
As a shell, PowerShell should do its best to relay arguments faithfully behind the scenes, and allow the caller to focus exclusively on satisfying only PowerShell's syntax rules:
Unfortunately, PowerShell has to date (PowerShell 7.2) generally done a very poor job in this regard, irrespective of cmd.exe's quirks - see this answer for a summary.
With respect to cmd.exe's (batch-file call) quirks, PowerShell could predictably compensate for them in a future version - but it looks like that isn't going to happen, unfortunately; see GitHub issue #15143.
I don't have access to an Azure tennant right now to test and I actually don't have experiences with the Azure CLI in general but I'd expect this to work:
az rest `
--method GET `
--uri 'https://graph.microsoft.com/v1.0/groups?$count=true&$filter=startsWith(displayName,some+filter+text)&$select=id,displayName' `
--headers 'Content-Type=application/json'
or this:
az rest --method GET --headers "Content-Type=application/json" `
--% --uri "https://graph.microsoft.com/v1.0/groups?$count=true&$filter=startsWith(displayName,some+filter+text)&$select=id,displayName"
I only added the backticks for better readability - you may remove them in your actual code.

Powershell script is failing when files with a single quote are passed through script. Alternate batch file is also failing with & and ! characters

This is a deceptively complex issue, but I'll do my best to explain the problem.
I have a simple wrapper script as follows called VSYSCopyPathToClipboard.ps1:
param (
[Parameter(Mandatory,Position = 0)]
[String[]]
$Path,
[Parameter(Mandatory=$false)]
[Switch]
$FilenamesOnly,
[Parameter(Mandatory=$false)]
[Switch]
$Quotes
)
if($FilenamesOnly){
Copy-PathToClipboard -Path $Path -FilenamesOnly
}else{
Copy-PathToClipboard -Path $Path
}
Copy-PathToClipboard is just a function I have available that copies paths/filenames to the clipboard. It's irrelevant to the issue, just assume it does what it says.
The way the wrapper is called is through the Windows right click context menu. This involves creating a key here: HKEY_CLASSES_ROOT\AllFilesystemObjects\shell\.
Mine looks like this:
The command is as follows:
"C:\Tools\scripts\BIN\SingleInstanceAccumulator.exe" -q:' "-c:pwsh -noprofile -windowstyle hidden -Command "C:\Tools\scripts\VSYSCopyPathToClipboard.ps1" -Path $files" "%1"
And similarly for the "Copy as Filename":
"C:\Tools\scripts\BIN\SingleInstanceAccumulator.exe" -q:' "-c:pwsh -noprofile -windowstyle hidden -Command "C:\Tools\scripts\VSYSCopyPathToClipboard.ps1" -FilenamesOnly -Path $files" "%1"
I am using a tool here called SingleInstanceAccumulator. This allows me to pass multiple selected files to a single instance of PowerShell. If I didn't use this program and ran my command with multiple files selected it would launch multiple instances of PowerShell for each file selected. It's the next best thing to creating your own shell extension and implementing IPC etc.
This has been working great until today when I encountered a file with a single quote in its filename (I.E.testing'video.mov) and the entire script failed. It's failing because the delimiter I'm using with SingleInstanceAccumulator is also a single quote and PowerShell sees no matching quote... thus errors out.
I could fix this if my variables were static by just doubling up the offending single quote, but since my parameters are files I have no opportunity to escape the single quote beyond renaming the file itself ... which is a non-solution.
So now I have no clue how to handle this.
My first try at solving the problem was as such:
Create a batch file and redirect my registry command to it.
Change the SingleInstanceAccumulator delimiter to '/' (All files will be separated by a forward slash.)
Replace the offending single quote to two single quotes.
Replace the '/' delimiters with single quotes.
Finally pass the whole argument list back to Powershell.
This image demonstrates how the above process looks:
This is the batch file's code:
#echo off
setlocal EnableDelayedExpansion
:: This script is needed to escape filenames that have
:: a single quote ('). It's replaced with double single
:: quotes so the filenames don't choke powershell
:: echo %cmdcmdline%
set "fullpath=%*"
echo Before
echo !fullpath!
echo ""
echo After
set fullpath=%fullpath:'=''%
set fullpath=%fullpath:/='%
echo !fullpath!
:: pwsh.exe -noprofile -windowstyle hidden -command "%~dpn0.ps1 -Path !fullpath!
pause
Once I got that wired up I started celebrating ... until I hit a file with an ampersand (&) or an exclamation point (!). Everything fell apart again. I did a whole bunch of google-fu with regards to escaping the & and ! characters but nothing suggested worked at all for me.
If I pass 'C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\testing&video.mov' into my batch file, I get 'C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\testing back.
It truncates the string at the exact position of the ampersand.
I feel like there has to be a way to solve this, and that I'm missing something stupid. If I echo %cmdcmdline% it shows the full commandline with the &, so it's available somehow with that variable.
In conclusion: I'm sorry for the novel of a post. There is a lot of nuance in what I'm trying to accomplish that needs to be explained. My questions are as follows:
Can I accomplish this with Powershell only and somehow pre-escape single quotes?
Can I accomplish this with a batch file, and somehow pre-escape & and ! (and any other special characters that would cause failure)?
Any help at all would be hugely appreciated.
Edit1:
So in the most hideous and hackish way possible, I managed to solve my problem. But since it's so horrible and I feel horrible for doing it I am still looking for a proper solution.
Basically, to recap, when I do either of these variable assignments:
set "args=%*"
set "args=!%*!"
echo !args!
& and ! characters still break things, and I don't get a full enumeration of my files. Files with & get truncated, etc.
But I noticed when I do:
set "args=!cmdcmdline!"
echo !args!
I get the full commandline call with all special characters retained:
C:\WINDOWS\system32\cmd.exe /c ""C:\Tools\scripts\VSYSCopyPathToClipboardTest.bat" /C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\KylieCan't.mov/,/C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\The !Rodinians - Future Forest !Fantasy - FFF Trailer.mov/,/C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\Yelle - Je Veu&x Te Voir.mov/,/C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\Erik&Truffaz.mov/,/C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\my_file'name.mov/,/C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\testing&video.mov/"
So what I did was simply strip out the initial C:\WINDOWS\system32\cmd.exe /c ""C:\Tools\scripts\VSYSCopyPathToClipboardTest.bat" part of the string:
#echo off
setlocal enableDelayedExpansion
set "args=!cmdcmdline!"
set args=!args:C:\WINDOWS\system32\cmd.exe=!
set args=!args: /c ""C:\Tools\scripts\VSYSCopyPathToClipboard.bat" =!
set args=!args:'=''!
set args=!args:/='!
set args=!args:~0,-1!
echo !args!
pwsh.exe -noprofile -noexit -command "%~dpn0.ps1 -Path !args!
And... it works flawlessly. It handles any crazy character I throw at it without needing to escape anything. I know It's totally the most degenerate garbage way of approaching this, but not finding a solution anywhere leads me to desperate measures. :)
I am probably going to make the string removal a bit more universal since it literally breaks if I change the filename.
I am still VERY much open to other solutions should anyone know of a way to accomplish the same thing in a more elegant way.
A fully robust solution based on PowerShell's -Command (-c) CLI parameter that can handle ' characters in paths as well as $ and ` ones requires a fairly elaborate workaround, unfortunately:[1]
Use an aux. cmd.exe call that echoes the $files macro as-is and pipe that to pwsh.exe; make SingleInstanceAccumulator.exe double-quote the individual paths (as it does by default), but use no delimiter (d:"") in order to effectively output a string in the form "<path 1>""<path 2>""...
Make pwsh.exe reference the piped input via the automatic $input variable and split it into an array of individual paths by " (removing empty elements that are a side effect of splitting with -ne ''). The necessity for providing the paths via the pipeline (stdin) is discussed in more detail in this related answer.
The resulting array can safely be passed to your scripts.
Also, enclose the entire -Command (-c) argument passed to pwsh.exe in \"...\" inside the "-c:..." argument.
Note: You may get away without doing this; however, this would result in whitespace normalization, which (however unlikely) would alter a file named, say, foo bar.txt to foo bar.txt (the run of multiple spaces was normalized to a single space).
Escaping " characters as \" is necessary for PowerShell's -Command (-c) CLI parameter to treat them verbatim, as part of the PowerShell code to execute that is seen after initial command-line parsing, during which any unescaped " characters are stripped.
Therefore, the first command stored in the registry should be (adapt the second one analogously; note that there must be no space between the echo $files and the subsequent |):
"C:\Tools\scripts\BIN\SingleInstanceAccumulator.exe" -d:"" "-c:cmd /c echo $files| pwsh.exe -noprofile -c \"& 'C:\Tools\scripts\VSYSCopyPathToClipboard.ps1' -Path ($input -split '\\\"' -ne '')\"" "%1"
Note:
If you modified your scripts to accept the paths as individual arguments rather than as an array, a much simpler solution via the -File CLI parameter (rather than -Command (-c)) is possible. This could be as simple as decorating the $Path parameter declaration with [Parameter(ValueFromRemainingArguments)] and then invoking the script without naming the target parameter explicitly (-Path):
"C:\Tools\scripts\BIN\SingleInstanceAccumulator.exe" -d:" " "-c:pwsh.exe -noprofile -File \"C:\Tools\scripts\VSYSCopyPathToClipboard.ps1\" $files" "%1"
Note the use of -d:" " to make SingleInstanceAccumulator.exe space-separate the (double-quoted by default) paths. Since -File passes the pass-through arguments verbatim, there is no concern about what characters the paths are composed of.
Self-contained PowerShell sample code:
The following code defines a Copy Paths to Clipboard shortcut-menu command for all file-system objects (except drives):
No separate .ps1 script is involved; instead, the code passed to -Command / -c directly performs the desired operation (copying the paths passed to the clipboard).
The following helps with troubleshooting:
The full command line with which PowerShell was invoked ([Environment]::CommandLine) is printed, as is the list of paths passed ($file)
-windowstyle hidden is omitted to keep the console window in which the PowerShell commands visible and -noexit is added so as to keep the window open after the command has finished executing.
Prerequisites:
Download and build the SingleInstanceAccumulator project using Visual Studio (using the .NET SDK is possible, but requires extra work).
Place the resulting SingleInstanceAccumulator.exe file in one of the directories listed in your $env:Path environment variable. Alternatively, specify the full path to the executable below.
Note:
reg.exe uses \ as its escape character, which means that \ characters that should become part of the string stored in the registry must be escaped, as \\.
The sad reality as of PowerShell 7.2 is that an extra, manual layer of \-escaping of embedded " characters is required in arguments passed to external programs. This may get fixed in a future version, which may require opt-in. See this answer for details. The code below does this by way of a -replace '"', '\"' operation, which can easily be removed if it should no longer be necessary in a future PowerShell version.
# RUN WITH ELEVATION (AS ADMIN).
# Determine the full path of SingleInstanceAccumulator.exe:
# Note: If it isn't in $env:PATH, specify its full path instead.
$singleInstanceAccumulatorExe = (Get-Command -ErrorAction Stop SingleInstanceAccumulator.exe).Path
# The name of the shortcut-menu command to create for all file-system objects.
$menuCommandName = 'Copy Paths To Clipboard'
# Create the menu command registry key.
$null = reg.exe add "HKEY_CLASSES_ROOT\AllFilesystemObjects\shell\$menuCommandName" /f /v "MultiSelectModel" /d "Player"
if ($LASTEXITCODE) { throw }
# Define the command line for it.
# To use *Windows PowerShell* instead, replace "pwsh.exe" with "powershell.exe"
# SEE NOTES ABOVE.
$null = reg.exe add "HKEY_CLASSES_ROOT\AllFilesystemObjects\shell\$menuCommandName\command" /f /ve /t REG_EXPAND_SZ /d (#"
"$singleInstanceAccumulatorExe" -d:"" "-c:cmd /c echo `$files| pwsh.exe -noexit -noprofile -c \\"[Environment]::CommandLine; `$paths = `$input -split [char] 34 -ne ''; `$paths; Set-Clipboard `$paths\\"" "%1"
"# -replace '"', '\"')
if ($LASTEXITCODE) { throw }
Write-Verbose -Verbose "Shortcut menu command '$menuCommandName' successfully set up."
Now you can right-click on multiple files/folders in File Explorer and select Copy Paths to Clipboard in order to copy the full paths of all selected items to the clipboard in a single operation.
[1] An alternative is to use the -f option instead, which causes SingleInstanceAccumulator.exe to write all file paths line by line to an auxiliary text file, and then expands $files to that file's full path. However, this requires the target scripts to be designed accordingly, and it is their responsibility to clean up the auxiliary text file.

Execute a PowerShell script within RunAs in a script

I have a need to run a PowerShell script as another user (the users will actually be doing the auth) based on detected environment. The script leverages a smartcard for login. The problem I have is when the PowerShell.exe instance launches from the runas, it simply prints my command and doesn't actually run it.
Note: The filepaths have spaces that get escaped with double-`, just not shown here hence the escaped ``' in the actual command. Have also used ```".
Example:
Start-Process runas.exe "/netonly /smartcard /user:domain\$env:USERNAME ""powershell.exe -noexit -Command `'&$filePath -arg1 -arg2`' "" "
or
Start-Process runas.exe "/netonly /smartcard /user:domain\$env:USERNAME ""powershell.exe -noexit -File `'$filePath -arg1 -arg2`' "" "
File path points to the same .ps1 file. The dir is similar to: C:\Users\Username\My Documents\script.ps1
When this runs I simply get a script window that doesn't actually run, it just prints the File name. I have also tried using -File but that simply crashes (regardless of -noexit). The runas bit works fine, I get my smartcard prompt etc its just the actual PowerShell launch that I am struggling with.
These work just fine calling them directly from PowerShell Cli but when in a .ps1 it just won't work.
Any help would be greatly appreciate.
There's generally no reason to use Start-Process to run a console application such as runas.exe - unless you explicitly want the application to run in a new window, asynchronously (by default) - see this answer for more information.
Eliminating Start-Process simplifies the quoting:
runas.exe /netonly /smartcard /user:domain\$env:USERNAME "powershell.exe -noexit -File \`"$filePath\`" -arg1 -arg2"
Note the - unfortunate - need to manually \-escape the embedded " chars. (`"), which is required up to at least v7.1 - see this answer.
As for what you tried:
On Windows, passing a '...'-enclosed string to the PowerShell CLI's -Command (-c) parameter from outside PowerShell (such as via runas.exe) causes it to be interpreted as a verbatim PowerShell string, printing its content as-is (except for whitespace normalization).
You can verify this by running the following from cmd.exe, for instance:
C:\>powershell -c 'Get-Date; whatever I type here is used as-is - almost'
Get-Date; whatever I type here is used as-is - almost
Note how the multiple spaces before the word almost were normalized to a single one.
The reason is that on Windows only double-quoting ("...") has syntactic function on the command line - ' characters are interpreted verbatim and therefore passed through.
Therefore, when the -Command (-c) argument sees its argument(s) -- after command-line parsing - and then interprets the resulting space-joined, possibly double-quote-stripped arguments as PowerShell source code, a span of '...' is interpreted as a verbatim PowerShell string, as usual (see the conceptual about_Quoting_Rules help topic, which discusses the types of PowerShell string literals).
In concrete terms:
'Get-Date; whatever I type here is used as-is - almost' is parsed by the PowerShell CLI as the following arguments(!):
'Get-Date;, whatever, I, type, here, is, used, as-is, -, almost' - note how any information about the amount of whitespace that originally separated these argument is lost in the process.
These arguments are then joined with a single space to form the string that PowerShell then interprets as PowerShell source code, which yields:
'Get-Date; whatever I type here is used as-is - almost'
This amounts to a verbatim (single-quoted) PowerShell string literal, whose content is then output as-is.

How to escape all quotes in $* to pass it to PowerShell from CMD

I experience some problem with executing PowerShell command from CMD.
I have a function MyFunction in PowerShell which takes two parameters: Id and Text.
Example:
MyFunction 12345 "Name = Karl"
I need to call this function from CMD:
powershell -Command . { MyFunction $* }
where $*: 12345 "Name = Karl"
But I get error:
A positional parameter cannot be found that accepts argument 'Karl'
The cause of it is quotes are being removed from passing parameters
I figured out I need to escape all quotes in passing parameters but I really don't know how to do it within $*. I'm not expert in CMD, could you please help me to solve the issue?
Update (12/25/2019)
I forgot to mention I'm creating an alias via doskey and I don't know how to escaping quotes within $*
MyFunction=powershell -Command . { MyFunction $* }
doskey macros do not support pre-processing of arguments passed ($*), which is what is required in your case.
Preprocessing of the arguments is required, because you're using the PowerShell CLI's -Command parameter - of necessity, since you're calling a function - where the arguments undergo an additional round of parsing after PowerShell strips them of enclosing "...": the resulting values are concatenated and then interpreted as a piece of PowerShell code - and by that time what was originally "Name = Karl" is seen as 3 individual arguments, Name, =, and Karl.
To solve that problem, you'd have to use embedded quoting, by passing "Name=Karl" as "\"Name = Karl\"" (sic) instead.
However, you wouldn't have that problem if you used -File instead of -Command, but that only works when you call a script file (*.ps1).
Here's a workaround:
Create an auxiliary *.ps1 file named, say, MyFunction.ps1, with the following content:
MyFunction $args
Be sure that function MyFunction is actually defined in your PowerShell sessions. By virtue of being a function, it takes precedence over a script with the same base name.
Then redefine your doskey macro as follows (adjust the path as needed; if you placed MyFunction.ps1 in a directory listed in $env:PATH, you don't need to specify a path):
doskey MyFunction=powershell -File C:\path\to\MyFunction.ps1 $*

Passing newline character to PowerShell via Cmd

I'm trying to run a PowerShell script from Windows cmd.exe. The input to the PowerShell script is a string, which contains newline characters using PowerShell backtick escaping - i.e:
`r`n
For demonstration purposes, the input string is then written to the console, and also dumped to a file.
The issue I have is that when the script is run from cmd.exe using the syntax
powershell.exe script.ps1 "TEST`r`nTEST"
The newline characters in the string are not treated as newline, and are included literally in both the console output and the output text file.
TEST`r`nTEST
However, if I run this from a PowerShell environment, I get the expected result (i.e. the newline characters are parsed correctly, and a newline is inserted in the appropriate location).
TEST
TEST
Similarly, if I pass in \r\n instead of the escaped newline characters through Windows cmd.exe, and do a .replace in the PowerShell script
$date = $data.replace("\r\n","`r`n")
I get the expected output:
TEST
TEST
Is anyone able to shed some light on why this happens?
The test script is as follows:
param([string]$data) # data to send
Write-Host $data
[IO.File]::WriteAllText('d:\temp.txt', $data)
return 0
And the file is called from the command line as:
powershell.exe script.ps1 "TEST`r`nTEST"
The script is running on Windows Server 2012 R2, using PowerShell v4.0
tl;dr
Use -Command and pass the entire PowerShell command as a single string; e.g.:
C:\> powershell -NoProfile -Command "script.ps1 \"TEST`r`nTEST\""
TEST
TEST
Note how the internal " instances are escaped as \", which PowerShell requires when called from the outside (alternatively, for full robustness, use "^"" (sic) in Windows PowerShell and "" in PowerShell (Core) v6+).
In your specific case,
powershell -NoProfile -Command script.ps1 "TEST`r`nTEST" would have worked too, but generally that only works as intended if the string has no embedded spaces.
Given that -Command is the default up to PSv5.1, your command - as currently posted - should work as-is.
As of PowerShell v5.1, arguments passed to powershell.exe from the outside:
ARE subject to interpretation by PowerShell, including string interpolation, by default and when you use -Command (i.e., specifying neither -File nor -Command currently defaults to -Command).
Caveat: The default behavior will change in v6: -File will be the default then - see the relevant change on GitHub.
are NOT subject to interpretation if you use -File to invoke a script - (after potential interpretation by cmd.exe) PowerShell treats all arguments as literal strings.
Caveat: This behavior is currently being discussed with respect to v6, given that it is overtly problematic in at least one case: trying to pass Boolean values.
Optional reading: Why you should pass the entire PowerShell command as a single argument when using -Command:
When you use -Command with multiple arguments, PowerShell essentially assembles them into a single command line behind the scenes before executing it.
Any "..."-quoting around the individual arguments is lost in the process, which can have unexpected results; e.g.:
C:\> powershell -NoProfile -Command "& { $args.count }" "line 1`r`nline 2"
3 # !! "line 1`r`nline 2" was broken into 3 arguments
Given that the outer "..." quoting was removed in the process of parsing the command line, the actual command line that PowerShell ended up executing was:
C:\ PS> & { $args.Count } line 1`r`nline 2
3
To illustrate why, let's look at an equivalent command that uses explicit quoting:
C:\ PS> & { $args.Count } "line" "1`r`nline" "2"
In other words: After the enclosing " were removed, the resulting token was broken into multiple arguments by spaces, as usual.
The parameter will need to be reinterpreted as a PowerShell string. Will this get you down the road?
The reason your -replace did not work is that the original string actually contains a backtick. It needs to be escaped in the search string.
C:\src\t>type p1.ps1
Param([string]$s)
Write-Host $s
$p = Invoke-Expression `"$s`"
Write-Host $p
$p2 = $s -replace "``r``n","`r`n"
Write-Host $p2
C:\src\t>powershell -noprofile -file .\p1.ps1 "TEST`r`nTEST"
TEST`r`nTEST
TEST
TEST
TEST
TEST
Carriage return and Linefeed are bytes with values 13 and 10, you can't write them, you can't see them.
As a convenience, when writing PowerShell code, the language will let you write:
"`r`n"
in a double quoted string, and when processing PowerShell source code (and at no other time), it will read those and replace them with bytes value 13 and 10.
It is this line of code in the PowerShell tokenizer which does it.
There is nothing special about backtick-n to the cmd.exe interpreter, and nothing special about having it in a string - you can put it there in a single quoted string
'`n'
or replacing it in a string - except that you have to note when the replacement happens. e.g. in your comment:
For example, if you pass in 'r'n and then replace 'r'n with 'r'n, the 'r'n is still output literally
Because your code
-replace "`r`n"
becomes
-replace "[char]13[char]10"
and your string passed in from outside contains
`r`n
and they don't match. Backtick-n in a string isn't magic, strings are not all interpreted by the PowerShell engine as PowerShell code, nor are parameters, or anything. And it's only in that context - when you write your -replace code, that is when the swap for actual newline characters happens.