#
# Dotfiles management script. For details, run
# $ dotfiles.sh --help
+#
+# Copyright (C) 2011-2013 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# 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'
DOTFILES_DIR="${PWD}"
#####
# 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:-$(which diff)}
+GIT=${DOTFILES_GIT:-$(which git)}
+LN=${DOTFILES_LN:-$(which ln)}
+MV=${DOTFILES_MV:-$(which mv)}
+PATCH=${DOTFILES_PATCH:-$(which patch)}
+SED=${DOTFILES_SED:-$(which sed)}
+RM=${DOTFILES_RM:-$(which rm)}
+RSYNC=${DOTFILES_RSYNC:-$(which rsync)}
+TAR=${DOTFILES_TAR:-$(which tar)}
+TOUCH=${DOTFILES_TOUCH:-$(which touch)}
+WGET=${DOTFILES_WGET:-$(which wget)}
#####
# Compatibility checks
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
+ continue # repositories are directories
fi
"${COMMAND}" "${@}" "${REPO}" || return 1
done
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 [ -n "${SERVER_ETAG}" ]; then # store new ETag
REPO_SOURCE_DATA['etag']="${SERVER_ETAG}"
set_repo_source "${REPO}" || return 1
- else
- if [ -n "${ETAG}" ]; then # clear old ETag
- unset "${REPO_SOURCE_DATA['etag']}"
- set_repo_source "${REPO}" || return 1
- fi
+ elif [ -n "${ETAG}" ]; then # clear old ETag
+ unset "${REPO_SOURCE_DATA['etag']}"
+ set_repo_source "${REPO}" || return 1
fi
echo "extracting ${BUNDLE} to ${REPO}"
"${TAR}" -xf "${BUNDLE}" -C "${REPO}" --strip-components 1 --overwrite || return 1
# 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
+ 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 [ "${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 '
- "${LN}" -sv "${DOTFILES_DIR}/${REPO}/patched-src/${FILE}" "${TARGET}/${FILE}" || return 1
+ "${LN}" ${LINK_OPTS} "${SOURCE}" "${TARGET}/${FILE}" || return 1
fi
}
}
###
-# fetch command
+# diff command
COMMANDS+=('diff')
'--removed')
MODE='removed'
;;
- '--local-patch')
+ '--local-patch')
MODE='local-patch'
;;
*)
if [ ! -e "${TARGET}/${FILE}" ]; then
echo "${FILE}"
fi
- else
- if [ -f "${TARGET}/${FILE}" ]; then
- (cd "${REPO}/src" && "${DIFF}" -u "${FILE}" "${TARGET}/${FILE}")
- fi
+ elif [ -f "${TARGET}/${FILE}" ]; then
+ (cd "${REPO}/src" && "${DIFF}" -u "${FILE}" "${TARGET}/${FILE}")
fi
done <<-EOF
$(list_files "${REPO}/src")
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.
- 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()
{
- 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
- BACKUP='yes'
+ BACKUP_OPT='yes'
+ LINK_OPTS='-sv'
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='yes'
+ ;;
+ '--force-link')
+ FORCE_LINK='yes'
;;
'--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
DOTFILES_SRC="${DOTFILES_DIR}/${REPO}/patched-src"
while read FILE; do
+ BACKUP="${BACKUP_OPT}"
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
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"
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
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.
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()
{
+ 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()
- 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
"${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 "${VERSION}"
return
;;
- '--dotfiles-dir')
- DOTFILES_DIR="${2}"
+ '--dotfiles-dir')
+ DOTFILES_DIR="${2}"
shift
;;
'--target')
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
- "${COMMAND}" "${OPTIONS[@]}" "${1}" || return 1
+ "${COMMAND}" "${OPTIONS[@]}" "${1}" || return 1
fi
fi
}