#!/bin/bash # txt2regex.sh - Regular Expressions "wizard" made with Bash builtins # # Website : https://aurelio.net/projects/txt2regex/ # Author : Aurelio Jargas (verde@aurelio.net) # License : GPL # Requires: bash >= 3.0 # # shellcheck disable=SC1117,SC2034 # SC1117 because it was obsoleted in shellcheck >0.5 # SC2034 because it considers unused vars that I load with eval (ax_*) # # Please, read the README file. # # $STATUS: # 0 beginning of the regex # 1 defining regex # 12 choosing subregex # 2 defining quantifier # 3 really quit? # 4 choosing session programs # 9 end of the regex # # 20001019 ** 1st version # 20001026 ++ lots of changes and tests # 20001028 ++ improvements, public release # 20001107 ++ bash version check (thanks eliphas) # 20001113 ++ php support, Progs command # 20010223 ++ i18n, --all, freshmeat announce (oh no!) # 20010223 v0.1 # 20010420 ++ id.po, \lfunction_name, s/regexp/regex/ig # 20010423 ++ --nocolor, --history, Usage(), doNextHist{,Args}() # ++ flags: interactive, color, allprogs # ++ .oO(¤user parameters history) # 20010424 v0.2 # 20010606 ++ option --whitebg # -- grep from $progs to fit on 24 lines by default # 20010608 -- clear command (not bash), ++ Clear() # -- stty command (not bash), ++ $LINES # -- *Progs*(), ++ Choice(), ChoiceRefresh() # ++ POSIX character classes [[:abc:]] # ++ special combinations inside [] # ++ $HUMAN improved with getString, getNumber, Choice # ++ detailed --help, moved to sourceforge # 20010613 v0.3 # 20010620 -- seq command (not bash), ++ sek() # 20010613 v0.3.1 # 20010731 ++ Reset: "RegEx prog :" with automatic length # ++ new progs: postgres, javascript, vbscript, procmail # ++ ax_prog: new item: escape char - escape is ok now # ++ improved meta knowledge on perl, tcl and gawk # 20010802 v0.4 # 20010821 ++ ShowMeta(), new option: --showmeta # 20010824 ++ getMeta(), ShowInfo(), new option: --showinfo, $cR color # 20010828 ++ getItemIndex(), getLargestItem() # <> Clear(): using \033c, ALL: using for((;;)) ksh syntax # <> vi == Nvi # 20010828 v0.5 # 20010831 ++ group & or support- cool!, clearN() # ++ nice groups balance check -> ((2)), use $COLUMNS # <> TopTitle(): BLOAT, 3 lines, smart, arrays # <> Menu(): s/stupid recursion/while/ # ++ Z status to handle 0,menu,0 situation # <> s/eval/${!var}/ # 20010903 <> Choice: fixed outrange answers # ++ trapping ^c do clearEnd, ++ new prog: mysql # ++ history now works with Choice() menus # ++ history appears when quitting # 20010905 v0.6 # 20020225 ++ "really quit?" message, ++ --version # 20020304 <> --history just shows final RE on STDOUT # ++ --make, --prog, printError() # ++ groups are now quantifiable # ++ ready_(date[123], hour[123], number[123]) # 20020304 v0.7 # 20040928 <> bash version test (works in 3.x and newer) # 20040928 v0.8 # 20040929 <> --help split into individual messages (helps i18n) # 20051229 <> fixed bug on bash3 for eval contents (thanks Marcus Habermehl) # 20121221 ** moved to GitHub, please see the Git history from now on # Every command in this script is a Bash builtin. This is by design. # Make sure we don't break that rule in future code by strictly # disallowing any system command. export PATH= TEXTDOMAIN=txt2regex TEXTDOMAINDIR=po VERSION=0.10b printError() { printf '%s: ' $"ERROR" # shellcheck disable=SC2059 printf "$@" exit 1 } case "$BASH_VERSION" in [3-9].*) : # do nothing ;; *) printError 'Bash version >=3.0 required, but you have %s\n' "$BASH_VERSION" ;; esac Usage() { # Ugly code, but isolates in $"..." only the strings that need # translation and tries to keep the option descriptions aligned even # when long words are used as meta vars. printf '%s txt2regex [--nocolor|--whitebg] [--all|--prog %s]\n' \ $"usage:" $"PROGRAMS" printf '%s txt2regex --showmeta\n' \ $"usage:" printf '%s txt2regex --showinfo %s [--nocolor]\n' \ $"usage:" $"PROGRAM" printf '%s txt2regex --history %s [--all|--prog %s]\n' \ $"usage:" $"VALUE" $"PROGRAMS" printf '%s txt2regex --make %s [--all|--prog %s]\n' \ $"usage:" $"LABEL" $"PROGRAMS" printf '\n' printf '%s\n' $"Options:" printf ' %-22s%s\n' '--all' \ $"Select all the available programs" printf ' %-22s%s\n' '--nocolor' \ $"Do not use colors" printf ' %-22s%s\n' '--whitebg' \ $"Adjust colors for white background terminals" printf ' %-22s%s\n' '--prog '$"PROGRAMS" \ $"Specify which programs to use, separated by commas" printf '\n' printf ' %-22s%s\n' '--showmeta' \ $"Print a metacharacters table featuring all the programs" printf ' %-22s%s\n' '--showinfo '$"PROGRAM" \ $"Print regex-related info about the specified program" printf ' %-22s%s\n' '--history '$"VALUE" \ $"Print a regex from the given history data" printf ' %-22s%s\n' '--make '$"LABEL" \ $"Print a ready regex for the specified label" printf '\n' printf ' %-22s%s\n' '-V, --version' \ $"Print the program version and quit" printf ' %-22s%s\n' '-h, --help' \ $"Print the help message and quit" printf '\n' exit "${1:-0}" # $1 is the exit code (default is 0) } # The defaults is_interactive=1 use_colors=1 has_white_background=0 has_not_supported=0 mode_show_meta=0 mode_show_info=0 GRP1=0 GRP2=0 # Here's the default list of programs shown. # Edit here or use --prog to overwrite it. progs=(python egrep grep sed vim emacs) ### IMPORTANT DATA ### # To generate this array: # grep version: tests/regex-tester.txt | sort | cut -d ' ' -f 1 allprogs=( awk chicken ed egrep emacs expect find gawk grep javascript lex mawk mysql perl php postgres procmail python sed tcl vi vim ) # To generate this array: # grep version: tests/regex-tester.txt | sort | sed "s/.* version: //;s/.*/'&'/" allversions=( 'awk version 20121220' 'CHICKEN 4.12.0' 'GNU Ed 1.10' 'grep (GNU grep) 3.1' 'GNU Emacs 25.2.2' 'expect version 5.45.4' 'find (GNU findutils) 4.7.0-git' 'GNU Awk 4.1.4' 'grep (GNU grep) 3.1' 'node v8.10.0' 'flex 2.6.4' 'mawk 1.3.3 Nov 1996' 'mysql Ver 14.14 Distrib 5.7.29' 'perl v5.26.1' 'PHP 7.2.24-0ubuntu0.18.04.4' 'psql (PostgreSQL) 10.12' 'procmail v3.23pre 2001/09/13' 'Python 3.6.9' 'sed (GNU sed) 4.4' 'tcl 8.6' 'nvi 1.81.6-13' 'VIM - Vi IMproved 8.0 (2016 Sep 12)' ) label_names=( date date2 date3 hour hour2 hour3 number number2 number3 ) label_descriptions=( 'date LEVEL 1: mm/dd/yyyy: matches from 00/00/0000 to 99/99/9999' 'date LEVEL 2: mm/dd/yyyy: matches from 00/00/1000 to 19/39/2999' 'date LEVEL 3: mm/dd/yyyy: matches from 00/00/1000 to 12/31/2999' 'hour LEVEL 1: hh:mm: matches from 00:00 to 99:99' 'hour LEVEL 2: hh:mm: matches from 00:00 to 29:59' 'hour LEVEL 3: hh:mm: matches from 00:00 to 23:59' 'number LEVEL 1: integer, positive and negative' 'number LEVEL 2: level 1 plus optional float point' 'number LEVEL 3: level 2 plus optional commas, like: 34,412,069.90' ) label_data=( # date '26521652165¤:2¤2¤/¤:2¤2¤/¤:2¤4' '24161214161214165¤01¤:2¤/¤0123¤:2¤/¤12¤:2¤3' '2(2161|2141)121(2161|4161|2141)1214165¤0¤:2¤1¤012¤/¤0¤:2¤12¤:2¤3¤01¤/¤12¤:2¤3' # hour '2652165¤:2¤2¤:¤:2¤2' '24161214161¤012¤:2¤:¤012345¤:2' '2(4161|2141)1214161¤01¤:2¤2¤0123¤:¤012345¤:2' # number '24264¤+-¤:2' '24264(2165)2¤+-¤:2¤.¤:2¤2' '24266(2165)3(2165)2¤+-¤:2¤3¤,¤:2¤3¤.¤:2¤2' ) #date3 : perl: (0[0-9]|1[012])/(0[0-9]|[12][0-9]|3[01])/[12][0-9]{3} #hour3 : perl: ([01][0-9]|2[0123]):[012345][0-9] #number3: perl: [+-]?[0-9]{1,3}(,[0-9]{3})*(\.[0-9]{2})? ### -- ### getItemIndex() { # item, array_items local item="$1" local i=0 shift while [ -n "$1" ]; do [ "$1" == "$item" ] && printf '%d\n' "$i" && return i=$((i + 1)) shift done } validateProgramNames() { local name for name in "$@"; do [ -z "$(getItemIndex "$name" "${allprogs[@]}")" ] && printError '%s: %s\n' $"unknown program" "$name" done } # Parse command line options while [ $# -gt 0 ]; do case "$1" in --history) [ -z "$2" ] && Usage 1 history="$2" shift is_interactive=0 use_colors=0 hists="0${history%%¤*}" histargs="¤${history#*¤}" [ "${hists#0}" == "${histargs#¤}" ] && unset histargs ;; --make) shift is_interactive=0 use_colors=0 label_name="${1%1}" # final 1 is optional (date1 == date) label_index=$(getItemIndex "$label_name" "${label_names[@]}") # Sanity check [ -z "$label_index" ] && printError '%s: "%s": %s\n%s %s\n' \ '--make' "$1" $"invalid argument" \ $"valid names:" "${label_names[*]}" # Set history data hist="${label_data[$label_index]}" hists="0${hist%%¤*}" histargs="¤${hist#*¤}" printf '\n### %s\n\n' "${label_descriptions[$label_index]}" ;; --prog) [ -z "$2" ] && Usage 1 shift eval "progs=(${1//,/ })" validateProgramNames "${progs[@]}" ;; --nocolor) use_colors=0 ;; --whitebg) has_white_background=1 ;; --showmeta) mode_show_meta=1 ;; --showinfo) [ -z "$2" ] && Usage 1 infoprog="$2" shift mode_show_info=1 validateProgramNames "$infoprog" ;; --all) progs=("${allprogs[@]}") ;; -V | --version) printf 'txt2regex %s\n' "$VERSION" exit 0 ;; -h | --help) Usage 0 ;; *) printf '%s: %s\n\n' "$1" $"invalid option" Usage 1 ;; esac shift done set -o noglob ### The Regex show S0_txt=( $"start to match" $"on the line beginning" $"in any part of the line" ) S0_re=( '' '^' '' ) S1_txt=( $"followed by" $"any character" $"a specific character" $"a literal string" $"an allowed characters list" $"a forbidden characters list" $"a special combination" $"a POSIX combination (locale aware)" $"a ready regex (not implemented)" $"anything" ) S1_re=( '' '.' '' '' '' '' '' '' '' '.*' ) S2_txt=( $"how many times (repetition)" $"one" $"zero or one (optional)" $"zero or more" $"one or more" $"exactly N" $"up to N" $"at least N" ) # COMBO combo_txt=( $"uppercase letters" $"lowercase letters" $"numbers" $"underscore" $"space" $"TAB" ) combo_re=( 'A-Z' 'a-z' '0-9' '_' ' ' '@' ) #TODO use all posix components? posix_txt=( $"letters" $"lowercase letters" $"uppercase letters" $"numbers" $"letters and numbers" $"hexadecimal numbers" $"whitespaces (space and TAB)" $"graphic chars (not-whitespace)" ) posix_re=( 'alpha' 'lower' 'upper' 'digit' 'alnum' 'xdigit' 'blank' 'graph' ) # Title (line 1) # shellcheck disable=SC2256 tit1_txt=( $"quit" $"reset" $"color" $"programs" '' '' '' '' '' '^txt2regex$' ) tit1_cmd=( '.' '0' '*' '/' '' '' '' '' '' '' ) # Title (line 2-3) tit2_txt=( $"or" $"open group" $"close group" '' '' '' '' '' '' $"not supported" ) tit2_cmd=( '|' '(' ')' '' '' '' '' '' '' '!!' ) # S2_* arrays: The list of quantifiers (to be used when STATUS=2) # Every array will be named S2_: S2_awk, S2_ed, S2_egrep, ... # The array index refers to the menu item in the "repetition" screen. # To update this data: # make test-regex # grep ' S2 .*OK$' tests/regex-tester.txt # while read -r prog_id data; do # Set the S2_ array for each line. Example: # S2_egrep=('-' '-' '?' '*' '+' '{@}' '{1,@}' '{@,}') read -r -a "S2_$prog_id" <<< "$data" done << 'EOD' awk - - ? * + !! !! !! chicken - - ? * + {@} {1,@} {@,} ed - - \? * \+ \{@\} \{1,@\} \{@,\} egrep - - ? * + {@} {1,@} {@,} emacs - - ? * + \\{@\\} \\{1,@\\} \\{@,\\} expect - - ? * + {@} {1,@} {@,} find - - ? * + {@} {1,@} {@,} gawk - - ? * + {@} {1,@} {@,} grep - - \? * \+ \{@\} \{1,@\} \{@,\} javascript - - ? * + {@} {1,@} {@,} lex - - ? * + {@} {1,@} {@,} mawk - - ? * + !! !! !! mysql - - ? * + {@} {1,@} {@,} perl - - ? * + {@} {1,@} {@,} php - - ? * + {@} {1,@} {@,} postgres - - ? * + {@} {1,@} {@,} procmail - - ? * + !! !! !! python - - ? * + {@} {1,@} {@,} sed - - \? * \+ \{@\} \{1,@\} \{@,\} tcl - - ? * + {@} {1,@} {@,} vi - - \{0,1\} * \{1,\} \{@\} \{1,@\} \{@,\} vim - - \= * \+ \{@} \{1,@} \{@,} EOD # ax_* arrays: Extra regex-related data for all the programs. # Every array will be named ax_: ax_awk, ax_ed, ax_egrep, ... # To check how this data is used in this source code, search for # something like 'ax_.*5'. # # To update this data: # make test-regex # grep -E ' ax123 .+OK$' tests/regex-tester.txt # 1,2,3 # grep -E ' a\.b +OK$' tests/regex-tester.txt # 4 # grep -E ' ax5 .+OK$' tests/regex-tester.txt # 5 # grep -E ' ax6 ' tests/regex-tester.txt # 6 # grep -E ' ax7 ' tests/regex-tester.txt # 7 # grep -E ' ax8 ' tests/regex-tester.txt # 8 # # In PHP, we're using \\ instead of \ as the escape metacharacter # because it works consistently, being it inside single or double # quotes. Using only \ would work in some cases, but not in others: # The literal + is matched by: \+ \\+ [+] [\+] [\\+] # The literal \ is matched by: \\\\ [\\\\] # while read -r prog_id data; do # Set the ax_ array for each line. Example: # ax_awk=('' '|' '(' ')' '\' '\.*[---()|+?^$' '\' 'P' '\t') read -r -a "ax_$prog_id" <<< "$data" done << 'EOD' awk - | ( ) \ \.*[---()|+?^$ \ P \t chicken - | ( ) \\ \.*[---()|+?^$ \ P \t ed - \| \( \) \ \.*[---------- - P - egrep - | ( ) \ \.*[-{-(-|+?^$ - P - emacs - \\| \\( \\) \\ \.*[------+?-- \ P \t expect - | ( ) \ \.*[-{}()|+?^$ \ P \t find - | ( ) \ \.*[-{-(-|+?^$ - P - gawk - | ( ) \ \.*[---(-|+?^$ \ P \t grep - \| \( \) \ \.*[---------- - P - javascript - | ( ) \ \.*[---()|+?^$ \ - \t lex - | ( ) \ \.*[-{}()|+?-- \ P \t mawk - | ( ) \ \.*[---()|+?^$ \ - \t mysql - | ( ) \\ \.*[---(-|+?^$ \ P \t perl - | ( ) \ \.*[-{-()|+?^$ \ P \t php - | ( ) \\ \.*[-{-()|+?^$ \ P \t postgres - | ( ) \ \.*[---()|+?^$ \ P \t procmail - | ( ) \ \.*[---()|+?^$ - - - python - | ( ) \ \.*[-{-()|+?^$ \ - \t sed - \| \( \) \ \.*[---------- - P \t tcl - | ( ) \ \.*[-{}()|+?^$ \ P \t vi - !! \( \) \ \.*[---------- - P - vim - \| \( \) \ \.*[---------- \ P \t EOD # \.*[]{}()|+?^$ -=false # [0] Unused # [1] Which is the metacharacter for alternatives? # [2,3] Which are the metacharacters for grouping? # [4] Which is the escape metacharacter? # [5] Which chars of \.*[]{}()|+?^$ need to be escaped to be matched as # literals? Note that txt2regex has menus to insert all of those as # metacharacters (except $), so in user input they will always be # literal. For ^ and $, some tools consider them literal when not in # their special start/end position (marked here as -). # [6] To match '\' inside [], do you need to escape it? If yes, use '\'. # [7] Has support for [[:POSIX:]] character classes? If yes, use 'P'. # [8] Does \t inside [] match a tab? If yes, use '\t'. ColorOnOff() { # The colors: Normal, Prompt, Bold, Important [ "$use_colors" -eq 0 ] && return if [ -n "$cN" ]; then unset cN cP cB cI cR elif [ "$has_white_background" -eq 0 ]; then cN=$(printf '\033[m') # normal cP=$(printf '\033[1;31m') # red cB=$(printf '\033[1;37m') # white cI=$(printf '\033[1;33m') # yellow cR=$(printf '\033[7m') # reverse else cN=$(printf '\033[m') # normal cP=$(printf '\033[31m') # red cB=$(printf '\033[32m') # green cI=$(printf '\033[34m') # blue cR=$(printf '\033[7m') # reverse fi } # Emulate the 'seq N' command sek() { local z="$1" local a=1 while [ "$a" -le "$z" ]; do printf '%d\n' "$a" a=$((a + 1)) done } # Is the $1 char present in the $2 text? charInText() { local char="$1" local text="$2" local i for ((i = 0; i < ${#text}; i++)); do [ "${text:$i:1}" == "$char" ] && return 0 done return 1 } # Remove all duplicated chars from the $1 text uniqChars() { local text="$1" local text_uniq='' local i for ((i = 0; i < ${#text}; i++)); do charInText "${text:$i:1}" "$text_uniq" || text_uniq="$text_uniq${text:$i:1}" done printf '%s\n' "$text_uniq" } # Escape each $1 in $2 using $3 escapeChars() { local special_chars="$1" local text="$2" local escape_char="${3:-\\}" local escaped_text local i local this_char for ((i = 0; i < ${#text}; i++)); do this_char=${text:$i:1} if charInText "$this_char" "$special_chars"; then if [ "$this_char$this_char" == "$escape_char" ]; then # Special case: this_char=\ and escape_char=\\ # The normal escaping (see the next else) would make \\\ # (which is wrong). Here we ensure \\\\ is produced. escaped_text="$escaped_text$escape_char$escape_char" else # normal escaping escaped_text="$escaped_text$escape_char$this_char" fi else # no escaping escaped_text="$escaped_text$this_char" fi done printf '%s\n' "$escaped_text" } getLargestItem() { local largest while [ -n "$1" ]; do [ ${#1} -gt ${#largest} ] && largest="$1" shift done printf '%s\n' "$largest" } # Used to get values from the S2_* and ax_* metachar arrays getMeta() { # var-name index local m="$1[$2]" m=${!m} # Remove all non-metacharacters: @ ! - # Those are used only internally as markers m=${m//[@!-]/} # Remove when getting '?' or '+' for 'vi', since they are unsupported # and the current values are workarounds using '{}' [ "$1" == S2_vi ] && { [ "$2" -eq 2 ] || [ "$2" -eq 4 ]; } && m='' printf '%s\n' "$m" } ShowMeta() { local i g1 g2 prog progsize progsize=$(getLargestItem "${allprogs[@]}") for ((i = 0; i < ${#allprogs[@]}; i++)); do prog=${allprogs[$i]} g1=$(getMeta "ax_$prog" 2) g2=$(getMeta "ax_$prog" 3) printf "\n%-${#progsize}s" "$prog" # name printf '%7s' "$(getMeta "S2_$prog" 4)" # + printf '%7s' "$(getMeta "S2_$prog" 2)" # ? printf '%7s' "$(getMeta "S2_$prog" 5)" # {} printf '%7s' "$(getMeta "ax_$prog" 1)" # | printf '%8s' "$g1$g2" # () printf ' %s' "${allversions[$i]}" # version done printf '\n\n%s\n\n' $"NOTE: . [] [^] and * are the same on all programs." } ShowInfo() { local prog="$1" local escmeta local index local i local metas local needesc local posix=$"NO" local tabinlist=$"NO" local txtsize local ver local -a data local -a txt # Getting data index=$(getItemIndex "$prog" "${allprogs[@]}") ver="${allversions[$index]}" escmeta=$(getMeta "ax_$prog" 4) needesc=$(getMeta "ax_$prog" 5) [ "$(getMeta "ax_$prog" 7)" == 'P' ] && posix=$"YES" [ "$(getMeta "ax_$prog" 8)" == '\t' ] && tabinlist=$"YES" # Metacharacters list # printf arguments: + ? {} | ( ) metas="$( printf '. [] [^] * %s %s %s %s %s%s' \ "$(getMeta "S2_$prog" 4)" \ "$(getMeta "S2_$prog" 2)" \ "$(getMeta "S2_$prog" 5)" \ "$(getMeta "ax_$prog" 1)" \ "$(getMeta "ax_$prog" 2)" \ "$(getMeta "ax_$prog" 3)" )" # Populating cool i18n arrays # shellcheck disable=SC2256 txt=( $"program" $"metas" $"esc meta" $"need esc" $"\t in []" '[:POSIX:]' ) data=( "$prog: $ver" "$metas" "$escmeta" "${needesc//-/}" "$tabinlist" "$posix" ) # Show me! show me! show me! ColorOnOff printf '\n' txtsize=$(getLargestItem "${txt[@]}") for ((i = 0; i < ${#txt[@]}; i++)); do printf "%s %${#txtsize}s %s %s\n" \ "$cR" "${txt[$i]}" "${cN:-:}" "${data[$i]}" done printf '\n' } if [ "$mode_show_meta" -eq 1 ]; then ShowMeta exit 0 fi if [ "$mode_show_info" -eq 1 ]; then ShowInfo "$infoprog" exit 0 fi # Screen size/positioning issues ScreenSize() { # Note that those are all global variables x_regex=1 y_regex=4 x_hist=3 y_hist=$((y_regex + ${#progs[*]} + 1)) x_prompt=3 y_prompt=$((y_regex + ${#progs[*]} + 2)) x_menu=3 y_menu=$((y_prompt + 2)) x_prompt2=15 y_max=$((y_menu + ${#S1_txt[*]})) # The defaults case not exported : ${LINES:=25} : ${COLUMNS:=80} #TODO automatic check when selecting programs if [ "$is_interactive" -eq 1 ] && [ $LINES -lt "$y_max" ]; then printError '\n%s\n%s\n%s\n' \ "$( printf \ $"Your terminal has %d lines, but txt2regex needs at least %d lines." \ "$LINES" "$y_max" )" \ $"Increase the number of lines or select less programs using --prog." \ $"If this line number detection is incorrect, export the LINES variable." fi } _eol=$(printf '\033[0K') # clear trash until EOL # The cool control chars functions gotoxy() { [ "$is_interactive" -eq 1 ] && printf '\033[%d;%dH' "$2" "$1" } clearEnd() { [ "$is_interactive" -eq 1 ] && printf '\033[0J' } clearN() { [ "$is_interactive" -eq 1 ] && printf '\033[%dX' "$1" } Clear() { [ "$is_interactive" -eq 1 ] && printf '\033c' } # Ideas: tab between, $cR on cmd, yellow-white-yellow printTitleCmd() { printf '[%s%s%s]%s ' "$cI" "$1" "$cN" "$2" } TopTitle() { gotoxy 1 1 local color local cmd local i local j local showme local txt [ "$is_interactive" -eq 0 ] && return # 1st line: aplication commands for ((i = 0; i < 10; i++)); do showme=0 txt=${tit1_txt[$i]} cmd=${tit1_cmd[$i]} case $i in [01]) showme=1 ;; 2) [ "$use_colors" -eq 1 ] && showme=1 ;; 3) [ "$STATUS" -eq 0 ] && showme=1 ;; 9) gotoxy $((COLUMNS - ${#txt})) 1 printf '%s\n' "$txt" ;; esac if [ $showme -eq 1 ]; then printTitleCmd "$cmd" "$txt" else clearN $((${#txt} + 3)) fi done # 2nd line: grouping and or if [ "$STATUS" -eq 0 ]; then printf %s "$_eol" else if [ "$STATUS" -eq 1 ]; then for i in 0 1 2; do txt=${tit2_txt[$i]} cmd=${tit2_cmd[$i]} showme=1 [ $i -eq 2 ] && [ $GRP1 -eq $GRP2 ] && showme=0 if [ $showme -eq 1 ]; then printTitleCmd "$cmd" "$txt" else clearN $((${#txt} + 3)) fi done else # delete commands only clearN $((${#tit2_txt[0]} + 5 + ${#tit2_txt[1]} + 5 + ${#tit2_txt[2]} + 5)) fi # open groups gotoxy $((COLUMNS - GRP1 - GRP2 - ${#GRP1})) 2 color="$cP" [ "$GRP1" -eq "$GRP2" ] && color="$cB" for ((j = 0; j < GRP1; j++)); do printf '%s(%s' "$color" "$cN"; done [ $GRP1 -gt 0 ] && printf %s "$GRP1" for ((j = 0; j < GRP2; j++)); do printf '%s)%s' "$color" "$cN"; done fi # 3rd line: legend txt=${tit2_txt[9]} cmd=${tit2_cmd[9]} gotoxy $((COLUMNS - ${#txt} - ${#cmd} - 1)) 3 if [ "$has_not_supported" -eq 1 ]; then printf '%s%s%s %s' "$cB" "$cmd" "$cN" "$txt" else clearN $((${#txt} + ${#cmd} + 1)) fi } doMenu() { local i local -a Menui eval "Menui=(\"\${$1[@]}\")" menu_n=$((${#Menui[*]} - 1)) # ini (global var) if [ "$is_interactive" -eq 1 ]; then # history gotoxy $x_hist $y_hist printf ' %s.oO(%s%s%s)%s%s(%s%s%s)%s%s\n' \ "$cP" "$cN" "$REPLIES" "$cP" "$cN" \ "$cP" "$cN" "$uins" "$cP" "$cN" \ "$_eol" # title gotoxy $x_menu $y_menu printf '%s%s:%s%s\n' "$cI" "${Menui[0]}" "$cN" "$_eol" # itens for i in $(sek $menu_n); do printf ' %s%d%s) %s%s\n' "$cB" "$i" "$cN" "${Menui[$i]}" "$_eol" i=$((i + 1)) done clearEnd # prompt gotoxy $x_prompt $y_prompt printf '%s[1-%d]:%s %s' "$cP" "$menu_n" "$cN" "$_eol" read -r -n 1 else doNextHist REPLY=$hist fi } Menu() { local name="$1" local ok=0 while [ $ok -eq 0 ]; do doMenu "$name" case "$REPLY" in [1-9]) [ "$REPLY" -gt "$menu_n" ] && continue ok=1 REPLIES="$REPLIES$REPLY" ;; .) ok=1 LASTSTATUS=$STATUS STATUS=3 ;; 0) ok=1 STATUS=Z ;; \*) ColorOnOff TopTitle ;; [\(\)\|]) [ "$STATUS" -ne 1 ] && continue [ "$REPLY" == ')' ] && { [ $GRP1 -gt 0 ] && [ $GRP1 -eq $GRP2 ] || [ $GRP1 -eq 0 ]; } && continue [ "$REPLY" == ')' ] && STATUS=2 ok=1 REPLIES="$REPLIES$REPLY" ;; /) ok=1 STATUS=4 ;; esac done } doNextHist() { hists=${hists#?} # deleting previous item hist=${hists:0:1} : "${hist:=.}" # if last, quit } doNextHistArg() { histargs=${histargs#*¤} histarg=${histargs%%¤*} } getChar() { gotoxy $x_prompt2 $y_prompt if [ "$is_interactive" -eq 1 ]; then printf '%s%s%s ' "$cP" $"which one?" "$cN" read -n 1 -r USERINPUT uin="$USERINPUT" else doNextHistArg uin=$histarg fi uins="${uins}¤$uin" F_ESCCHAR=1 } getCharList() { gotoxy $x_prompt2 $y_prompt if [ "$is_interactive" -eq 1 ]; then printf '%s%s%s ' "$cP" $"which?" "$cN" read -r USERINPUT uin="$USERINPUT" else doNextHistArg uin=$histarg fi # dedup is safe because $uin contains only literal chars (no ranges) uin="$(uniqChars "$uin")" uins="${uins}¤$uin" # putting not special chars in not special places: [][^-] [ "${uin#^}" != "$uin" ] && uin="${uin#^}^" # move leading ^ to the end [ "${uin#?*-}" != "$uin" ] && uin="${uin/-/}-" # move non-leading - to the end [ "${uin/]/}" != "$uin" ] && uin="]${uin/]/}" # move ] to the start # if any $1, negated list [ -n "$1" ] && uin="^$uin" # make it a list uin="[$uin]" F_ESCCHARLIST=1 } getString() { gotoxy $x_prompt2 $y_prompt if [ "$is_interactive" -eq 1 ]; then printf '%stxt:%s ' "$cP" "$cN" read -r USERINPUT uin="$USERINPUT" else doNextHistArg uin=$histarg fi uins="${uins}¤$uin" F_ESCCHAR=1 } getNumber() { gotoxy $x_prompt2 $y_prompt if [ "$is_interactive" -eq 1 ]; then printf '%sN=%s%s' "$cP" "$cN" "$_eol" read -r USERINPUT uin="$USERINPUT" else doNextHistArg uin=$histarg fi # Remove !numbers uin="${uin//[^0-9]/}" # ee if [ "${uin/666/x}" == 'x' ]; then gotoxy 36 1 printf '%s]:|%s\n' "$cP" "$cN" fi if [ -n "$uin" ]; then uins="${uins}¤$uin" else getNumber # there _must_ be a number fi } getPosix() { local psx local rpl unset SUBHUMAN if [ "$is_interactive" -eq 1 ]; then Choice --reset "${posix_txt[@]}" else ChoiceAuto fi for rpl in $CHOICEREPLY; do psx="${psx}[:${posix_re[$rpl]}:]" SUBHUMAN="$SUBHUMAN, ${posix_txt[$rpl]/ (*)/}" done SUBHUMAN=${SUBHUMAN#, } F_POSIX=1 uin="[$psx]" uins="${uins}¤:${CHOICEREPLY// /}" } getCombo() { local cmb local rpl unset SUBHUMAN if [ "$is_interactive" -eq 1 ]; then Choice --reset "${combo_txt[@]}" else ChoiceAuto fi for rpl in $CHOICEREPLY; do cmb="$cmb${combo_re[$rpl]}" SUBHUMAN="$SUBHUMAN, ${combo_txt[$rpl]}" done # In this menu, @ is used as a placeholder for the tab char # It will have to be replaced later, so let's set the flag charInText @ "$cmb" && F_GETTAB=1 SUBHUMAN=${SUBHUMAN#, } uin="[$cmb]" uins="${uins}¤:${CHOICEREPLY// /}" } getREady() { #TODO unset SUBHUMAN uin='' } # convert [@] -> [\t] or [] based on ax_*[8] value getListTab() { local x if [ "$(getMeta "ax_${progs[$1]}" 8)" == '\t' ]; then x='\t' else x='' fi uin="${uin/@/$x}" } # Set $uin to !! when POSIX character classes are not supported getHasPosix() { [ "$(getMeta "ax_${progs[$1]}" 7)" == 'P' ] || uin='!!' } # Escape possible metachars in user input so they will be matched literally escChar() { local index="$1" local escape_metachar local special_chars escape_metachar=$(getMeta "ax_${progs[$index]}" 4) special_chars=$(getMeta "ax_${progs[$index]}" 5) uin=$(escapeChars "$special_chars" "$uin" "$escape_metachar") } # Escape user input: maybe '\' inside [] needs to be escaped escCharList() { local escape_metachar # shellcheck disable=SC1003 if [ "$(getMeta "ax_${progs[$1]}" 6)" == '\' ]; then escape_metachar=$(getMeta "ax_${progs[$1]}" 4) if [[ ${BASH_VERSINFO[0]} -lt 5 ]]; then uin="${uin/\\/$escape_metachar$escape_metachar}" else uin="${uin/\\/"$escape_metachar$escape_metachar"}" fi fi } Reset() { local p # It's all global variables in this function gotoxy $x_regex $y_regex unset REPLIES uins HUMAN "Regex[*]" has_not_supported=0 GRP1=0 GRP2=0 maxprogname=$(getLargestItem "${progs[@]}") # global var for p in "${progs[@]}"; do [ "$is_interactive" -eq 1 ] && printf " Regex %-${#maxprogname}s: %s\n" "$p" "$_eol" done } showRegex() { gotoxy $x_regex $y_regex local i local new_part local save="$uin" # For each program for ((i = 0; i < ${#progs[@]}; i++)); do [ "$F_ESCCHAR" == 1 ] && escChar "$i" [ "$F_ESCCHARLIST" == 1 ] && escCharList "$i" [ "$F_GETTAB" == 1 ] && getListTab "$i" [ "$F_POSIX" == 1 ] && getHasPosix "$i" # Check status case "$1" in ax | S2) eval new_part="\${$1_${progs[$i]}[$REPLY]/@/$uin}" [ "$new_part" == '-' ] && new_part='' Regex[$i]="${Regex[$i]}$new_part" [ "$new_part" == '!!' ] && has_not_supported=1 ;; S0) Regex[$i]="${Regex[$i]}${S0_re[$REPLY]}" ;; S1) Regex[$i]="${Regex[$i]}${uin:-${S1_re[$REPLY]}}" # When a program does not support POSIX character classes, $uin # will be set to !! by getHasPosix(). Also check $REPLY to avoid # a false positive when the user wants to match the !! string. [ "$REPLY" -eq 7 ] && [ "$uin" == '!!' ] && has_not_supported=1 ;; esac [ "$is_interactive" -eq 1 ] && printf " Regex %-${#maxprogname}s: %s\n" "${progs[$i]}" "${Regex[$i]}" uin="$save" done unset uin USERINPUT F_ESCCHAR F_ESCCHARLIST F_GETTAB F_POSIX } # ### And now the cool-smart-MSclippy choice menu/prompt # # number of items <= 10, 1 column # number of items > 10, 2 columns # maximum number of items = 26 (a-z) # # Just refresh the selected item on the screen ChoiceRefresh() { local xy=$1 local a=$2 local stat=$3 local opt=$4 # colorizing case status is ON [ "$stat" == '+' ] && stat="$cI$stat$cN" gotoxy "${xy#*;}" "${xy%;*}" printf ' %s%s%s) %s%s ' "$cB" "$a" "$cN" "$stat" "$opt" } # --reset resets the stat array Choice() { local choicereset=0 [ "$1" == '--reset' ] && shift && choicereset=1 local alf local alpha local cols local i local line local lines local numopts=$# local op local opt local opts local optxy local rpl alpha=(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) # Reading options and filling default status (off) i=0 for opt in "$@"; do opts[$i]="$opt" [ "$choicereset" -eq 1 ] && stat[$i]='-' i=$((i + 1)) done # Checking our number of items limit [ "$numopts" -gt "${#alpha[*]}" ] && printError 'too much itens (>%d)' "${#alpha[*]}" # The header Clear printTitleCmd '.' $"exit" printf '| %s' $"press the letters to (un)select the items" # We will need 2 columns? cols=1 [ "$numopts" -gt 10 ] && cols=2 # And how much lines? (remember: odd number of items, requires one more line) lines=$((numopts / cols)) [ "$((numopts % cols))" -eq 1 ] && lines=$((lines + 1)) # Filling the options screen's position array (+3 = header:2, sek:1) for ((line = 0; line < lines; line++)); do # Column 1 optxy[$line]="$((line + 3));1" # Column 2 [ "$cols" == 2 ] && optxy[$((line + lines))]="$((line + 3));40" done # Showing initial status for all options for ((op = 0; op < numopts; op++)); do ChoiceRefresh "${optxy[$op]}" "${alpha[$op]}" "${stat[$op]}" "${opts[$op]}" done # And now the cool invisible prompt while :; do read -s -r -n 1 CHOICEREPLY case "$CHOICEREPLY" in [a-z]) # Inverting the option status for ((alf = 0; alf < numopts; alf++)); do if [ "${alpha[$alf]}" == "$CHOICEREPLY" ]; then if [ "${stat[$alf]}" == '+' ]; then stat[$alf]='-' else stat[$alf]='+' fi break fi done # Showing the change [ -z "${opts[alf]}" ] && continue ChoiceRefresh "${optxy[$alf]}" "${alpha[$alf]}" \ "${stat[$alf]}" "${opts[$alf]}" ;; .) # Getting the user choices and exiting unset CHOICEREPLY for ((rpl = 0; rpl < numopts; rpl++)); do [ "${stat[$rpl]}" == '+' ] && CHOICEREPLY="$CHOICEREPLY $rpl" done break ;; esac done } # Non-interative, just return the answers ChoiceAuto() { local i local z unset CHOICEREPLY doNextHistArg z=${histarg#:} # marker for ((i = 0; i < ${#z}; i++)); do CHOICEREPLY="$CHOICEREPLY ${z:$i:1}" done } # Fills the stat array with the actual active programs ON statActiveProgs() { local i local p local ps=" ${progs[*]} " # For each program for ((i = 0; i < ${#allprogs[@]}; i++)); do # Default OFF p="${allprogs[$i]}" stat[$i]='-' # Case found, turn ON [ "${ps/ $p /}" != "$ps" ] && stat[$i]='+' done } ############################################################################### ######################### ariel, ucla, vamos! ################################# ############################################################################### STATUS=0 # default status Clear ScreenSize ColorOnOff # turning color ON trap "clearEnd; echo; exit" SIGINT while :; do case ${STATUS:=0} in 0 | Z) STATUS=${STATUS/Z/0} Reset TopTitle Menu S0_txt [ -z "${STATUS/[Z34]/}" ] && continue # 0,3,4: escape status HUMAN="${S0_txt[0]} ${S0_txt[$REPLY]}" showRegex S0 STATUS=1 ;; 1) TopTitle Menu S1_txt [ -z "${STATUS/[Z34]/}" ] && continue # 0,3,4: escape status if [ -n "${REPLY/[1-9]/}" ]; then HUMAN="$HUMAN $REPLY" if [ "$REPLY" == '|' ]; then REPLY=1 elif [ "$REPLY" == '(' ]; then REPLY=2 GRP1=$((GRP1 + 1)) elif [ "$REPLY" == ')' ]; then REPLY=3 GRP2=$((GRP2 + 1)) else printf '\n\n' printError 'unknown reply type "%s"\n' "$REPLY" fi showRegex ax else HUMAN="$HUMAN, ${S1_txt[0]} ${S1_txt[$REPLY]/ (*)/}" case "$REPLY" in 1) STATUS=2 ;; 2) STATUS=2 getChar ;; 3) STATUS=1 getString HUMAN="$HUMAN {$uin}" ;; 4) STATUS=2 getCharList ;; 5) STATUS=2 getCharList negated ;; [678]) STATUS=12 continue ;; 9) STATUS=1 ;; esac showRegex S1 fi ;; 12) [ "$REPLY" -eq 6 ] && STATUS=2 && getCombo [ "$REPLY" -eq 7 ] && STATUS=2 && getPosix [ "$REPLY" -eq 8 ] && STATUS=1 && getREady Clear TopTitle HUMAN="$HUMAN {$SUBHUMAN}" showRegex S1 ;; 2) TopTitle Menu S2_txt [ -z "${STATUS/[Z34]/}" ] && continue # 0,3,4: escape status rep_middle=$"repeated" rep_txt="${S2_txt[$REPLY]}" rep_txtend=$"times" [ "$REPLY" -ge 5 ] && getNumber && rep_txt=${rep_txt/N/$uin} HUMAN="$HUMAN, $rep_middle ${rep_txt/ (*)/} $rep_txtend" showRegex S2 STATUS=1 ;; 3) [ "$is_interactive" -eq 0 ] && STATUS=9 && continue warning=$"Really quit?" read -r -n 1 -p "..$cB $warning [.] $cN" STATUS=$LASTSTATUS [ "$REPLY" == '.' ] && STATUS=9 ;; 4) statActiveProgs Choice "${allprogs[@]}" i=0 unset progs # Rewriting the progs array with the user choices for rpl in $CHOICEREPLY; do progs[$i]=${allprogs[$rpl]} i=$((i + 1)) done ScreenSize Clear STATUS=0 ;; 9) gotoxy $x_hist $y_hist clearEnd if [ "$is_interactive" -eq 1 ]; then noregex_txt=$"no regex" printf "%stxt2regex --history '%s%s'%s\n\n" \ "$cB" "$REPLIES" "$uins" "$cN" printf '%s.\n' "${HUMAN:-$noregex_txt}" else for ((i = 0; i < ${#progs[@]}; i++)); do # for each program printf " Regex %-${#maxprogname}s: %s\n" \ "${progs[$i]}" "${Regex[$i]}" done printf '\n' fi exit 0 ;; *) printError 'STATUS = "%s"\n' "$STATUS" ;; esac done