1

I have a large set of variable initializations that I think could be greatly reduced. The file below being parsed:

                   -------------------- ACL Stats Per Interface ----------------------
                   Entries         Packets                         Dropped  
                            Recent      Total  PerMax      Recent    Total    PerMax 
Slot 0 /Port 0
Trusted              1         196    1311578     386           0          0       0
Untrusted            3          20  217217953  852794           0          0       0

... and the snippet of code in the script are below:

expect_results="stats.txt"

acl_stats_per_interface_trusted_entries_s0_p0=`grep -A 2 "Slot 0 /Port 0" $expect_results | grep "Trusted" | awk '{print $2}' `
acl_stats_per_interface_trusted_recent_packets_s0_p0=`grep -A 2 "Slot 0 /Port 0" $expect_results | grep "Trusted" | awk '{print $3}' `
acl_stats_per_interface_trusted_total_packets_s0_p0=`grep -A 2 "Slot 0 /Port 0" $expect_results | grep "Trusted" | awk '{print $4}' `
acl_stats_per_interface_trusted_permax_packets_s0_p0=`grep -A 2 "Slot 0 /Port 0" $expect_results | grep "Trusted" | awk '{print $5}' `
acl_stats_per_interface_trusted_dropped_recent_s0_p0=`grep -A 2 "Slot 0 /Port 0" $expect_results | grep "Trusted" | awk '{print $6}' `
acl_stats_per_interface_trusted_dropped_total_s0_p0=`grep -A 2 "Slot 0 /Port 0" $expect_results | grep "Trusted" | awk '{print $7}' `
acl_stats_per_interface_trusted_dropped_permax_s0_p0=`grep -A 2 "Slot 0 /Port 0" $expect_results | grep "Trusted" | awk '{print $8}' `

Rather than grepping the same file 7 times, is there a way to grep this a single time and set these variables to each individual awk output?

5
  • I believe the formatting of your input got messed up, can you fix it?
    – jesse_b
    Commented Dec 9, 2020 at 14:50
  • 1
    Also, using variables 50+ characters long is not attractive for people to review your code (maybe it is ok for Java).
    – thanasisp
    Commented Dec 9, 2020 at 15:00
  • 1
    You could consider outputting the whole line, and reading it into a shell indexed array variable (bash read -a acl_stats_per_interface_trusted_s0_p0) rather than individual variables. Also remember that awk can do regex matching - there's rarely any good reason to chain grep and awk. Commented Dec 9, 2020 at 15:11
  • This is a bash script that is used with Prometheus' Textfile Collector. These variables are eventually fed into a large echo statement and jammed into a .prom file, which is later read by Prometheus.
    – Kahn
    Commented Dec 9, 2020 at 15:37
  • @steeldriver I think reading it into an array would be much easier, and then just echoing individual elements of the array when I need them, instead of having so many variables.
    – Kahn
    Commented Dec 9, 2020 at 15:49

3 Answers 3

2

FWIW I'd just use awk to extract the text as well as the values from the input file and print it's output to the .prom file without having to manually create a bunch of shell variables, etc.:

$ cat tst.awk
$1 ~ /^-+$/ {
    #    -------------------- ACL Stats Per Interface ----------------------
    gsub(/^[[:space:]]*-+[[:space:]]+|[[:space:]]+-+[[:space:]]*$/,"")
    fileHdr = $0
    next
}

/^[[:space:]]/ {
    if (NF == 3) {
        #    Entries         Packets                         Dropped
        colName[1] = $1
        for (i=2; i<=NF; i++) {
            colHdr[i] = $i
        }
    }
    else {
        #        Recent      Total  PerMax      Recent    Total    PerMax
        for (i=1; i<=3; i++) {
            colName[i+1] = $i "_" colHdr[2]
        }
        for (; i<=NF; i++) {
            colName[i+1] = $i "_" colHdr[3]
        }
    }
    next
}

/^Slot/ {
    # Slot 0 /Port 0
    slot = $2
    port = $NF
    next
}

/^[[:alpha:]]/ {
    # Trusted              1         196    1311578     386           0          0       0
    # Untrusted            3          20  217217953  852794           0          0       0
    rowName = $1
    for (i=2; i<=NF; i++) {
        out = tolower(fileHdr "_" rowName "_" colName[i-1] "_s" slot "_p" port) "=" $i
        gsub(/[[:space:]]+/,"_",out)
        print out
    }
}

$ awk -f tst.awk file
acl_stats_per_interface_trusted_entries_s0_p0=1
acl_stats_per_interface_trusted_recent_packets_s0_p0=196
acl_stats_per_interface_trusted_total_packets_s0_p0=1311578
acl_stats_per_interface_trusted_permax_packets_s0_p0=386
acl_stats_per_interface_trusted_recent_dropped_s0_p0=0
acl_stats_per_interface_trusted_total_dropped_s0_p0=0
acl_stats_per_interface_trusted_permax_dropped_s0_p0=0
acl_stats_per_interface_untrusted_entries_s0_p0=3
acl_stats_per_interface_untrusted_recent_packets_s0_p0=20
acl_stats_per_interface_untrusted_total_packets_s0_p0=217217953
acl_stats_per_interface_untrusted_permax_packets_s0_p0=852794
acl_stats_per_interface_untrusted_recent_dropped_s0_p0=0
acl_stats_per_interface_untrusted_total_dropped_s0_p0=0
acl_stats_per_interface_untrusted_permax_dropped_s0_p0=0

If you have tools generating other files in the same format as the input file you posted then the above awk script should work for them too as-is, no need to create specialized shell scripts for them. For input files in other formats you can tweak the script to use the same approach.

1

You can use the read builtin to read a line and assign multiple values to variables.

Example with error handling and shorter variable names:

if read dummy var1 var2 var3 var4 var5 var6 var7 extra
then
  echo OK
else
  echo EOF
fi < <(grep -A 2 "Slot 0 /Port 0" $expect_results | grep "Trusted")
if [ -n "$extra" ]
then
  echo too many values
fi
if [ -z "$var7" ]
then
  echo not enough values
fi
echo vars: $var1 $var2 $var3 $var4 $var5 $var6 $var7

or shorter without error handling

read dummy var1 var2 var3 var4 var5 var6 var7 extra < <(grep -A 2 "Slot 0 /Port 0" $expect_results | grep "Trusted")
echo vars: $var1 $var2 $var3 $var4 $var5 $var6 $var7             

The code expects that your input consists of a single line that contains simple values separated by whitespace, without embedded spaces, special characters, quoting...

To understand the error handling for EOF or wrong number of values, read the documentation of the read builtin, e.g. here https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html

The specific error handling for EOF doesn't seem to be necessary. All variables are empty in this case (on my system), but I didn't find a specification of this behavior in the documentation of the read builtin. EOF would occur when no matching line was found in the input.

1

I have opted to read the output of my grep statement into an array, and access these elements individually, rather than instantiate so many variables.

acl_stats_trusted=( $(grep "Trusted" "$expect_results") )
echo "${acl_stats_trusted[1]}" # Outputs 1
echo "${acl_stats_trusted[3]}" # Outputs 1311578

This is much more concise and clear to any user.

7
  • 1
    Thanks @EdMorton I think I was originally concerned with something else in the file, but didn't stop to fix up excessive statements like mine. I've amended.
    – Kahn
    Commented Dec 9, 2020 at 16:04
  • 1
    You're welcome. Are you going to do the same for Untrusted and/or other types of metrics from your input file? If so there's a better solution.
    – Ed Morton
    Commented Dec 9, 2020 at 16:07
  • Yes @EdMorton there are Untrusted fields as well! What would you suggest?
    – Kahn
    Commented Dec 9, 2020 at 16:15
  • 2
    I posted an answer. There might be better (more concise and/or robust for handling different input) ways to parse the input file if I knew more about it's format but what I posted will work for a format such as in your question.
    – Ed Morton
    Commented Dec 9, 2020 at 16:28
  • 1
    Yup. Love Prometheus but there are some really funky edge cases you might have to craft, say, a textfile collector around!
    – Kahn
    Commented Dec 9, 2020 at 16:37

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .