Checking neighbour elements in flat array - arrays

Problem description:
I want to iterate over an array (flattened 2D --> 1D array right now) and keep checking it's nearest neighbours. From that I want to determine if they are dead/alive ('X' or '.') and change their state accordingly to my rules(simplified conway's).
My grid looks like this e.g.:
...............
...........X...
.X.......X...X.
...............
....X..........
....X..........
...............
.....XX........
..........X....
...............
Cells alive: 9
But I have this array flattened to 1D array to iterate over it. So basically it turns to something like this: ....X...X....X. etc.
After writing it down on a paper I think there are several cases to check in this "grid":
TopLeft element (i = 0) - first element, 3 neighbours/cases to check
TopRight element (i = nColumns - 1), as above
BottomLeft element (i = nColumns * nRows - nColumns), as above
BottomRight element (i = nColumns * nRows - 1) - last element, as above
"Border" elements (5 neighbours each without corner elements)
Middle elements with 8 neighbours
But it seems totally stupid to check it with some if's and case statements. If I could use real 2D arrays I think I could just create array of offset (-1, 1), (0, 1)... and so on. But I can't think of a way how to handle this with my code.
I will be very glad for any tips/examples and so on.
My code so far:
cellsAlive=0
#STDIN variables
geneFile=$1
nRows=$2
nColumns=$3
let "cells = $nRows * $nColumns"
declare -i tick_rate # instead maybe use watch or sleep
readarray -t geneArr < $geneFile # -t removes a trailing newline from each line read.
elementsCounts=${#geneArr[#]}
echo -e "--- Coordinates ---"
for (( i = 0; i < $elementsCounts; i++ )); do
echo "${geneArr[$i]}" #| cut -d' ' -f2 $geneFile | head -2
done
echo -e "\n--- Grid ---"
#file must end with a newline
[[ $geneFile && -f $geneFile && -r $geneFile ]] || { printf >&2 'arg must be readable file.\n'; exit; }
array=()
for ((i=0; i<nRows*nColumns; ++i)); do
array+=( '.' )
done
printf "\n"
while read -r x y; do
[[ $x && $y ]] || continue
[[ $x = +([[:digit:]]) && $y = +([[:digit:]]) ]] || continue
((x=10#$x,y=10#$y)) #10 digit base
(( x<nRows && y<nColumns )) || continue
array[x+y*nRows]='X'
if [[ ${array[x+y*nRows]} == 'X' ]]; then
let "cellsAlive += 1"
fi
done < "$geneFile"
# print to stdout and to file
for((i=0;i<nColumns;++i)); do
printf '%s' "${array[#]:i*nRows:nRows}" $'\n'
done | tee currentState
arrayCopy=("${array[#]}")
printf "Cells alive: %d" $cellsAlive ; printf "\n"
# printf "\n"
for (( i = 0; i < ${#arrayCopy[#]}; i++ )); do
#neighboursCount=0
case $i in
"0") if [[ ${arrayCopy[$(($i - 1))]} == 'X' ]] || [[ ${arrayCopy[$(($i + $nColumns))]} == 'X' ]] || [[ ${arrayCopy[$(($i + $nColumns + 1))]} == 'X' ]] ; then #TopLeft
echo "That is just ridiculous way to check it..."
fi ;;
"$(($nColumns - 1))") printf "${arrayCopy[$i]}" ;; #TopRight
"$(($nColumns*$nRows-1))") printf "${arrayCopy[$i]}" ;; #BottomRight
"$(($nColumns*$nRows-$nColumns))") printf "${arrayCopy[$i]}" ;; #BottomLeft
*) ;; #Middle elements with 8 neighbours
esac
done
printf "\n"
Thanks in advance for help.
Example geneFile.txt (add blank like at the end):
1 2
4 5
6 7
13 2
5 7
4 4
9 2
11 1
10 8

ok. Here we go. Because i found this question interesting to be implemented in bash i just wrote an implementation of conway's game of life.
The main part to answer your question is probably: how to access neighbours for a position in a matrix if it is linearized?.
So you can access an element in a flatted matrix by
(row*fieldwidth)+columnoffset.
Every neighbour then can be accessed by adjusting row and columnoffset by +/-1 starting with row and columnoffset at 0.
Have a look at the getnextstate function to view the specialcases.
So here is the implementation.
You are able to provide a file as input containing just CELLALIVEMARKER,CELLDEADMARKER and spaces. If the length for the flatted matrix does not fit the width/height parameter for the FIELD it just pads with random values.
#!/bin/bash
# system values
BASENAME="/usr/bin/basename"
ECHO="/bin/echo"
SLEEP="/bin/sleep"
TPUT="/usr/bin/tput"
GREP="/bin/grep"
WC="/usr/bin/wc"
CAT="/bin/cat"
if [ "${#}" != "4" -a "${#}" != "5" ]; then
${ECHO} "USAGE: ./$(${BASENAME} ${0}) FIELDWIDTH FIELDHEIGHT RULESALIVE RULESDEAD [LINSTARTMATRIX]"
${ECHO} "EXAMPLES: ./$(${BASENAME} ${0}) 50 50 \"2 3\" \"3\""
${ECHO} " ./$(${BASENAME} ${0}) 50 50 \"2 3\" \"3\"" init.mtx
exit
fi
# field values
FWIDTH=${1}
FHEIGHT=${2}
# number of living neighbours for a living cell to stay alive in the next generation
RULESALIVE=($(${ECHO} ${3}))
# number of living neighbours for a dead cell to become alive in the next generation
RULESDEAD=($(${ECHO} ${4}))
CELLALIVEMARKER="o"
CELLDEADMARKER="."
FIELD=() # flatted matrix representation
# if there are just marker values or spaces in the file it is a valid one
${CAT} ${5} | ${GREP} -oq '[^\'${CELLALIVEMARKER}'\'${CELLDEADMARKER}'\ ]'
isvalid="${?}"
if [ "${5}" != "" ] && [ "${isvalid}" == "1" ]; then
FIELD=($(${CAT} ${5}))
# fill up with randoms if the length won't fit the dimension parameters
if [ "${#FIELD[#]}" != "$((${FWIDTH}*${FHEIGHT}))" ]; then
${ECHO} "I: Padding matrix with random values."
# fill up field with randoms if its too short
for((i=${#FIELD[#]}; i<${FWIDTH}*${FHEIGHT}; i=$((${i}+1)))); do
cell="${CELLALIVEMARKER}"
alive=$((${RANDOM}%2))
if [ "x${alive}" == "x1" ]; then
cell="${CELLDEADMARKER}"
fi
FIELD[${#FIELD[#]}]="${cell}"
done
fi
else
# fill random field
for((i=0; i<${FWIDTH}*${FHEIGHT}; i=$((${i}+1)))); do
cell="${CELLALIVEMARKER}"
alive=$((${RANDOM}%2))
if [ "x${alive}" == "x1" ]; then
cell="${CELLDEADMARKER}"
fi
FIELD[${#FIELD[#]}]="${cell}"
done
fi
# evaluate rules and get the next state for the cell
getnextstate() {
local i="${1}" # row
local j="${2}" # col
local neighbours=""
# left upper
if [ "${i}" -eq "0" -a "${j}" -eq "0" ]; then
neighbours="${FIELD[$(((${i}*${FWIDTH})+(${j}+1)))]} ${FIELD[$((((${i}+1)*${FWIDTH})+${j}))]} ${FIELD[$((((${i}+1)*${FWIDTH})+(${j}+1)))]}"
# right upper
elif [ "${i}" -eq "0" -a "${j}" -eq "$((${FWIDTH}-1))" ]; then
neighbours="${FIELD[$(((${i}*${FWIDTH})+(${j}-1)))]} ${FIELD[$((((${i}+1)*${FWIDTH})+(${j}-1)))]} ${FIELD[$((((${i}+1)*${FWIDTH})+${j}))]}"
# left bottom
elif [ "${i}" -eq "$((${FHEIGHT}-1))" -a "${j}" -eq "0" ]; then
neighbours="~${FIELD[$((((${i}-1)*${FWIDTH})+${j}))]} ${FIELD[$((((${i}-1)*${FWIDTH})+(${j}+1)))]} ${FIELD[$(((${i}*${FWIDTH})+(${j}+1)))]}"
# right bottom
elif [ "${i}" -eq "$((${FHEIGHT}-1))" -a "${j}" -eq "$((${FWIDTH}-1))" ]; then
neighbours="?${FIELD[$((((${i}-1)*${FWIDTH})+(${j}-1)))]} ${FIELD[$((((${i}-1)*${FWIDTH})+${j}))]} ${FIELD[$(((${i}*${FWIDTH})+(${j}-1)))]}"
# upper
elif [ "${i}" -eq "0" -a "${j}" -gt "0" ]; then
neighbours="-${FIELD[$(((${i}*${FWIDTH})+(${j}-1)))]} ${FIELD[$(((${i}*${FWIDTH})+(${j}+1)))]} ${FIELD[$((((${i}+1)*${FWIDTH})+(${j}-1)))]} ${FIELD[$((((${i}+1)*${FWIDTH})+${j}))]} ${FIELD[$((((${i}+1)*${FWIDTH})+(${j}+1)))]}"
# bottom
elif [ "${i}" -eq "$((${FHEIGHT}-1))" -a "${j}" -gt "0" ]; then
neighbours="=${FIELD[$((((${i}-1)*${FWIDTH})+(${j}-1)))]} ${FIELD[$((((${i}-1)*${FWIDTH})+${j}))]} ${FIELD[$((((${i}-1)*${FWIDTH})+(${j}+1)))]} ${FIELD[$(((${i}*${FWIDTH})+(${j}-1)))]} ${FIELD[$(((${i}*${FWIDTH})+(${j}+1)))]}"
# right
elif [ "${i}" -gt "0" -a "${j}" -eq "0" ]; then
neighbours="#${FIELD[$((((${i}-1)*${FWIDTH})+${j}))]} ${FIELD[$((((${i}-1)*${FWIDTH})+(${j}+1)))]} ${FIELD[$(((${i}*${FWIDTH})+(${j}+1)))]} ${FIELD[$((((${i}+1)*${FWIDTH})+${j}))]} ${FIELD[$((((${i}+1)*${FWIDTH})+(${j}+1)))]}"
# left
elif [ "${i}" -gt "0" -a "${j}" -eq "$((${FWIDTH}-1))" ]; then
neighbours="_${FIELD[$((((${i}-1)*${FWIDTH})+(${j}-1)))]} ${FIELD[$((((${i}-1)*${FWIDTH})+${j}))]} ${FIELD[$(((${i}*${FWIDTH})+(${j}-1)))]} ${FIELD[$((((${i}+1)*${FWIDTH})+(${j}-1)))]} ${FIELD[$((((${i}+1)*${FWIDTH})+${j}))]}"
# center
else
neighbours="#${FIELD[$((((${i}-1)*${FWIDTH})+(${j}-1)))]} ${FIELD[$((((${i}-1)*${FWIDTH})+${j}))]} ${FIELD[$((((${i}-1)*${FWIDTH})+(${j}+1)))]} ${FIELD[$(((${i}*${FWIDTH})+(${j}-1)))]} ${FIELD[$(((${i}*${FWIDTH})+(${j}+1)))]} ${FIELD[$((((${i}+1)*${FWIDTH})+(${j}-1)))]} ${FIELD[$((((${i}+1)*${FWIDTH})+${j}))]} ${FIELD[$((((${i}+1)*${FWIDTH})+(${j}+1)))]}"
fi
# count neighbours alive
ncnt=$(${ECHO} ${neighbours} | ${GREP} -o ${CELLALIVEMARKER} | ${WC} -l)
# evaluate rules
local next=""
if [ "${FIELD[$(((${i}*${FWIDTH})+${j}))]}" == "${CELLALIVEMARKER}" ] && [[ "$(${ECHO} ${RULESALIVE[#]})" =~ ${ncnt} ]]; then
next="${CELLALIVEMARKER}"
elif [ "${FIELD[$(((${i}*${FWIDTH})+${j}))]}" == "${CELLDEADMARKER}" ] && [[ "$(${ECHO} ${RULESDEAD[#]})" =~ ${ncnt} ]]; then
next="${CELLALIVEMARKER}"
else
next="${CELLDEADMARKER}"
fi
${ECHO} ${next}
}
firstrun=1
while [ true ]; do
# print lines
FIELD_UPDATE=()
for((i=0; i<${FHEIGHT}; i=$((${i}+1)))); do
line=""
# calculate lines
for((j=0; j<${FWIDTH}; j=$((${j}+1)))); do
if [ "${firstrun}" == "1" ]; then
line="${line}${FIELD[$(((${i}*${FWIDTH})+${j}))]} "
# start calculation just after the first print
elif [ "${firstrun}" == "0" ]; then
line="${line}$(getnextstate ${i} ${j}) "
fi
done
FIELD_UPDATE=($(${ECHO} ${FIELD_UPDATE[#]}) $(${ECHO} ${line}))
${ECHO} ${line}
done
FIELD=(${FIELD_UPDATE[#]})
${SLEEP} 2
# refresh lines in the field
for((i=0; i<${FHEIGHT}; i=$((${i}+1)))); do
# refresh lines
${TPUT} cuu1
${TPUT} el
done
firstrun=0
done
So providing the file init.mtx containing the following matrix
. o . . . . . . . .
. . o . . . . . . .
o o o . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
you are able to create a simple glider (from the upper left to the bottom right)
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . o o
. . . . . . . . o o
using Conway's default rules by running this script as follows:
./gameoflife 10 10 "2 3" "3" init.mtx
Hope this helps.
And btw it was fun to implement this in bash :)

Related

Comparing 2 Arrays with sum values

I have an Array filled with sum values of the numbers 1 to 10 and in a for loop i want to check the array for duplicates but these values are e.g. 00034 1 which are 2 numbers so the error message i get is "too many arguments" how can i change it?
Code:
while [ $k -le 10 ]
do
#the line below is the problem i is the value of another
#loop the list is in
if [ ${sumList[i]} -e ${sumList[k]} ]
then
if [$k != $i]
then
echo "collision k: $k mit i: $i"
fi
fi
k=$(($k+1))
done
Quote your variables. And use = instead of -eq to compare strings rather than numbers.
You also need $ before the i and k variables, and you need spaces around [ and ] in your second if.
if [ "${sumList[$i]}" = "${sumList[$k]}" ] && [ $i -ne $k ]
then
echo "Collision k: $k mit i: $i"
fi

Bash Delete doublons within array

I have a file containing a several number of fields. I am trying to delete doublons (ex: two same attributes with a different date.) within a same field. For example from this :
Andro manual gene 1 100 . + . ID=truc;Name=truc;modified=13-09-1993;added=13-09-1993;modified=13-09-1997
Andro manual mRNA 1 100 . + . ID=truc-mRNA;Name=truc-mRNA;modified=13-09-1993;added=13-09-1993;modified=13-09-1997
We can see modified=13-09-1993 and modified=13-09-1997 are doublons. So I want to obtain this :
Andro manual gene 1 100 . + . ID=truc;Name=truc;added=13-09-1993;modified=13-09-1997
Andro manual mRNA 1 100 . + . ID=truc-mRNA;Name=truc-mRNA;added=13-09-1993;modified=13-09-1997
I want to keep the latest occurence of particular attribute and deleting the oldest one. They will only have at maximum twice the same attribute in a same row.
I've tried this code (which is now working):
INPUT=$1
ID=$2
ALL_FEATURES=()
CONTIG_FEATURES=$(grep $ID $INPUT)
while read LINE; do
FEATURES=$(echo -e "$LINE" | cut -f 9)
#For each line, store all attributes from every line in an array
IFS=';' read -r -a ARRAY <<< "$FEATURES"
#Once the array is created, loop in array to look for doublons
for INDEX in "${!ARRAY[#]}"
do
ELEMENT=${ARRAY[INDEX]}
#If we are not at the end of the array, compare actual element and next element
ACTUAL=$ELEMENT
for INDEX2 in "${!ARRAY[#]}"
do
NEXT="${ARRAY[INDEX2]}"
ATTRIBUTE1=$(echo -e "$ACTUAL" | cut -d'=' -f1)
ATTRIBUTE2=$(echo -e "$NEXT" | cut -d'=' -f1)
echo "Comparing element number $INDEX ($ATTRIBUTE1) with element number $INDEX2 ($ATTRIBUTE2) ..."
if [[ $ATTRIBUTE1 = $ATTRIBUTE2 ]] && [[ $INDEX -ne $INDEX2 ]]
then
echo "Deleting features..."
#Delete actual element, because next element will be more recent
NEW=()
for VAL in "${ARRAY[#]}"
do
[[ $VAL != "${ARRAY[INDEX]}" ]] && NEW+=($VAL)
done
ARRAY=("${NEW[#]}")
unset NEW
fi
done
done
#Rewriting array into string separated by ;
FEATURES2=$( IFS=$';'; echo "${ARRAY[*]}" )
sed -i "s/$FEATURES/$FEATURES2/g" $INPUT
done < <(echo -e "$CONTIG_FEATURES")
I need advices because I think my array approache may not be a clever one, but I want a bash solution in any case. If anyone has some bash adives/shortcuts, any suggestions will be appreciated to improve my bash understanding.
I'm sorry if I forgot any details, thanks for your help.
Roxane
In awk:
$ awk '
{
n=split($NF,a,";") # split the last field by ;
for(i=n;i>=1;i--) { # iterate them backwards to keep the last "doublon"
split(a[i],b,"=") # split key=value at =
if(b[1] in c==0) { # if key not in c hash
d=a[i] (d==""?"":";") d # append key=value to d with ;
c[b[1]] # hash key into c
}
}
$NF=d # set d to last field
delete c # clear c for next record
d="" # deetoo
}
1 # output
' file
Andro manual gene 1 100 . + . ID=truc;Name=truc;added=13-09-1993;modified=13-09-1997
Andro manual mRNA 1 100 . + . ID=truc-mRNA;Name=truc-mRNA;added=13-09-1993;modified=13-09-1997
Following awk could also help you in same.
awk -F';' '{
for(i=NF;i>0;i--){
split($i, array,"=");
if(++a[array[1]]>1){
$i="\b"
}
};
delete a
}
1
' OFS=";" Input_file
Output will be as follows.
Andro manual gene 1 100 . + . ID=truc;Name=truc;added=13-09-1993;modified=13-09-1997
Andro manual mRNA 1 100 . + . ID=truc-mRNA;Name=truc-mRNA;added=13-09-1993;modified=13-09-1997

Sorting an array passed to a function as parameter

I wanted to have some helper function for array sorting, that I would be able
to use whenever I need it. So I did something like this. It works.
#!/usr/bin/bash
# sorts an array given as param
function array_sort {
declare -a source_array=("${!1}")
sorted_array=($(for elmnt in "${source_array[#]}"; do echo $elmnt; done | sort))
echo "${sorted_array[#]}" # wont echo to stdout, because of assignment to a variable
}
# TEST CASE
# orginal array
arr=(c b a 3 2 1)
#assign function call to a variable
sorted=$(array_sort arr[#])
# echo-out the results
echo ${sorted[#]}
My question is, is there some better(cleaner) way of doing this, in terms of returning array elements from function (not implementing better sorting algorithm)?
If you want a robust sorting function (i.e., one that will deal with spaces and newlines flawlessly), you may consider implementing a sorting algorithm in Bash: here's a quicksort.
quicksort() {
# sorts the positional elements wrt alphanumerical sort
# return is in array quicksort_ret
if (($#==0)); then
quicksort_ret=()
return
fi
local pivot=$1 greater=() lower=() i
shift
for i; do
if [[ "$i" < "$pivot" ]]; then
lower+=( "$i" )
else
greater+=( "$i" )
fi
done
quicksort "${greater[#]}"
greater=( "${quicksort_ret[#]}" )
quicksort "${lower[#]}"
quicksort_ret+=( "$pivot" "${greater[#]}" )
}
$ quicksort c b a 3 2 1
$ printf '%s\n' "${quicksort_ret[#]}"
1
2
3
a
b
c
You can change the ordering test in the line
if [[ "$i" < "$pivot" ]]; then
by whatever you like. E.g., for numerical only sort, you'd use
if ((i<pivot)); then
You can even use a variable (e.g., quicksort_order) that will expand to an ordering function. In this case, replace the former line by
if $quicksort_order "$i" "$pivot"; then
and use with, e.g., if you want alphanumerical sort:
order_alnum() { [[ $1 < $2 ]]; }
quicksort_order=order_alnum
The quicksort function uses the positional parameters for input and the variable quicksort_ret for output. It's now trivial to make a wrapper around this function to handle an array name as input.
For a method that, like yours, uses sort but fixes the issues with wildcards and spaces (but doesn't fix issues with newlines). Uses the builtin mapfile, so this is Bash≥4 only. For Bash<4, there are other workarounds (but you shouldn't be using Bash<4 anymore anyways).
#!/usr/bin/bash
# sorts an array given as param
# return is in array sorted_array
array_sort() {
mapfile -t sorted_array < <( printf '%s\n' "${!1}" | sort )
}
# TEST CASE 1
echo "Test 1"
# original array
arr=(c b a 3 2 1)
# sort array
array_sort "arr[#]"
# display array
declare -p "sorted_array"
# TEST CASE 2
echo "Test 2"
# original array
arr=( '*' 'a space in this field' )
# sort array
array_sort "arr[#]"
# display array
declare -p "sorted_array"
# TEST CASE 3 (fails)
echo "Test 3"
# original array
arr=( $'there is\na newline\nin this array' )
# sort array
array_sort "arr[#]"
# display array
declare -p "sorted_array"
will output:
Test 1
declare -a sorted_array='([0]="1" [1]="2" [2]="3" [3]="a" [4]="b" [5]="c")'
Test 2
declare -a sorted_array='([0]="*" [1]="a space in this field")'
Test 3
declare -a sorted_array='([0]="a newline" [1]="in this array" [2]="there is")'
Answering your questions in comment:
So that way I would have to know the name of that sorted_array variable, to use it in my scripts. Can that be avoided?
If you want to give the name of the sorted array, modify array_sort as:
array_sort() {
# $1 is the name of array to sort (with the trailing [#])
# $2 is the name of the returned array (without [#])
# Note: name of output array can be name of input array
mapfile -t "$2" < <( printf '%s\n' "${!1}" | sort )
}
and use as:
$ a=( a g e z j r )
$ array_sort "a[#]" a_sorted
$ declare -p a_sorted
declare -a a_sorted='([0]="a" [1]="e" [2]="g" [3]="j" [4]="r" [5]="z")'
If you want to use the quicksort function from my first answer, you'd use a wrapper function (sorry about the name)(*):
quicksort_gniourf() {
# $1 is the name of array to sort (with the trailing [#])
# $2 is the name of the returned array (without [#])
# Note: name of output array can be name of input array
# This is a wrapper function around the quicksort function
quicksort "${!1}"
local k=0 v
declare -g "$2=()"
for v in "${quicksort_ret[#]}"; do
printf -v "$2[$k]" '%s' "$v"
((++k))
done
}
and use as (here I'm using the same array name for input and output):
$ a=( a g e z j r )
$ quicksort_gniourf "a[#]" a
$ declare -p a
declare -a a='([0]="a" [1]="e" [2]="g" [3]="j" [4]="r" [5]="z")'
Also, how would you echo out that resulting array, preventing expansion of *, with declare -p it is ok, however with printf or echo it expands on filenames?
To print an array array using echo without expanding wildcards (observe the quotes):
echo "${array[#]}"
and using printf, one field per line (observe the quotes):
printf '%s\n' "${array[#]}"
(*) As #konsolebox mentions in his comment, declare -g appeared in bash 4.2. You can replace this line with eval "$2=()" if you like (it's fairly safe at this point since $2 is supposed to be a variable name anyways).
Forwarded from PlayShell's array/sort.sh, here's a pure Bash solution that uses Quicksort algorithm.
# ---- array.sh ----
# array_copy (avar <src>, avar <dest>) :: boolean
#
# Copies a whole array including index (key) structure.
#
# For a faster method that does not copy key structure, see
# array_get_all().
#
# This function will return true status code even if the source array
# is empty. It may only return false if other problem occurs like for
# example if source or destination array is not an indexed array
# variable or if the two array variables are not compatible.
# On the other hand, array_get_all() returns false if source array is
# empty.
#
function array_copy {
local -i __I
eval "$2=() && for __I in \${!$1[#]}; do $2[__I]=\${$1[__I]}; done"
# I hope AVAR=() does not reset variable's attributes. I've been
# wondering if I should use 'unset AVAR\[*\]' instead. The latter
# version probably is a bit slower though since it's a builtin call.
}
# array_reset (avar <array>, [mixed <element_value>, ...])
#
# Clears an array or resets it to optional elements.
#
function array_reset {
eval "$1=(\"\${#:2}\")"
}
# ---- array/sort.sh ----
# ----------------------------------------------------------------------
# array/sort.sh
#
# A portable utility that provides a function that sorts an array of a
# specific type. The sorted output can be in the form of values or
# indices.
#
# This methods were based from QuickSort (the one described in
# "A Book on C 4th Ed.").
#
# Credits have got to be given to the authors of the book
# "A Book on C 4th Ed." for this great algorithm. The algorithm was
# originally described by someone and was completely explained in the
# book with an implementation that's written in C.
#
# I knew C from many sources but most of what I learned came from this
# book and I therefore recommend it to starters for a good start and
# also to experienced programmers for a good reference and new methods
# that they may discover from it.
#
# I hope you enjoy using these functions and/or algorithms.
#
# Author: konsolebox
# Copyright free, 2008-2013
# ----------------------------------------------------------------------
# array_sort
# (
# ["from=<array>"],
# ["type=<string|integer>"],
# ["to=<array>"],
# ["as=<values|indices>"],
# ["--" [ SOURCEVALUES[#] ]]
# )
#
function array_sort {
[[ $# -eq 0 ]] && return
local __FROM __TYPE __TO __AS
local -a __ARRAY
local -a -i __INDICES
while [[ $# -gt 0 ]]; do
case "$1" in
from=*)
__FROM=${1#from=}
;;
type=*)
__TYPE=${1#type=}
;;
to=*)
__TO=${1#to=}
;;
as=*)
__AS=${1#as=}
;;
--)
shift
break
;;
#beginsyntaxcheckblock
*)
array_sort_error "unknown parameter: $1"
;;
#endsyntaxcheckblock
esac
shift
done
#beginsyntaxcheckblock
[[ -n $__FROM && $__FROM != [[:alpha:]_]*([[:alpha:][:digit:]_]) ]] && \
array_sort_error "variable name not valid for the source array: $__FROM"
[[ -n $__TYPE && $__TYPE != #(string|integer) ]] && \
array_sort_error "argument is not valid for type: $__TYPE"
[[ -n $__TO && $__TO != [[:alpha:]_]*([[:alpha:][:digit:]_]) ]] && \
array_sort_error "variable name not valid for the target array: $__TO"
[[ -n $__AS && $__AS != #(values|indices) ]] && \
array_sort_error "argument is not valid for as: $__AS"
[[ -z $__FROM && $# -eq 0 ]] && \
array_sort_error "a source should be specified either by 'from=<array>' or '-- CONTENTS[#]'"
#endsyntaxcheckblock
if [[ $# -gt 0 ]]; then
__ARRAY=("$#")
elif [[ -n $__FROM ]]; then
array_copy "$__FROM" __ARRAY || \
array_sort_error "failed to make a temporary working copy of $__FROM."
fi
[[ -z $__TYPE ]] && __TYPE=string
[[ -z $__TO ]] && __TO=__
[[ -z $__AS ]] && __AS=values
__INDICES=("${!__ARRAY[#]}")
if [[ ${#__INDICES[#]} -gt 1 ]]; then
case "$__TYPE" in
string)
array_sort_strings 0 "$(( ${#__INDICES[#]} - 1 ))"
;;
integer)
array_sort_integers 0 "$(( ${#__INDICES[#]} - 1 ))"
;;
esac
fi
case "$__AS" in
values)
local -i I J=0
array_reset "$__TO"
eval "for I in \"\${__INDICES[#]}\"; do ${__TO}[J++]=\${__ARRAY[I]}; done"
;;
indices)
eval "$__TO=(\"\${__INDICES[#]}\")"
;;
esac
}
# array_sort_strings (uint LEFT, uint RIGHT)
#
function array_sort_strings {
[[ $1 -lt $2 ]] || return
local -i LEFT=$1 RIGHT=$2 PIVOT PARTITION
if array_sort_strings_findpivot; then
array_sort_strings_partition
array_sort_strings "$LEFT" "$(( PARTITION - 1 ))"
array_sort_strings "$PARTITION" "$RIGHT"
fi
}
# array_sort_strings_findpivot () :: boolean
#
function array_sort_strings_findpivot {
local -i A B C P MIDDLE
(( MIDDLE = LEFT + (RIGHT - LEFT) / 2 ))
(( A = __INDICES[LEFT] ))
(( B = __INDICES[MIDDLE] ))
(( C = __INDICES[RIGHT] ))
[[ ${__ARRAY[A]} > "${__ARRAY[B]}" ]] && (( A = $B, B = $A ))
[[ ${__ARRAY[A]} > "${__ARRAY[C]}" ]] && (( A = $C, C = $A ))
[[ ${__ARRAY[B]} > "${__ARRAY[C]}" ]] && (( B = $C, C = $B ))
if [[ ${__ARRAY[A]} < "${__ARRAY[B]}" ]]; then
PIVOT=$B
return 0
fi
if [[ ${__ARRAY[B]} < "${__ARRAY[C]}" ]]; then
PIVOT=$C
return 0
fi
for (( P = LEFT + 1; P < MIDDLE; ++P )); do
if [[ ${__ARRAY[P]} > "${__ARRAY[A]}" ]]; then
PIVOT=$P
return 0
fi
if [[ ${__ARRAY[P]} < "${__ARRAY[A]}" ]]; then
PIVOT=$A
return 0
fi
done
for (( P = MIDDLE + 1; P < RIGHT; ++P )); do
if [[ ${__ARRAY[P]} > "${__ARRAY[A]}" ]]; then
PIVOT=$P
return 0
fi
if [[ ${__ARRAY[P]} < "${__ARRAY[A]}" ]]; then
PIVOT=$A
return 0
fi
done
return 1
}
# array_sort_strings_partition ()
#
function array_sort_strings_partition {
local -i L R T
local P=${__ARRAY[PIVOT]}
for (( L = LEFT, R = RIGHT; L <= R; )); do
while [[ ${__ARRAY[__INDICES[L]]} < "$P" ]]; do
(( ++L ))
done
until [[ ${__ARRAY[__INDICES[R]]} < "$P" ]]; do
(( --R ))
done
[[ L -lt R ]] && (( T = __INDICES[L], __INDICES[L] = __INDICES[R], __INDICES[R] = T, ++L, --R ))
done
(( PARTITION = L ))
}
# array_sort_integers (uint LEFT, uint RIGHT)
#
function array_sort_integers {
[[ $1 -lt $2 ]] || return
local -i LEFT=$1 RIGHT=$2 PIVOT PARTITION
if array_sort_integers_findpivot; then
array_sort_integers_partition
array_sort_integers "$LEFT" "$(( PARTITION - 1 ))"
array_sort_integers "$PARTITION" "$RIGHT"
fi
}
# array_sort_integers_findpivot () :: boolean
#
function array_sort_integers_findpivot {
local -i A B C P MIDDLE
(( MIDDLE = LEFT + (RIGHT - LEFT) / 2 ))
(( A = __INDICES[LEFT] ))
(( B = __INDICES[MIDDLE] ))
(( C = __INDICES[RIGHT] ))
[[ __ARRAY[A] -gt __ARRAY[B] ]] && (( A = $B, B = $A ))
[[ __ARRAY[A] -gt __ARRAY[C] ]] && (( A = $C, C = $A ))
[[ __ARRAY[B] -gt __ARRAY[C] ]] && (( B = $C, C = $B ))
if [[ __ARRAY[A] -lt __ARRAY[B] ]]; then
PIVOT=$B
return 0
fi
if [[ __ARRAY[B] -lt __ARRAY[C] ]]; then
PIVOT=$C
return 0
fi
for (( P = LEFT + 1; P < MIDDLE; ++P )); do
if [[ __ARRAY[P] -gt __ARRAY[A] ]]; then
PIVOT=$P
return 0
fi
if [[ __ARRAY[P] -lt __ARRAY[A] ]]; then
PIVOT=$A
return 0
fi
done
for (( P = MIDDLE + 1; P < RIGHT; ++P )); do
if [[ __ARRAY[P] -gt __ARRAY[A] ]]; then
PIVOT=$P
return 0
fi
if [[ __ARRAY[P] -lt __ARRAY[A] ]]; then
PIVOT=$A
return 0
fi
done
return 1
}
# array_sort_integers_partition ()
#
function array_sort_integers_partition {
local -i L R T P
for (( L = LEFT, R = RIGHT, P = __ARRAY[PIVOT]; L <= R; )); do
for (( ; __ARRAY[__INDICES[L]] < P; ++L )); do
continue
done
for (( ; __ARRAY[__INDICES[R]] >= P; --R )); do
continue
done
[[ L -lt R ]] && (( T = __INDICES[L], __INDICES[L] = __INDICES[R], __INDICES[R] = T, ++L, --R ))
done
(( PARTITION = L ))
}
# array_sort_error (string <message>)
#
function array_sort_error {
echo "array_sort: $1"
exit 1
}
# ----------------------------------------------------------------------
# Footnotes:
#
# * In some versions of bash, conditional statements does not properly
# parse the second string operand so sometimes this form doesn't work:
#
# [[ $STRINGVAR1 < $STRINGVAR2 ]]
#
# So to make it work, we have no choice but put it around quotes:
#
# [[ $STRINGVAR1 < "$STRINGVAR2" ]]
#
# * In some versions of bash, a segmentation fault occurs when
# assignment statements where sources are arrays are compounded.
#
# (( A = __A0[INDEX1], B = __A0[INDEX2] ))
# ----------------------------------------------------------------------
You can sort an array like this:
arr=(c b a 3 2 1)
sarr=( $(sort < <(printf "%s\n" "${arr[#]}")) )
printf "%s\n" "${sarr[#]}"
1
2
3
a
b
c
EDIT: To make it into a function:
array_sort() {
declare -a source_array=("${!1}");
sarr=( $(sort < <(printf "%s\n" "${arr[#]}")) );
echo "${sarr[#]}";
}

In Bash, how to convert number list into ranges of numbers?

Currently I have a sorted output of numbers from a command:
18,19,62,161,162,163,165
I would like to condense these number lists into a list of single numbers or ranges of numbers
18-19,62,161-163,165
I thought about trying to sort through the array in bash and read the next number to see if it is +1... I have a PHP function that does essentially the same thing, but I'm having trouble transposing it to Bash:
foreach ($missing as $key => $tag) {
$next = $missing[$key+1];
if (!isset($first)) {
$first = $tag;
}
if($next != $tag + 1) {
if($first == $tag) {
echo '<tr><td>'.$tag.'</td></tr>';
} else {
echo '<tr><td>'.$first.'-'.$tag.'</td></tr>';
}
unset($first);
}
}
I'm thinking there's probably a one-liner in bash that could do this but my Googling is coming up short....
UPDATE:
Thank you #Karoly Horvath for a quick answer which I used to finish my project. I'd sure be interested in any simpler solutions out there.
Yes, shell does variable substitution, if prev is not set, that line becomes:
if [ -ne $n+1]
Here is a working version:
numbers="18,19,62,161,162,163,165"
echo $numbers, | sed "s/,/\n/g" | while read num; do
if [[ -z $first ]]; then
first=$num; last=$num; continue;
fi
if [[ num -ne $((last + 1)) ]]; then
if [[ first -eq last ]]; then echo $first; else echo $first-$last; fi
first=$num; last=$num
else
: $((last++))
fi
done | paste -sd ","
18-19,62,161-163,165
Only with a function in bash:
#!/bin/bash
list2range() {
set -- ${#//,/ } # convert string to parameters
local first a b string IFS
local -a array
local endofrange=0
while [[ $# -ge 1 ]]; do
a=$1; shift; b=$1
if [[ $a+1 -eq $b ]]; then
if [[ $endofrange -eq 0 ]]; then
first=$a
endofrange=1
fi
else
if [[ $endofrange -eq 1 ]]; then
array+=($first-$a)
else
array+=($a)
fi
endofrange=0
fi
done
IFS=","; echo "${array[*]}"
}
list2range 18,19,62,161,162,163,165
Output:
18-19,62,161-163,165

Bash passing array to expr

I'm writing a simple calculator in BASH. Its aim is to loop through the arguments given, check if they're correct, do the power function and pass the rest to the expr to do the calculation. Everything except multiplication works.
When I do something like
my_script.sh 2 \* 2
I get syntax error from expr. Checking with bash -x lets me know that
expr 2 '\*' 2
The * is in apostrophes. I don't know how to get rid of it so the expr can parse it properly.
if [ $# -le 0 ]
then
usage
exit 1
fi
ARGS=("${#}")
while [ $# -gt 0 ]
do
if [ $OP -eq 0 ]
then
if [[ $1 =~ ^[-]{0,1}[0-9]+$ ]]
then
ELEMS[$J]=$1
shift
let OP=1
let J=$J+1
else
echo $1' is not a number'
usage
exit 3
fi
else
if [[ $1 =~ ^[-+/\^\*]{1}$ ]]
then
if [[ $1 =~ ^[\^]{1}$ ]]
then
if ! [[ $2 =~ ^[0-9]+$ ]]
then
echo 'Bad power exponent'
usage
exit 3
fi
let BASE=${ELEMS[$J-1]}
let EX=$2
pow $BASE $EX
let ELEMS[$J-1]=$RES
shift 2
else
if [[ $1 =~ [\*]{1} ]]
then
ELEMS[$J]=\\*
else
ELEMS[$J]=$1
fi
let J=$J+1
shift
let OP=0
fi
else
echo $1' is not an operator'
if [[ $1 =~ ^[0-9]+$ ]]
then
let TMP=${ELEMS[$J-1]}
echo "Are you missing an operator beetwen $TMP and $1?"
fi
usage
exit 3
fi
fi
done
if [ $OP -eq 0 ]
then
echo 'Missing argument after last operator'
usage
exit 3
fi
echo "Calculation: ${ARGS[*]}"
echo -n 'Result: '
expr ${ELEMS[*]}
Change ELEMS[$J]=\\* to ELEMS[$J]="*" (or ELEMS[$J]=\*) and use:
expr "${ELEMS[#]}"
The key is to use # instead of * in the array dereference, which allows you to use double quotes. This is equivalent to expr "2" "*" "2", instead of expr "2 * 2" which you get when using expr "${ELEMS[*]}"

Resources