Bump to version 0.4
[dotfiles-framework.git] / dotfiles.sh
index 53dbe00c288ef39aa467d3e7942073b79ee3c998..5f0c5e5cb9cb8f4202fa42db8fcd4ff7926d65ca 100755 (executable)
@@ -3,7 +3,7 @@
 # Dotfiles management script.  For details, run
 #   $ dotfiles.sh --help
 #
 # Dotfiles management script.  For details, run
 #   $ dotfiles.sh --help
 #
-# Copyright (C) 2011-2012 W. Trevor King <wking@tremily.us>
+# Copyright (C) 2011-2015 W. Trevor King <wking@tremily.us>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -18,7 +18,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-VERSION='0.2'
+VERSION='0.4'
 DOTFILES_DIR="${PWD}"
 TARGET=~
 CHECK_WGET_TYPE_AND_ENCODING='no'
 DOTFILES_DIR="${PWD}"
 TARGET=~
 CHECK_WGET_TYPE_AND_ENCODING='no'
@@ -26,17 +26,17 @@ CHECK_WGET_TYPE_AND_ENCODING='no'
 #####
 # External utilities
 
 #####
 # External utilities
 
-DIFF=$(which diff)
-GIT=$(which git)
-LN=$(which ln)
-MV=$(which mv)
-PATCH=$(which patch)
-SED=$(which sed)
-RM=$(which rm)
-RSYNC=$(which rsync)
-TAR=$(which tar)
-TOUCH=$(which touch)
-WGET=$(which wget)
+DIFF=${DOTFILES_DIFF:-$(command -v diff)}
+GIT=${DOTFILES_GIT:-$(command -v git)}
+LN=${DOTFILES_LN:-$(command -v ln)}
+MV=${DOTFILES_MV:-$(command -v mv)}
+PATCH=${DOTFILES_PATCH:-$(command -v patch)}
+SED=${DOTFILES_SED:-$(command -v sed)}
+RM=${DOTFILES_RM:-$(command -v rm)}
+RSYNC=${DOTFILES_RSYNC:-$(command -v rsync)}
+TAR=${DOTFILES_TAR:-$(command -v tar)}
+TOUCH=${DOTFILES_TOUCH:-$(command -v touch)}
+WGET=${DOTFILES_WGET:-$(command -v wget)}
 
 #####
 # Compatibility checks
 
 #####
 # Compatibility checks
@@ -106,7 +106,7 @@ function run_on_all_repos()
        COMMAND="${1}"
        shift
        if [ -z "${REPO}" ]; then  # run on all repositories
        COMMAND="${1}"
        shift
        if [ -z "${REPO}" ]; then  # run on all repositories
-               for REPO in *; do
+               for REPO in *; do
                        if [ "${REPO}" = '*' ]; then
                                break  # no known repositories
                        elif [ -f "${REPO}" ]; then
                        if [ "${REPO}" = '*' ]; then
                                break  # no known repositories
                        elif [ -f "${REPO}" ]; then
@@ -130,7 +130,7 @@ function list_files()
        done < <(cd "${DIR}" && find .)
 }
 
        done < <(cd "${DIR}" && find .)
 }
 
-# Global variable to allow passing associative arrats between functions
+# Global variable to allow passing associative arrays between functions
 
 if [ "${BASH_MAJOR}" -ge 4 ]; then
        declare -A REPO_SOURCE_DATA
 
 if [ "${BASH_MAJOR}" -ge 4 ]; then
        declare -A REPO_SOURCE_DATA
@@ -172,6 +172,8 @@ function get_repo_source()
                REPO_SOURCE_DATA['repo']="${REPO}"
                if [ -d "${REPO}/.git" ]; then
                        REPO_SOURCE_DATA['transfer']='git'
                REPO_SOURCE_DATA['repo']="${REPO}"
                if [ -d "${REPO}/.git" ]; then
                        REPO_SOURCE_DATA['transfer']='git'
+                       REPO_SOURCE_DATA['url']=$(
+                               git --git-dir "${REPO}/.git/" config remote.origin.url)
                else
                        echo "ERROR: no source location found for ${REPO}" >&2
                        return 1
                else
                        echo "ERROR: no source location found for ${REPO}" >&2
                        return 1
@@ -232,11 +234,13 @@ function wget_fetch()
 # usage: link_file REPO FILE
 #
 # Create the symbolic link to the version of FILE in the REPO
 # usage: link_file REPO FILE
 #
 # Create the symbolic link to the version of FILE in the REPO
-# repository, overriding the target if it exists.
+# repository, overriding the target if it exists.  If you want to
+# override the options passed to ${LN}, set LINK_OPTS.
 function link_file()
 {
        REPO=$(nonempty_option 'link_file' 'REPO' "${1}") || return 1
        FILE=$(nonempty_option 'link_file' 'FILE' "${2}") || return 1
 function link_file()
 {
        REPO=$(nonempty_option 'link_file' 'REPO' "${1}") || return 1
        FILE=$(nonempty_option 'link_file' 'FILE' "${2}") || return 1
+       LINK_OPTS="${LINK_OPTS:--sv}"  # default to `-sv`
        if [ "${BACKUP}" = 'yes' ]; then
                if [ -e "${TARGET}/${FILE}" ] || [ -h "${TARGET}/${FILE}" ]; then
                        if [ "${DRY_RUN}" = 'yes' ]; then
        if [ "${BACKUP}" = 'yes' ]; then
                if [ -e "${TARGET}/${FILE}" ] || [ -h "${TARGET}/${FILE}" ]; then
                        if [ "${DRY_RUN}" = 'yes' ]; then
@@ -256,8 +260,9 @@ function link_file()
        if [ "${DRY_RUN}" = 'yes' ]; then
                echo "link ${TARGET}/${FILE} to ${DOTFILES_DIR}/${REPO}/patched-src/${FILE}"
        else
        if [ "${DRY_RUN}" = 'yes' ]; then
                echo "link ${TARGET}/${FILE} to ${DOTFILES_DIR}/${REPO}/patched-src/${FILE}"
        else
+               SOURCE="${DOTFILES_DIR}/${REPO}/patched-src/${FILE}"
                echo -n 'link '
                echo -n 'link '
-               "${LN}" -sv "${DOTFILES_DIR}/${REPO}/patched-src/${FILE}" "${TARGET}/${FILE}" || return 1
+               "${LN}" ${LINK_OPTS} "${SOURCE}" "${TARGET}/${FILE}" || return 1
        fi
 }
 
        fi
 }
 
@@ -331,6 +336,49 @@ function clone()
        fi
 }
 
        fi
 }
 
+###
+# list command
+
+COMMANDS+=('list')
+
+function list_help()
+{
+       echo 'List current dotfiles repositories.'
+       if [ "${1}" = '--one-line' ]; then return; fi
+
+       cat <<-EOF
+
+               usage: $0 ${COMMAND} [REPO]
+
+               List information for 'REPO' in a form simular to the 'clone'
+               command's arguments.  If 'REPO' is not give, all repositories will
+               be listed.  Examples:
+
+                 $0 list public
+                 public wget http://example.com/public-dotfiles.tar.gz
+                 $0 list
+                 public wget http://example.com/public-dotfiles.tar.gz
+                 private git ssh://example.com/~/private-dotfiles.git
+       EOF
+}
+
+function list()
+{
+       # multi-repo case handled in main() by run_on_all_repos()
+       REPO=$(nonempty_option 'list' 'REPO' "${1}") || return 1
+       maxargs 'list' 1 "${@}" || return 1
+       if [ "${BASH_MAJOR}" -ge 4 ]; then
+               get_repo_source "${REPO}" || return 1
+               TRANSFER=$(nonempty_option 'list' 'TRANSFER' "${REPO_SOURCE_DATA['transfer']}") || return 1
+               URL=$(nonempty_option 'list' 'URL' "${REPO_SOURCE_DATA['url']}") || return 1
+       else
+               echo "WARNING: Bash version < 4.0, cannot use assuming all repos use git transfer" >&2
+               TRANSFER='git'
+               URL=$(git --git-dir "${REPO}/.git/" config remote.origin.url)
+       fi
+       echo "${REPO} ${TRANSFER} ${URL}"
+}
+
 ###
 # fetch command
 
 ###
 # fetch command
 
@@ -373,7 +421,7 @@ function fetch()
 }
 
 ###
 }
 
 ###
-# fetch command
+# diff command
 
 COMMANDS+=('diff')
 
 
 COMMANDS+=('diff')
 
@@ -411,7 +459,7 @@ function diff()
                        '--removed')
                                MODE='removed'
                                ;;
                        '--removed')
                                MODE='removed'
                                ;;
-                       '--local-patch')
+                       '--local-patch')
                                MODE='local-patch'
                                ;;
                        *)
                                MODE='local-patch'
                                ;;
                        *)
@@ -523,36 +571,54 @@ function link_help()
 
        cat <<-EOF
 
 
        cat <<-EOF
 
-               usage: $0 ${COMMAND} [--force|--force-file] [--dry-run] [--no-backup] [REPO]
+               usage: $0 ${COMMAND} [--force] [--force-dir] [--force-file] [--force-link]
+                          [--dry-run] [--no-backup] [--relative] [REPO]
 
                Where 'REPO' is the name the dotfiles repository to link.  If it
                is not given, all repositories will be linked.
 
 
                Where 'REPO' is the name the dotfiles repository to link.  If it
                is not given, all repositories will be linked.
 
-               By default, link.sh only replaces missing files and simlinks.  You
-               can optionally overwrite any local files by passing the --force
-               option.
+               By default, ${COMMAND} only replaces missing directories, files,
+               simlinks.  You can optionally overwrite any local stuff by passing
+               the --force option.  If you only want to overwrite a particular
+               type, use the more granular --force-dir, etc.
+
+               If you have coreutils 8.16 (2012-03-26) or greater, you can set
+               the --relative option to create symlinks that use relative paths.
        EOF
 }
 
 function link()
 {
        EOF
 }
 
 function link()
 {
-       FORCE='no'   # If 'file', overwrite existing files.
-                    # If 'yes', overwrite existing files and dirs.
+       FORCE_DIR='no'    # If 'yes', overwrite existing directories.
+       FORCE_FILE='no'   # If 'yes', overwrite existing files.
+       FORCE_LINK='no'   # If 'yes', overwrite existing symlinks.
        DRY_RUN='no' # If 'yes', disable any actions that change the filesystem
        DRY_RUN='no' # If 'yes', disable any actions that change the filesystem
-       BACKUP='yes'
+       BACKUP_OPT='yes'
+       LINK_OPTS='-sv'
        while [ "${1::2}" = '--' ]; do
                case "${1}" in
                        '--force')
        while [ "${1::2}" = '--' ]; do
                case "${1}" in
                        '--force')
-                               FORCE='yes'
+                               FORCE_DIR='yes'
+                               FORCE_FILE='yes'
+                               FORCE_LINK='yes'
+                               ;;
+                       '--force-dir')
+                               FORCE_DIR='yes'
                                ;;
                        '--force-file')
                                ;;
                        '--force-file')
-                               FORCE='file'
+                               FORCE_FILE='yes'
+                               ;;
+                       '--force-link')
+                               FORCE_LINK='yes'
                                ;;
                        '--dry-run')
                                DRY_RUN='yes'
                                ;;
                        '--no-backup')
                                ;;
                        '--dry-run')
                                DRY_RUN='yes'
                                ;;
                        '--no-backup')
-                               BACKUP='no'
+                               BACKUP_OPT='no'
+                               ;;
+                       '--relative')
+                               LINK_OPTS="${LINK_OPTS} --relative"
                                ;;
                        *)
                                echo "ERROR: invalid option to link (${1})" >&2
                                ;;
                        *)
                                echo "ERROR: invalid option to link (${1})" >&2
@@ -566,17 +632,27 @@ function link()
        DOTFILES_SRC="${DOTFILES_DIR}/${REPO}/patched-src"
 
        while read FILE; do
        DOTFILES_SRC="${DOTFILES_DIR}/${REPO}/patched-src"
 
        while read FILE; do
+               BACKUP="${BACKUP_OPT}"
                if [ "${DOTFILES_SRC}/${FILE}" -ef "${TARGET}/${FILE}" ]; then
                if [ "${DOTFILES_SRC}/${FILE}" -ef "${TARGET}/${FILE}" ]; then
-                       continue  # already simlinked
-               fi
-               if [ -d "${DOTFILES_SRC}/${FILE}" ] && [ -d "${TARGET}/${FILE}" ] && \
-                       [ "${FORCE}" != 'yes' ]; then
-                       echo "use --force to override the existing directory: ${TARGET}/${FILE}"
-                       continue  # allow unlinked directories
-               fi
-               if [ -e "$TARGET/${FILE}" ] && [ "${FORCE}" = 'no' ]; then
-                       echo "use --force to override the existing target: ${TARGET}/${FILE}"
-                       continue  # target already exists
+                       if [ "${FORCE_LINK}" = 'no' ]; then
+                               # don't prompt about --force-link, because this will happen a lot
+                               continue  # already simlinked
+                       elif [ ! -h "${TARGET}/${FILE}" ]; then
+                               # target file/dir underneath an already symlinked dir
+                               continue
+                       else
+                               # don't backup links that already point to the right place
+                               BACKUP='no'
+                       fi
+               else
+                       if [ -d "${DOTFILES_SRC}/${FILE}" ] && [ -d "${TARGET}/${FILE}" ] && \
+                               [ "${FORCE_DIR}" = 'no' ]; then
+                               echo "use --force-dir to override the existing directory: ${TARGET}/${FILE}"
+                               continue  # allow unlinked directories
+                       elif [ -f "${TARGET}/${FILE}" ] && [ "${FORCE_FILE}" = 'no' ]; then
+                               echo "use --force-file to override the existing target: ${TARGET}/${FILE}"
+                               continue  # target already exists
+                       fi
                fi
                link_file "${REPO}" "${FILE}" || return 1
        done <<-EOF
                fi
                link_file "${REPO}" "${FILE}" || return 1
        done <<-EOF
@@ -612,7 +688,7 @@ function disconnect_help()
 function disconnect()
 {
        # multi-repo case handled in main() by run_on_all_repos()
 function disconnect()
 {
        # multi-repo case handled in main() by run_on_all_repos()
-       REPO=$(nonempty_option 'link' 'REPO' "${1}") || return 1
+       REPO=$(nonempty_option 'disconnect' 'REPO' "${1}") || return 1
        maxargs 'disconnect' 1 "${@}" || return 1
        DOTFILES_SRC="${DOTFILES_DIR}/${REPO}/patched-src"
 
        maxargs 'disconnect' 1 "${@}" || return 1
        DOTFILES_SRC="${DOTFILES_DIR}/${REPO}/patched-src"
 
@@ -627,7 +703,7 @@ function disconnect()
        BASHRC='no'
 
        while read FILE; do
        BASHRC='no'
 
        while read FILE; do
-               if [ "${FILE}" = '.bashrc' ] && [ "$TARGET" -ef "${HOME}" ]; then
+               if [ "${FILE}" = '.bashrc' ] && [ "${TARGET}" -ef "${HOME}" ]; then
                        BASHRC='yes'
                fi
                if [ "${DOTFILES_SRC}/${FILE}" -ef "${TARGET}/${FILE}" ] && [ -h "${TARGET}/${FILE}" ]; then
                        BASHRC='yes'
                fi
                if [ "${DOTFILES_SRC}/${FILE}" -ef "${TARGET}/${FILE}" ] && [ -h "${TARGET}/${FILE}" ]; then
@@ -677,7 +753,7 @@ function update_help()
 
        cat <<-EOF
 
 
        cat <<-EOF
 
-               usage: $0 ${COMMAND} [REPO]
+               usage: $0 ${COMMAND} [options] [REPO]
 
                Where 'REPO' is the name the dotfiles repository to update.
                If it is not given, all repositories will be updateed.
 
                Where 'REPO' is the name the dotfiles repository to update.
                If it is not given, all repositories will be updateed.
@@ -686,13 +762,21 @@ function update_help()
                to bring them in sync with the central repositories.  Keeps track
                of the last update time to avoid multiple fetches in the same
                week.
                to bring them in sync with the central repositories.  Keeps track
                of the last update time to avoid multiple fetches in the same
                week.
+
+               ${COMMAND} passes any options it receives through to the link
+               command.
        EOF
 }
 
 function update()
 {
        EOF
 }
 
 function update()
 {
+       LINK_OPTS=''
+       while [ "${1::2}" = '--' ]; do
+               LINK_OPTS="${LINK_FN_OPTS} ${1}"
+               shift
+       done
        # multi-repo case handled in main() by run_on_all_repos()
        # multi-repo case handled in main() by run_on_all_repos()
-       REPO=$(nonempty_option 'link' 'REPO' "${1}") || return 1
+       REPO=$(nonempty_option 'update' 'REPO' "${1}") || return 1
        maxargs 'disconnect' 1 "${@}" || return 1
 
        # Update once a week from our remote repository.  Mark updates by
        maxargs 'disconnect' 1 "${@}" || return 1
 
        # Update once a week from our remote repository.  Mark updates by
@@ -705,7 +789,7 @@ function update()
                "${TOUCH}" "${UPDATE_FILE}" || return 1
                fetch "${REPO}" || return 1
                patch "${REPO}" || return 1
                "${TOUCH}" "${UPDATE_FILE}" || return 1
                fetch "${REPO}" || return 1
                patch "${REPO}" || return 1
-               link "${REPO}" || return 1
+               link ${LINK_OPTS} "${REPO}" || return 1
                echo "${REPO} dotfiles updated"
        fi
 }
                echo "${REPO} dotfiles updated"
        fi
 }
@@ -756,8 +840,8 @@ function main()
                                echo "${VERSION}"
                                return
                                ;;
                                echo "${VERSION}"
                                return
                                ;;
-                       '--dotfiles-dir')
-                                       DOTFILES_DIR="${2}"
+                       '--dotfiles-dir')
+                               DOTFILES_DIR="${2}"
                                shift
                                ;;
                        '--target')
                                shift
                                ;;
                        '--target')
@@ -786,10 +870,10 @@ function main()
                        shift
                done
                if [ "${#}" -eq 0 ]; then
                        shift
                done
                if [ "${#}" -eq 0 ]; then
-                       run_on_all_repos "${COMMAND}" "${OPTIONS[@]}" || return 1
+                       run_on_all_repos "${COMMAND}" "${OPTIONS[@]}" || return 1
                else
                        maxargs "${0}" 1 "${@}" || return 1
                else
                        maxargs "${0}" 1 "${@}" || return 1
-                       "${COMMAND}" "${OPTIONS[@]}" "${1}" || return 1
+                       "${COMMAND}" "${OPTIONS[@]}" "${1}" || return 1
                fi
        fi
 }
                fi
        fi
 }