7
\$\begingroup\$

These are the first lines of code I've ever written. I've been interested in the idea of learning to program for quite a while, but never really pulled the trigger, and now I've been playing around with some HTML & CSS and thought this might be a good starter project. So after a couple of days of furious googling, I managed to cobble this script together.

What I'm mostly curious about is the overall logic and structure of the code. The whole script grew organically from me looking up the Bash constructs and figuring out how to combine them to do what I want, so I don't know how sensible it is from the programming standpoint.

The idea is to keep the script in my project's folder and use relative paths, so I can just run it when I need to regenerate the CSS.

#!/bin/bash
# CSS ruleset template:
#
# .________::before { background-image: url(data:________;base64,__________); }
# filename mimetype base64 str
#
# CSS class names and other identifiers can contain the characters
# A-Z, a-z, 0-9, hyphen, underscore and Unicode characters U+00A0 and higher.
# They cannot start with a digit, two hyphens or a hyphen followed by a digit.
# (https://www.w3.org/TR/CSS2/syndata.html#characters)
#
# url(), unquoted, requires escaping the characters ()'" and whitespace.
# url('') & url("") require escaping newlines and the quote used for quoting.
# (https://www.w3.org/TR/CSS2/syndata.html#uri)
#
# Base64 encoding uses characters A-Z, a-z, 0-9 and any two of these: +-.,:/_~
# Therefore, there's never need to escape anything, so no quotes are necessary.
css_path="icons.css" # relative to the script
cd "${BASH_SOURCE%/*}"
: > "$css_path"
for file in icons/*; do
 echo Processing "$file"
 filename="$(name_ext="${file##*/}"; echo "${name_ext%.*}")"
 ext="${file##*.}"
 shopt -s nocasematch
 case "$ext" in
 avif ) mime="image/avif" ;;
 bmp ) mime="image/bmp" ;;
 gif ) mime="image/gif" ;;
 jpg | jpeg) mime="image/jpeg" ;;
 png ) mime="image/png" ;;
 svg ) mime="image/svg+xml";;
 * ) mime="unsupported" ;;
 esac
 if [[ "$mime" = "unsupported" ]]; then
 unsupported+=("$file")
 elif [[ ! "$filename" =~ ^-?[A-Za-z_][A-Za-z0-9_-]*$ ]]; then
 invalid_class_name+=("$file")
 else
 base64str="$(base64 --wrap=0 "$file")"
 printf ".%s::before { background-image: url(data:%s;base64,%s); }\n" \
 "$filename" "$mime" "$base64str" \
 >> "$css_path"
 fi
done
if [[ -z "$unsupported" && -z "$invalid_class_name" ]]; then
 echo "All done!"
else
 if [[ -n "$unsupported" ]]; then
 printf "\n\n%s\n\n" "UNSUPPORTED FILES (SKIPPED)"
 printf " %s\n" "${unsupported[@]}"
 fi
 if [[ -n "$invalid_class_name" ]]; then
 printf "\n\n%s\n" "FILENAMES INVALID AS CSS CLASS NAMES (SKIPPED)"
 printf " %s\n" "Allowed characters: A-Z, a-z, 0-9, '-', '_'"
 printf " %s\n\n" "Can't begin with: 0-9, '--', '-0' through '-9'"
 printf " %s\n" "${invalid_class_name[@]}"
 fi
fi
```
200_success
146k22 gold badges190 silver badges479 bronze badges
asked Jun 8, 2019 at 2:31
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$
cd "${BASH_SOURCE%/*}"
: > "$css_path"

What happens if cd fails? Nothing good. Make a habit of always, always testing the result of cd (and synonyms like pushd) for failure. Bash's -e switch will promote all error exits to fatal script errors; a less extreme approach is simply:

cd ... || exit 1

if [[ -z "$unsupported" && -z "$invalid_class_name" ]]

This is testing the first elements of these two arrays for zero length. You want to test the lengths of the arrays instead.

When testing for zero, the use of arithmetic (( ... )) is a nice touch: it returns truth for non-zero and false for zero.

When a conditional includes an else block, avoid negating the condition. In other words: prefer if x then yes else no over if not x then no else yes.

 if (( ${#unsupported[@]} || ${#invalid_class_name[@]} )) 

filename="$(name_ext="${file##*/}"; echo "${name_ext%.*}")"
ext="${file##*.}"

This is awkward. Most of the fault rests with Bash itself. A regular expression is a little cleaner:

[[ $file =~ ([^/]*)\.([^.]*)$ ]] && filename=${BASH_REMATCH[1]} ext=${BASH_REMATCH[2]}

if [[ "$mime" = "unsupported" ]]; then

Quotes aren't needed around variables (or literal strings that lack whitespace) inside [[ ... ]].


 printf ".%s::before { background-image: url(data:%s;base64,%s); }\n" \
 "$filename" "$mime" "$base64str" \
 >> "$css_path"

It's good practice to indent continuations. In the specific case of printf, it can be helpful to align variables with their format-string placeholders.


unsupported+=("$file")
invalid_class_name+=("$file")

If these exist in the shell that invokes your script, their initial values will persist and muck things up. Make a habit of zeroing variables you intend to append to (or increment, etc.). At the same time, improve readability by expressly declaring your arrays as such:

declare -a unsupported=() invalid_class_name=()

: > "$css_path"

This could be annoying if the script fails. Consider taking a backup of the output file before zeroing it, and restoring the backup on error exit.

answered Jun 8, 2019 at 6:32
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.