Coding a simple Bash Image Optimiser!

  • bash
  • linux
  • coding

Table of contents

Introduction

Nothing better than learning coding by practicing, right? Right??? I sure hope you agree, because that is precisely what we will be doing right now!

Bash is truly a blessing because you can start building amazing things using many kinds of libraries and CLI tools right away.

For today, let's try building the base for a picture optimizer (I'm really torn how I should refer to the thing in the text, because I prefer the Bri'ish spelling but want to keep everything programming-related with the American spelling).

Preparations

  • Install bash on your system if, for some reason, it is not installed yet.
  • Install optipng.
  • Install inotify-tools.
  • Get a grasp on the most basic bash syntax. Nothing too serious. Maybe skim through a few chapters of this or watch a few YouTube videos. It doesn't matter.
  • Learn how to exit vim or get your text editor of choice ready by creating and opening a file named imgoptimizer.sh

Getting started

We will start with the usual, let's add a shebang to our script:

1#!/bin/bash

Now, I'd like us to follow at least some of the self-proclaimed best practices. That's why the main code will go in the... bingo! main function:

1#!/bin/bash
2
3main() {
4 set -eo pipefail
5}
6
7main "$@" # here we call our function with the special parameter - $@ contains all parameters that were passed to the script

Note the set -eo pipefail. This thing makes the script failfast and makes the shell punish us if the script is poorly-written. Teehee~

Obviously, our script has to know what directory to monitor. Let's declare a variable called DIRECTORY (you can put any other value, really):

1#!/bin/bash
2
3main() {
4 set -eo pipefail
5
6 readonly DIRECTORY=$HOME/Pictures # readonly specifies a constant variable
7}
8
9main "$@"

The neat part

Now, let's setup the part where we launch the watches for new files in our directory:

1inotifywait --monitor --recursive --event create --format "%w%f" "$DIRECTORY" |
2 while read filename; do
3 optipng -o4 "$filename"
4 fi
5 done

Here's the inotifywait docs to help explain how the command works. Most of the used options are self-explanatory (since I used the full versions), I'll only explain --format. optipng needs to know which file it should optimize. That's why we pipe the output of the inotifywait command to the while loop that reads the said output and uses optipng on it: --format "%w%f" outputs the absolute path to the newly created file.

1optipng -o4 "$filename"

Runs the optipng program on the file with the optimization level (the -o flag) of 4. Choosing the right level is a dialectical process which is driven by the contradiction of optimization quality vs the time it takes to run the whole thing. See for yourself what's best for your machine. In my case, I settle for the level 4 as the most optimal (pun intended) one.

Here's how the thing should look so far:

1#!/bin/bash
2
3main() {
4 set -eo pipefail
5
6 readonly DIRECTORY=$HOME/Pictures
7
8 inotifywait --monitor --recursive --event create --format "%w%f" "$DIRECTORY" |
9 while read filename; do
10 optipng -o4 "$filename"
11 done
12}
13
14main "$@"

Let's try running it with

1bash imgoptimize.sh

Try to create a new .png file in the directory, for example, by creating a screenshot that would be saved there.

Wait, oh nyo! Sometimes it errors out, because inotifywait passes the filename before the file is actually fully created!

Let's add a sleep command with 3s as its argument. This time should be just enough to wait until the file is fully written to the disk.

We might as well wrap up the loop in an if statement checking if the file is a PNG one, just in case:

1#!/bin/bash
2
3main() {
4 set -eo pipefail
5
6 readonly DIRECTORY=$HOME/Pictures
7
8 inotifywait --monitor --recursive --event create --format "%w%f" "$DIRECTORY" |
9 while read filename; do
10 if [[ "$filename" == *.png ]]; then
11 sleep 3s
12 optipng -o4 "$filename"
13 fi
14 done
15}
16
17main "$@"

Great, now let's try running the thing again... It works! But since optipng overrides the file, it triggers the inotify watches again on the technically same file. Luckily, optipng won't try to optimize an already optimized file but it would still be nicer and faster for the script if we didn't bother with such files at all!

The most obvious solution is to add a variable that will store the last filename. Thus, the script should look something like this:

1#!/bin/bash
2
3main() {
4 set -eo pipefail
5
6 readonly DIRECTORY=$HOME/Pictures
7
8 last_filename=""
9
10 inotifywait --monitor --recursive --event create --format "%w%f" "$DIRECTORY" |
11 while read filename; do
12 if [[ "$filename" == *.png ]] && [[ "$filename" != "$last_filename" ]]; then
13 last_filename="$filename"
14 sleep 3s
15 optipng -o4 "$filename"
16 fi
17 done
18}
19
20main "$@"

Final touches

We have a quite useful script now, but we can make it a little bit better if we move the config to a separate file.

In your terminal, create a directory and a file for the rc of our script. For example, like this:

1mkdir ~/.config/imgoptimize && touch ~/.config/imgoptimize/imgoptimizerc

In that file, let's specify the directory we want our script to monitor:

1DIRECTORY=/home/user/Pictures

We should now rewrite our script to use the config file:

1#!/bin/bash
2
3main() {
4 set -eo pipefail
5
6 readonly CONFIG_FILE="$HOME/.config/imgoptimize/imgoptimizerc"
7 readonly DIRECTORY="$(cat "$CONFIG_FILE" | grep --regexp ^DIRECTORY | cut --delimiter "=" --fields 2)"
8
9 last_filename=""
10
11 inotifywait --monitor --recursive --event create --format "%w%f" "$DIRECTORY" |
12 while read filename; do
13 if [[ "$filename" == *.png ]] && [[ "$filename" != "$last_filename" ]]; then
14 last_filename="$filename"
15 sleep 3s
16 optipng -o4 "$filename"
17 fi
18 done
19}
20
21main "$@"

Check the DIRECTORY variable out:

$() is the syntax for executing shell commands in a subshell (command substitution). In there, I first use the cat command to output the contents of a file, then pipe the output to grep command that uses a regular expression to find the line containing the constant DIRECTORY. What's left now is to extract the actual path, which I do using the cut command: --delimiter option specifies the character where the string will be split, and the --fields specifies what part of the split string the command should return.

Note that we could just do

1source $HOME/.config/imgoptimize/imgoptimizerc

which will execute the file as shell code, and so simply declare the DIRECTORY variable, but that also means all code from the file will be executed, which may even lead to a hole in security. It's unlikely that something like that will happen to you with such a script, though.

Annnnd let's add some check whether the config file and the directory were found, using standart test flags:

1 #!/bin/bash
2
3main() {
4 set -eo pipefail
5
6 readonly CONFIG_FILE="$HOME/.config/imgoptimize/imgoptimizerc"
7 if [[ -f "$CONFIG_FILE" ]]; then
8 readonly DIRECTORY="$(cat "$CONFIG_FILE" | grep --regexp ^DIRECTORY | cut --delimiter "=" --fields 2)"
9 else
10 echo "Config file $CONFIG_FILE not found. Exiting."
11 exit 1
12 fi
13
14 if [[ ! -d $DIRECTORY ]]; then
15 echo "Invalid DIRECTORY specified in config file. Exiting."
16 exit 1
17 fi
18
19 last_filename=""
20
21 inotifywait --monitor --recursive --event create --format "%w%f" "$DIRECTORY" |
22 while read filename; do
23 if [[ "$filename" == *.png ]] && [[ "$filename" != "$last_filename" ]]; then
24 last_filename="$filename"
25 sleep 3s
26 optipng -o4 "$filename"
27 fi
28 done
29}
30
31main "$@"

This is it! We did it! Hooray!

What to do next

You didn't think the whole Code Along would be just me holding your hand and walking you through the whole thing, did you? Try extending the script in some ways to solidify what you might've learned today. For example, you can:

  • Add more config options! Optimization levels for optipng, sleep time, etc.
  • Make the script "optimize" (compress) other image formats, such as jpg. imagemagick can help you with that task.
  • Research how to make the script automatically execute on system start.- See what other things you can do with the images, such as adjusting the resolution to match your monitor. Why have a 2500x2500 image on your computer if your display's resolution is only 1920x1080?

Conclusion

Studying programming by building things is great, and it's even better when you have an idea of what you can do. That's why I wanted to start the Code Along series. Procrastination is a common issue in self-study, so I genuinely hope that these series will help someone ignite their enthusiasm and learn more quickly and easily.

ヾ(*▼・▼)ノ ⌒☆