Read key presses via REPLACE - New functions :getKey, :getAnyKey, :getMaskedInput

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Message
Author
dbenham
Expert
Posts: 1961
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: Read key presses via REPLACE - New functions :getKey, :getAnyKey, :getMaskedInput

#16 Post by dbenham » 06 Sep 2016 18:35

Eureeka - I think I solved it :idea:

Code: Select all

for "skip=1 delims=" %%A in ('replace ? . /w /u ^| findstr /n "^" ^| find /n /v ""') do ...

Last Line     Character
-----------   -----------
[3]3:         <LF>
[3]           <NULL>
[2]2:         <Ctrl-Z> (Win 10 only)
All Others    Last character of last line


Dave Benham

Aacini
Expert
Posts: 1465
Joined: 06 Dec 2011 22:15
Location: México City, México

Re: Read key presses via REPLACE - New functions :getKey, :getAnyKey, :getMaskedInput

#17 Post by Aacini » 06 Sep 2016 22:19

In my computer the <Ctrl-2> key don't works: REPLACE just keep waiting for a key. I must add that my Windows 8.1 lap-top computer keyboard do NOT have the numeric keypad in blue-digits (nor the Num-Lock key), so I have not means to generate bytes with any value via Alt-numbers.

If the <Ctrl-2> key would worked in my computer, my next test would be to put two REPLACE's this way:

Code: Select all

( replace.exe ? . /u /w & replace.exe ? . /u /w ) > out.txt

... and then press an extended key: arrow or function key. I suppose that the first REPLACE would get the byte with <NUL> (like the <Ctrl-2> key does) and the second REPLACE would get the next byte. This method would allow to read extended keys via standard Batch commands (although such a method would not work in my computer).

Antonio

jeb
Expert
Posts: 830
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Re: Read key presses via REPLACE - New functions :getKey, :getAnyKey, :getMaskedInput

#18 Post by jeb » 07 Sep 2016 01:49

@Aacini: It's strange, I just now tested <CTRL-2> with Win8.1 and it works for me with the same output as with Win7.
I used the "normal" 2-Key, surprisingly using the "2" from the NUM-Pad doesn't work at all, independent of NUM-Lock.

Extended keys doesn't work, as they don't seem to produce any key codes at all.
I suppose they use a complete different way and need a different way to detect them.
PAUSE can read them, two PAUSE commands consume one extended key, but I can't see a way to get the real data from the PAUSE command.

dbenham wrote:Wow :shock:
Interesting find about <Ctrl-2> and Null.

Yes, but not my finding, I just read this some time ago somewhere at dostips (perhaps from npocmaka_).

dbenham
Expert
Posts: 1961
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: Read key presses via REPLACE - New functions :getKey, :getAnyKey, :getMaskedInput

#19 Post by dbenham » 07 Sep 2016 07:19

OK, I've modified :getAnyKey to capture Null as an undefined variable. Null is entered by pressing [Ctrl-2].

I backtracked and decided to preserve :getKey because I am worried that the added complexity in :getAnyKey might be too slow in some applications. I also abandoned the idea of allowing alternate value mappings for returned characters.

The changes can be found in the first post (edited).


Dave Benham

penpen
Expert
Posts: 1454
Joined: 23 Jun 2013 06:15
Location: Germany

Re: Read key presses via REPLACE - New functions :getKey, :getAnyKey, :getMaskedInput

#20 Post by penpen » 07 Sep 2016 08:19

The "CTRL-2" also works under win 10, 32 bit and Win xp home/prof 32 bit (so it probably depends on the keyboard only if it works).

jeb wrote:I just read this some time ago somewhere at dostips (perhaps from npocmaka_).
I only saw it here (Liviu):
http://www.dostips.com/forum/viewtopic.php?p=39814#p39814(In some posts above that, MagicMovingImages uses "ctrl-@" which i assume should be the same key on us-en keyboards.)


penpen

thefeduke
Posts: 199
Joined: 05 Apr 2015 13:06
Location: MA South Shore, USA

Re: Read key presses via REPLACE - New functions :getKey, :getAnyKey, :getMaskedInput

#21 Post by thefeduke » 07 Sep 2016 15:08

dbenham wrote:I've fixed the two routines and modified the version to 1.1 in my first post.

It seems that a minor bug came in with the documentation. There is no longer a :getkey label. Neither of these candidates are a valid target:

Code: Select all

::getKey  KeyVar  [ValidVar]
::
::
:get key
set "%1="
BTW, I added some non-intrusive code at the beginning of my copy:

Code: Select all

@Echo Off
Set "InternalCall=%~1"
If "%InternalCall:~0,1%" EQU ":" SHIFT /1
If "%InternalCall:~0,1%" EQU ":" (
    Call %InternalCall% %1 %2 %3 %4 %5 %6 %7 %8
) else (
    Call :getMaskedInput %1 %2 %3 %4 %5 %6 %7 %8
)
Set "InternalCall="
exit /b
This allows the script to act the same as before to run getMaskedInput, but I can access the other functions directly as in

Code: Select all

getanykey :getkey keyvar
without peeling off the standalone function to its own script.

John A.

dbenham
Expert
Posts: 1961
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: Read key presses via REPLACE - New functions :getKey, :getAnyKey, :getMaskedInput

#22 Post by dbenham » 07 Sep 2016 16:43

Thanks John. If fixed the label. It should be :getKey, not :get Key.

I opted not to change the version.


Dave Benham

dbenham
Expert
Posts: 1961
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: Read key presses via REPLACE - New functions :getKey, :getAnyKey, :getMaskedInput

#23 Post by dbenham » 10 Jan 2017 19:45

Version 2.0 is here :!:

I can't believe I missed the fact that my 1.x ValidVar checks ignored case, but the documentation implied that it was case sensitive. :x
Well that simply can't stand...

I decided I would add an option to indicate whether case should be ignore or not, and I also wanted a prompt option, and that set off an intense development effort. It resulted in significant restructuring of the code, and I also changed the rules for validVar.
So version 2.0 of these functions is not compatible with version 1.x

All three functions use the same calling sequence - call :xxxxx [/P "prompt"] OutputVar [ValidVar [/I]]

Both :getKey and :getAnyKey will echo the pressed key if the /P option is used, and will remain silent if not used.

Case is ignored if the /I option follows ValidVar (old behavior), and case is honored if the /I option is not used (new behavior).

The :getAnyKey function has the following additional changes:
  • Defines variables to hold <Ctrl-Z>, <LineFeed>, and <CarriageReturn> characters
  • Auxilliary :getAnyKeyInit function can be used to pre-define the <Ctrl-Z>, <LineFeed>, and <CarriageReturn> variables
  • The ValidVar argument now only has a single binary 0/1 value at the front to indicate rejection/acceptance of <Null>. LineFeed and CarriageReturn are now included by adding the actual character(s) to ValidVar.

Code: Select all

::getMaskedInput  [/P "Prompt"]  StrVar  [ValidVar [/I]]
::
:: Get a user input string, echoing * for each key pressed. [Backspace] erases
:: the previous character. [Enter] completes the string. Additionally, any
:: method that generates Null (0x00), LineFeed (0x0A) or Carriage Return (0x0D)
:: will also terminate the string. On Windows 10 a [Ctrl-Z] (0x1A) will also
:: terminate the string. The final string may contain any characters between
:: 0x01 and 0xFF except Backspace, LineFeed, and Carriage Return. On Windows 10
:: Ctrl-Z is also excluded.
::
:: The optional /P parameter is used to specify a "Prompt" that is written to
:: stdout, without a newline.
::
:: The optional ValidVar parameter defines the characters that will be accepted.
:: If the variable is not given or not defined, then all characters are accepted.
:: If given and defined, then only characters within ValidVar are accepted.
::
:: If ValidVar is followed by the optional /I switch, then case of standard
:: English letters is ignored. The case of the pressed key is preserved in
:: the result, but English letters A-Z and a-z are not rejected due to case
:: differences when the /I switch is added.
::
:: Any value (except null) may be entered by holding the [Alt] key and pressing
:: the appropriate decimal code on the numeric keypad. For example, holding
:: [Alt] and pressing numeric keypad [1] and [0], and then releasing [Alt] will
:: result in a LineFeed.
::
:: The only way to enter a Null is by holding [Ctrl] and pressing the normal [2]
::
:: An alternate way to enter control characters 0x01 through 0x1A is by holding
:: the [Ctrl] key and pressing any one of the letter keys [A] through [Z].
:: However, [Ctrl-A], [Ctrl-F], [Ctrl-M], and [Ctrl-V] will be blocked on Win 10
:: if the console has Ctrl key shortcuts enabled.
::
:: This function works properly regardless whether delayed expansion
:: is enabled or disabled.
::
:: :getMaskedInput version 2.0 was written by Dave Benham, and originally
:: posted at http://www.dostips.com/forum/viewtopic.php?f=3&t=7396
::
:: This work was inspired by posts from carlos and others at
:: http://www.dostips.com/forum/viewtopic.php?f=3&t=6382
::
:getMaskedInput
setlocal
set "notDelayed=!"
if /i "%~1" equ "/P" (
  <nul set /p ^"=%2"
  shift /1
  shift /1
)
setlocal enableDelayedExpansion
set "mask=!%2!"
for /f %%A in ('"Prompt;$H&for %%A in (1) do rem"') do set "BS=%%A"
if defined mask set "mask=1!BS!!mask!"
set "str="
:getMaskedInputLoop
(
  call :getKey key mask %3
  if defined key (
    if not defined notDelayed (
      if "!key!" equ "^!" set "key=^^^!"
      if "!key!" equ "^" set "key=^^"
    )
    if "!key!" equ "!BS!" (
      if defined str (
        set "str=!str:~0,-1!"
        <nul set /p "=%BS% %BS%"
      )
    ) else (
      set "str=!str!!key!"
      <nul set /p "=*"
    )
    goto :getMaskedInputLoop
  )
  for /f "delims=" %%A in (""!str!"") do (
    endlocal
    endlocal
    set "%1=%%~A" !
    echo(
    exit /b
  )
)

::getKey  [/P "Prompt"]  KeyVar  [ValidVar [/I]]
::
:: Read a keypress representing a character between 0x00 and 0xFF and store the
:: value in variable KeyVar. Null (0x00), LineFeed (0x0A), and Carriage Return
:: (0x0D) will result in an undefined KeyVar. On Windows 10, Ctrl-Z (0x1A) will
:: also result in an undefined KeyVar. The simplest way to get an undefined
:: KeyVar is to press the [Enter] key.
::
:: The optional /P parameter is used to specify a "Prompt" that is written to
:: stdout, without a newline. Also, the accepted character is ECHOed after the
:: prompt if the /P option was used.
::
:: The optional ValidVar parameter defines the values that will be accepted.
:: If the variable is not given or not defined, then all characters are accepted.
:: If given and defined, then only characters within ValidVar are accepted. The
:: first character within ValidVar should either be 0, meaning ignore undefined
:: KeyVar, or 1, meaning accept undefined KeyVar. The remaining characters
:: represent themselves. For example, a ValidVar value of 0YN will only accept
:: uppercase Y or N. A value of 1YN will additionally accept [Enter] etc.
::
:: If ValidVar is followed by the optional /I switch, then case of standard
:: English letters is ignored. The case of the pressed key is preserved in
:: the result, but English letters A-Z and a-z are not rejected due to case
:: differences when the /I switch is added.
::
:: Any value (except null) may be entered by holding the [Alt] key and pressing
:: the appropriate decimal code on the numeric keypad. For example, holding
:: [Alt] and pressing numeric keypad [1] and [0], and then releasing [Alt] will
:: result in a LineFeed.
::
:: The only way to enter a Null is by holding [Ctrl] and pressing the normal [2]
::
:: An alternate way to enter control characters 0x01 through 0x1A is by holding
:: the [Ctrl] key and pressing any one of the letter keys [A] through [Z].
:: However, [Ctrl-A], [Ctrl-F], [Ctrl-M], and [Ctrl-V] will be blocked on Win 10
:: if the console has Ctrl key shortcuts enabled.
::
:: This function works properly regardless whether delayed expansion is enabled
:: or disabled.
::
:: :getKey version 2.0 was written by Dave Benham, and originally posted at
:: http://www.dostips.com/forum/viewtopic.php?f=3&t=7396
::
:: This work was inspired by posts from carlos and others at
:: http://www.dostips.com/forum/viewtopic.php?f=3&t=6382
::
:getKey
setlocal disableDelayedExpansion
if /i "%~1" equ "/P" (
  <nul set /p ^"=%2"
  shift /1
  shift /1
  set "getKey./P=1"
) else (
  set "getKey./P="
)
:getKeyRetry
(
  endlocal&setlocal disableDelayedExpansion
  (for /f skip^=1^ delims^=^ eol^= %%A in ('replace.exe ? . /u /w') do for /f delims^=^ eol^= %%B in ("%%A") do (
    endlocal
    if "%%B" equ "" (set "%1=^!") else set "%1=%%B"
    setlocal enableDelayedExpansion
  )) || (
    endlocal
    set "%1="
    setlocal enableDelayedExpansion
  )
  set "getKey./P=%getKey./P%"
  if defined %1 (set "getKey.key=!%1!") else set "getKey.key=x"
)
(
  if "!%2!" neq "" (
    if defined %1 (
      set "getKey.mask=!%2:~1!"
      if not defined getKey.mask goto :getKeyRetry
      if /i "%~3" equ "/I" (
        if "!%1!" equ "=" (
          set "getKey.mask=a!getKey.mask!"
          for /f "delims=" %%A in ("!getKey.mask!") do if /i "!getKey.mask:%%A=%%A!" equ "!getKey.mask!" goto :getKeyRetry
        ) else for /f delims^=^ eol^= %%A in ("!%1!") do if "!getKey.mask:*%%A=!" equ "!getKey.mask!" goto :getKeyRetry
      ) else (
        for /f tokens^=1*^ eol^=^%getKey.key%^ delims^=^%getKey.key% %%A in ("!getKey.mask!!getKey.mask!") do if "%%B" equ "" goto :getKeyRetry
      )
    ) else if "!%2:~0,1!" equ "0" goto :getKeyRetry
  )
  if defined getKey./P echo(!%1!
  exit /b
)

::getAnyKey  [/P "Prompt"]  KeyVar  [ValidVar [/I]]
::
:: Read a keypress representing any character between 0x00 and 0xFF and store
:: the character in variable KeyVar. A Null value of 0x00 is represented as an
:: undefined KeyVar.
::
:: Calling :getAnyKey will also define the following three variables:
::   getAnyKey.LF    = Linefeed        0x0A, decimal 10
::   getAnyKey.CR    = Carriage Return 0x0D, decimal 13
::   getAnyKey.CtrlZ = Control-Z       0x1A, decimal 26
:: The three variables can be defined in advance by calling :getAnyKeyInit.
::
:: The optional /P parameter is used to specify a "Prompt" that is written to
:: stdout, without a newline. Also, the accepted character is ECHOed after the
:: prompt if the /P option was used.
::
:: The optional ValidVar parameter defines the characters that will be accepted.
:: If the variable is not given or not defined, then all values are accepted.
:: If given and defined, then only characters within ValidVar are accepted. The
:: first character indicates whether Null (0x00) is accepted. A value of 1 means
:: acceptance, and 0 means rejection. The remaining characters represent
:: themselves. For example, a ValidVar value of 0YN will only accept upper case
:: Y or N. A value of 1YN will additionally accept Null.
::
:: Linefeed, Carriage Return, and/or Control-Z may be added to ValidVar by
:: enabling delayedExpansion, calling :getAnyKeyInit, and then appending
:: !getAnyKey.LF!, !getAnyKey.CR!, and/or !getAnyKey.CtrlZ! respectively.
::
:: If ValidVar is followed by the optional /I switch, then case of standard
:: English letters is ignored. The case of the pressed key is preserved in
:: the result, but English letters A-Z and a-z are not rejected due to case
:: differences when the /I switch is added.
::
:: Note that [Enter] is interpreted as a Carriage Return.
::
:: Any value (except null) may be entered by holding the [Alt] key and pressing
:: the appropriate decimal code on the numeric keypad. For example, holding
:: [Alt] and pressing numeric keypad [1] and [0], and then releasing [Alt] will
:: result in a Linefeed.
::
:: The only way to enter a Null is by holding [Ctrl] and pressing the normal [2]
::
:: An alternate way to enter control characters 0x01 through 0x1A is by holding
:: the [Ctrl] key and pressing any one of the letter keys [A] through [Z].
:: However, [Ctrl-A], [Ctrl-F], [Ctrl-M], and [Ctrl-V] will be blocked on Win 10
:: if the console has Ctrl key shortcuts enabled.
::
:: This function works properly regardless whether delayed expansion is enabled
:: or disabled.
::
:: :getAnyKey version 2.0 was written by Dave Benham, and originally posted at
:: http://www.dostips.com/forum/viewtopic.php?f=3&t=7396
::
:: This work was inspired by posts from carlos and others at
:: http://www.dostips.com/forum/viewtopic.php?f=3&t=6382
::
:getAnyKey
if not defined getAnyKey.CtrlZ call :getAnyKeyInit
setlocal
if "!!" equ "" set "getAnyKey.delayed=1"
(
  endlocal&setlocal disableDelayedExpansion
  set "getAnyKey.delayed=%getAnyKey.delayed%"
  if /i "%~1" equ "/P" (
    set "getAnyKey./P=1"
    <nul set /p ^"=%2"
    shift /1
    shift /1
  ) else (
    set "getAnyKey./P="
  )
)
:getAnyKeyRetry
(
  endlocal&setlocal disableDelayedExpansion
  for /f "skip=1 delims=" %%A in (
    'replace.exe ? . /u /w ^| findstr /n "^" ^| find /n /v ""'
  ) do set "str=%%A"
  setlocal enableDelayedExpansion
  if "!str!" equ "[3]3:" (  %= LineFeed =%
    endlocal&endlocal
    set ^"%1=^%getAnyKey.LF%%getAnyKey.LF%"
  ) else (                  %= Not LineFeed =%
    if "!str!" equ "[2]2:" (         %= Ctrl-Z on Win 10 =%
      set "key=!getAnyKey.CtrlZ!
    ) else if "!str!" equ "[3]" (    %= Null = %
      set "key="
    ) else (                         %= All others =%
      set "key=!str:~-1!"
      if "%getAnyKey.delayed%"=="1" if "!key!" equ "^!" set "key=^^^!"
    )
    for /f "delims=" %%A in (""!key!"") do endlocal&endlocal&set "%1=%%~A"
  )
  setlocal enableDelayedExpansion
  set "getAnyKey.delayed=%getAnyKey.delayed%"
  set "getAnyKey./P=%getAnyKey./P%"
  set "getAnyKey.key=x"
  if defined %1 if !%1! neq !getAnyKey.LF! if !%1! neq !getAnyKey.CR! set "getAnyKey.key=!%1!"
)
(
  if "!%2!" neq "" (
    if defined %1 (
      set "getAnyKey.mask=!%2:~1!"
      if not defined getAnyKey.mask goto :getAnyKeyRetry
      if "!%1!" equ "!getAnyKey.LF!" (
        for %%A in ("!%1!") do if "!getAnyKey.mask:%%~A=!" equ "!getAnyKey.mask!" goto :getAnyKeyRetry
      ) else if "!%1!" equ "!getAnyKey.CR!" (
        for /f %%A in (""!%1!"") do if "!getAnyKey.mask:%%~A=!" equ "!getAnyKey.mask!" goto :getAnyKeyRetry
      ) else if /i "%~3" equ "/I" (
        if "!%1!" neq "=" (
          for /f "delims=" %%A in (""!%1!"") do if "!getAnyKey.mask:*%%~A=!" equ "!getAnyKey.mask!" goto :getAnyKeyRetry
        ) else (
          for %%A in ("!getAnyKey.LF!") do set "getAnyKey.mask=a!getAnyKey.mask:%%~A=!"
          for /f "delims=" %%A in (""!getAnyKey.mask!"") do if /i "!getAnyKey.mask:%%~A=%%~A!" equ "!getAnyKey.mask!" goto :getKeyRetry
        )
      ) else (
        for %%A in ("!getAnyKey.LF!") do set "getAnyKey.mask=!getAnyKey.mask:%%~A=!"
        for /f tokens^=1*^ eol^=^%getAnyKey.key%^ delims^=^%getAnyKey.key% %%A in (
          "!getAnyKey.mask!!getAnyKey.mask!!getAnyKey.CR!"
        ) do if "%%B" equ "" goto :getAnyKeyRetry
      )
    ) else if "!%2:~0,1!" equ "0" goto :getAnyKeyRetry
  )
  if defined getAnyKey./P echo(!%1!
  exit /b
)

:getAnyKeyInit
:: Ctrl-Z  0x1A  decimal 26
copy nul "%temp%\ctrlZ.tmp" /a <nul >nul
(for /f "usebackq" %%A in ("%temp%\ctrlZ.tmp") do set "getAnyKey.CtrlZ=%%A")2>nul||goto :getAnyKeyInit
del "%temp%\ctrlZ.tmp" 2>nul
:: Linefeed  0x0A  decimal 10
(set getAnyKey.LF=^
%= Do not remove or alter this line =%
)
:: Carriage Return  0x0D  decimal 13
for /f %%A in ('copy /z "%~dpf0" nul') do set "getAnyKey.CR=%%A"
exit /b


Dave Benham

Post Reply