4
\$\begingroup\$

I manage my dotfiles in a github repo. The idea would be that I could clone the repo if I ever have to develop on a new system and symlink them to the home directory. I could do this piecemeal manually but I wanted to write a script that automates it.

The script calls a recursive function that symlinks to my dotfiles such that:

  • If a file or symlink already exists on the system, don't overwrite it.
  • If a directory exists on the system but is empty, delete the existing directory and then symlink to it
  • If a file does not exist in the system, create a symlink to my dotfiles
  • If a directory exists on the system but is NOT empty, call the recursive function on that file to my dotfiles. So if, for eample, there is a mismatch in the contents of my .config directory and the system .config folder the recursive function is called on both the .config folders, symlinking to my files, following the rules above, and recursively copying them to subdirectories.

It also outputs a text log of what's going on with indentation for the level of recursion.

My questions are: Is what I'm doing a good approach? Maybe there is a one liner that replicates my script (perhaps using find which I'm still learning about; I'm still learning bash).

Is there an obvious problem with my code? I've tested it on a system, and worked out some bugs but I just wonder if anyone more experienced can see obvious problems.

#/bin/bash

echo_indent() # an echo function whose argument is a certain number of tabs to indent by
{
    i=0
    while [ $i -lt $1 ]
    do
        echo -n "  "
        i=$((i+1))
    done
    echo "$2"
}

recursive_link() # takes three arguments; source file, target file, and number of indents for logging output
{
    n_indents=$3 # the third argument is how muhch to indent for each level of reccursion

    # these two conditions test if one is a file and the other is a directory or vice-versa
    echo_indent $3 ====================================================
    echo_indent $3 "trying to link $1 and $2"

    if [ -f "$2" ]
    then
        echo_indent $3 "$2 already exists (file)"
        return 1
    fi

    if [ -L "$2" ]
    then
        echo_indent $3 "$2 already exists (symlink)"
        return 1
    fi

    if [ -f "$1" ] && [ -d "$2" ]
    then
        echo_indent $3 "mismatch between $1 (file) and $2 (directory)"
        return 1
    fi

    if [ -d "$1" ] && [ -f "$2" ]
    then
        echo_indent $3 "mismatch between $1 (directory) and $2 (file)"
        return 1
    fi




# Case where they are both directories
if [ -d "$1" ] && [ -d "$2" ]
then
    # if the existing system directory is empty
    if [ $(ls -A "$2" | wc -l) -eq 0 ] 
    then
        echo_indent $3 "linking directory to empty system directory"
        rm -r $2 # deleting empty directory so that `ln` works right; could use `n` option but the man page cautions against `n` in scripts
        ln -sv $1 $2 
        return 0
    fi

    echo_indent $3 "both directories, system nonempty: recursively symlinking contents"

    for fle in "$1"/.* "$1"/* #looping over dotfiles and other files
    do
        echo_indent $3 '------------------------------------------'
        echo_indent $3 "file is $fle"
        fle_used="$(basename -- $fle)" # just want the ending filename, not enclosing directories
        echo_indent $3 "basename is $fle_used"
        if [ "$fle_used" != '..' ] && [ "$fle_used" != '.' ] && [ "$fle_used" != 'symlink_recursive.sh' ] && [ "$fle_used" != "dotfiles" ] && [ "$fle_used" != "README.md" ] && [ "$fle_used" != "dotfiles/" ] && [ "$fle_used" != ".git" ] #list of files that we don't want ot copy
        then
            echo_indent $3 "applying recursion"
            #adding and subtracting indent because of bash scope rules
            n_indents=$((n_indents+1))
            recursive_link "$1/$fle_used" "$2/$fle_used" $((n_indents))
            n_indents=$((n_indents-1))
        else
            echo_indent $3 "file rejected for recursion"
        fi
    done

    return 0 # done recursing over subdirectories
    fi

    echo "linking $1 and $2"
    ln -sv $1 $2 #the file does not exist on the system, symlinking to my dotfiles
}

#actually calling the function on my cloned dotfiles and the homedirectory
recursive_link ~/dotfiles ~ 0 #third arguments is 
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Not a complete code review but there are a few things that I would maybe change:

  • Indent by n spaces instead of tabs and use printf instead of a slow bash loop:

    echo_indent() {
       printf "%$1s%s\n" "" "$2"
    }
    
  • Or a global variable that you would augment or shrink by one tab when entering or leaving a recursion level:

    declare indent=
    echo_indent() {
       printf "%s%s\n" "$indent" "$1"
    }
    ...
    indent="    $indent"
    ...
    indent="${indent:1}"
    
  • Test empty directories with find instead of ls which is not really for scripting:

    if [ -z "$(find "$2" -type f -print -quit)" ]; then
      ...
    fi
    
  • Use shell options to simplify some constructs:

    shopt -s nullglob dotglob
    ...
    for fle in "$1"/*; do
    ...
    

    (note that without the nullglob shell option your for fle in "$1"/.* "$1"/* could lead to unexpected results if there are no dotfiles or no non-dotfiles.)

  • Use a bash associative array to store the set of files to ignore and test if a name is in the set:

    declare -A ignore=(
      [".."]=1
      ["."]=1
      ["symlink_recursive.sh"]=1
      ["dotfiles"]=1
      ["README.md"]=1
      ["dotfiles/"]=1
      [".git"]=1
    )
    ...
    if [[ -z ${ignore["$fle_used"]:-} ]]; then
    ...
    
  • Use in-place increment for the indentation:

    recursive_link "$1/$fle_used" "$2/$fle_used" $(( $3 + 1 ))
    

    Or, with spaces:

    recursive_link "$1/$fle_used" "$2/$fle_used" $(( $3 + 4 ))
    

But I would probably use find for all this. Example with a simplified logging part and a helper bash function; the find command must be executed in the dotfiles directory:

helper() {
  printf 'trying to symlink %s to %s\n' "$1" "$2"
  if [[ -f $2 ]] || [[ -L $2 ]]; then
    printf 'skip: file or symlink %s exists\n' "$2"
    return
  elif [[ -d $1 ]] && [[ -d $2 ]] &&
       [[ -z "$(find "$2" -type f -print -quit)" ]] ||
       [[ ! -e $2 ]]; then
    rm -rf "$2"
    ln -srf "$1" "$2"
    printf 'symlink\n'
  else
    printf 'skip: %s exists and is not a file, a symlink or an empty directory\n' "$2"
  fi
}

export -f helper
find . -mindepth 1 -exec bash -c 'helper "{}" "$HOME/{}"' \;
\$\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.