1

This shell script gets me the max and min values, but I would like to get those values and the associated students when there are repeats.

Code:

#!/bin/sh
Notamax=0
Notamin=0
alumneMax=''
alumneMin=''
while IFS=";" read alumne nota
do
    (( nota > Notamax )) && Notamax=$nota alumneMax=$alumne
    (( nota < Notamin || Notamin == 0)) && Notamin=$nota alumneMin=$alumne
done <notas.txt
echo "Nota maxima $Notamax ** Alumnos con nota maxima: $alumneMax"
echo "Nota minima $Notamin ** Alumnos con nota minima: $alumneMin"

notas.txt

pepe;5
marcos;7
marta;70
luis;70
ana;5

Actual output

Nota maxima 70 ** Alumnos con nota maxima: marta
Nota minima  5 ** Alumnos con nota minima: pepe

Desired output

Max: marta 70, luis 70
Min: pepe   5, ana   5

How could I achieve it?

markp-fuso
  • 28,790
  • 4
  • 16
  • 36
A. Cedano
  • 557
  • 7
  • 39
  • What is your _actual_ output, and how is it wrong? – Charles Duffy May 14 '23 at 16:37
  • 2
    (...honestly, it looks to me like you could just make `alumneMin` and `alumneMax` be arrays, and you're done -- when you see something that matches the prior min/max value you append to the array, when you see something smaller/larger you clear the array and start from one item again) – Charles Duffy May 14 '23 at 16:38
  • 2
    why are you looking to print `5` as the min when the data shows the min should be `1`? – markp-fuso May 14 '23 at 16:38
  • I'm not sure POSIX shells are guaranteed to understand `((`...`))`. Maybe safer to use `#!/bin/bash` explicitly – jhnc May 14 '23 at 16:46
  • @markp-fuso I took a wrong example, sorry. – A. Cedano May 14 '23 at 16:48
  • the `echo` statements don't match the text and format of the desired output ... ??? – markp-fuso May 14 '23 at 16:49
  • @CharlesDuffy I added actual output. – A. Cedano May 14 '23 at 16:51
  • @markp-fuso I added actual output. I need all the students with max a min values, but I'm having only one. – A. Cedano May 14 '23 at 16:52
  • See [why-is-using-a-shell-loop-to-process-text-considered-bad-practice](https://unix.stackexchange.com/questions/169716/why-is-using-a-shell-loop-to-process-text-considered-bad-practice). – Ed Morton May 14 '23 at 18:29
  • This question really is not related to bash, but to how to devise an algorithm in general. Since you can have several max and min values, basically you need an *array* to store the names. Basically, whenever you encounter an entry which is better than the current maximum, initialize the array to the name of this person having the new maximum. If you encounter an entry which is equal to the current maximum, you add this name to the array. Simulate the algorithm with pen and paper first, before implementing it in bash. – user1934428 May 15 '23 at 07:14
  • @jhnc : POSIX shells understand $((...)), but not ((....)) by itself. However, the OP asks specifically about a bash solution, and therefore it's fine. – user1934428 May 15 '23 at 07:15
  • @EdMorton : From the way this task is defined, I assume that the OP wants to learn programming, and it's OK to do this in bash - in particular since this exercises naturally leads to employing bash arrays. Also, no child process is created inside the loop, and unless the size of the input files goes into the millions of lines, speed should also be acceptable. – user1934428 May 15 '23 at 07:19
  • @user1934428 on my system `/bin/sh` is not bash. – jhnc May 15 '23 at 08:03
  • @jhnc : It usually isn't, but the OP did not say how he is invoking his script. Probably he is usig an explicit `bash SCRIPTNAME`. The #! line still would make sense, because many texteditors use it for determing the syntax schema, and quite some editors would not recognize a i.e. #!/bin/bash. #!/bin/sh is a safe bet; I put it even into files which are supposed to be **sourced** (such as the rc files in my home directory). However I think it's a good idea to tell the OP that he needs to run his script explicitly via `bash ...`, just for the save side. – user1934428 May 15 '23 at 09:05

2 Answers2

5

This is both easier and more versatile with Awk. See also Bash while read loop extremely slow compared to cat, why?

awk -F ';' 'NR==1 { min = max = $2; mina = maxa = $1 " " $2; next }
$2==max { maxa = maxa ", " $1 " " $2 }
$2==min { mina = mina ", " $1 " " $2 } 
$2>max { max = $2; maxa = $1 " " $2 }
$2<min { min = $2; mina = $1 " " $2 }
END { print "Max:", maxa
  print "Min:", mina }' notas.txt

Your output format is somewhat noisy; I would personally prefer a less redundant and more machine-readable display.

The awk tag info page has links to basic learning resources. Learning the fundamentals of the language is quick and easy (give it an hour and you'll already have a good idea).

tripleee
  • 175,061
  • 34
  • 275
  • 318
  • 1
    nitpick - if you put `{ val = $1 " " $2 }` as the first line then you can replace the existing 5 `$1 " " $2`s with `val` (or whatever name you like) so it's easier to change the output in future if necessary. – Ed Morton May 14 '23 at 18:33
  • Yeah, your comment is already better. Depending on what you intend to use it for and what the real names look like, maybe get rid of the commas, or at least the spaces around them; or think about generating a properly standardized format such as YAML. – tripleee May 14 '23 at 19:30
3

If we need to stick with bash ...

Making a few changes to OP's current code:

NotaMax=0
NotaMin=0

while IFS=";" read -r alumne nota
do
    (( nota == NotaMax ))                 &&               alumneMax="${alumneMax}, ${alumne} ${NotaMax}"
    (( nota  > NotaMax ))                 && NotaMax=$nota alumneMax="Max: ${alumne} ${NotaMax}"
    (( nota == NotaMin ))                 &&               alumneMin="${alumneMin}, ${alumne} ${NotaMin}"
    (( nota  < NotaMin || NotaMin == 0 )) && NotaMin=$nota alumneMin="Min: ${alumne} ${NotaMin}"
done < notas.txt

echo "${alumneMax}"
echo "${alumneMin}"

Using a pair of arrays and matching namerefs (namerefs require bash 4.2+):

NotaMax=0
NotaMin=0

while IFS=";" read -r alumne nota
do
    (( nota == NotaMax ))                &&               alumneMax+=("${alumne}")
    (( nota  > NotaMax ))                && NotaMax=$nota alumneMax=("${alumne}")
    (( nota == NotaMin ))                &&               alumneMin+=("${alumne}")
    (( nota  < NotaMin || NotaMin == 0)) && NotaMin=$nota alumneMin=("${alumne}")
done < notas.txt

for op in Max Min
do
    pfx="$op: "
    declare -n oparr="alumne${op}"
    declare -n opval="Nota${op}"

    for alumne in "${oparr[@]}"
    do
        printf "%s%s %s" "$pfx" "$alumne" "$opval"
        pfx=", "
    done
    echo ""
done

Both of these generate:

Max: marta 70, luis 70
Min: pepe 5, ana 5
markp-fuso
  • 28,790
  • 4
  • 16
  • 36
  • The first block of code works fine for Max, but for Min it just prints `pepe 5`. In the secod block of code I have these errors: `test.sh:declare:31: bad option: -n` and `test.sh:declare:32: bad option: -n` – A. Cedano May 14 '23 at 17:18
  • @A.Cedano not sure what the issue is with the 1st block of code (I just cut-n-pasted from the answer directly to my command line and generated the 'correct' output); as for the 2nd block of code .... the error messages indicate you may have an old(er) version of `bash` .... nameref (`declare -n`) support requires `bash 4.2+` .... I've update the answer to include this detail – markp-fuso May 14 '23 at 17:42
  • 1
    The problem in the first block of code was that `ana;5` was at the end of the file and there was no newline after it. When I add a line break, it also shows me `ana, 5`. Regarding the second block of code, my version of bash is `3.2.57`. – A. Cedano May 14 '23 at 17:50
  • For the record, proper text files should always have a newline at the end. There is a separate idiom for reading files which might not have one on the last line, but my recommendation is always to make sure they are proper standard POSIX text files. – tripleee May 14 '23 at 19:33