Read key presses via REPLACE - New functions :getKey, :getAnyKey, :getMaskedInput
Posted: 05 Sep 2016 12:50
Version 2, with new behavior and bug fixes, is available at http://www.dostips.com/forum/viewtopic.php?f=3&t=7396&p=50819#p50819
In the past I have used the XCOPY method to read key presses. I first learned of the XCOPY method while developing SNAKE.BAT. However, it does not support all possible characters. For example, I could not figure out a way to capture <Ctrl-Z> (0x1A). Also, it is difficult, though possible, to distinguish LineFeed (0x0A) from Carriage Return (0x0D). And ! is also a bit difficult to get.
I only recently read Carlos' thread where he uses REPLACE to read a key: Password Input (new method) - most excellent
I find REPLACE much easier to use, and am able to capture all possible single byte characters, including NULL (0x00).
Using this basic technique, I have developed three robust functions that share the following key features:
:getAnyKey - Capture any character, including NULL. Some characters can only be entered using <Alt> with the numeric keypad. Null is entered using [Ctrl-2] and is returned as an empty string (undefined variable).
:getKey - Same as :getAnyKey except Null, LineFeed, CarriageReturn, (and sometimes Ctrl-Z) are all reported as an empty string (undefined variable)
:getMaskedInput - Similar to the carlos :PasswordInput function, with the added ability to specify which characters are accepted, and delayed expansion need not be enabled. This function is dependent on the :getKey function.
EDITS
2016-09-06: Bug fix for :getKey and :getAnyKey when ValidVar option not specified. Also documentation modified slightly for all three routines.
2016-09-07: Added ability to capture Null in :getAnyKey, and improved documentation of all three routines.
Dave Benham
In the past I have used the XCOPY method to read key presses. I first learned of the XCOPY method while developing SNAKE.BAT. However, it does not support all possible characters. For example, I could not figure out a way to capture <Ctrl-Z> (0x1A). Also, it is difficult, though possible, to distinguish LineFeed (0x0A) from Carriage Return (0x0D). And ! is also a bit difficult to get.
I only recently read Carlos' thread where he uses REPLACE to read a key: Password Input (new method) - most excellent
I find REPLACE much easier to use, and am able to capture all possible single byte characters, including NULL (0x00).
Using this basic technique, I have developed three robust functions that share the following key features:
- Each function has an option to specify which characters are accepted
- Each function works regardless whether delayed expansion is enabled or disabled
- Full documentation is embedded within comments at the top of each function
:getAnyKey - Capture any character, including NULL. Some characters can only be entered using <Alt> with the numeric keypad. Null is entered using [Ctrl-2] and is returned as an empty string (undefined variable).
:getKey - Same as :getAnyKey except Null, LineFeed, CarriageReturn, (and sometimes Ctrl-Z) are all reported as an empty string (undefined variable)
:getMaskedInput - Similar to the carlos :PasswordInput function, with the added ability to specify which characters are accepted, and delayed expansion need not be enabled. This function is dependent on the :getKey function.
Code: Select all
::getMaskedInput StrVar [ValidVar]
::
:: 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 ValidVar variable defines the characters that will be accepted.
:: If not specified or not defined, then all characters are accepted.
:: If specified and defined, then only characters within ValidVar are accepted.
::
:: 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 1.2 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=!"
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
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 KeyVar [ValidVar]
::
:: 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 ValidVar variable defines the values that will be accepted.
:: If 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 0YNyn will only accept upper
:: or lower case Y or N. A value of 1YNyn will additionally accept [Enter] etc.
::
:: 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 1.3 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
set "%1="
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
if "!%2!" neq "" (
if not defined %1 if "!%2:~0,1!" equ "0" (endlocal&endlocal&goto :getKey) else exit /b
set "getKey.key=!%1!"
set "mask=!%2:~1!"
if not defined mask endlocal&endlocal&goto :getKey
if "!getKey.key!" equ "=" (
set "test=a!mask!"
for /f "delims=" %%A in ("!test!") do if /i "!test:%%A=%%A!" equ "!test!" endlocal&endlocal&goto :getKey
)
for /f delims^=^ eol^= %%A in ("!getKey.key!") do if "!mask:*%%A=!" equ "!mask!" endlocal&endlocal&goto :getKey
)
exit /b
::getAnyKey KeyVar [ValidVar]
::
:: 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.
::
:: The optional ValidVar variable holds the characters that will be accepted.
:: If not specified or not defined, then all values are accepted. If specified
:: and defined, then only characters within ValidVar are accepted. The first
:: three characters indicate whether Null (0x00), LineFeed (0x0A), and Carriage
:: Return (0x0D) are accepted, respectively. A value of 1 indicates acceptance,
:: and 0 indicates rejection. The remaining characters represent themselves.
:: For example, a ValidVar value of 000YNyn will only accept upper or lower case
:: Y or N. A value of 011YNyn will additionally accept LineFeed and Carriage
:: Return. A value of 111YNyn adds Null to the list.
::
:: 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 1.3 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
setlocal
set "notDelayed=!"
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 "[2]2:" ( %= Ctrl-Z on Win 10 =%
copy nul "%temp%\ctrlZ.tmp" /a >nul
for /f "usebackq" %%A in ("%temp%\ctrlZ.tmp") do set "key=%%A"
del "%temp%\ctrlZ.tmp"
) else if "!str!" equ "[3]3:" ( %= LineFeed =%
set "key="
) else if "!str!" equ "[3]" ( %= Null = %
set "key=NULL"
) else ( %= All others =%
set "key=!str:~-1!"
if not defined notDelayed if "!key!" equ "^!" set "key=^^^!"
)
for /f "delims=" %%A in (""!key!"") do (
endlocal&endlocal&endlocal
set "%1=%%~A"
)
if not defined %1 (set %1=^
%= Do not remove or alter this line =%
)
setlocal enableDelayedExpansion
if !%1! equ NULL (
endlocal
set "%1="
setlocal enableDelayedExpansion
)
if "!%2!" equ "" exit /b
set "getAnyKey.key=!%1!"
set "mask=!%2!"
(set LF=^
%= Do not remove or alter this line =%
)
if not defined getAnyKey.key if "!mask:~0,1!" equ "0" (endlocal&goto :getAnyKey) else exit /b
if !getAnyKey.key! equ !LF! if "!mask:~1,1!" equ "0" (endlocal&goto :getAnyKey) else exit /b
for /f %%A in ('copy /z "%~dpf0" nul') do if !getAnyKey.key! equ %%A if "!mask:~2,1!" equ "0" (endlocal&goto :getAnyKey) else exit /b
set "mask=!mask:~3!"
if not defined mask endlocal&goto :getAnyKey
if "!getAnyKey.key!" equ "=" (
set "test=a!mask!"
for /f "delims=" %%A in ("!test!") do if /i "!test:%%A=%%A!" equ "!test!" endlocal&goto :getAnyKey
)
for /f delims^=^ eol^= %%A in ("!getAnyKey.key!") do if "!mask:*%%A=!" equ "!mask!" endlocal&goto :getAnyKey
exit /b
EDITS
2016-09-06: Bug fix for :getKey and :getAnyKey when ValidVar option not specified. Also documentation modified slightly for all three routines.
2016-09-07: Added ability to capture Null in :getAnyKey, and improved documentation of all three routines.
Dave Benham