X-Git-Url: http://git.tremily.us/?a=blobdiff_plain;f=dotfiles.sh;h=cf3af80cbdc0925dea8f49bbd3fd52ff34c49f8d;hb=4b2cc72e9d4875658425a8a0f302c074e45d78dd;hp=1e7b8a61cb2f08a4c2b9cf83074510217e8d3027;hpb=78231cae87795e879d8e6f59a0880cc8e1c73176;p=dotfiles-framework.git diff --git a/dotfiles.sh b/dotfiles.sh index 1e7b8a6..cf3af80 100755 --- a/dotfiles.sh +++ b/dotfiles.sh @@ -2,6 +2,21 @@ # # Dotfiles management script. For details, run # $ dotfiles.sh --help +# +# Copyright (C) 2011-2012 W. Trevor King +# +# 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 . VERSION='0.2' DOTFILES_DIR="${PWD}" @@ -23,6 +38,19 @@ TAR=$(which tar) 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 @@ -76,12 +104,15 @@ function get_selection() function run_on_all_repos() { COMMAND="${1}" + shift if [ -z "${REPO}" ]; then # run on all repositories 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 @@ -100,10 +131,18 @@ function list_files() } # 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 @@ -114,6 +153,11 @@ function set_repo_source() # 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 @@ -136,6 +180,17 @@ function get_repo_source() 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 @@ -161,11 +216,9 @@ function wget_fetch() 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 @@ -175,6 +228,7 @@ function wget_fetch() fi } + # usage: link_file REPO FILE # # Create the symbolic link to the version of FILE in the REPO @@ -203,7 +257,7 @@ function link_file() echo "link ${TARGET}/${FILE} to ${DOTFILES_DIR}/${REPO}/patched-src/${FILE}" else echo -n 'link ' - "${LN}" -sv "${DOTFILES_DIR}/${REPO}/patched-src/${FILE}" "${TARGET}/${FILE}" || return 1 + "${LN}" -rsv "${DOTFILES_DIR}/${REPO}/patched-src/${FILE}" "${TARGET}/${FILE}" || return 1 fi } @@ -301,10 +355,15 @@ function fetch() # 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 @@ -349,16 +408,16 @@ function diff() MODE='standard' while [ "${1::2}" = '--' ]; do case "${1}" in - '--removed') - MODE='removed' - ;; + '--removed') + MODE='removed' + ;; '--local-patch') - MODE='local-patch' - ;; - *) - echo "ERROR: invalid option to diff (${1})" >&2 - return 1 - esac + MODE='local-patch' + ;; + *) + echo "ERROR: invalid option to diff (${1})" >&2 + return 1 + esac shift done # multi-repo case handled in main() by run_on_all_repos() @@ -378,7 +437,7 @@ function diff() exec 3<&1 # save stdout to file descriptor 3 echo "save local removed to ${REPO}/local-patch/000-local.remove" exec 1>"${REPO}/local-patch/000-local.remove" # redirect stdout - diff "${REPO}" --removed + diff --removed "${REPO}" exec 1<&3 # restore old stdout exec 3<&- # close temporary fd 3 return @@ -389,10 +448,8 @@ function diff() 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") @@ -432,7 +489,7 @@ function patch() 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 @@ -440,6 +497,7 @@ function patch() # 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 @@ -465,41 +523,52 @@ function link_help() 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] [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. 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' while [ "${1::2}" = '--' ]; do - case "${1}" in - '--force') - FORCE='yes' - ;; - '--force-file') - FORCE='file' - ;; - '--dry-run') - DRY_RUN='yes' - ;; - '--no-backup') - BACKUP='no' - ;; - *) - echo "ERROR: invalid option to link (${1})" >&2 - return 1 - esac + case "${1}" in + '--force') + FORCE_DIR='yes' + FORCE_FILE='yes' + FORCE_LINK='yes' + ;; + '--force-dir') + FORCE_DIR='yes' + ;; + '--force-file') + FORCE_FILE='yes' + ;; + '--force-link') + FORCE_LINK='yes' + ;; + '--dry-run') + DRY_RUN='yes' + ;; + '--no-backup') + BACKUP_OPT='no' + ;; + *) + echo "ERROR: invalid option to link (${1})" >&2 + return 1 + esac shift done # multi-repo case handled in main() by run_on_all_repos() @@ -508,17 +577,24 @@ function link() 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 @@ -569,7 +645,7 @@ function disconnect() 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 @@ -689,27 +765,27 @@ function main() { COMMAND='' while [ "${1::2}" = '--' ]; do - case "${1}" in - '--help') - main_help || return 1 - return - ;; - '--version') - echo "${VERSION}" - return - ;; - '--dotfiles-dir') - DOTFILES_DIR="${2}" - shift - ;; - '--target') - TARGET="${2}" - shift - ;; - *) - echo "ERROR: invalid option to ${0} (${1})" >&2 - return 1 - esac + case "${1}" in + '--help') + main_help || return 1 + return + ;; + '--version') + echo "${VERSION}" + return + ;; + '--dotfiles-dir') + DOTFILES_DIR="${2}" + shift + ;; + '--target') + TARGET="${2}" + shift + ;; + *) + echo "ERROR: invalid option to ${0} (${1})" >&2 + return 1 + esac shift done COMMAND=$(get_selection "${1}" "${COMMANDS[@]}") || return 1 @@ -728,7 +804,7 @@ function main() 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