3

I am working on learning Bash scripting but I am struggling with this problem. Given a bunch of lines from STDIN, sort them first by the length of the line in increasing order. Then, if there are any lines with the same number of characters, sort them by the number of nonblank characters contained in the lines (also in increasing order).

I've tried this a couple of different ways but I usually get caught up in some of the idiosyncrasies of Bash.

Here's what I've got so far:

#!/bin/bash

sorted=()
while IFS='' read -r line; do
    length=${#line}
    if [[ ${sorted[$length]} == "" ]] ; then
        sorted[$length]="$line"
    else
        #non unique length
        #sorted[$length]="${sorted[$length]}\n$line"
        IFS=$'\n' arr=("${sorted[$length]}")
        arr+=("$line")

        spaces=()

        for ((i=0 ; i < ${#arr[@]} ; ++i )) ; do
            spaces[$i]=$(echo "${arr[$i]}" | sed "s: : \n:g" | grep -c " ")
        done

        arr_sorted=()

        for ((i =0 ; i < ${#spaces[@]} ; i++ )) ; do
                for ((j=0 ; j < ${#arr[@]} ; i++ )) ; do

                        this_line_length=$(echo "${arr[$j]}" | sed "s: : \n:g" | grep -c " ")
                        if [[ "$this_line_length" == "${spaces[$i]}" ]] ; then
                            arr_sorted+=("${arr[$j]}")
                            unset arr[$j]
                        fi
                done
        done


    sorted[$length]="${arr_sorted[@]}"


    fi
done

I'm going to go ahead and guess this is nowhere near the best way to do it. I thought I would try to implement everything without relying too heavily on bash builtins but now it seems pretty pointless.

  • Please clarify what "increasing order" means... i.e. longest line first, or longest line last. – agc May 11 '17 at 6:35
  • 2
    Cross-posted using a different account? askubuntu.com/questions/913199/… – muru May 11 '17 at 7:17
  • coud you post sample of input -> output? – F. Hauri May 11 '17 at 7:23
  • @muru, Same class, different students perhaps. The answers for this Q seem a bit sharper. – agc May 11 '17 at 7:44
4

If you're allowed to use evil external contraptions such as sort and cut:

#! /bin/bash
while IFS= read -r line; do
    squeezed=$( tr -d '[:blank:]' <<<"$line" )
    printf '%d\t%d\t%s\n' ${#line} ${#squeezed} "$line"
done | sort -n -k 1 -k 2 | cut -f 3-

Edit: Since everybody's doing it, here's a solution with perl:

perl -e 'print sort { length $a <=> length $b || $a =~ y/ \t//c <=> $b =~ y/ \t//c } <>'
  • Our answers are nearly the same; yours is better for using tab separators, and IFS= read -r. (Descriptive variables are a matter of taste.) – agc May 11 '17 at 6:42
  • @agc Yeah, except mine was posted first. Not that I personally care, but there are a few people here that like to make a fuss about these things. shrug – Satō Katsura May 11 '17 at 6:49
  • Being second, I'll delete mine if anybody cares. It's not that... it's that they're nearly the same answer. I've been slower by minutes before, but never with virtually the same code. Details differ, but not the tools or method. – agc May 11 '17 at 6:54
3

Using the same principles as the others (get the line length, with and without whitespace characters, sort on those, and then remove them), but with awk:

awk '{NC = length(gensub(/[[:space:]]/, "", "g")); print length, NC, $0}' file |
  sort -nk1,2 |
  sed -r 's/^([0-9]+ ){2}//'

gensub(/[[:space:]]/, "", "g") deletes all whitespace characters in the line, and then we get the length of the remaining string

Using the question's text up to the code block, folded to 80 characters wide:

$ awk '{NC = length(gensub(/[[:space:]]/, "", "g")); print length, NC, $0}' foo | sort -nk1,2 | sed -r 's/^([0-9]+ ){2}//'


 increasing order).
Here's what I've got so far:
f the idiosyncrasies of bash.
iven a bunch of lines from STDIN, sort them first by the length of the line in i
I've tried this a couple of different ways but I usually get caught up in some o
, sort them by the number of nonblank characters contained in the lines (also in
I am working on learning bash scripting but I am struggling with this problem. G
ncreasing order. Then, if there are any lines with the same number of characters
  • You could avoid using the gensub GNUism with gsub. gsub also returns the number of substitutions made therefore you could shed the call to length and replace -nk1,2 in the sort with -k1,1n -k2,2nr – iruvar May 11 '17 at 14:02
  • @iruvar: but give gsub a different var that is a copy of $0 (gensub does not modify its var but gsub and sub do) – dave_thompson_085 May 12 '17 at 7:25
  • @dave_thompson_085, Drat! yes, thanks – iruvar May 12 '17 at 13:51
3

Pure

sortByLength () 
{ 
    local -a sorted=() sort2
    local line sline sline2 pointer
    while IFS= read -r line; do
        sorted[${#line}]+="$line"
    done
    for pointer in ${!sorted[@]}
    do
        #  ((pointer)) || echo 0: # This will trace empty lines
        sort2=()
        line="${sorted[pointer]}"
        while [ "$line" ]; do
            sline=${line:0:pointer}
            line=${line:pointer}
            sline2=${sline// }
            sort2[${#sline2}]+=${sline}$'\n'
        done
        # echo $pointer:   # This will trace lines length
        printf "%s" "${sort2[@]}"
    done
}

This may be a lot quicker as there is no forks!

  • Hmm, printf "a b c\nabcde\nabcdefg\na\nabcd\n" | sortByLength outputs in the 4th line a leading space that shouldn't be there. Changing sline2=${sline// } to sline2=${sline//} seems to fix it, at least with that input. – agc May 11 '17 at 8:00
  • @agc Thanks, there was a little bug; corrected! – F. Hauri May 11 '17 at 8:02
2

I couldn't resist to add an sed solution:

sed 'h;s/.*/0:0;0123456789+/;G;:count
s/\(.\)\(;.*\1\)\(.\)\(.*\n\)[^[:space:]]/\3\2\3\4x/
s/\(.\)\(:.*\1\)\(.\)\(.*\n\)./\3\2\3\4/;:overflow
s/^+/10/;s/:+/:10/;s/\(.\)+\(.*\1\)\(.\)\(.*\n\)/\30\2\3\4/;t overflow
/\n./b count
G;s/;.*\n/:/' file|sort -t: -n -k 1 -k 2|cut -d: -f 3-

The sed script counts the chars and nonblanks and places them at the beginning of the line, sort and cut are straight forward. And please don't tell me that this is nonsense. For me, it's fun. (-:

2

Function:

sortlen() { while read x ; do \
              y=`tr -d '[:blank:]' <<< "$x"` ; echo ${#x} ${#y} "$x" ; \
            done | sort -k 1g,2 -k 2g,3 | cut -d' ' -f3-; }

Test:

printf "a b c\nabcde\nabcdefg\na\nabcd\n" | sortlen

Output:

a
abcd
a b c
abcde
abcdefg
  • I expect most people would consider 1, 4, 5, 5, 7 to be ascending order.    :-)    ⁠ – G-Man May 12 '17 at 0:47
  • @G-Man Oh, alright... – agc May 12 '17 at 0:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.