Question

How do I do a pattern-match for this: 4,7,9....n - a comma delimited string of digits as user input? I'm using case statement for that and I believe, case rather use pattern-matching than regex. This is what user gets:

Do you want to delete any of these?
     [ 1 ]   launch-EOsgR4
     [ 2 ]   launch-SWZQdJ
     [ 3 ]   launch-tHAdIm
     [ 4 ]   launchd-235.z4KTVx
     [ 5 ]   launchd-257.nM2wOZ
     [ 6 ]   progress.log
     [ 7 ]   ssh-8pISGGnlZ5
----------------------------------------
Single: 4; Multiple: 2,3; Range: 4..7
a to delete all; n to cancel and exit
----------------------------------------
( [1][2][3][4][5][6][7] | a | n ): 

And, as the showed above, users got the option for a single number (easy), a range: 6..9 (also not so hard) or multiple: 3,5,6 (find a bit hard with 'single number' option). This is what I've done so far....

#!/usr/bin/env bash

NL=$(echo -e "\033[0;0m")
BD=$(echo -e "\033[0;1m")
ERR=$(printf "\n%-25s" "$(echo -e "\033[1;31m[ ERROR ]\033[0m")")
WRN=$(printf "\n%-25s" "$(echo -e "\033[1;33m[ WARN  ]\033[0m")")


ls /tmp | tail -n9 > list_of_file
s=$(printf "%-40s" "-")

function lstFile()
{
    local file=$1
    if [[ -s $file ]]
    then
        LINES=( $(cat $file) ); echo ""
        for ix in ${!LINES[@]}
        do
            printf "%-5s%-14s%s\n" "" "${BD}[ $(( ix+1 )) ]" "${LINES[$ix]}${NL}"
        done
   else
       exit 0
   fi

   LST=$(echo ${!LINES[@]}|awk -v ORS=']' '{for (i=1; i<=NF; i++) print "["($i+1)}')
}

function delOpt()
{
    echo "${s// /-}"
    echo -e "Single: 4; Multiple: 2,3; Range: 4..7"
    echo -e "${BD}a${NL} to delete all; ${BD}n${NL} to cancel and exit"
    echo "${s// /-}"
    echo -n "( $1 | a | n ): "
}

echo -e "\nDo you want to delete any of these?"
lstFile list_of_file

ANS=
until [[ "${ANS}" == "N" || "${ANS}" == "n" || "${ANS}" == "e" ]]
do
    delOpt $LST
    read ANS && echo ""

    ANS=$( tr '[:upper:]' '[:lower:]' <<< "$ANS" )
    [[ -n $(echo $ANS|grep -E -w "^[aen0-9,]{1,}") ]] && : || ANS="X"

case ${ANS} in
    [0-9]..[0-9] )

    for ix in $(eval echo \{$ANS\}); do
        LINE=${LINES[(( $ix-1 ))]}
        echo -e "Deleting:  ${BD}${LINE}${NL}"
        sed -i -c "/$LINE/d" list_of_file
    done
    unset LINE
    lstFile list_of_file
    ;;

    [0-9,]* )
    ANS=$(echo $ANS | awk -F' |,' '{for (i=1; i<=NF; i++) print $i}')
    for ix in $ANS; do
        if [[ $ix -gt ${#LINES[@]} ]]
        then
            echo  "${ERR}Out-of-range value: ${bd}$ix${NL}"
        else
            LINE=${LINES[(( $ix-1 ))]}
            echo -e "Deleting:  ${BD}${LINE}${NL}"
            sed -i -c "/$LINE/d" list_of_file > /dev/null 2>&1
        fi
    done
    unset LINE
    lstFile list_of_file
    ;;

    a )              
    for ix in ${LINES[@]}; do
        echo "Deleting:  ${BD}${ix}${NL}"
        sed -i -c "/$ix/d" list_of_file
    done
    exit 0
    ;;

    n|e ) exit 0
    ;;

    * ) echo "${WRN}Invalid entry! Should be digit or ${BD}a${NL} for All."
    printf "%-14s%s\n\n" "" "Otherwise, enter ${UL}n${NL}o or ${UL}e${NL}xit to quit"
    ;;
esac
done

which is working fine (sort of) but there are some race conditions. e.g.

2,d,7 - throws in: bad array subscript
6..10 - throws in: (( 6..11-1 )): syntax error: invalid arithmetic operator (error token is "..11-1 ))")
but, 
6..9  - throws in: first RE may not be empty

Is there any way to have separate options to catch 'single' and 'multiple' number input? Also, any suggestions on overall improvement?

Any help greatly appreciated. Cheers!!


Update: 31/10

It's working now. Thanks to Alepac for the suggestion.
Just in case, if someone else is also looking for something similar, I'm putting it in here. As per my original code, this function will delete the line(s) from a file: list_of_file according to the user input.

function chkINPUT()
{
    local I_PUT=( "$@" )
    local MAX=${#LINES[@]}
    #IFS=', ' read -a SPLITTED <<< "$I_PUT"
    local SPLITTED=( $(echo "${I_PUT[@]}" | awk -F',| ' '{for (i=1; i<=NF; i++) print $i}') )

    for idx in "${!SPLITTED[@]}"
    do
        SPLTD=${SPLITTED[idx]}

        # Check if it's a range [4..7]
        if [[ "${SPLTD}" =~ ^[0-9]{1,2}\.\.[0-9]{1,2}$ ]]
        then
            for ix in $(eval echo \{$SPLTD\}); do
                if (( ${ix} <= $MAX )); then
                    LINE=${LINES[(( ix-1 ))]}
                    echo -e "Deleting:  ${BD}${LINE}${NL}"
                    sed -i -e "/$LINE/d" list_of_file 2>&1 > /dev/null
                    unset LINE
                else
                    echo -e "\t${ix} => Out of range"
                    break
                fi
            done

        # Check if it's a single input
        elif [[ "${SPLTD}" =~ ^[[:digit:]]+$ ]]
        then
            if (( ${SPLTD} <= $MAX )); then
                LINE=${LINES[(( SPLTD-1 ))]}
                echo -e "Deleting:  ${BD}${LINE}${NL}"
                sed -i -e "/$LINE/d" list_of_file 2>&1 > /dev/null
                unset LINE
            else
                echo  "${ERR}Out-of-range value: ${bd}$SPLTD${NL}"
            fi

        else
            echo  "${ERR}Invalid entry: ${bd}$SPLTD${NL}; must be an integer from the list!"
        fi
    done
}

and then, use it like this:

ANS= 
until [[ "${ANS}" == "N" || "${ANS}" == "n" || "${ANS}" == "e" ]] 
do
    delOpt $LST
    read ANS && echo ""
    ANS=$( tr '[:upper:]' '[:lower:]' <<< "$ANS" )

    case ${ANS} in
        [0-9]* )
        chkINPUT ${ANS}
        ;;    

Hope it helps. Cheers!!

Was it helpful?

Solution

I think the best option could be to split string into array, then cycle on the array. The next question could be how to split string, here is an option:

#!/bin/sh
ANS="1,2,3..4"
echo ans = ${ANS}
IFS=', ' read -a SPLITTED <<< "$ANS"
for index in "${!SPLITTED[@]}"
do
    echo "$index ${SPLITTED[index]}"
done

result

alepac@mypc:/tmp$ . test.sh 
ans = 1,2,3..4
0 1
1 2
2 3..4

in this way you could even some like 1,3..5,8,n and parse it correctly

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top