2

In my script I need to expand an interval, e.g.:

input: 1,5-7

to get something like the following:

output: 1,5,6,7

I've found other solutions here, but they involve python and I can't use it in my script.

Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
Tom
  • 85
  • 1
  • 9

8 Answers8

2

Solution with Just Bash 4 Builtins

You can use Bash range expansions. For example, assuming you've already parsed your input you can perform a series of successive operations to transform your range into a comma-separated series. For example:

value1=1
value2='5-7'
value2=${value2/-/..}
value2=`eval echo {$value2}`
echo "input: $value1,${value2// /,}"

All the usual caveats about the dangers of eval apply, and you'd definitely be better off solving this problem in Perl, Ruby, Python, or AWK. If you can't or won't, then you should at least consider including some pipeline tools like tr or sed in your conversions to avoid the need for eval.

Todd A. Jacobs
  • 81,402
  • 15
  • 141
  • 199
2

Try something like this:

#!/bin/bash

for f in ${1//,/ }; do
  if [[ $f =~ - ]]; then
    a+=( $(seq ${f%-*} 1 ${f#*-}) )
  else
    a+=( $f )
  fi  
done

a=${a[*]}
a=${a// /,}

echo $a

Edit: As @Maxim_united mentioned in the comments, appending might be preferable to re-creating the array over and over again.

Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
1

This should work with multiple ranges too.

#! /bin/bash
input="1,5-7,13-18,22"
result_str=""
for num in $(tr ',' ' ' <<< "$input"); do
    if [[ "$num" == *-* ]]; then
        res=$(seq -s ',' $(sed -n 's#\([0-9]\+\)-\([0-9]\+\).*#\1 \2#p' <<< "$num"))
    else
        res="$num"
    fi
    result_str="$result_str,$res"
done
echo ${result_str:1}

Will produce the following output:

1,5,6,7,13,14,15,16,17,18,22
Tuxdude
  • 47,485
  • 15
  • 109
  • 110
0
expand_commas()
{
    local arg
    local st en i

    set -- ${1//,/ }
    for arg
    do
        case $arg in
        [0-9]*-[0-9]*)
            st=${arg%-*}
            en=${arg#*-}
            for ((i = st; i <= en; i++))
            do
                echo $i
            done
        ;;
        *)
            echo $arg
        ;;
        esac
    done
}

Usage:

result=$(expand_commas arg)

eg:

result=$(expand_commas 1,5-7,9-12,3)
echo $result

You'll have to turn the separated words back into commas, of course.

It's a bit fragile with bad inputs but it's entirely in bash.

torek
  • 448,244
  • 59
  • 642
  • 775
  • It works! I've piped to this function | uniq | sort -n to remove duplicates and sort the output. Thank you! – Tom Mar 09 '13 at 18:08
  • If you have bash v4, combine with @CodeGnome's trick to eliminate the loop (the output will have range values on one line though). – torek Mar 09 '13 at 18:16
0

Here's my stab at it:

input=1,5-7,10,17-20
IFS=, read -a chunks <<< "$input"

output=()
for chunk in "${chunks[@]}"
do
    IFS=- read -a args <<< "$chunk"
    if (( ${#args[@]} == 1 )) # single number                                   
    then
        output+=(${args[*]})
    else # range                                                                
        output+=($(seq "${args[@]}"))
    fi
done
joined=$(sed -e 's/ /,/g' <<< "${output[*]}")
echo $joined

Basically split on commas, then interpret each piece. Then join back together with commas at the end.

FatalError
  • 52,695
  • 14
  • 99
  • 116
0

A generic bash solution using the sequence expression `{x..y}'

#!/bin/bash
function doIt() {
  local inp="${@/,/ }"
  declare -a args=( $(echo ${inp/-/..}) )
  local item
  local sep
  for item in "${args[@]}"
  do
    case ${item} in
        *..*)  eval "for i in {${item}} ; do echo -n \${sep}\${i}; sep=, ; done";;
        *) echo -n ${sep}${item};;
    esac
    sep=,
  done
}
doIt "1,5-7"
  • Should work with any input following the sample in the question. Also with multiple occurrences of x-y
  • Use only bash builtins
Community
  • 1
  • 1
H.-Dirk Schmitt
  • 1,159
  • 8
  • 14
0

Using ideas from both @Ansgar Wiechers and @CodeGnome:

input="1,5-7,13-18,22"
for s in ${input//,/ }
do
    if [[ $f =~ - ]]
    then
        a+=( $(eval echo {${s//-/..}}) )
    else
        a+=( $s )
    fi  
done
oldIFS=$IFS; IFS=$','; echo "${a[*]}"; IFS=$oldIFS

Works in Bash 3

Maxim_united
  • 1,911
  • 1
  • 14
  • 23
0

Considering all the other answers, I came up with this solution, which does not use any sub-shells (but one call to eval for brace expansion) or separate processes:

# range list is assumed to be in $1 (e.g. 1-3,5,9-13)

# convert $1 to an array of ranges ("1-3" "5" "9-13")
IFS=,
local range=($1)
unset IFS

list=() # initialize result list
local r
for r in "${range[@]}"; do
    if [[ $r == *-* ]]; then
        # if the range is of the form "x-y",
        # * convert to a brace expression "{x..y}",
        # * using eval, this gets expanded to "x" "x+1" … "y" and
        # * append this to the list array
        eval list+=( {${r/-/..}} )
    else
        # otherwise, it is a simple number and can be appended to the array
        list+=($r)
    fi
done

# test output
echo ${list[@]}
Martin Nyolt
  • 4,463
  • 3
  • 28
  • 36