Sorting an array passed to a function as parameter - arrays

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[#]}";
}

Related

Find Item in Array / Replace With Array Using Awk?

I'm trying to make my code more clean. My function seems rather oblique to do a simple find/replace. Is there a cleaner way to do this find/replace array function with awk or something similar?
# Finds first item in array that matches find item, and replaces it with 1 or more array elements.
# Preserves sort order of original array.
find_replace_in_list(){
# $1 = list
# $2 = find item string
# $3 = replace array.
local -n _list=$1
local -n _replace_list=$3
for i in "${!_list[#]}"; do # Iterate over indices.
if [ ${_list[$i]} == "$2" ]; then
# Insert the replace array starting at indice.
local p=$((i+1)) # Get indice just after match.
local array_pre=${_list[#]:0:i}
local array_post=${_list[#]:p}
local new_array=("${array_pre[#]}" "${_replace_list[#]}" "${array_post[#]}")
echo "${new_array[#]}"
return # Break out of loop. Only replace first match.
fi
done
# Nothing found. Return orginal array.
echo "${_list[#]}"
}
: ' Example use ^^^
list=('a' 'b' 'c' 'd')
find='b'
replace_list=('b1' 'b2' 'b3')
test=$(find_replace_in_list list "$find" replace_list)
echo "RESULT:${test[*]}"
RESULT: a b1 b2 b3 c d
'
Return to another shared global variable:
find_replace_in_list() {
local -n _list=$1 _replace_list=$3
__A0=()
for i in "${!_list[#]}"; do
if [[ ${_list[i]} == "$2" ]]; then
__A0+=("${_replace_list[#]}" "${_list[#]:i + 1}")
break
fi
__A0[i]=${_list[i]}
done
}
Modify original list:
find_replace_in_list() {
local -n _list=$1 _replace_list=$3
local counter=0
for i in "${!_list[#]}"; do
if [[ ${_list[i]} == "$2" ]]; then
_list=("${_list[#]:0:counter}" "${_replace_list[#]}" "${_list[#]:i + 1}")
break
fi
(( ++counter ))
done
}
You can use nameref's (declare -n) to pass data to/from the function.
Also, instead of trying to reindex a current array we'll just build a new array (newlist aka _newlist) on-the-fly thus allowing us to simplify the code.
Modifying the current code ...
find_replace_in_list(){
# $1 = list (array) : read from
# $2 = find item (string)
# $3 = replace list (array) : read from
# $4 = newlist (array) : write to
# $5 = number of times to match-n-replace (optional; default=1)
local -n _list=$1
local ptn=$2
local -n _replace_list=$3
local -n _newlist=$4
local match_count=${5:-1} # OP can add more logic to validate $5 is a positive integer
_newlist=()
for item in "${_list[#]}"
do
if [[ "${item}" = "${ptn}" && "${match_count}" -gt 0 ]]
then
_newlist+=( "${_replace_list[#]}" )
(( match_count-- ))
else
_newlist+=( "${item}" )
fi
done
}
Test #1 (one match & replacement):
list=('a' 'b' 'c' 'd')
find='b'
replace_list=('b1' 'b2' 'b3')
newlist=()
find_replace_in_list list "$find" replace_list newlist
typeset -p newlist
This generates:
declare -a newlist=([0]="a" [1]="b1" [2]="b2" [3]="b3" [4]="c" [5]="d")
Test #2 (no matches found):
list=('a' 'b' 'c' 'd')
find='z'
replace_list=('z1' 'z2' 'z3')
newlist=()
find_replace_in_list list "$find" replace_list newlist
typeset -p newlist
This generates:
declare -a newlist=([0]="a" [1]="b" [2]="c" [3]="d")
Test #3a (one match & replacement):
list=('a' 'b' 'c' 'd' 'c')
find='c'
replace_list=('c7' 'c8' 'c9')
newlist=()
find_replace_in_list list "$find" replace_list newlist
typeset -p newlist
This generates:
declare -a newlist=([0]="a" [1]="b" [2]="c7" [3]="c8" [4]="c9" [5]="d" [6]="c")
Test #3b (allow up to 999 match & replacements):
list=('a' 'b' 'c' 'd' 'c')
find='c'
replace_list=('c7' 'c8' 'c9')
newlist=()
find_replace_in_list list "$find" replace_list newlist 999
typeset -p newlist
This generates:
declare -a newlist=([0]="a" [1]="b" [2]="c7" [3]="c8" [4]="c9" [5]="d" [6]="c7" [7]="c8" [8]="c9")
You can return the array as stdout and readarray the result but it forks a sub-shell:
#!/usr/bin/env bash
# Finds first item in array that matches find item, and replaces it with 1 or more array elements.
# Preserves sort order of original array.
find_replace_in_list() {
# $1 = list
# $2 = find item string
# $3 = replace array.
local -n _list=$1
local -n _replace_list=$3
for i in "${!_list[#]}"; do # Iterate over indices.
if [ "${_list[$i]}" == "$2" ]; then
# Insert the replace array starting at indice.
local p=$((i + 1)) # Get indice just after match.
local new_array=("${_list[#]:0:i}" "${_replace_list[#]}" "${_list[#]:p}")
printf '%s\0' "${new_array[#]}"
return # Break out of loop. Only replace first match.
fi
done
# Nothing found. Return orginal array.
echo "${_list[#]}"
}
# shellcheck disable=SC2034 # nameref use
list=('a' 'b' 'c' 'd')
find='b'
# shellcheck disable=SC2034 # nameref use
replace_list=('b1' 'b2' 'b3')
readarray -td '' test < <(find_replace_in_list list "$find" replace_list)
printf 'RESULT: %s\n' "${test[*]}"
Or you can return the new array as reference:
#!/usr/bin/env bash
# Finds first item in array that matches find item, and replaces it with 1 or more array elements.
# Preserves sort order of original array.
find_replace_in_list() {
# $1 = list nameref
# $2 = find item string
# $3 = replace array.nameref
# ?$4 = optional new array nameref
local -n _list=$1
local -n _replace_list=$3
if [ $# -eq 4 ]; then
local -n _new_list=$4
else
local -a _new_list=()
fi
for i in "${!_list[#]}"; do # Iterate over indices.
if [ "${_list[$i]}" == "$2" ]; then
# Insert the replace array starting at indice.
local p=$((i + 1)) # Get indice just after match.
_new_list=("${_list[#]:0:i}" "${_replace_list[#]}" "${_list[#]:p}")
# If no new array reference, print null delimited
[ $# -lt 4 ] && printf '%s\0' "${_new_list[#]}"
return # Break out of loop. Only replace first match.
fi
done
# Nothing found. Return orginal array.
echo "${_list[#]}"
}
# shellcheck disable=SC2034 # nameref use
list=('a' 'b' 'c' 'd')
find='b'
# shellcheck disable=SC2034 # nameref use
replace_list=('b1' 'b2' 'b3')
find_replace_in_list list "$find" replace_list test
# shellcheck disable=SC2154 # nameref use
printf 'RESULT: %s\n' "${test[*]}"

Arrays in Bash Shell

I want to write a shell script to get the following output:
$ Enter String: a2b3
aabbb
I tried using for loops and arrays, but the loop count messes with the array index and leaves null elements within the array, making it impossible to print out the array as required.
The script used:
echo "Enter your alphanumeric string: "
read a
n=${#a}
for (( i=0;i<n;i++ ))
do
string[i]=${a:i:1}
if [[ ${string[i]} =~ [a-zA-Z] ]]
then
alpha[i]=${string[i]}
elif [[ ${string[i]} =~ [0-9] ]]
then
if [[ ${string[i+1]} =~ [0-9] ]]
then
num[i]=${string[i]}${string[i+1]}
elif ! [[ ${string[i+1]} =~ [0-9] ]]
then
num[i]=${string[i]}
fi
fi
done
n=${#num[*]}
for (( i=0;i<n;i++ ))
do
echo num[$i] = ${num[i]}
done
n=${#alpha[*]}
for (( i=0;i<n;i++ ))
do
echo alpha[$i] = ${alpha[i]}
done
The output I get for the same:
$ sh Q1.sh
Enter your alphanumeric string:
a6b3
num[0] =
num[1] = 6
alpha[0] = a
alpha[1] =
A better way to append an element to an array is with array+=(...). Then you don't have to worry about having the correct index on the left-hand side.
alpha+=("${string[i]}")
num+=("${string[i]}" "${string[i+1]}")

Bash, how to iterate through an array once & append to end of line?

I am tasked with writing a script that analyzes code and attaches a comment with #Loopn or #Selection n that corresponds with the correct statements.
echo "enter full file name: "
read file
getArray(){
arr=()
while IFS= read -r line
do
arr+=("$line")
done < "$1"
}
getArray $file
echo "What file looks like before editing"
printf "%s\n" "${arr[#]}" #Test function to see if array works (it does)
#Declare variables
x=1
y=1
#Start main loop
for (( i=0; i<${#arr[#]}; i++ ));
do
if [[ "${arr[$i]}" == "while" ]] || [[ "${arr[$i]}" == "until" ]]
then sed -i 's/$/ #Loop'$x'/' $file && let "x++"
continue
elif [[ "${arr[$i]}" == "for" ]]
then sed -i 's/$/ #Loop'$x'/' $file && let "x++"
continue
elif [[ "${arr[$i]}" == "break" ]] || [[ "${arr[$i]}" == "done" ]]
then sed -i 's/$/ #Loop'$x'/' $file && let "x--"
continue
elif [[ "${arr[$i]}" == "if" ]] || [[ "${arr[$i]}" == "case" ]]
then sed -i 's/$/ #Selection'$y'/' $file && let "y++"
continue
elif [[ "${arr[$i]}" == "fi" ]] || [[ "${arr[$i]}" == "esac" ]]
then sed -i 's/$/ #Selection'$y'/' $file && let "y--"
continue
else
continue
fi
done < $file
Obviously I'm a newbie in bash, and my loop logic/language usage might be a bit wonky. Can anyone help? Right now the output makes it seem like I am iterating through the array more than once and Sed appends additional text per line.
In case it wasn't clear: each array element is a line of strings; if an array element contains while || for || until then it adds a #loop n and with each of the corresponding break or done, it adds the same #loop n. And likewise for if and case and fi esac except it adds #selection n.
Sample Input:
Before
Final=$(date -d "2016-12-15 14:00" "+%j")
while true ; do
Today=$(date "+%j")
Days=$((Final - Today))
if (( Days >= 14 )) ; then
echo party
elif (( Days >= 2 )) ; then
echo study
elif (( Days == 1 )) ; then
for Count in 1 2 3
do
echo panic
done
else
break
fi
sleep 8h
done
Expected Output:
After
Final=$(date -d "2016-12-15 14:00" "+%j")
while true ; do # loop 1
Today=$(date "+%j")
Days=$((Final - Today))
if (( Days >= 14 )) ; then # selection 1
echo party
elif (( Days >= 2 )) ; then
echo study
elif (( Days == 1 )) ; then
for Count in 1 2 3 # loop 2
do
echo panic
done # loop 2
else
break
fi # selection 1
sleep 8h
done # loop 1
Right now the output makes it seem like I am iterating through the array more than once and Sed appends additional text per line.
This is because the comment to be attached to one line is appended to each line of the file, since no line number is specified for the sed substitute commands in your script. There surely are more efficient solutions, but prepending the corresponding line number is sufficient.
Though your script is quite close to working, two more problems have to be addressed. One is that the == expressions you use to test for the keywords match only if the whole line contains nothing else than the keyword (not even leading space); to allow for indentation, =~ with an appropriate regular expression is useful. The other problem is the counting of the nesting depth (including the simple, but special case of break, where the depth remains unchanged); this seems more easy if we start at depth 0. So, your main loop could be:
x=0
y=0
#Start main loop
for (( i=0; i<${#arr[#]}; i++ ))
do let l=1+i # line numbers start at 1
if [[ "${arr[$i]}" =~ ^[$IFS]*(while|until|for) ]]
then sed -i $l"s/$/ #Loop$((++x))/" $file
elif [[ "${arr[$i]}" =~ ^[$IFS]*break ]]
then sed -i $l"s/$/ #Loop$x/" $file # no x-- here!
elif [[ "${arr[$i]}" =~ ^[$IFS]*done ]]
then sed -i $l"s/$/ #Loop$((x--))/" $file
elif [[ "${arr[$i]}" =~ ^[$IFS]*(if|case) ]]
then sed -i $l"s/$/ #Selection$((++y))/" $file
elif [[ "${arr[$i]}" =~ ^[$IFS]*(fi|esac) ]]
then sed -i $l"s/$/ #Selection$((y--))/" $file
fi
done <$file
You can test this;
echo "enter full file name: "
read file
getArray(){
arr=()
while IFS= read -r line
do
arr+=("$line")
done < "$1"
}
getArray $file
echo "What file looks like before editing"
printf "%s\n" "${arr[#]}" #Test function to see if array works (it does)
#Declare variables
x=1
y=1
#Start main loop
for (( i=0; i<${#arr[#]}; i++ ));
do
#echo $i "====" ${arr[$i]}
#echo "-------"
#echo "x"$x
if [[ "${arr[$i]}" == "while"* ]] || [[ "${arr[$i]}" == "until" ]]
then
sed -i "$((i+1))s/$/ #Loop $x/" $file
let "x++"
continue;
fi
if [[ "${arr[$i]}" == *"for"* ]]
then sed -i "$((i+1))s/$/ #Loop'$x'/" $file && let "x++"
continue
fi
if [[ "${arr[$i]}" == *"done"* ]]
then sed -i "$((i+1))s/$/ #Loop'$((x-1))'/" $file
continue
fi
if [[ "${arr[$i]}" == *"if"* ]] || [[ "${arr[$i]}" == *"case"* ]]
then
if [[ "${arr[$i]}" != *"elif"* ]]
then
sed -i "$((i+1))s/$/ #Selection'$y'/" $file && let "y++"
fi
continue
fi
if [[ "${arr[$i]}" == *"fi"* ]] || [[ "${arr[$i]}" == *"esac"* ]]
then sed -i "$((i+1))s/$/ #Selection'$((y-1))'/" $file
continue
fi
done < $file

Can't empty a bash array (bash --version 3.2.25)

My bash array never empty itself.
I am using bash 3.2.25.
I tried using the folowing methods:
declare -a array
# fill array...
# 1
array=()
# 2
empty_array=()
array=( "${empty_array[#]}" )
# 3
unset array
My array never get emptied, am I doing something wrong?
Full code as requested :
declare -a array
function get_array() {
#active_tills=()
#unset active_tills
#active_tills=( "${active_tills[#]}" )
# fill array
while read -r line || [[ -n "$line" ]]; do
line=$(echo "$line" | cut -d' ' -f1)
if [ -n "$line" ] ; then
to_add+="$line "
fi
done < "$request_tmp"
array=($(echo $to_add))
return 0
}
Then
get_array
for host in "${array[#]}"; do
echo "=> $host"
done
# 1
# 2
# 3
get_array
for host in "${array[#]}"; do
echo "=> $host"
done
# 1
# 2
# 3
# 1
# 2
# 3
to_add is also a global variable, and you don't reset its value before appending to it. However, you don't need it: you can append directly to the array.
declare -a array
function get_array() {
local line rest
array=()
while read -r line rest || [[ -n "$line" ]]; do
if [ -n "$line" ] ; then
array+=("$line")
fi
done < "$request_tmp"
return 0
}
As an aside, if you can guarantee that the input file ends with a newline (as is required of a proper text file in POSIX), you don't need the || [[ -n $line ]] hack in your while loop.

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

Resources