I've written a script to aid in creating new watchfaces for the awesome AsteroidOS project. A watchface there typically consists of directory structure like this:

└── usr
    └── share
        ├── asteroid-launcher
        │   ├── watchface-img
        │   │   ├── asteroid-logo.svg
        │   │   ├── digital-shifted-0.png
        │   │   ├── digital-shifted-1.png
        │   │   ├── digital-shifted-2.png
        │   │   ├── digital-shifted-3.png
        │   │   ├── digital-shifted-4.png
        │   │   ├── digital-shifted-5.png
        │   │   ├── digital-shifted-6.png
        │   │   ├── digital-shifted-7.png
        │   │   ├── digital-shifted-8.png
        │   │   ├── digital-shifted-9.png
        │   │   └── digital-shifted-ring.svg
        │   └── watchfaces
        │       └── digital-shifted.qml
        └── fonts
            ├── OpenSans-CondLight.ttf
            └── OpenSans-Light.ttf

If we want to clone this watchface, named digital-shifted to a new watchface, let's say digital-swirl we need to copy that complete directory tree, but everywhere digital-shifted appears in a file name, we need to change it to digital-swirl and finally, the lines within the QML file that refers to those files should also be changed to refer to the renamed files. This is a fairly common operation for creating a new watchface from an existing one, so this script is intended to perform that task. It must be invoked in the same top-level directory that contains digital-shifted and the new digital-swirl must not already exist. The copying is fairly straightforward, but then find is used and the script runs itself with a third argument which is the name of each individual file that contains the original name. That part is not so obvious and is, in particular, the place I'd like comments and suggestions.

# clone watchface changing names where required

# remember source and dest, trimming trailing / if needed
SOURCE=$(echo $1 | sed 's:/*$::')
DEST=$(echo $2 | sed 's:/*$::')

function showHelp {
    cat << EOF
./clone.sh source destination
Clone a existing "source" watchface to a new "destination" watchface.
The script copies and renames files that contain the "source" name and 
updates the watchface qml file to point to the renamed sources.


# the three-argument form is only intended to be used internally
if [ -f "$3" ] ; then
    if [[ "$3" == *.qml ]] ; then
        sed "s/$SOURCE/$DEST/g" "$3" > "${3//$SOURCE/$DEST}"
        rm "$3"
        mv "$3" "${3//$SOURCE/$DEST}"

if [ "$#" != "2" ] ; then

if [ ! -d "$SOURCE" ] ; then
    echo "Error:  $SOURCE does not exist, exiting program"

if [ -e "$DEST" ] ; then
    echo "Error:  $DEST already exists, exiting program"

# copy all of the files
cp -r "${SOURCE}" "${DEST}"

# rename any file that contains the source dir name 
find "${DEST}" -type f -iname "*${SOURCE}*" -execdir "$(pwd)/cloner.sh" "${SOURCE}" "${DEST}" '{}' \;

1 Answer 1

SOURCE=$(echo $1 | sed 's:/*$::')
DEST=$(echo $2 | sed 's:/*$::')

We don't need an external sed for this transformation, I think.
Are we really likely to be invoked with multiple trailing /? If we only ever need to remove a single /, then it's a trivial parameter substitution:


Note also use of lower-case for variable names - that's a good practice, since all-caps environment variable names are often used for communicating with child processes, and we don't want to accidentally collide with them.

This is worrying:

    sed "s/$SOURCE/$DEST/g" "$3" > "${3//$SOURCE/$DEST}"

The source variable is being interpreted both as regular expression and as a shell pattern. The two substitutions could give different results, unless we constrain the allowed characters, and I see no validation that will do that. And what if a name is given that contains / somewhere (not at the end)?

if [ "$#" != "2" ] ; then

I think that's a user error, so would expect

    showHelp >&2
    exit 1

Similarly with the other error conditions - write the message to standard error stream and exit with a non-zero status value.

  • \$\begingroup\$ Good stuff. Have you a suggestion for addressing the "worrying" code? \$\endgroup\$
    – Edward
    Commented Dec 13, 2021 at 13:14
  • \$\begingroup\$ The only thing simple that comes to mind is a quick case "$source" in *[][/*.+]*) exit 1;; esac to outright refuse to work in those cases (extend the [] to include all regex metacharacters plus newline, and add a friendly message). That was what I was cryptically referencing with my "constrain the allowed characters" reference (sorry - v. busy today). I think it's easier to transform a pattern to regexp than vice versa, if you're considering trying to make both substitutions equivalent; I imagine you'd want some pretty rigorous testing if you go that way. \$\endgroup\$ Commented Dec 13, 2021 at 13:21
  • 1
    \$\begingroup\$ Oh, forgot to say: the entire script appears to use only the POSIX features of Bash, so could easily write #!/bin/sh for more portable script - check Shellcheck results with both as an extra check. \$\endgroup\$ Commented Dec 13, 2021 at 13:26

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.