Page 1 of 1

ReadFormattedLine.bat: Read a line with specific format

Posted: 16 Jul 2014 13:58
by Aacini
I have heard this request for Batch files many times before: read a line in a similar way of SET /P command, but with certain restrictions, like a given number of characters, or read only digits, or show the characters read as asterisks, etc. Frequently the asker wants a solution that don't require 3rd party external programs.

I developed a pure Batch file solution that works perfectly. The input is defined via a mask comprised of several characters that specify the input format at each input position. The method used in this program may be used as base for other similar data-input routines for Batch files.

EDIT 2014-09-23: I modified the original routine in order to use a standard input method and added some additional features. I also improved the presentation of the input fields.

EDIT 2014-09-28: I added a couple modifications suggested by foxidrive and dbenham, besides some others of my own.

Code: Select all

@echo off

rem ReadFormattedLine.bat: Read a line from keyboard with specific format
rem Antonio Perez Ayala

cls
echo Read a line from keyboard with specific format
echo/
echo call :ReadFormattedLine var="mask"  [/M "message"]  [/P]  [/A^|/W^|/F]
echo/
echo The mask specify valid input characters per position via the following chars:
echo/
echo    #  -  Any digit
echo    _  -  Any letter
echo    +  -  A letter that is converted to uppercase
echo    ?  -  Any letter or digit
echo    @  -  Letter or digit, convert letter to uppercase
echo/
echo The following characters are just displayed/inserted at their positions:
echo    $  /  \  (  )  [  ]  :  ;  ,  .  -  space  letters  digits
echo/
echo Any character in the mask different than previous ones cause an error.
echo/
echo If /P (password) switch is given, input characters are displayed as asterisks.
echo/
echo Normally the input is completed when Enter key is pressed after read at least
echo one character, but the following switches changes this behavior.
echo/
echo    /A (auto):   Input is auto-completed after the last character; Enter key
echo                 is ignored.
echo    /W (whole):  Enter key is accepted at first or last input positions only,
echo                 that is, when input is empty or whole.
echo    /F (fields): Enter key fills the field with spaces and move the cursor to
echo                 the next input field in the line.
echo/
echo To input a whole value terminated by Enter, use /W switch and insert any
echo character at the first position in the mask.
echo/
echo/
echo Some examples:
echo/

setlocal
rem Define the following variable before call the subroutine
set "thisFile=%~F0"
call :ReadFormattedLine test="(_-#-+-#-_)" /M "Enter (_-#-+-#-_): " /W
echo Read: "%test%"
echo/
call :ReadFormattedLine number="#####" /M "Enter a number of up to 5 digits: "
echo Number: "%number%"
echo/
call :ReadFormattedLine pass="########" /M "Enter your password (8 digits): " /P /A
echo Password: "%pass%"
echo/
call :ReadFormattedLine phone="(###)###-####" /M "Enter the telephone with area code: " /W
echo Telephone: "%phone%"
echo/
call :ReadFormattedLine RFC="[++++-######-@@@]" /M "Enter your RFC [4letters-6digits-3alpha]: " /W
echo RFC: "%RFC%"
echo/
call :ReadFormattedLine timeStamp="####/##/##  HH:MM: ##:##" /M "Enter a time-stamp.  YYYY/MM/DD: " /W
echo Time-stamp: "%timeStamp%"
echo/
call :ReadFormattedLine name="+_______________  Last name: +_______________" /M "First name: " /F
echo Name: "%name%"
echo/
echo Enter the list of IP addresses
echo (press Enter in an empty line to end the list)
set i=0
:nextIP
   set /A i+=1
   call :ReadFormattedLine IP[%i%]="###.###.###.###" /M "%i%-  " /W
if defined IP[%i%] goto nextIP
set /A n=i-1
echo/
echo IP addresses read: %n%
set IP[
echo/
echo/
echo End of examples
pause
goto :EOF


:ReadFormattedLine var="mask" [/M "message"] [/P] [/F /W /A]

if "%~2" equ "" echo ERROR: Missing parameters & exit /B 1
setlocal EnableDelayedExpansion

set "var=%~1"
set "mask=%~2"
shift & shift
set "message="
if /I "%1" equ "/M" set "message=%~2" & shift & shift
set "password="
if /I "%1" equ "/P" set "password=1" & shift
set "switch=%~1"

set quote="
set "digit= 0 1 2 3 4 5 6 7 8 9 "
set "letter= A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "
set "alphaNum=%digit%%letter%"
set "fmt=#_+?@"
set "show=$/\()[]:;,.- %digit: =%%letter: =%"
for /F %%a in ('copy /Z "%thisFile%" NUL') do set "CR=%%a"
for /F %%a in ('echo prompt $H ^| cmd') do set "BS=%%a" & set "SP=.%%a "

< NUL (
   set /P "=%message%"
   for /F "eol=| delims=" %%a in ('cmd /U /C echo !mask!^| find /V ""') do (
      if "!fmt:%%a=!" neq "%fmt%" (
         set /P "=Û"
      ) else if "%%a" neq " " (
         set /P "=%%a"
      ) else (
         set /P "=!SP!"
      )
   )
   set /P "=!SP!!CR!%message%"
)
set "input="
set /A i=0, key=0
goto checkFormat

:nextKey
   set "key="
   for /F "delims=" %%a in ('xcopy /W "%thisFile%" "%thisFile%" 2^>NUL') do if not defined key set "key=%%a"
   if "!key:~-1!" neq "!CR!" goto endif
      if /I "%switch%" equ "/A" goto nextKey
      if /I "%switch%" neq "/F" goto check/W
         :nextField
         set "format=!mask:~%i%,1!"
         if "%format%" equ "" goto endRead
         if "!fmt:%format%=!" equ "%fmt%" goto checkFormat
         set /P "=Û" < NUL
         set "input=%input% "
         set /A i+=1
         goto nextField
      :check/W
      if /I "%switch%" neq "/W" goto checkEmpty
         if %i% equ 0 goto endRead
         if "%format%" equ "" goto endRead
         goto nextKey
      :checkEmpty
      if %i% gtr 0 goto endRead
      goto nextKey
   :endif
   set "key=!key:~-1!"
   if "!key!" equ "!BS!" (
      if %i% gtr 0 (
         if "%format%" equ "" (
            set /P "=!SP!!BS!!BS!Û!BS!" < NUL
         ) else (
            set /P "=Û!BS!!BS!Û!BS!" < NUL
         )
         set "input=%input:~0,-1%"
         set /A i-=1
         if !i! equ 0 set key=0
      )
      goto checkFormat
   )
   if "%format%" equ "" goto nextKey
   if "!key!" equ "=" goto nextKey
   if "!key!" equ "!quote!" goto nextKey
   if "%format%" equ "#" ( rem Any digit
      if "!digit: %key% =!" equ "%digit%" goto nextKey
   ) else if "%format%" equ "_" ( rem Any letter
      if "!letter: %key% =!" equ "%letter%" goto nextKey
   ) else if "%format%" equ "+" ( rem Any letter, convert it to uppercase
      if "!letter: %key% =!" equ "%letter%" goto nextKey
      for %%a in (%letter%) do if /I "!key!" equ "%%a" set "key=%%a"
   ) else (
      rem Rest of formats are alphanumeric: ? @
      if "!alphaNum: %key% =!" equ "%alphaNum%" goto nextKey
      if "%format%" equ "@" ( rem Convert letters to uppercase
         for %%a in (%letter%) do if /I "!key!" equ "%%a" set "key=%%a"
      ) else if "%format%" neq "?" echo ERROR: Invalid format in mask: "%format%" & exit /B 2
      )
   )
   if defined password (
      set /P "=*" < NUL
   ) else (
      set /P "=%key%" < NUL
   )
   set "input=%input%%key%"

   :nextFormat
   set /A i+=1
   :checkFormat
   set "format=!mask:~%i%,1!"
   if "%format%" equ "" (
      if /I "%switch%" equ "/A" goto endRead
      if /I "%switch%" equ "/M" goto endRead
      goto nextKey
   )
   if "!show:%format%=!" neq "%show%" (
      if "!key!" equ "!BS!" (
         if "%format%" neq " " (
            set /P "=%format%!BS!!BS!Û!BS!" < NUL
         ) else (
            set /P "=!SP!!BS!!BS!Û!BS!" < NUL
         )
         set "input=%input:~0,-1%"
         set /A i-=1
         if !i! equ 0 set key=0
         goto checkFormat
      ) else (
         if "%format%" neq " " (
            set /P "=%format%" < NUL
         ) else (
            set /P "=!SP!" < NUL
         )
         set "input=%input%%format%"
         goto nextFormat
      )
   )
   if "%input:~-1%!key!" equ " !BS!" (
      set /P "=Û!BS!!BS!" < NUL
      set "input=%input:~0,-1%"
      set /A i-=1
      goto checkFormat
   )

goto nextKey
:endRead
echo/
endlocal & set "%var%=%input%"
exit /B

Antonio

Re: ReadFormattedLine.bat: Read a line with specific format

Posted: 23 Sep 2014 19:43
by Aacini
I modified my ReadFormattedLine routine so it now use a standard input method based on xcopy instead of the original choice based one, that was more limited. I also improved the presentation of the input fields.

Antonio

Re: ReadFormattedLine.bat: Read a line with specific format

Posted: 23 Sep 2014 20:52
by foxidrive
That's great work Antonio!

The way you show the examples is nice too - I just have two comments about that.

echo Enter the list of IP addresses (press just Enter to end)

If the user presses enter here then it aborts.

set i=0
:nextIP
set /A i+=1
call :ReadFormattedLine IP[%i%]="###.###.###.###" /M "%i%- " /W
if defined IP[%i%] goto nextIP
set /A n=i-1
echo/
echo IP addresses read: %n%
set IP[

at this point it might be nice to say that it is ending, and then pause for the people that have clicked on the batch file.

echo.
echo. End of examples
pause

goto :EOF


This is a really polished method of keyboard input entry!

Re: ReadFormattedLine.bat: Read a line with specific format

Posted: 24 Sep 2014 06:02
by dbenham
I agree, this is a very cool and useful routine 8)

There are a number of key presses that give errors and/or crash the routine. A few examples include & < ) = "

I think I've managed to modify the :nextKey routine to eliminate the problem. Here is a code excerpt with # showing the added/changed lines (I think I remember all the changes):

Code: Select all

:nextKey
   set "key="
   for /F "delims=" %%a in ('xcopy /W "%thisFile%" "%thisFile%" 2^>NUL') do if not defined key set "key=%%a"
   if "!key:~-1!" neq "!CR!" goto endif
      if /I "%switch%" equ "/A" goto nextKey
      if /I "%switch%" neq "/W" goto endRead
      if %i% equ 0 goto endRead
      if "%format%" equ "" goto endRead
      goto nextKey
   :endif
   set "key=!key:~-1!"
   if "!key!" equ "!BS!" (
      if %i% gtr 0 (
         set /P "=!BkSpc!" < NUL
         set "input=%input:~0,-1%"
         set /A i-=1
         if !i! equ 0 set key=0
      )
      goto checkFormat
   )
   if "%format%" equ "" goto nextKey
#  if "!key!" equ "=" goto nextKey
#  if "!key!!key!" equ """" goto nextKey
   if "%format%" equ "#" ( rem Any digit
      if "!digit: %key% =!" equ "%digit%" goto nextKey
   ) else if "%format%" equ "l" ( rem Any letter
      if "!letter: %key% =!" equ "%letter%" goto nextKey
   ) else if "%format%" equ "L" ( rem Any letter, convert it to uppercase
      if "!letter: %key% =!" equ "%letter%" goto nextKey
#     for %%a in (%letter%) do if /I "!key!" equ "%%a" set "key=%%a"
   ) else (
      rem Rest of formats are alphanumeric: a A *
      if "!alphaNum: %key% =!" equ "%alphaNum%" goto nextKey
      if "%format%" equ "A" ( rem Convert letters to uppercase
#        for %%a in (%letter%) do if /I "!key!" equ "%%a" set "key=%%a"
      ) else if "%format%" neq "a" if "%format%" neq "*" echo ERROR: Invalid format in mask & exit /B 2
      )
   )
   if "%format%" equ "*" (
      set /P "=*" < NUL
   ) else (
      set /P "=%key%" < NUL
   )
   set "input=!input!!key!"


One more minor bug - the backspace implementation is a bit wonky. When at end, it adds an extra grey block to end, and it also replaces formatting characters with grey blocks. It shouldn't be hard to make the backspace implementation a bit more intelligent.

I also have a few feature suggestions. Let me know if you are not interested, and I will take a stab at implementing them myself. Otherwise I will leave it to you.

1) I think the * format feature should be converted into a mode implemented as an option. This would allow * masking no matter what format is used. For example, it might be nice to mask purely numeric input. I can't think of any cases where I've needed a mixture of masked and unmasked input in a single field.

2) A minimum length option would be nice. Required positions could be indicated with your current grey block, and optional positions could be indicated by some other character.

3) If masking is implemented as an option, then * can be used to represent unrestricted input. (perhaps a few chars should be rejected). You could add an option that overrides and specifies exactly which characters are accepted. This would be good for a password field.

4) Perhaps an option to specify what character is used to represent a required position, and another to specify what character represents an optional position.


Dave Benham

Re: ReadFormattedLine.bat: Read a line with specific format

Posted: 24 Sep 2014 14:50
by Aacini
@dbenham,

Yes, I started to work on the backspace grey block problem and the optimization of the ":init" loop via a for. I like the replacement of * format by a switch and then use * for "any character", but I don't like the minimum length option nor required/optional positions because these features will made the code much more complex. /W and /A switches are used to indicate that all positions are required; I don't see the usefulness of a line with required and optional positions...

@foxidrive,

I will add your suggestions in the next modification, thanks!

Antonio

Re: ReadFormattedLine.bat: Read a line with specific format

Posted: 28 Sep 2014 19:33
by Aacini
A couple of the suggested modifications in ReadFormattedLine routine are ready besides some others of my own. The new version is in the same place as the original code, at beginning of this thread. Please, test it and report the result.

Antonio

Re: ReadFormattedLine.bat: Read a line with specific format

Posted: 28 Sep 2014 22:03
by foxidrive
Small bug Antonio - second echo should be echo/

Code: Select all

echo                 the next input field in the line.
echo


Some extra screen formatting is needed:
When I start the script then text disappears off the top of the screen as 25 lines is default (mine is 30 lines).

Otherwise it seems fine.

Re: ReadFormattedLine.bat: Read a line with specific format

Posted: 06 Dec 2015 14:18
by Aacini
Although ReadFormattedLine subroutine works perfectly, the code is somewhat large and complex and frequently the users are reluctant to use it when they have simpler requirements. The subroutine below is a much simpler version that just emulates the operation of SET /P command. This basic subroutine may be easily modified in order to fulfill any other requirement, like in this example that read only digits with a limited number of characters.

Code: Select all

@echo off
setlocal

call :ReadLine line="Enter a line: "
echo Line read: "%line%"
goto :EOF


:ReadLine var="prompt"

rem Read a line emulating SET /P command
rem Antonio Perez Ayala

rem Initialize variables
setlocal EnableDelayedExpansion
echo > _
for /F %%a in ('copy /Z _ NUL') do set "CR=%%a"
for /F %%a in ('echo prompt $H ^| cmd') do set "BS=%%a"

rem Show the prompt and start reading
set /P "=%~2" < NUL
set "input="
set i=0

:nextKey
   set "key="
   for /F "delims=" %%a in ('xcopy /W _ _ 2^>NUL') do if not defined key set "key=%%a"

   rem If key is CR: terminate input
   if "!key:~-1!" equ "!CR!" goto endRead

   rem If key is BS: delete last char, if any
   set "key=!key:~-1!"
   if "!key!" equ "!BS!" (
      if %i% gtr 0 (
         set /P "=!BS! !BS!" < NUL
         set "input=%input:~0,-1%"
         set /A i-=1
      )
      goto nextKey
   )

   rem Insert here any filter on the key
   rem if "%key%" don't pass the filter:  goto nextKey

   rem Else: show and accept the key
   set /P "=.!BS!%key%" < NUL
   set "input=%input%%key%"
   set /A i+=1

goto nextKey

:endRead
echo/
del _
endlocal & set "%~1=%input%"
exit /B

For example, to show the input characters as asterisks (in order to read a password), just change the "show and accept key" line by SET /P "=*" < NUL.

Antonio

Re: ReadFormattedLine.bat: Read a line with specific format

Posted: 02 Feb 2016 23:11
by RaceQuest
I am running this under WIN XP SP3 is this what is should look like?

Image

Re: ReadFormattedLine.bat: Read a line with specific format

Posted: 03 Feb 2016 07:48
by foxidrive
It doesn't appear like that here...

If you use a special text editor then try pasting the code into notepad and saving it.
Or do you use a non-latin language?
The console font you using in CMD.EXE may also be a factor.

Re: ReadFormattedLine.bat: Read a line with specific format

Posted: 04 Feb 2016 07:31
by RaceQuest
foxidrive wrote:It doesn't appear like that here...

If you use a special text editor then try pasting the code into notepad and saving it.
Or do you use a non-latin language?
The console font you using in CMD.EXE may also be a factor.

Thanks for the tip I was copy and pasting using notepad+ which saved it as "UTF-8" which caused these lines (see below) that have 8bit char codes to be behave different. Edit.com show it as two chars like the image in my previous post. Notepad saved same pasting as "ANSI". Never encountered that issue before - tricky.
These lines:

Code: Select all

            
             set /P "=Û"   
...
                          set /P "=Û" < NUL         
...
                set /P "=!SP!!BS!!BS!Û!BS!" < NUL
...
                set /P "=Û!BS!!BS!Û!BS!" < NUL
...
               set /P "=%format%!BS!!BS!Û!BS!" < NUL
...
                set /P "=!SP!!BS!!BS!Û!BS!" < NUL
...
          set /P "=Û!BS!!BS!" < NUL
 


Copied to pastebin

Re: ReadFormattedLine.bat: Read a line with specific format

Posted: 26 Aug 2018 17:57
by Aacini
Although :ReadFormattedLine subroutine works very well, it is large and complicated, so new users frequently are reluctant to use it... I developed two new simpler versions of such a subroutine based on CHOICE command that are much smaller, but with less capabilities:

ReadInput simplest version:

Code: Select all

@echo off
setlocal

echo Input like: XY########
call :ReadInput INPUT="Type input: " "++########"
echo INPUT=%INPUT%
goto :EOF


:ReadInput var= "Prompt" "mask"
setlocal EnableDelayedExpansion

rem Characters for the mask are: # digit, + upcase letter, @ digit or upcase letter

rem Define the character sets used in the input
set "set[#]= 0123456789"
set "set[+]= ABCDEFGHIJKLMNOPQRSTUVWXYZ"
set "set[@]=%set[+]%%set[#]:~1%"

rem Initialize input process
set "var="
set /P "=%~2" < NUL
set "mask=%~3"
set "i=0"

:NextChar
   set "maskChar=!mask:~%i%,1!"
   choice /N /C !set[%maskChar%]! > NUL
   set "var=!var!!set[%maskChar%]:~%errorlevel%,1!"
   set /P "=!var:~-1!" < NUL
   set /A i+=1
if "!mask:~%i%!" neq "" goto NextChar
echo/

endlocal & set "%~1=%var%"
exit /B
This version have these capabilities:
  • Characters allowed in the mask are: # digit, + upcase letter, @ digit or upcase letter. There is no way to enter any other character, including white space.
  • There is no way to delete input characters.
  • The input is completed automatically after the last character is entered. No needed/allowed ENTER key.
ReadInput full version:

Code: Select all

@echo off
setlocal

echo To delete a character, just wait 3 seconds
echo/

echo Telephone number in (##)###-#### format
call :ReadInput PHONE="Telephone number:   " "(##)###-####"
echo PHONE=%PHONE%
echo/

echo Enter RFC in XXXX-######-??? format
call :ReadInput RFC="Enter RFC:   " "++++-######-@@@"
echo RFC=%RFC%
echo/

call :ReadInput PASS="Enter password (8 chars): " "????????" /P
echo PASS=%PASS%
goto :EOF


:ReadInput var= "Prompt" "mask" [/P]ass
setlocal EnableDelayedExpansion

rem Characters for the mask are: _ letter, # digit, ? letter or digit
rem                              + upcase letter,  @ upcase letter or digit

rem Define the DEL delay (seconds after the last character will be deleted)
set "DelDelay=2"

rem Define the character sets used in the input
set "set[#]= þ0123456789"
set "set[+]= þABCDEFGHIJKLMNOPQRSTUVWXYZ"
set "set[@]=%set[+]%%set[#]:~2%"
set "set[_]=%set[+]%abcdefghijklmnopqrstuvwxyz" & set "caseSwitch[_]=/CS"
set "set[?]=%set[_]%%set[#]:~2%"                & set "caseSwitch[?]=/CS"
for /F %%a in ('echo prompt $H ^| cmd') do set "BS=%%a"

rem Initialize input process
set "var="
set /P "=%~2" < NUL
set "mask=%~3"
set "i=0"

:NextChar
   set "maskChar=!mask:~%i%,1!"
   if defined set[%maskChar%] goto else
      rem Not mask input char: just show/insert it
      set "char=%maskChar%"
      goto endif
   :else
      rem Apply masked input
      choice /N /C !set[%maskChar%]! !caseSwitch[%maskChar%]! /T %DelDelay% /D þ > NUL
      if errorlevel 2 goto endif2
         :Delete
         if %i% equ 0 goto NextChar
         set /P "=.%BS%%BS%  %BS%%BS%" < NUL
         set "var=%var:~0,-1%"
         set /A i-=1
         set "maskChar=!mask:~%i%,1!"
         if not defined set[%maskChar%] goto Delete
         goto NextChar
      :endif2
      set "char=!set[%maskChar%]:~%errorlevel%,1!"
   :endif
   set "var=%var%%char%"
   (if /I "%~4" neq "/P" (set /P "=%var:~-1%") else set /P "=*") < NUL
   set /A i+=1
if "!mask:~%i%!" neq "" goto NextChar
echo/
endlocal & set "%~1=%var%"
exit /B
This version have these capabilities, besides the ones in previous version:
  • Additional characters allowed in the mask are: _ letter, ? letter or digit. This point allows to enter upcase/lowcase letters.
  • Any other character in the mask is displayed and included in the returned value.
  • The last input character is automatically deleted after 3 seconds of no input activity.
  • The /P optional fourth parameter allows to enter passwords where asterisks are displayed in place of input characters.
Antonio