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
```
\$\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.

\$\endgroup\$

Your Answer

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

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