Baby's First Shell Script
Table of Contents
Since I switch frequently between Linux, macOS, & Windows, one of the things that has become increasingly clear to me is that the Linux terminal is pretty awesome. At first, I thought it was just fun, but that doing things with a UI was still more intuitive and efficient. But over time, my mind has changed on that. Recently, I was performing a task that I do frequently and that can get very repetitive, and I thought that there just had to be a better way.
The Problem #
As a web developer and photographer, I am gathering, resizing, and optimizing images for the web on a daily basis. Typically this involves putting the images I need together in the appropriate folder, running them all through something like ImageOptim, and then using export for web in Photoshop to achieve a manageable file size.
This workflow works well, but has a few downsides:
- One, it’s time consuming. Sure, I can use Adobe Bridge and batch process files, but I find the UI a bit clunky and confusing, it then requires multiple apps open at once, and it’s generally inefficient
- Two, Adobe recently began charging more per month to get both Lightroom and Photoshop, and I simply don’t want to spend money every month to do such a simple task
- And three, the above tools are not available for Linux, pushing me to look at alternatives.
Enter the Terminal #
The terminal allows the user to tell the computer to do something simply by typing. And there are apps that use this interface to complete tasks. For example, ImageMagick provides a bunch of useful image processing options via the command line. Two that are important to me are resizing and optimization. Another great app is jpegoptim, which has similar features to ImageOptim, but without the UI.
Using these is great, but they present a couple of additional issues: typing can be cumbersome, and you have to remember the commands.
The Solution #
Bash allows any user to create and run scripts within the terminal. These can be as simple as echoing out “Hello World” or as complicated as entire games (yes, you play them within the terminal). For my purposes, what I liked most about the idea of writing a script, is that it would allow me to string together a series of tasks, taking into account my input for certain things, by just running a single command.
To test the idea, I started relatively simple. You see, on GNU/Linux systems you can update all of your apps using the terminal. Keeping track of what you can update with which command, though can be tricky, because while my Debian-based system uses the APT package manager for apps and packages, I also use apps that use Snap and Flatpak. But, what if I could update all three with just one command? A script lets me do that.
It Starts With A Bang #
Shebang, to be precise. This basically tells the system that what it’s about to read is meant to be executed by a specified interpreter. In my instance, that is Bash.
From there a bash script can contain a meriad of things including echoing out text, creating and accepting variables, conditionals, etc. It can also contain commands. For my purposes, I start by running the standard command to update apt package repositories.
sudo apt update
Followed by the command to upgrade any apps with available updates.
sudo apt upgrade
I also include the ‘-y’ flag, telling the program to automatically select Yes when prompted to upgrade something. Altogether, the first line looks like this:
sudo apt update && sudo apt upgrade -y. Pretty simple, right? That takes care of our core apps and packages through apt.
What about Snaps? That is also pretty simple:
sudo snap refresh.
And lastly, Flatpaks:
I also threw in some text to break up the updates and let myself know where things are at in the process. The whole script looks like this:
#! /usr/bin/bash echo "First, let's update your packages and .deb apps." sudo apt update && sudo apt upgrade -y echo "Nice, now let's update your snaps." sudo snap refresh echo "Now that's done, how about flatpaks?" flatpak update echo "Done! Now check for errors and fix accordingly."
Seems pretty simple, right? It is! But that’s sort of the beauty of it. This one simple block of code allows me to run all of those updates, complete with a bit of flavor text, by typing one word (followed by .sh) into my terminal! Doesn’t seem like a whole lot, I know, so let’s look at another script I wrote.
Image Optimization #
This one is a bit more involved, and utilizes the programs I mentioned earlier. What’s the goal? To be able to pop into any folder of images (assuming they’re jpegs) and run one command to resize, optimize, and strip the EXIF data off all of them in one go. We start with…
But you already knew that.
Next, we’ll use imagemagick to resize the images. But to what size? Well, I’d like to be able to tell it what size I want when I run the script, that way I can potentially use it for multiple purposes. The simplest command to resize (that I’ve found) is
mogrify -resize (your width in pixels) *.jpg. This will resize all .jpg files in the directory to the width you choose. It will automatically skip folders and files that it can’t modify. But, we want to be able to tell the program what width to use in the script, and to do this we’ll need two things: user prompts and variables. We need a prompt for the user to enter a number, and a variable to put that number into the mogrify command.
Learning to Read #
read command essentially lets us prompt the user to type something in and, assuming the user does, stores that value. We’ll be asking the user to input their desired image width and then storing that input into a variable called ‘width.’ It’s a pretty simple, but powerful, combination that looks like this
read width. This variable can be used later with
$width. But, what do we do with that value once we have it?
If This Then That #
Now, we’ll plug that number into the
-resize command I mentioned earlier. But it’s important to consider what happens if the user doesn’t do what’s expected. I’ll admit that I could have accounted for more scenarios, but I did at least want to have a fallback, or default, for how wide to make images if the user leaves their response blank. To do this, we’ll use a conditional that says, “If the user inputs a number, use that as the image width. But, if the user didn’t answer, make the images 2400px wide.” Why 2400px? Idk.
if [ $width ]; then echo "Running..." mogrify -resize $width *.jpg else echo "No input, defaulting to 2400w." width=2400 mogrify -resize $width *.jpg fi
Of course, imagemagick has some conditionals of its own, and while you’re running my script, if it runs into any errors, it’ll let you know.
Quality Over Quality #
Next, we’ll set the quality we want. We prompt the user for their desired quality (in this case a percentage)…
echo "What quality level do you want your images to be?" read quality
And then plug that into the next command, with 80 percent being the default…
if [ $quality ]; then echo "Running..." mogrify -quality $quality *.jpg else echo "No input, defaulting to 80 quality." quality=80 mogrify -quality $quality *.jpg fi
Make sense? I’m sure it does. You’re very smart after all. Which is why you’re asking, we’re going to strip the EXIF data now, right? But what if we don’t want to do that this time?
Let’s Strip #
I’m going to be honest, I haven’t completely wrapped my head around this one yet. But I do know that it works. We’re going to use a while loop with a case statement to ask the user if they want to strip the EXIF data and, if they do, do it.
while true; do read -p "Would you like to strip EXIF data? " yn case $yn in [Yy]* ) mogrify -strip *.jpg; break;; [Nn]* ) exit;; * ) echo "Please answer yes or no.";; esac done
And that’s pretty much it! Altogether it looks like this:
#! /usr/bin/bash echo "How wide do you want your images to be?" read width if [ $width ]; then echo "Running..." mogrify -resize $width *.jpg else echo "No input, defaulting to 2400w." width=2400 mogrify -resize $width *.jpg fi echo "What quality level do you want your images to be?" read quality if [ $quality ]; then echo "Running..." mogrify -quality $quality *.jpg else echo "No input, defaulting to 80 quality." quality=80 mogrify -quality $quality *.jpg fi while true; do read -p "Would you like to strip EXIF data? " yn case $yn in [Yy]* ) mogrify -strip *.jpg; break;; [Nn]* ) exit;; * ) echo "Please answer yes or no.";; esac done echo "Done."
And here it is in action:
user@linux:~/test$ img.sh How wide do you want your images to be? 1280 Running... What quality level do you want your images to be? 75 Running... Would you like to strip EXIF data? y Done.
With that, I’ve resized all .jpg files in the current directory to 1280px wide at a quality of 75 and with EXIF data stripped. Pretty cool!
A Whole New World! #
To anyone who has had to batch resize, strip, and optimize large numbers of images using the likes of Adobe Bridge and Photoshop the benefits of this CLI approach are obvious. What a time saver! Not to mention it’s significantly lighter on resources than Adobe’s offerings. And it’s gotten me excited about the possibilities of other experiments I can play with and problems I can solve with Bash scripts.
The benefits of scripting aren’t just for Linux users, too. I’ve actually moved that script over to both macOS (at work) and WSL2 on my Windows partition. Thanks to Windows Subsystem for Linux, Windows users can enjoy Bash scripting in a native environment.
By the way, I’m quite new to all of this Linux Bash scripty stuff. If you spot any quality or security issues with my code, feel free to shoot me an email and let me know!