Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What can I do with ! (exclamation point) when using Read-Host?

Tags:

powershell

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.

like image 614
jpmc26 Avatar asked Feb 18 '16 21:02

jpmc26


3 Answers

tl;dr

  • Read-Host, as the name suggests, is host-specific.

    • As of PSv5.1, the standard console host (console windows) and, in PSv3+, the ISE too exhibit the unexpected behavior. (In PSv2-, the ISE used its own GUI 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.

    • Given that the behavior is undocumented and exposed in this obscure manner, it should be considered a bug.
  • Work around the bug as follows:

    • Write-Host -NoNewline 'Enter something: '; Read-Host
    • I.e.: Use 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.

    • In the context of 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).

    • In the context of 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.
like image 173
mklement0 Avatar answered Nov 15 '22 03:11

mklement0


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)));
}
like image 42
Χpẘ Avatar answered Nov 15 '22 02:11

Χpẘ


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:
like image 43
mjolinor Avatar answered Nov 15 '22 04:11

mjolinor