-1

I made this motd script:

#!/bin/bash

# Calulate avg CPU usage
usage=$((100-$(vmstat 1 2|tail -1|awk '{print $15}')))
aptlist=$(apt list --upgradable 2> /dev/null | sed 1d)

# Check if current usage isn't to high, else don't display the motd
if [[ ${usage%.*} -ge 95 ]];
then
  printf "motd disabled because current usage is %s\n" "$usage"
else
  # The magic
  date=$(date)
#  usage=$(tail /proc/loadavg | awk '{print $1}')
  root_usage=$(df -h / | awk '/\// {print $(NF-1)}')
#  memory_usage=$(free -m | grep Mem | awk '{print 100/$2*($2-$6+$5)}' | xargs printf "$1""%.2f")
  memory_usage=$(printf "%0.2f MB %d%%\n" $(( $(sed -E '/^(MemTotal|MemFree|Cached|Buffers): *([0-9]*).*/'\
'{s//\2/;H;};$!d;x;s/[[:cntrl:]]//;s__/1024-_g;s_$_/1024_' /proc/meminfo) )) \
$(( $(sed -E '/^(MemTotal|MemFree|Cached|Buffers): *([0-9]*).*/'\
'{s//\2/;H;};$!d;x;s/[[:cntrl:]]//;s_([^\n]+)\n_@\1@100*(\1/1024-(_;s_\n_/1024+_g;'\
's_@([^@]+)@(.*)$_\2/1024))/(\1/1024)_' /proc/meminfo) )) | awk '{print $3}')
  users=$(users | wc -w)
  time=$(uptime | grep -ohe 'up .*' | sed 's/,/\ hours/g' | awk '{ printf $2" "$3 }')
  processes=$(ps aux | wc -l)
  ip=$(ip addr | awk '/inet / { print $2 }' | sed -n '2{p;q}' | cut -d '/' -f1)
  ipv6=$(ip -6 addr |awk '{print $2}'| grep -v "^::" | grep "/" | head -n1 | cut -d '/' -f1)
  packages=$(dpkg-query -l | grep -c "^ii")
  updates=$(echo -n $aptlist | wc -l )
  secupdates=$(echo $aptlist | grep -c security )

  # The updates and secupdates var get's it's info thanks to this crontab/command
  # sudo apt-get -s dist-upgrade | grep "^Inst" | wc -l > /etc/update-motd.d/pkg.stats && apt-get -s dist-upgrade | grep "^Inst" | grep -i security | wc -l >> /etc/update-motd.d/pkg.stats
  # sudo crontab -l | { cat; echo "0 0 * * * apt-get -s dist-upgrade | grep "^Inst" | wc -l > /etc/update-motd.d/pkg.stats && apt-get -s dist-upgrade | grep "^Inst" | grep -i security | wc -l >> /etc/update-motd.d/pkg.stats"; } | crontab -
  # If you wish to not use crontab, switch the updates and secupdates comment's.

  # Header & motd
  printf "Welcome to %s (%s)" "$(lsb_release -s -d)" "$(uname -rm)"
  printf "\n"

  echo "     __ __  __ _  __            _______"
  echo "    / //_/ / /(_)/ /__ ____    \|_____|"
  echo "   / ,<   / // // //_// __ \    | │ │ |"
  echo "  / /| | / // // ,<  / /_/ /    |     |"
  echo " /_/ |_|/_//_//_/|_| \____/     ◯_____| "
  echo

  # System information
  echo "System information as of: $date"
  echo
#  printf "System Load:\t%s\tSystem Uptime:\t%s\n" "$load" "$time" # Use this one if you prefer the linux proc avg
  printf "System Load:\t%s%%\tSystem Uptime:\t%s\n" "$usage" "$time"  # Overall processor usage
  printf "Memory Usage:\t%s\tIP Address:\t%s\n" "$memory_usage" "$ip"
  if [[ $ipv6 == "" ]]
  then
    printf "Usage On /:\t%s\tIPv6 Addres:\tNo ipv6 address found\n" "$root_usage"
  else
    printf "Usage On /:\t%s\tIPv6 Addres:\t%s\n" "$root_usage" "$ipv6"
  fi
  printf "Local Users:\t%s\tProcesses:\t%s\n" "$users" "$processes"
  printf "Packages dpkg:\t%s\tSession fortune:\ \n\n" "$packages"

  /usr/games/fortune

  # Check if there are updates
  echo
  if [[ $updates != 0 ]]
  then
    printf "%s updates can be installed immediately.\n" "$updates"
    printf "%s of these updates are security updates.\n" "$secupdates"
    printf "To see these additional updates run: apt list --upgradable\n\n"
  else
    printf "System is up-to-date!\n\n"
  fi

  # Check if a reboot is needed
  if [[ -f /var/run/reboot-required ]]
  then
    echo '*** System restart required ***'
  fi
fi
printf "%s" "$aptlist"

Now the execution time is 2 seconds b/c of these two commands:

usage=$((100-$(vmstat 1 2|tail -1|awk '{print $15}')))
aptlist=$(apt list --upgradable 2> /dev/null | sed 1d)

I tried to find a way to run both of the commands at the same time, so that it only takes a second (which would be amazing). Sadly using var1=cmd1 & var2=cmd2 doesn't work and only the second get's executed properly.

Anyone has a clue how to run two commands that go into each of their variables at the same time? (i prefer to use the default tools in debian/Raspberry but if it's impossible, i'm ok to install that package :))

Thank you for reading!

Macley
  • 101
  • 6
  • One of the possible solutions: https://stackoverflow.com/a/3018124/509977 – Pitto Sep 04 '22 at 20:00
  • @Pitto: How can you use it to populate variables? – choroba Sep 04 '22 at 20:01
  • Note [mre] guidance -- the code shown in your question should be the _shortest possible thing_ that lets others reproduce the problem you're asking about when run without changes. Providing the full motd script just makes the question less clear. – Charles Duffy Sep 04 '22 at 20:28
  • That said... think about running your full script through http://shellcheck.net/; it's got a lot of quoting bugs (including but not limited to that discussed in [I just assigned a variable; why does `echo $variable` show something different?](https://stackoverflow.com/questions/29378566/i-just-assigned-a-variable-but-echo-variable-shows-something-else)). – Charles Duffy Sep 04 '22 at 20:28
  • Reduce your code to a minimum so that the problem is still reproducible. – Cyrus Sep 04 '22 at 21:32

3 Answers3

2

One approach:

  • kick the 2x time consuming commands off in the background, making sure to redirect stdout/stderr to a pair of temp files
  • wait
  • process the 2x temp files and populate the 2x variables

For example:

(vmstat 1 2 | tail -1 | awk '{print $15}'    > tmp.1 2>&1) &
(apt list --upgradable 2> /dev/null | sed 1d > tmp.2 2>&1) &

wait

usage=$((100 - $(cat tmp.1)))
read -r aptlist < tmp.2

NOTE: OP could add any necessary error checking after the wait and before the usage=$((...)); read -r aptlist ... commands

markp-fuso
  • 28,790
  • 4
  • 16
  • 36
1

As a variant on markp-fuso's answer that avoids the need for temporary files by using process substitutions and automatic FD allocation (and is thus expected to work only with newer bash 4.x releases, or all bash 5.0+ releases):

# start both vmstat and apt-list in the background
exec {vmstat_fd}< <(vmstat 1 2 | tail -1 | awk '{print $15}')
exec {aptlist_fd}< <(apt list --upgradable 2> /dev/null | sed 1d)

# read one line of output from each
read -r vmstat <&$vmstat_fd
read -r aptlist <&$aptlist_fd

# close both file descriptors
exec {vmstat_fd}<&-
exec {aptlist_fd}<&-
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Heya! I like this solution the most! But may i ask what the variables are that i should use? Are they still $usage and $aptlist? Thank you in advance! – Macley Sep 07 '22 at 20:27
  • @Macley, yes, `$vmstat` and `$aptlist` are the variables populated by the `read` commands. (The paired `_fd` variables track the file descriptor numbers from which the data from those subcommands can be read). – Charles Duffy Sep 07 '22 at 20:28
  • Ah yes i now understand how you did it! Thank you so much! While FD allocation is something completely new to me, i definitely love this approach and will keep it in my notes to speed up bash scripts in the future if needed! Thank you so much again! – Macley Sep 08 '22 at 20:32
-2

use a dummy single-cycle for loop as a proxy for waiting :

 fgc; a0000=''; b0000=''; gdcmd='%n%n %D %T %s.%-N %n%n'

 gdate +"${gdcmd}"

 ( time ( for idx in 1; do {

       a0000=` lsof &` 
       b0000="$( df -h | mawk 'END { print }' & )" 
 } done
 
 echo "${b0000}\n"

 printf '%s' "${a0000}" | wc5 ) )
 gdate +"${gdcmd}";


  09/04/22 16:24:30 1662323070.513516 


 //guest:@192.168.1.3/WD_4TB_RAID1/55_MyCloud_12T   
 11Ti   11Ti    0Bi   100% 11625431486 0  100% /Volumes/55_MyCloud_12T


  rows = 7330. | UTF8 chars = 1153070. | bytes      = 1153312.

 
 ( for idx in 1; do; { a0000=` lsof &` ; b0000= ; }; done; echo "${b0000}\n"; )
 0.03s user 0.03s system 46% cpu 0.125 total


 09/04/22 16:24:30 1662323070.642752 

As for ur commands, u can streamline them into

vmstat 1 2 | mawk 'END { print 100 - $15 }'

and

apt list --upgradable 2>/dev/null | mawk '_{ exit }--_'

UPDATE 1 : trying another approach

a1=''; b1=''; tmp0=''
echo " starting point :: ${a1}=${b1}=${tmp0}"

gdcmd='%n %D %T %s.%-N %n'
gdate +"${gdcmd}"; fgc
 
( time ( tmp0="$( for idx in 1; do 

    ( ( lsof  | mawk -F'^$' '$!NF="\1" $_' ) & ) 
    ( ( df -h | mawk -F'^$' '$!NF="\2" $_' ) & ) 

 done | gcat -)"

 a1="$( printf '%s' "${tmp0}" | mawk 'NF*=!_<NF' FS='^\1' OFS= )"
 b1="$( printf '%s' "${tmp0}" | mawk 'NF*=!_<NF' FS='^\2' OFS= )"

printf '%s' "${a1}" | wc5; printf '%s' "${b1}" | wc5 ) )
gdate +"${gdcmd}"

starting point :: ==

09/04/22 17:29:42 1662326982.253876 


rows = 7394. | UTF8 chars = 1160393. | bytes = 1160635.
    
rows =   14. | UTF8 chars =    1800. | bytes =    1800.


( tmp0= ; a1="$( printf '%s' "${tmp0}" | mawk 'NF*=!_<NF' FS='^\1' OFS= )" ; )
0.07s user 0.06s system 80% cpu 0.167 total

09/04/22 17:29:42 1662326982.426079 
RARE Kpop Manifesto
  • 2,453
  • 3
  • 11
  • 2
    `var=$(foo &)` needs to wait for the stdout of `foo` to be fully written before it can complete, so it blocks just as long as `var=$(foo)` does. – Charles Duffy Sep 04 '22 at 20:40
  • @CharlesDuffy : what about my updated solution ? does it still block ? – RARE Kpop Manifesto Sep 04 '22 at 21:32
  • @CharlesDuffy : how so ? each have a unique single byte prefix marker to enable the proper splitting – RARE Kpop Manifesto Sep 04 '22 at 21:37
  • _nod_, reread, and I see that now. That... could usually work. I don't like it -- it's doing a lot more than it needs to (every time you use parens it forks off a subshell; that's extra overhead being paid for no reason), and it's depending on each copy of awk writing its output as an atomic syscall, which isn't strictly guaranteed... but it _could_ work. – Charles Duffy Sep 04 '22 at 21:38
  • That said, I don't see how you're getting `tmp0` out of the subshell it's assigned in into the parent process where you try to split it into a1 and a2. Remember, when you run `(a=5)` with the parens, there's no longer an `a` assigned after the `)`. – Charles Duffy Sep 04 '22 at 21:39
  • @CharlesDuffy : that's what the `for` loop is for - it aggregates the output of all sub-processes via `done | gcat -`; OP mentioned it's a 2-second process, so the overhead is close to absolutely minuscule relative to processing time. Maybe u can show me an example where the outputs fail to properly match back ? – RARE Kpop Manifesto Sep 04 '22 at 21:42
  • @CharlesDuffy - that's why in the updated solution neither `lsof` nor `df -h` assigns into anything, but both aggregated via the single cycle dummy `for` loop into `$tmp0` – RARE Kpop Manifesto Sep 04 '22 at 21:43
  • Yes, but that aggregation _into `tmp0`_ is itself in a subshell. In `( time ( tmp0="$( for idx in 1; do`, you've got _two_ sets of parens outside the assignment, one before `time` and one after it. – Charles Duffy Sep 04 '22 at 21:46
  • @CharlesDuffy - run it urself if u don't believe me. `fgc` is just by custom shell function to run command `fg` multiple times to clear anything stuck in the background. `wc5` is my own custom-made `awk`-based replacement of `wc`, as i've found both `gnu-wc` and `bsd-wc` way tooooo slow for my needs. – RARE Kpop Manifesto Sep 04 '22 at 21:47
  • You _can_ use (the shell-builtin version of) `time` without a subshell, but to do that it needs to be `time { ...; }`, not `time (...)` – Charles Duffy Sep 04 '22 at 21:47
  • @CharlesDuffy : i didn't know that. learnt something new today. – RARE Kpop Manifesto Sep 04 '22 at 21:47
  • I don't need to run your code to prove the point, I can run a much simpler example. `a=nothing; (time (a=5)); echo "$a"` echos `nothing`. – Charles Duffy Sep 04 '22 at 21:49
  • ...I'm guessing that you assigned `tmp0` while testing by hand before modifying your code into the version with the parens, so when you tested the final version it _looked_ like it worked because the variable already had a value. – Charles Duffy Sep 04 '22 at 21:50
  • @CharlesDuffy : that i know. then how did i extract out the info in `$tmp0` when i've explicitly set all 3 variables to a blank string at the beginning ? i purposely did that `echo starting point` to prove all 3 weren't pre-set before the execution – RARE Kpop Manifesto Sep 04 '22 at 21:51
  • Ahh -- I misread where your outer subshell ended; using better indentation may have made that clear; so now I _do_ see how it could possibly work. – Charles Duffy Sep 04 '22 at 21:54
  • still, I'm not about to retract my downvote as this is currently written. You're using a bunch of custom helpers nobody else would have (so your code needs to be modified before it can be tested); writing unnecessarily verbose and inefficient code (`( foo ) & ( bar ) &` when it could just be `foo & bar` -- again, those subshells all come with a performance cost -- and relying on the reader to figure out where the magic is instead of writing prose to explain the reasoning behind the code. – Charles Duffy Sep 04 '22 at 21:55
  • @CharlesDuffy : i didn't pretend to be a shell code expert - but ur utter lack of understanding of it doesn't make my code "the hidden voodoo magic secret recipe" – RARE Kpop Manifesto Sep 04 '22 at 21:58
  • Perhaps, but it's a goal to write for accessibility. I'm pretty well-represented on https://stackoverflow.com/tags/bash/topusers; while admittedly I didn't take a lot of effort, if I don't follow what you're doing at a quick glance, chances are that a lot of people who are new here won't follow it even when trying to read closely. – Charles Duffy Sep 06 '22 at 19:20
  • (btw, I'd argue that the single-cycle for loop doesn't really do anything to help clarity; think about `sleep 1; foo` instead of `for idx in 1; do foo; done` -- with the `sleep` you're letting users actually observe behavior with a delay in place). – Charles Duffy Sep 06 '22 at 19:22
  • @CharlesDuffy : did i ever pretend to be good at shell scripting ? – RARE Kpop Manifesto Sep 08 '22 at 06:11