0

Can you please help me to understand why this piece of code is not working. I am meant to write insertion sort and apply it to the file to sort its elements. However, it even fails to print the k-th elements in the while construct. I am using Windows and working in Git Bash.

j=0
for line in $(cat $1)
do
        elements[$j]="$line"
        j=$((j+1))
done


for i in $(seq "${#elements[@]}");
do
        key=${elements[i]}
        echo index: $i Key: $key
        k=$((i-1))
        while [ $k -gt 0 ] && [ ${elements[k]} > $key ]; do
                echo ${elements[k]} $key
                elements[$((k+1))]=${elements[k]}
                k=$((k-1))
        done
        elements[$((k+1))]=$key
done

The file I am reading as array elements is a .txt file with the following information:

3
1
2
3
2


4
  • @KamilCuk I did not know about readarray. It is an exercise to implement insertion sort. I made this changes and now it does not print anything. Commented Sep 14, 2019 at 0:03
  • 1
    The first loop starts with variable j uninitialized and thus array elements is not properly assigned. On line 13 symbol ">" is treated by bash as output redirection not as comparison operator. Commented Sep 14, 2019 at 2:04
  • @YuriGinsburg I forgot to type j but I had it originally. I will edit it Commented Sep 14, 2019 at 2:22
  • Don't use for and cat together like this; see Bash FAQ 001. Commented Sep 14, 2019 at 12:44

1 Answer 1

3

You have a significant number of basic errors in your script. Before posting on StackOverflow, you will always want to use ShellCheck to fix all basic errors.

While you generally do not use hand-written sorting routines in shell programming (utilities like sort are used), for a learning exercise there is nothing wrong with doing so. As mentioned in the comments, to fill an array from the lines of an input file, use readarray or use mapfile (both are equivalent). For example if providing the filename as the first argument to your script, you can fill array from the filename given (or read from stdin by default) using:

infile="${1:-/dev/stdin}"           ## set filename or read from stdin
[ -r "$infile" ] || {               ## validate file readable
    printf "error: invalid filename.\n" >&2
    exit 1
}

readarray -t array < "$infile"      ## readarray from file

By using $(seq "${#elements[@]}") you are creating an indexing nightmare. In bash valid array indexes are from 0 -> n-1, however by using $(seq "${#elements[@]}") you are providing numbers 1 -> n (which you will have to subtract 1 from to map to valid indexes. Instead you are better served by using a C-style for loop and staying with indexes from the array itself with, for example:

for ((i = 0; i < ${#elements[@]}; i++)); do
    ...
done

Next, as noted above in the comments your attempted comparison with [ ${elements[k]} > $key ] fails because within [ ... ] the > operates as a redirection instead of a comparison. If you are using the arithmetic operator ((....)) for the test, then > provides the greater-than comparison.

Finally, if you are going to the trouble of writing an insertion sort for an array in bash, you may as well make it a function so it is reusable. The only caveat there is bash doesn't provide a direct mechanism for passing an array to a function. (which is another reason you generally do not find hand-rolled sorting functions in bash)

You can however, for the purpose of this learning exercise, expand the array (e.g. "${elements[@]}" ) and then capture the elements in a local array within the function for sorting and then output the elements for reading back into the original array. For example, you could write an inssort() function taking advantage of that similar to:

## inssort() expects expanded array as positional parameters
#  outputs sorted array to stdout
inssort() {
    [ $# -gt 1 ] || {   ## validate at least 2 arguments given
        printf "error: cannot sort single value.\n" >&2
        return 1
    }
    local arr=( "$@" )  ## local array to insert values in sort order
    local n=${#arr[@]}  ## local values to use in sort
    local i=0
    local j=0
    local tmp=0

    ## insertion sort arr
    for ((i = 0; i < n; i++)); do
        for ((j = i-1; j >= 0; j--)); do
            if (( ${arr[j]} > ${arr[$((j+1))]} )); then
                tmp=${arr[j]}
                arr[j]=${arr[$((j+1))]}
                arr[$((j+1))]=$tmp
            else
                break
            fi
        done
    done

    echo "${arr[@]}"    ## output sorted array
}

An then back in your script your could call and capture the results similar to:

array=( $(inssort "${array[@]}") )  ## insertion sort array

I can't impress on you enough, for sorting in an actual shell script, rely on the utilities that have been written to handle sorting, like sort. But for completion of this example, you could put your script together similar to the following:

#!/bin/bash

## inssort() expects expanded array as positional parameters
#  outputs sorted array to stdout
inssort() {
    [ $# -gt 1 ] || {   ## validate at least 2 arguments given
        printf "error: cannot sort single value.\n" >&2
        return 1
    }
    local arr=( "$@" )  ## local array to insert values in sort order
    local n=${#arr[@]}  ## local values to use in sort
    local i=0
    local j=0
    local tmp=0

    ## insertion sort arr
    for ((i = 0; i < n; i++)); do
        for ((j = i-1; j >= 0; j--)); do
            if (( ${arr[j]} > ${arr[$((j+1))]} )); then
                tmp=${arr[j]}
                arr[j]=${arr[$((j+1))]}
                arr[$((j+1))]=$tmp
            else
                break
            fi
        done
    done

    echo "${arr[@]}"    ## output sorted array
}

infile="${1:-/dev/stdin}"           ## set filename or read from stdin
[ -r "$infile" ] || {               ## validate file readable
    printf "error: invalid filename.\n" >&2
    exit 1
}

readarray -t array < "$infile"      ## readarray from file
echo "array  : ${array[*]}"         ## output original array

array=( $(inssort "${array[@]}") )  ## insertion sort array

echo "sorted : ${array[*]}"         ## output sorted array

Then you can either pass input to the script on stdin or provide a filename to read the array elements from, e.g.

Example Input File

$ cat dat/20int.txt
91
-72
5
-33
-60
-54
95
13
62
1
70
-3
68
-99
89
77
100
8
31
-43

Example Use/Output

$ bash inssortarr.sh dat/20int.txt
array  : 91 -72 5 -33 -60 -54 95 13 62 1 70 -3 68 -99 89 77 100 8 31 -43
sorted : -99 -72 -60 -54 -43 -33 -3 1 5 8 13 31 62 68 70 77 89 91 95 100

Look things over and let me know if you have questions, then after you have finished your exercise, rely on the shell utility sort to handle your sorting needs. It will be much faster and much less error-prone...

Sign up to request clarification or add additional context in comments.

2 Comments

One small nit: unless the file system as an entry for /dev/stdin, the -r won't work (I think). bash only handles /dev/stdin itself if it is part of a redirection.
Also, there's no reason you can't sort a single value; if n == 1, then your loop simply isn't entered and arr is output as-is.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.