3 # Dotfiles management script. For details, run
6 # Copyright (C) 2011-2015 W. Trevor King <wking@tremily.us>
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 CHECK_WGET_TYPE_AND_ENCODING='no'
29 DIFF=${DOTFILES_DIFF:-$(command -v diff)}
30 GIT=${DOTFILES_GIT:-$(command -v git)}
31 LN=${DOTFILES_LN:-$(command -v ln)}
32 MV=${DOTFILES_MV:-$(command -v mv)}
33 PATCH=${DOTFILES_PATCH:-$(command -v patch)}
34 SED=${DOTFILES_SED:-$(command -v sed)}
35 RM=${DOTFILES_RM:-$(command -v rm)}
36 RSYNC=${DOTFILES_RSYNC:-$(command -v rsync)}
37 TAR=${DOTFILES_TAR:-$(command -v tar)}
38 TOUCH=${DOTFILES_TOUCH:-$(command -v touch)}
39 WGET=${DOTFILES_WGET:-$(command -v wget)}
42 # Compatibility checks
44 BASH="${BASH_VERSION%.*}"
45 BASH_MAJOR="${BASH%.*}"
46 BASH_MINOR="${BASH#*.}"
48 if [ "${BASH_MAJOR}" -eq 3 ] && [ "${BASH_MINOR}" -eq 0 ]; then
49 echo "ERROR: ${0} requires Bash version >= 3.1" >&2
50 echo "you're running ${BASH}, which doesn't support += array assignment" >&2
57 # usage: nonempty_option LOC NAME VALUE
58 function nonempty_option()
63 if [ -z "${VALUE}" ]; then
64 echo "ERROR: empty value for ${NAME} in ${LOC}" >&2
70 # usage: maxargs LOC MAX "${@}"
72 # Print and error and return 1 if there are more than MAX arguments.
78 if [ "${#}" -gt "${MAX}" ]; then
79 echo "ERROR: too many arguments (${#} > ${MAX}) in ${LOC}" >&2
84 # usage: get_selection CHOICE OPTION ...
86 # Check that CHOICE is one of the valid options listed in OPTION. If
87 # it is, echo the choice and return 0, otherwise print an error to
88 # stderr and return 1.
89 function get_selection()
94 if [ "${OPT}" = "${CHOICE}" ]; then
99 echo "ERROR: invalid selection (${CHOICE})" >&2
100 echo "valid choices: ${@}" >&2
104 function run_on_all_repos()
108 if [ -z "${REPO}" ]; then # run on all repositories
110 if [ "${REPO}" = '*' ]; then
111 break # no known repositories
112 elif [ -f "${REPO}" ]; then
113 continue # repositories are directories
115 "${COMMAND}" "${@}" "${REPO}" || return 1
121 function list_files()
123 DIR=$(nonempty_option 'list_files' 'DIR' "${1}") || return 1
125 if [ "${FILE}" = '.' ]; then
128 FILE="${FILE:2}" # strip the leading './'
130 done < <(cd "${DIR}" && find .)
133 # Global variable to allow passing associative arrays between functions
135 if [ "${BASH_MAJOR}" -ge 4 ]; then
136 declare -A REPO_SOURCE_DATA
139 function set_repo_source()
141 if [ "${BASH_MAJOR}" -lt 4 ]; then
142 echo "ERROR: ${0}'s set_repo_source requires Bash version >= 4.0" >&2
143 echo "you're running ${BASH}, which doesn't support associative arrays" >&2
146 REPO=$(nonempty_option 'set_repo_source' 'REPO' "${1}") || return 1
147 > "${REPO}/source_cache" || return 1
148 for KEY in "${!REPO_SOURCE_DATA[@]}"; do
149 echo "${KEY}=${REPO_SOURCE_DATA[${KEY}]}" >> "${REPO}/source_cache" || return 1
153 # usage: get_repo_source REPO
154 function get_repo_source()
156 if [ "${BASH_MAJOR}" -lt 4 ]; then
157 echo "ERROR: ${0}'s get_repo_source() requires Bash version >= 4.0" >&2
158 echo "you're running ${BASH}, which doesn't support associative arrays" >&2
161 REPO=$(nonempty_option 'get_repo_source' 'REPO' "${1}") || return 1
163 if [ -f "${REPO}/source_cache" ]; then
167 REPO_SOURCE_DATA["${KEY}"]="${VALUE}"
168 done < "${REPO}/source_cache"
170 # autodetect verson control system
172 REPO_SOURCE_DATA['repo']="${REPO}"
173 if [ -d "${REPO}/.git" ]; then
174 REPO_SOURCE_DATA['transfer']='git'
175 REPO_SOURCE_DATA['url']=$(
176 git --git-dir "${REPO}/.git/" config remote.origin.url)
178 echo "ERROR: no source location found for ${REPO}" >&2
181 # no need to get further fields for these transfer mechanisms
187 REPO=$(nonempty_option 'git_fetch' 'REPO' "${1}") || return 1
188 REMOTES=$(cd "${REPO}" && "${GIT}" remote) || return 1
189 if [ -n "${REMOTES}" ]; then
190 (cd "${REPO}" && "${GIT}" pull) || return 1
192 echo "no remote repositories found for ${REPO}"
196 function wget_fetch()
198 REPO=$(nonempty_option 'wget_fetch' 'REPO' "${1}") || return 1
199 # get_repo_source() was just called on this repo in fetch()
200 TRANSFER=$(nonempty_option 'wget_fetch' 'TRANSFER' "${REPO_SOURCE_DATA['transfer']}") || return 1
201 URL=$(nonempty_option 'wget_fetch' 'URL' "${REPO_SOURCE_DATA['url']}") || return 1
202 ETAG="${REPO_SOURCE_DATA['etag']}"
204 HEAD=$("${WGET}" --server-response --spider "${URL}" 2>&1) || return 1
205 SERVER_ETAG=$(echo "${HEAD}" | "${SED}" -n 's/^ *etag: *"\(.*\)"/\1/ip') || return 1
206 if [ "${CHECK_WGET_TYPE_AND_ENCODING}" = 'yes' ]; then
207 TYPE=$(echo "${HEAD}" | "${SED}" -n 's/^ *content-type: *//ip') || return 1
208 ENCODING=$(echo "${HEAD}" | "${SED}" -n 's/^ *content-encoding: *//ip') || return 1
209 if [ "${TYPE}" != 'application/x-gzip' ] || [ "${ENCODING}" != 'x-gzip' ]; then
210 echo "ERROR: invalid content type (${TYPE}) or encoding (${ENCODING})." >&2
211 echo "while fetching ${URL}" >&2
215 if [ -z "${ETAG}" ] || [ "${SERVER_ETAG}" != "${ETAG}" ]; then
216 # Previous ETag not known, or ETag changed. Download new copy.
217 "${WGET}" --output-document "${BUNDLE}" "${URL}" || return 1
218 if [ -n "${SERVER_ETAG}" ]; then # store new ETag
219 REPO_SOURCE_DATA['etag']="${SERVER_ETAG}"
220 set_repo_source "${REPO}" || return 1
221 elif [ -n "${ETAG}" ]; then # clear old ETag
222 unset "${REPO_SOURCE_DATA['etag']}"
223 set_repo_source "${REPO}" || return 1
225 echo "extracting ${BUNDLE} to ${REPO}"
226 "${TAR}" -xf "${BUNDLE}" -C "${REPO}" --strip-components 1 --overwrite || return 1
227 "${RM}" -f "${BUNDLE}" || return 1
229 echo "already downloaded the ETag=${ETAG} version of ${URL}"
234 # usage: link_file REPO FILE
236 # Create the symbolic link to the version of FILE in the REPO
237 # repository, overriding the target if it exists. If you want to
238 # override the options passed to ${LN}, set LINK_OPTS.
241 REPO=$(nonempty_option 'link_file' 'REPO' "${1}") || return 1
242 FILE=$(nonempty_option 'link_file' 'FILE' "${2}") || return 1
243 LINK_OPTS="${LINK_OPTS:--sv}" # default to `-sv`
244 if [ "${BACKUP}" = 'yes' ]; then
245 if [ -e "${TARGET}/${FILE}" ] || [ -h "${TARGET}/${FILE}" ]; then
246 if [ "${DRY_RUN}" = 'yes' ]; then
247 echo "move ${TARGET}/${FILE} to ${TARGET}/${FILE}.bak"
250 mv -v "${TARGET}/${FILE}" "${TARGET}/${FILE}.bak" || return 1
254 if [ "${DRY_RUN}" = 'yes' ]; then
255 echo "rm ${TARGET}/${FILE}"
257 "${RM}" -fv "${TARGET}/${FILE}"
260 if [ "${DRY_RUN}" = 'yes' ]; then
261 echo "link ${TARGET}/${FILE} to ${DOTFILES_DIR}/${REPO}/patched-src/${FILE}"
263 SOURCE="${DOTFILES_DIR}/${REPO}/patched-src/${FILE}"
265 "${LN}" ${LINK_OPTS} "${SOURCE}" "${TARGET}/${FILE}" || return 1
272 # An array of available commands
280 CLONE_TRANSFERS=('git' 'wget')
282 function clone_help()
284 echo 'Create a new dotfiles repository.'
285 if [ "${1}" = '--one-line' ]; then return; fi
289 usage: $0 ${COMMAND} REPO TRANSFER URL
291 Where 'REPO' is the name the dotfiles repository to create,
292 'TRANSFER' is the transfer mechanism, and 'URL' is the URL for the
293 remote repository. Valid TRANSFERs are:
295 ${CLONE_TRANSFERS[@]}
299 $0 clone public wget http://example.com/public-dotfiles.tar.gz
300 $0 clone private git ssh://example.com/~/private-dotfiles.git
306 REPO=$(nonempty_option 'clone' 'REPO' "${1}") || return 1
307 TRANSFER=$(nonempty_option 'clone' 'TRANSFER' "${2}") || return 1
308 URL=$(nonempty_option 'clone' 'URL' "${3}") || return 1
309 maxargs 'clone' 3 "${@}" || return 1
310 TRANSFER=$(get_selection "${TRANSFER}" "${CLONE_TRANSFERS[@]}") || return 1
311 if [ -e "${REPO}" ]; then
312 echo "ERROR: destination path (${REPO}) already exists." >&2
317 case "${TRANSFER}" in
321 "${GIT}" clone "${URL}" "${REPO}" || return 1
327 echo "PROGRAMMING ERROR: add ${TRANSFER} support to clone command" >&2
330 if [ "${CACHE_SOURCE}" = 'yes' ]; then
331 REPO_SOURCE_DATA=(['transfer']="${TRANSFER}" ['url']="${URL}")
332 set_repo_source "${REPO}" || return 1
334 if [ "${FETCH}" = 'yes' ]; then
335 fetch "${REPO}" || return 1
346 echo 'List current dotfiles repositories.'
347 if [ "${1}" = '--one-line' ]; then return; fi
351 usage: $0 ${COMMAND} [REPO]
353 List information for 'REPO' in a form simular to the 'clone'
354 command's arguments. If 'REPO' is not give, all repositories will
358 public wget http://example.com/public-dotfiles.tar.gz
360 public wget http://example.com/public-dotfiles.tar.gz
361 private git ssh://example.com/~/private-dotfiles.git
367 # multi-repo case handled in main() by run_on_all_repos()
368 REPO=$(nonempty_option 'list' 'REPO' "${1}") || return 1
369 maxargs 'list' 1 "${@}" || return 1
370 if [ "${BASH_MAJOR}" -ge 4 ]; then
371 get_repo_source "${REPO}" || return 1
372 TRANSFER=$(nonempty_option 'list' 'TRANSFER' "${REPO_SOURCE_DATA['transfer']}") || return 1
373 URL=$(nonempty_option 'list' 'URL' "${REPO_SOURCE_DATA['url']}") || return 1
375 echo "WARNING: Bash version < 4.0, cannot use assuming all repos use git transfer" >&2
377 URL=$(git --git-dir "${REPO}/.git/" config remote.origin.url)
379 echo "${REPO} ${TRANSFER} ${URL}"
387 function fetch_help()
389 echo 'Get the current dotfiles from the server.'
390 if [ "${1}" = '--one-line' ]; then return; fi
394 usage: $0 ${COMMAND} [REPO]
396 Where 'REPO' is the name the dotfiles repository to fetch. If it
397 is not given, all repositories will be fetched.
403 # multi-repo case handled in main() by run_on_all_repos()
404 REPO=$(nonempty_option 'fetch' 'REPO' "${1}") || return 1
405 maxargs 'fetch' 1 "${@}" || return 1
406 if [ "${BASH_MAJOR}" -ge 4 ]; then
407 get_repo_source "${REPO}" || return 1
408 TRANSFER=$(nonempty_option 'fetch' 'TRANSFER' "${REPO_SOURCE_DATA['transfer']}") || return 1
410 echo "WARNING: Bash version < 4.0, assuming all repos use git transfer" >&2
413 if [ "${TRANSFER}" = 'git' ]; then
414 git_fetch "${REPO}" || return 1
415 elif [ "${TRANSFER}" = 'wget' ]; then
416 wget_fetch "${REPO}" || return 1
418 echo "PROGRAMMING ERROR: add ${TRANSFER} support to fetch command" >&2
430 echo 'Show differences between targets and dotfiles repositories.'
431 if [ "${1}" = '--one-line' ]; then return; fi
435 usage: $0 ${COMMAND} [--removed|--local-patch] [REPO]
437 Where 'REPO' is the name the dotfiles repository to query. If it
438 is not given, all repositories will be queried.
440 By default, ${COMMAND} will list differences between files that
441 exist in both the target location and the dotfiles repository (as
442 a patch that could be applied to the dotfiles source).
444 With the '--removed' option, ${COMMAND} will list files that
445 should be removed from the dotfiles source in order to match the
448 With the '--local-patch' option, ${COMMAND} will create files in
449 list files that should be removed from the dotfiles source in
450 order to match the target.
457 while [ "${1::2}" = '--' ]; do
466 echo "ERROR: invalid option to diff (${1})" >&2
471 # multi-repo case handled in main() by run_on_all_repos()
472 REPO=$(nonempty_option 'diff' 'REPO' "${1}") || return 1
473 maxargs 'diff' 1 "${@}" || return 1
475 if [ "${MODE}" = 'local-patch' ]; then
476 mkdir -p "${REPO}/local-patch" || return 1
478 exec 3<&1 # save stdout to file descriptor 3
479 echo "save local patches to ${REPO}/local-patch/000-local.patch"
480 exec 1>"${REPO}/local-patch/000-local.patch" # redirect stdout
482 exec 1<&3 # restore old stdout
483 exec 3<&- # close temporary fd 3
485 exec 3<&1 # save stdout to file descriptor 3
486 echo "save local removed to ${REPO}/local-patch/000-local.remove"
487 exec 1>"${REPO}/local-patch/000-local.remove" # redirect stdout
488 diff --removed "${REPO}"
489 exec 1<&3 # restore old stdout
490 exec 3<&- # close temporary fd 3
495 if [ "${MODE}" = 'removed' ]; then
496 if [ ! -e "${TARGET}/${FILE}" ]; then
499 elif [ -f "${TARGET}/${FILE}" ]; then
500 (cd "${REPO}/src" && "${DIFF}" -u "${FILE}" "${TARGET}/${FILE}")
503 $(list_files "${REPO}/src")
512 function patch_help()
514 echo 'Patch a fresh checkout with local adjustments.'
515 if [ "${1}" = '--one-line' ]; then return; fi
519 usage: $0 ${COMMAND} [REPO]
521 Where 'REPO' is the name the dotfiles repository to patch. If it
522 is not given, all repositories will be patched.
528 # multi-repo case handled in main() by run_on_all_repos()
529 REPO=$(nonempty_option 'patch' 'REPO' "${1}") || return 1
530 maxargs 'patch' 1 "${@}" || return 1
532 echo "copy clean checkout into ${REPO}/patched-src"
533 "${RSYNC}" -avz --delete "${REPO}/src/" "${REPO}/patched-src/" || return 1
535 # apply all the patches in local-patch/
536 for FILE in "${REPO}/local-patch"/*.patch; do
537 if [ -f "${FILE}" ]; then
539 pushd "${REPO}/patched-src/" > /dev/null || return 1
540 "${PATCH}" -p1 < "../../${FILE}" || return 1
541 popd > /dev/null || return 1
545 # remove any files marked for removal in local-patch
546 for REMOVE in "${REPO}/local-patch"/*.remove; do
547 if [ -f "${REMOVE}" ]; then
550 if [ -z "${LINE}" ] || [ "${LINE:0:1}" = '#' ]; then
551 continue # ignore blank lines and comments
553 if [ -e "${REPO}/patched-src/${LINE}" ]; then
554 echo "remove ${LINE}"
555 "${RM}" -rf "${REPO}/patched-src/${LINE}"
569 echo 'Link a fresh checkout with local adjustments.'
570 if [ "${1}" = '--one-line' ]; then return; fi
574 usage: $0 ${COMMAND} [--force] [--force-dir] [--force-file] [--force-link]
575 [--dry-run] [--no-backup] [--relative] [REPO]
577 Where 'REPO' is the name the dotfiles repository to link. If it
578 is not given, all repositories will be linked.
580 By default, ${COMMAND} only replaces missing directories, files,
581 simlinks. You can optionally overwrite any local stuff by passing
582 the --force option. If you only want to overwrite a particular
583 type, use the more granular --force-dir, etc.
585 If you have coreutils 8.16 (2012-03-26) or greater, you can set
586 the --relative option to create symlinks that use relative paths.
592 FORCE_DIR='no' # If 'yes', overwrite existing directories.
593 FORCE_FILE='no' # If 'yes', overwrite existing files.
594 FORCE_LINK='no' # If 'yes', overwrite existing symlinks.
595 DRY_RUN='no' # If 'yes', disable any actions that change the filesystem
598 while [ "${1::2}" = '--' ]; do
621 LINK_OPTS="${LINK_OPTS} --relative"
624 echo "ERROR: invalid option to link (${1})" >&2
629 # multi-repo case handled in main() by run_on_all_repos()
630 REPO=$(nonempty_option 'link' 'REPO' "${1}") || return 1
631 maxargs 'link' 1 "${@}" || return 1
632 DOTFILES_SRC="${DOTFILES_DIR}/${REPO}/patched-src"
635 BACKUP="${BACKUP_OPT}"
636 if [ "${DOTFILES_SRC}/${FILE}" -ef "${TARGET}/${FILE}" ]; then
637 if [ "${FORCE_LINK}" = 'no' ]; then
638 # don't prompt about --force-link, because this will happen a lot
639 continue # already simlinked
640 elif [ ! -h "${TARGET}/${FILE}" ]; then
641 # target file/dir underneath an already symlinked dir
644 # don't backup links that already point to the right place
648 if [ -d "${DOTFILES_SRC}/${FILE}" ] && [ -d "${TARGET}/${FILE}" ] && \
649 [ "${FORCE_DIR}" = 'no' ]; then
650 echo "use --force-dir to override the existing directory: ${TARGET}/${FILE}"
651 continue # allow unlinked directories
652 elif [ -f "${TARGET}/${FILE}" ] && [ "${FORCE_FILE}" = 'no' ]; then
653 echo "use --force-file to override the existing target: ${TARGET}/${FILE}"
654 continue # target already exists
657 link_file "${REPO}" "${FILE}" || return 1
659 $(list_files "${DOTFILES_SRC}")
666 COMMANDS+=('disconnect')
668 function disconnect_help()
670 echo 'Freeze dotfiles at their current state.'
671 if [ "${1}" = '--one-line' ]; then return; fi
675 usage: $0 ${COMMAND} [REPO]
677 Where 'REPO' is the name the dotfiles repository to disconnect.
678 If it is not given, all repositories will be disconnected.
680 You're about to give your sysadmin account to some newbie, and
681 they'd just be confused by all this efficiency. This script
682 freezes your dotfiles in their current state and makes everthing
683 look normal. Note that this will delete your dotfiles repository
684 and strip the dotfiles portion from your ~/.bashrc file.
688 function disconnect()
690 # multi-repo case handled in main() by run_on_all_repos()
691 REPO=$(nonempty_option 'disconnect' 'REPO' "${1}") || return 1
692 maxargs 'disconnect' 1 "${@}" || return 1
693 DOTFILES_SRC="${DOTFILES_DIR}/${REPO}/patched-src"
695 # See if we've constructed any patched source files that might be
696 # possible link targets
697 if [ ! -d "${DOTFILES_SRC}" ]; then
698 echo 'no installed dotfiles to disconnect'
702 # See if the bashrc file is involved with dotfiles at all
706 if [ "${FILE}" = '.bashrc' ] && [ "${TARGET}" -ef "${HOME}" ]; then
709 if [ "${DOTFILES_SRC}/${FILE}" -ef "${TARGET}/${FILE}" ] && [ -h "${TARGET}/${FILE}" ]; then
711 echo "de-symlink ${TARGET}/${FILE}"
712 "${RM}" -f "${TARGET}/${FILE}"
713 "${MV}" "${DOTFILES_SRC}/${FILE}" "${TARGET}/${FILE}"
716 $(list_files "${REPO}/patched-src")
719 if [ "${BASHRC}" == 'yes' ]; then
720 echo 'strip dotfiles section from ~/.bashrc'
721 "${SED}" '/DOTFILES_DIR/d' ~/.bashrc > bashrc_stripped
723 # see if the stripped file is any different
724 DIFF_OUTPUT=$("${DIFF}" ~/.bashrc bashrc_stripped)
726 if [ "${DIFF_RC}" -eq 0 ]; then
727 echo "no dotfiles section found in ~/.bashrc"
728 "${RM}" -f bashrc_stripped
729 elif [ "${DIFF_RC}" -eq 1 ]; then
730 echo "replace ~/.bashrc with stripped version"
732 "${MV}" bashrc_stripped ~/.bashrc
734 return 1 # diff failed, bail
738 if [ -d "${DOTFILES_DIR}/${REPO}" ]; then
739 echo "remove the ${REPO} repository"
740 "${RM}" -rf "${DOTFILES_DIR}/${REPO}"
749 function update_help()
751 echo 'Utility command that runs fetch, patch, and link.'
752 if [ "${1}" = '--one-line' ]; then return; fi
756 usage: $0 ${COMMAND} [options] [REPO]
758 Where 'REPO' is the name the dotfiles repository to update.
759 If it is not given, all repositories will be updateed.
761 Run 'fetch', 'patch', and 'link' sequentially on each repository
762 to bring them in sync with the central repositories. Keeps track
763 of the last update time to avoid multiple fetches in the same
766 ${COMMAND} passes any options it receives through to the link
774 while [ "${1::2}" = '--' ]; do
775 LINK_OPTS="${LINK_FN_OPTS} ${1}"
778 # multi-repo case handled in main() by run_on_all_repos()
779 REPO=$(nonempty_option 'update' 'REPO' "${1}") || return 1
780 maxargs 'disconnect' 1 "${@}" || return 1
782 # Update once a week from our remote repository. Mark updates by
783 # touching this file.
784 UPDATE_FILE="${REPO}/updated.$(date +%U)"
786 if [ ! -e "${UPDATE_FILE}" ]; then
787 echo "update ${REPO} dotfiles"
788 "${RM}" -f "${REPO}"/updated.* || return 1
789 "${TOUCH}" "${UPDATE_FILE}" || return 1
790 fetch "${REPO}" || return 1
791 patch "${REPO}" || return 1
792 link ${LINK_OPTS} "${REPO}" || return 1
793 echo "${REPO} dotfiles updated"
802 echo 'Dotfiles management script.'
803 if [ "${1}" = '--one-line' ]; then return; fi
807 usage: $0 [OPTIONS] COMMAND [ARGS]
810 --help Print this help message and exit.
811 --version Print the $0 version and exit.
812 --dotfiles-dir DIR Directory containing the dotfiles reposotories. Defaults to '.'.
813 --target DIR Directory to install dotfiles into. Defaults to '~'.
817 for COMMAND in "${COMMANDS[@]}"; do
818 echo -en "${COMMAND}\t"
819 "${COMMAND}_help" --one-line
823 To get help on any command, pass the '--help' as the first option
824 to the command. For example:
826 ${0} ${COMMANDS[0]} --help
833 while [ "${1::2}" = '--' ]; do
836 main_help || return 1
852 echo "ERROR: invalid option to ${0} (${1})" >&2
857 COMMAND=$(get_selection "${1}" "${COMMANDS[@]}") || return 1
860 cd "${DOTFILES_DIR}" || return 1
862 if [ "${1}" = '--help' ]; then
863 "${COMMAND}_help" || return 1
864 elif [ "${COMMAND}" = 'clone' ]; then
865 "${COMMAND}" "${@}" || return 1
868 while [ "${1::2}" = '--' ]; do
872 if [ "${#}" -eq 0 ]; then
873 run_on_all_repos "${COMMAND}" "${OPTIONS[@]}" || return 1
875 maxargs "${0}" 1 "${@}" || return 1
876 "${COMMAND}" "${OPTIONS[@]}" "${1}" || return 1
881 main "${@}" || exit 1