#
# Dotfiles management script. For details, run
# $ dotfiles.sh --help
+#
+# Copyright (C) 2011-2012 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}"
TOUCH=$(which touch)
WGET=$(which wget)
+#####
+# Compatibility checks
+
+BASH="${BASH_VERSION%.*}"
+BASH_MAJOR="${BASH%.*}"
+BASH_MINOR="${BASH#*.}"
+
+if [ "${BASH_MAJOR}" -eq 3 ] && [ "${BASH_MINOR}" -eq 0 ]; then
+ echo "ERROR: ${0} requires Bash version >= 3.1" >&2
+ echo "you're running ${BASH}, which doesn't support += array assignment" >&2
+ exit 1
+fi
+
#####
# Utility functions
for REPO in *; do
if [ "${REPO}" = '*' ]; then
break # no known repositories
+ elif [ -f "${REPO}" ]; then
+ continue # repositories are directories
fi
- "${COMMAND}" "${REPO}" "${@}" || return 1
+ "${COMMAND}" "${@}" "${REPO}" || return 1
done
return
fi
}
# Global variable to allow passing associative arrats between functions
-declare -A REPO_SOURCE_DATA
+
+if [ "${BASH_MAJOR}" -ge 4 ]; then
+ declare -A REPO_SOURCE_DATA
+fi
function set_repo_source()
{
+ if [ "${BASH_MAJOR}" -lt 4 ]; then
+ echo "ERROR: ${0}'s set_repo_source requires Bash version >= 4.0" >&2
+ echo "you're running ${BASH}, which doesn't support associative arrays" >&2
+ return 1
+ fi
REPO=$(nonempty_option 'set_repo_source' 'REPO' "${1}") || return 1
> "${REPO}/source_cache" || return 1
for KEY in "${!REPO_SOURCE_DATA[@]}"; do
# usage: get_repo_source REPO
function get_repo_source()
{
+ if [ "${BASH_MAJOR}" -lt 4 ]; then
+ echo "ERROR: ${0}'s get_repo_source() requires Bash version >= 4.0" >&2
+ echo "you're running ${BASH}, which doesn't support associative arrays" >&2
+ return 1
+ fi
REPO=$(nonempty_option 'get_repo_source' 'REPO' "${1}") || return 1
REPO_SOURCE_DATA=()
if [ -f "${REPO}/source_cache" ]; then
fi
}
+function git_fetch()
+{
+ REPO=$(nonempty_option 'git_fetch' 'REPO' "${1}") || return 1
+ REMOTES=$(cd "${REPO}" && "${GIT}" remote) || return 1
+ if [ -n "${REMOTES}" ]; then
+ (cd "${REPO}" && "${GIT}" pull) || return 1
+ else
+ echo "no remote repositories found for ${REPO}"
+ fi
+}
+
function wget_fetch()
{
REPO=$(nonempty_option 'wget_fetch' 'REPO' "${1}") || return 1
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
fi
}
+
# 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
}
# multi-repo case handled in main() by run_on_all_repos()
REPO=$(nonempty_option 'fetch' 'REPO' "${1}") || return 1
maxargs 'fetch' 1 "${@}" || return 1
- get_repo_source "${REPO}" || return 1
- TRANSFER=$(nonempty_option 'fetch' 'TRANSFER' "${REPO_SOURCE_DATA['transfer']}") || return 1
+ if [ "${BASH_MAJOR}" -ge 4 ]; then
+ get_repo_source "${REPO}" || return 1
+ TRANSFER=$(nonempty_option 'fetch' 'TRANSFER' "${REPO_SOURCE_DATA['transfer']}") || return 1
+ else
+ echo "WARNING: Bash version < 4.0, assuming all repos use git transfer" >&2
+ TRANSFER='git'
+ fi
if [ "${TRANSFER}" = 'git' ]; then
- "${GIT}" --git-dir "${REPO}/.git" pull || return 1
+ git_fetch "${REPO}" || return 1
elif [ "${TRANSFER}" = 'wget' ]; then
wget_fetch "${REPO}" || return 1
else
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")
if [ -f "${FILE}" ]; then
echo "apply ${FILE}"
pushd "${REPO}/patched-src/" > /dev/null || return 1
- "${PATCH}" -p0 < "../../${FILE}" || return 1
+ "${PATCH}" -p1 < "../../${FILE}" || return 1
popd > /dev/null || return 1
fi
done
# remove any files marked for removal in local-patch
for REMOVE in "${REPO}/local-patch"/*.remove; do
if [ -f "${REMOVE}" ]; then
+ echo "apply ${FILE}"
while read LINE; do
if [ -z "${LINE}" ] || [ "${LINE:0:1}" = '#' ]; then
continue # ignore blank lines and comments
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
+ 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
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