X-Git-Url: http://git.tremily.us/?p=dotfiles-framework.git;a=blobdiff_plain;f=dotfiles.sh;h=5f0c5e5cb9cb8f4202fa42db8fcd4ff7926d65ca;hp=45c3f974f38986b54868e53e96e0a639c1be0e7b;hb=HEAD;hpb=13c41a039fc5d3d537e4a2356c886660fad5a7b0 diff --git a/dotfiles.sh b/dotfiles.sh index 45c3f97..5f0c5e5 100755 --- a/dotfiles.sh +++ b/dotfiles.sh @@ -2,8 +2,23 @@ # # Dotfiles management script. For details, run # $ dotfiles.sh --help +# +# Copyright (C) 2011-2015 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' +VERSION='0.4' DOTFILES_DIR="${PWD}" TARGET=~ CHECK_WGET_TYPE_AND_ENCODING='no' @@ -11,17 +26,30 @@ CHECK_WGET_TYPE_AND_ENCODING='no' ##### # 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 + +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 + 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 @@ -99,11 +130,19 @@ function list_files() done < <(cd "${DIR}" && find .) } -# Global variable to allow passing associative arrats between functions -declare -A REPO_SOURCE_DATA +# Global variable to allow passing associative arrays between functions + +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 @@ -128,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['url']=$( + git --git-dir "${REPO}/.git/" config remote.origin.url) else echo "ERROR: no source location found for ${REPO}" >&2 return 1 @@ -136,6 +182,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 +218,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,14 +230,17 @@ function wget_fetch() 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 @@ -202,8 +260,9 @@ function link_file() 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 } @@ -253,7 +312,6 @@ function clone() echo "ERROR: destination path (${REPO}) already exists." >&2 return 1 fi - mkdir -p "${REPO}" CACHE_SOURCE='yes' FETCH='yes' case "${TRANSFER}" in @@ -263,6 +321,7 @@ function clone() "${GIT}" clone "${URL}" "${REPO}" || return 1 ;; 'wget') + mkdir -p "${REPO}" ;; *) echo "PROGRAMMING ERROR: add ${TRANSFER} support to clone command" >&2 @@ -277,6 +336,49 @@ function clone() 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 @@ -301,10 +403,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 @@ -314,7 +421,7 @@ function fetch() } ### -# fetch command +# diff command COMMANDS+=('diff') @@ -348,17 +455,17 @@ function diff() { MODE='standard' while [ "${1::2}" = '--' ]; do - case "${1}" in - '--removed') - MODE='removed' - ;; - '--local-patch') - MODE='local-patch' - ;; - *) - echo "ERROR: invalid option to diff (${1})" >&2 - return 1 - esac + case "${1}" in + '--removed') + MODE='removed' + ;; + '--local-patch') + 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 +485,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 +496,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 +537,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 +545,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 +571,59 @@ 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] [--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-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' + ;; + '--relative') + LINK_OPTS="${LINK_OPTS} --relative" + ;; + *) + 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 +632,27 @@ 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 + 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 @@ -554,7 +688,7 @@ function disconnect_help() 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" @@ -569,7 +703,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 @@ -619,7 +753,7 @@ function update_help() 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. @@ -628,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. + + ${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 @@ -647,7 +789,7 @@ function update() "${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 } @@ -689,27 +831,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,10 +870,10 @@ 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 + "${COMMAND}" "${OPTIONS[@]}" "${1}" || return 1 fi fi }