1
\$\begingroup\$

This is for a cheap VPS I use for personal projects. I like having a message of the day displaying system info on login; I wanted to add the status of my docker containers to it, to see if all are running and whatnot.

The scripts I found online for this displayed all the containers, but since I run mailcow for my email and it has 10+ containers in that single docker-compose project, the MOTD was much too long for my liking. So I decided to write my own although I've never written anything in Bash before this.

My script lists Docker containers and their status, but containers that are part of a docker-compose project are condensed into one as if they were a single container with the name of the compose project. If a container of a docker-compose project isn't running then the status of the "condensed container" is changed to "partial", and the container that isn't running is listed with its full name.
Screenshot (highlighted with a box is the output of the script)

My concerns about this are:

  • I don't want to miss out on relevant info (other than the amount of time a container has been up for; I don't care about that), I'm not too familiar with docker and possible status a container could have and whether my way of doing ...!= "Up" to check the status is good enough.
  • When a compose project has all its containers stopped/exited (any status that isn't "Up") I'd like to "condense" it again, since at the moment if that's the case then it will display partial, even though all its containers are not running, then it also list all of its containers. I don't know how I'd achieve this without running multiple chained docker ps commands that would slow down the script considerably. For now I'm content that a compose project having all its containers being down isn't something frequent.
  • I'm not too happy with the way I order the containers. I use two arrays put what I want near the top in one, and then what I want near the bottom in the second. I then concat the arrays. Not sure if this matters or if there's a cleaner way of doing it.
  • Any unforeseen effects, or any way this doesn't work how I intended (outlined above) that is caused by my relative unfamiliarity with Docker or complete unfamiliarity with Bash scripts. And lastly and super minor, almost not worth mentioning: my choice of wording for saying "partial" when not all compose project's containers are running. Couldn't think of any better word, not sure if there is one.
#!/bin/bash

COLUMNS=2
# colors
green="\e[1;32m"
yellow="\e[1;33m"
red="\e[1;31m"
blue="\e[36;1m"
undim="\e[0m"

mapfile -t containers \
< <(docker ps -a --format='{{.Label "com.docker.compose.project"}},{{.Names}},{{.Status}}' \
| sort -r -k3 -t "," \
| awk -F '[ ,]' -v OFS=',' '{ print $1,$2,$3 }')
declare -A upper_containers # To later concat with the ones I want to display first at the top
declare -A lower_containers
for i in "${!containers[@]}"; do
    IFS="," read proj_name name status <<< ${containers[i]}
    if [[ -n $proj_name ]]; then # is docker-compose
        color=$green;
        if [[ "$status" != "Up" ]]; then
            lower_containers[$name]="${name}:,${red}${status,,}${undim},";
            color=$yellow;
            status="partial";
        fi
        upper_containers[$proj_name]="${blue}${proj_name}:,${color}${status,,}${undim},"
    else # not docker-compose
        if [[ "$status" != "Up" ]]; then
            lower_containers[$name]="${name}:,${red}${status,,}${undim},";
        else
            upper_containers[$name]="${name}:,${green}${status,,}${undim},";
        fi
    fi
done;

i=1
out=""
containers=("${upper_containers[@]}" "${lower_containers[@]}")
for el in "${containers[@]}"; do
    out+=$el
    if [ $(($i % $COLUMNS)) -eq 0 ]; then
        out+="\n"
    fi
    i=$(($i+1))
done;
out+="\n"

printf "\ndocker status:\n"
printf "$out" | column -ts $',' | sed -e 's/^/  /'
printf "\n\n"


\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

Addressing your concerns

  • I don't want to miss out on relevant info (other than the amount of time a container has been up for; I don't care about that), I'm not too familiar with docker and possible status a container could have and whether my way of doing ...!= "Up" to check the status is good enough.

I don't know docker compose very well either, but I have some ideas here:

  • When it's not docker compose, the status value will be always printed. So when "Up" is no longer good enough, you will see it in the output, and then you can take appropriate action.
  • When it's docker compose, the status value will be printed for $name, but not for $proj_name (where you print "partial" instead). This seems ok too, because again, the original value gets printed, so the logic in the previous point works for this one too.
  • When a compose project has all its containers stopped/exited (any status that isn't "Up") I'd like to "condense" it again, since at the moment if that's the case then it will display partial, even though all its containers are not running, then it also list all of its containers. I don't know how I'd achieve this without running multiple chained docker ps commands that would slow down the script considerably. For now I'm content that a compose project having all its containers being down isn't something frequent.

You can do with a single docker ps command, storing data about projects, to process in a second pass. The data you would store:

  • A regular array of unique project names
  • An associative array any_container_up to track for each project if any of its containers was up
  • An associative array any_container_down to track for each project if any of its containers was down

You could populate these data structures in the first pass over the output of docker ps. Next, you could loop over the project names, and decide based on any_container_up[$proj_name] and any_container_down[$proj_name] the correct status for the project itself:

  • if any_container_up[$proj_name] and any_container_down[$proj_name] -> partial: some containers are up, others are down
  • else if any_container_up[$proj_name] -> all containers are up
  • else -> all containers are down
  • I'm not too happy with the way I order the containers. I use two arrays put what I want near the top in one, and then what I want near the bottom in the second. I then concat the arrays. Not sure if this matters or if there's a cleaner way of doing it.

As written, the last few containers up and the first few containers down may appear on the same line. So for each line I would have to scan horizontally, which is a mental burden. It seems to me that the different statuses are significant enough that they would deserve to be listed with a clean break in between. I would even split to 3 groups: up, partial, down.

  • Any unforeseen effects, or any way this doesn't work how I intended (outlined above) that is caused by my relative unfamiliarity with Docker or complete unfamiliarity with Bash scripts. And lastly and super minor, almost not worth mentioning: my choice of wording for saying "partial" when not all compose project's containers are running. Couldn't think of any better word, not sure if there is one.

The script looks pretty good to me, I don't see major causes for concern.

As for the wording of "partial", it felt natural to me, and I'm very sensitive to naming.

And on that note... I didn't like the names upper_containers and lower_containers. My first assumption was that "upper" and "lower" refers to uppercasing and lowercasing names, that these are data structures for formatting. (Then of course I saw that's not the case, I'm just sharing my initial impressions.) I would rename these to containers_up and containers_down.

Looping in Bash

Instead of:

for i in "${!containers[@]}"; do
    IFS="," read proj_name name status <<< ${containers[i]}

It would be simpler this way:

for container in "${containers[@]}"; do
    IFS="," read proj_name name status <<< ${container}

Instead of:

containers=("${upper_containers[@]}" "${lower_containers[@]}")
for el in "${containers[@]}"; do

You could write:

for container in "${upper_containers[@]}" "${lower_containers[@]}"; do

Using arithmetic evaluation

Instead of this:

if [ $(($i % $COLUMNS)) -eq 0 ]; then
    out+="\n"
fi
i=$(($i+1))

It would be simpler to use arithmetic evaluation:

if (( i % COLUMNS == 0 )); then
    out+="\n"
fi
((++i))

For more info about arithmetic evaluation, see the ARITHMETIC EVALUATION section in man bash.

Odd semicolons

Several lines end with unnecessary ; that can be simply removed.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ What a brilliant and comple response, thank you! I didn't even think to ask about the loops, but i did feel like they were clunky. Regarding the semi-colons is not using them a convention? Or is it because I was inconsistent in my usage (which I hadn't noticed) \$\endgroup\$
    – DavidPH
    Commented Nov 1, 2021 at 16:50
  • \$\begingroup\$ @DavidPH Your loop is find, I didn't think it's clunky. My suggestion on it is rather cosmetic. The semi-colons in Bash are to separate commands. Since the newline already has that effect, a semi-colon is completely unnecessary there, therefore it shouldn't be there. \$\endgroup\$
    – janos
    Commented Nov 1, 2021 at 16:57
  • 1
    \$\begingroup\$ Yeah by clunky I meant as in I never liked the way they looked, but didn't consider there being alternatives. Also thanks for pointing out, didn't notice I hadn't upvoted that months ago. \$\endgroup\$
    – DavidPH
    Commented Nov 1, 2021 at 21:33
2
\$\begingroup\$
# colors
green="\e[1;32m"
yellow="\e[1;33m"
red="\e[1;31m"
blue="\e[36;1m"

You don't know what kind of terminal (if indeed a terminal) that output will be going to, so don't hard-code these terminal-specific codes.

Instead, use tput which will generate the right strings for terminals that support them:

green=$(tput setaf 2)
yellow=$(tput setaf 3)
red=$(tput setaf 1)
blue=$(tput setaf 4)

Don't set COLUMNS yourself globally for the script - normally Bash sets this automatically to the terminal's correct number of columns, and setting it to 2 will certainly confuse many applications.

It looks like you meant to create a variable for your own use - don't use all-caps for your own variables, to avoid collisions like this with environment variables that affect child processes.

\$\endgroup\$
2
  • \$\begingroup\$ Ah thanks, was wondering why the colors were a bit different on my laptop and pc, didn't think much of it. When you say "if indeed a terminal", since the script is located in /etc/update-motd.d/ along my other MOTD messages is it not guaranteed to be a terminal? Also if am I fine with making a differently named variable / just lower case columns if I want always 2? \$\endgroup\$
    – DavidPH
    Commented Jun 4, 2021 at 11:00
  • \$\begingroup\$ Yes, use a lower-case name so that you're not setting COLUMNS which already has a meaning. \$\endgroup\$ Commented Jun 4, 2021 at 11:44

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