Parsing Optional Arguments in Bash
When writing a Bash script, there are times when it can grow to cover multiple scenarios. For example, im-map-tiles is a script I use to convert an image into tiles to use with Leaflet. The image can be a different shape and I might want a different image format for the map tiles.
Here's the command I used to have:
# ./im-map-tiles.sh <input> <output> [<format>]
Very simple. So I wanted to add the option to "square" the
<input> since map tiles need to be square images. Previously, I squared the image as a separate command before passing it through, but it was a common usecase and I'd rather have it in one script. How would I go about it?
# ./im-map-tiles.sh <input> <output> [<format>] [square]
This approach is okay, but what happens if I don't want to set a
<format>? Well, it messes up the argument order so my script won't easily be able to tell what
square is intended for.
There's a common solution to this: flags.
# ./im-map-tiles.sh <input> [--format <format>] [--square] <output>
Since each argument here is flagged, there's no ambiguity. Remove
--format <format> and it's still obvious
--square is its own flag.
Most languages provide libraries to parse these flags into key-value maps. However in Bash, it's not as simple. Since one of the appeals of Bash is its portability, introducing an external dependency has a higher bar and should be avoided where possible.
There is a standard
getopts command which can parse single-letter flags, but I'm not a huge fan of those as it affects readability for long-term scripts.
So I looked around and found a StackOverflow answer which is almost perfect. Essentially the solution is to iterate through all of your arguments and handle them accordingly until there's none left. Here's what I got:
Let's go through it.
POSITIONAL is used to store any arguments that aren't flagged. In my case the
output. Their order is maintained so they can be used later in the script.
The flags are used for conditional logic. All flags are assumed to start with at least one
-, so if any unknown flags are used, they're caught by the
-* matcher and the script fails.
After one argument is handled, it's removed from the arguments array using
shift, which removes the first element of the argument list. If it also handles a value, such as
--format jpg, that will also be removed with a second
shift. This way on each iteration
$1 will always point to the next argument and
$2 will be a value, if any.
$# is the length of the array, so once all the arguments are handled and removed, the parsing stops.
set -- replaces the now empty arguments array with the
POSITIONAL array so that those values can be used as normal (
$1, $2, etc.).
This is pretty much a perfect solution. It's easy to understand, the process is easy to remember and it's all using simple Bash.
Sure it's a bit error prone; needing to use
shift a specific number of times is prone to human error. But that can easily be fixed with some small functions tailored of specific use cases.
Thanks for reading.