Let's start with the normal behavior. When I execute Read-Host
without a prompt, I can enter a string starting with an exclamation point:
PS C:\> Read-Host | Foreach-Object { Write-Host 'Entered' $_ }
!hi, mom
Entered !hi, mom
(Note that I only pipe to Foreach-Object
as an easy way to prefix the output. The behavior of interest is the same if you execute Read-Host
without it.)
But if I give Read-Host
a prompt argument, the behavior is quite different:
PS C:\> Read-Host 'Enter something' | Foreach-Object { Write-Host 'Entered' $_ }
Enter something: !hi, mom
"!hi, mom" cannot be recognized as a valid Prompt command.
Enter something: !!hi, mom
Entered !hi, mom
It seems the exclamation allows me to do some things besides just type in a string. PowerShell is interpreting the exclamation to mean I am entering some sort of command for it to run, but I can't find any documentation on what is allowed. Aside from doubling the exclamation to escape it, I can't figure out what is a valid command, either.
Note that the input has to begin with an exclamation point. Ending with it doesn't trigger this behavior:
PS C:\> Read-Host 'Enter something' | Foreach-Object { Write-Host 'Entered' $_ }
Enter something: hi, mom!
Entered hi, mom!
So what can I do with !
here? What is a valid command, besides just escaping the exclamation? A work around is useful, but I'm actually wondering if I can execute code or something here.
I'm using PowerShell 4, but this seems to date back to much earlier.
tl;dr
Read-Host
, as the name suggests, is host-specific.
Read-Host
prompt, which wasn't affected).The behavior is in effect an unrelated feature that is seemingly accidentally exposed, namely only if Read-Host
's prompt-string parameter (-Prompt
) is specified.
Work around the bug as follows:
Write-Host -NoNewline 'Enter something: '; Read-Host
Write-Host
to print the prompt string separately, beforehand (without a line break), then invoke Read-Host
without the -Prompt
parameter:To answer the question's title, "What can I do with ! when using Read-Host?": Nothing useful.
To see why, and for background information, read on.
The behavior - and complaints about it - dates back to the very first version of PowerShell; in the post also linked to by the OP an MS employee calls it a "Prompt command", and one of its architects explains it in more detail herediscovered by @TheMadTechnician in a comment on the question.
Note that is is discussed in the context of how PowerShell itself prompts for missing mandatory parameter values, which really should have no relationship with Read-Host
:
When prompted for a mandatory parameter value that wasn't specified on the command line (and unexpectedly also when using Read-Host
with a -Prompt
value), !
as the very first character (only) starts a "prompt command":
!?
invokes the help string for (the description of) the parameter at hand.
Read-Host
, the prompt string(!) is interpreted as the parameter name whose help is sought, and consequently no help is found (No help is available for <prompt-string>
).!""
allows entering an empty string as a parameter value for an array parameter while allowing entry of additional values (just pressing Enter would instantly terminate the prompt).
Read-Host
, this simply outputs an empty string.!!
allows entering a literal !
Χpẘ's great answer uses disassembly of the underlying assemblies to show that, at least as of PS v3, no additional commands are supported.
The linked forum post concludes (emphasis mine):
Note that this is an artifact of the current implementation of prompting in the PowerShell host. It's not part of the core engine. A GUI host would be unlikely to use this notation. A reasonable enhancement would be to allow the prompt command to be user-configurable.
10 years later, the behavior - at least in the context of Read-Host
- is neither documented nor configurable.
Before the above post was discovered, jpmc26 had himself found that the behavior is related to how PowerShell itself prompts for missing mandatory arguments; e.g.:
# Define a test function with a mandatory parameter.
> function foo([Parameter(Mandatory=$true,HelpMessage='fooParam help')][string]$fooParam) {}
# Invoke the test function *without* that mandatory parameter,
# which causes Powershell to *prompt* for it.
> foo
cmdlet foo at command pipeline position 1
Supply values for the following parameters:
(Type !? for Help.)
fooParam: !? # !? asks for help on the parameter, which prints the HelpMessage attribute.
fooParam help
fooParam: # After having printed parameter help, the prompt is shown again.
Using JetBrains dotPeek I found the implementation for where '!' is handled. It is in Microsoft.PowerShell.ConsoleHostUserInterface.PromptCommandMode
in the assembly Microsoft.Powershell.ConsoleHost
. This is PS 3.0. The disassembled code is below.
The check for strA.StartsWith
must be to see if the '!' is escaped with another '!'.
Note the check for strA[0] == 63
is a check for '?' (0x3F). Any other single char input yields the error message in the OP. Two double quotes yields the empty string (which is what Bruce Payette said in the link referenced in the OP comments) and the string '$null'
yields $null
.
Anything else gives the same error message. So, short of some kind of proxying, or writing your Host, the '!' can't really be leveraged for other commands.
private string PromptCommandMode(string input, FieldDescription desc, out bool inputDone)
{
string strA = input.Substring(1);
inputDone = true;
if (strA.StartsWith("!", StringComparison.OrdinalIgnoreCase))
return strA;
if (strA.Length == 1)
{
if ((int) strA[0] == 63)
{
if (string.IsNullOrEmpty(desc.HelpMessage))
{
string str = StringUtil.Format(ConsoleHostUserInterfaceStrings.PromptNoHelpAvailableErrorTemplate, (object) desc.Name);
ConsoleHostUserInterface.tracer.TraceWarning(str);
this.WriteLineToConsole(this.WrapToCurrentWindowWidth(str));
}
else
this.WriteLineToConsole(this.WrapToCurrentWindowWidth(desc.HelpMessage));
}
else
this.ReportUnrecognizedPromptCommand(input);
inputDone = false;
return (string) null;
}
if (strA.Length == 2 && string.Compare(strA, "\"\"", StringComparison.OrdinalIgnoreCase) == 0)
return string.Empty;
if (string.Compare(strA, "$null", StringComparison.OrdinalIgnoreCase) == 0)
return (string) null;
this.ReportUnrecognizedPromptCommand(input);
inputDone = false;
return (string) null;
}
private void ReportUnrecognizedPromptCommand(string command)
{
this.WriteLineToConsole(this.WrapToCurrentWindowWidth(StringUtil.Format(ConsoleHostUserInterfaceStrings.PromptUnrecognizedCommandErrorTemplate, (object) command)));
}
Here's another piece of the puzzle.
I'm running V5, and it works in the ISE:
PS C:\> $PSVersionTable
Name Value
---- -----
PSVersion 5.0.10586.51
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.10586.51
CLRVersion 4.0.30319.34209
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
PS C:\> Read-Host 'Enter something' | Foreach-Object { Write-Host 'Entered' $_}
Enter something: !Hi, mom
Entered !Hi, mom
But it doesn't work from a normal command prompt:
PS C:\> Read-Host 'Enter something' | Foreach-Object { Write-Host "Entered $_"}
Enter something: !Hi, mom
"!Hi, mom" cannot be recognized as a valid Prompt command.
Enter something:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With