Home > Net >  How to sort an array by dates substring in bash?
How to sort an array by dates substring in bash?

Time:06-10

In bash I have the following array with data:

(
"01.05.2022 01:01:35 [ERROR] test
 error message"
"01.05.2022 00:00:55 [SUCCESS] test
 success message"
"02.05.2022 00:00:35 [ERROR] test
 error message"
"01.05.2022 00:05:00 [WARNING] test
 warning message"
)

Expected Result:

(
"01.05.2022 00:00:55 [SUCCESS] test
 success message"
"01.05.2022 00:05:00 [WARNING] test
 warning message"
"01.05.2022 01:01:35 [ERROR] test
 error message"
"02.05.2022 00:00:35 [ERROR] test
 error message"
)

How can I sort a given array by dates only directly in bash and save it to a file?

CodePudding user response:

How can I sort a given array by dates only directly in bash and save it to a file?

I’m assuming that “only directly in Bash” implies that additional processes and external tools such as sort are disallowed.

A solution in pure Bash can be based around the fact that indexed “arrays” in Bash are sparse and automatically sorted by (possibly sparse) indices.

That said, the problem translates into a choice of sparse indices such that their sorting order corresponds to the sorting order of dates. For example, one can pick indices in which four highest decimal orders represent years, lowest two decimal orders represent seconds and everything else is in between, rather obviously:

sort_my_special_array() {
  local -n array="$1"  # passing array by reference
  local -a reindexed   # sparse array indexed by YYYYMMDDhhmmss
  local -i idx         # index in the above
  local a year month day hour minute second drop
  for a in "${array[@]}"; do
    IFS='.: ' read day month year hour minute second drop <<< "$a"
    idx="10#${year}${month}${day}${hour}${minute}${second}"
    reindexed[idx]="$a"
  done
  array=("${reindexed[@]}")
}

A simple test for the code above:

my_bash_array=(
    '01.05.2022 01:01:35 [ERROR] test
 error message'
    '01.05.2022 00:00:55 [SUCCESS] test
 success message'
    '02.05.2022 00:00:35 [ERROR] test
 error message'
    '01.05.2022 00:05:00 [WARNING] test
 warning message'
)

echo -e 'Before:'
(IFS=$'\n'; echo "${my_bash_array[*]@Q}";)

sort_my_special_array my_bash_array  # Here!

echo -e '\nAfter:'
(IFS=$'\n'; echo "${my_bash_array[*]@Q}";)

The output of the test is (…drumroll…):

Before:
$'01.05.2022 01:01:35 [ERROR] test\n error message'
$'01.05.2022 00:00:55 [SUCCESS] test\n success message'
$'02.05.2022 00:00:35 [ERROR] test\n error message'
$'01.05.2022 00:05:00 [WARNING] test\n warning message'

After:
$'01.05.2022 00:00:55 [SUCCESS] test\n success message'
$'01.05.2022 00:05:00 [WARNING] test\n warning message'
$'01.05.2022 01:01:35 [ERROR] test\n error message'
$'02.05.2022 00:00:35 [ERROR] test\n error message'

To store a Bash array into a file in a parsable format, you can use the A modifier:

echo "${my_bash_array[@]@A}" > /path/to/some/file

This↑↑↑ yields the following output:

declare -a my_bash_array=([0]=$'01.05.2022 00:00:55 [SUCCESS] test\n success message' [1]=$'01.05.2022 00:05:00 [WARNING] test\n warning message' [2]=$'01.05.2022 01:01:35 [ERROR] test\n error message' [3]=$'02.05.2022 00:00:35 [ERROR] test\n error message')

CodePudding user response:

Your dates appear at the start of each string and are in a format that allows to sort them alphabetically (i.e. without the need of conversion to EPOCH time).
update: I was wrong. As @markp-fuso noticed, the dates are not in sortable format so you'll have to decorerate | sort | undecorate them

So, given the bash array:

my_bash_array=(
"01.05.2022 01:01:35 [ERROR] test
 error message"
"01.05.2022 00:00:55 [SUCCESS] test
 success message"
"02.05.2022 00:00:35 [ERROR] test
 error message"
"01.05.2022 00:05:00 [WARNING] test
 warning message"
)

Here's a simple way to sort this array by date (that is, if your sort command supports the -z option):

# for bash >= 4.3:
readarray -d '' my_bash_array < <(
    printf '%s\0' "${my_bash_array[@]}" |
    sed -z -E 's/^((..)\.(..)\.(....) (..):(..):(..) )/\4\3\2\5\6\7 \1/' |
    sort -z -n -k1,1 |
    sed -z 's/^[^ ]* //'
)
# for older bash:
i=0
while IFS='' read -r -d '' element
do
   my_bash_array[i  ]=$element
done < <(
    printf '%s\0' "${my_bash_array[@]}" |
    sed -z -nE 's/^((..)\.(..)\.(....) (..):(..):(..) )/\4\3\2\5\6\7 \1/p' |
    sort -z -n -k1,1 |
    sed -z 's/^[^ ]* //'
)
  • Related