#! /usr/bin/env bash set -e shopt -s extglob [ -n "$DOTIFY_DEBUG" ] && set -x # dotify 0.0.1 # https://github.com/jimeh/dotify # # Copyright (c) 2013 Jim Myhrberg. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # # Helper functions # # Checks for specified argument. # # Example: # # $ has-argument help h "-t none" # > returns 1 # $ has-argument help h "-t none --help" # > returns 0 # $ has-argument help h "-t none -h" # > returns 0 # # Returns 0 if argument was found, returns 1 otherwise. has-argument() { local long short long="--$1" short="-$2" shift 2 if [[ " $@ " == *" $long "* ]] || [[ " $@ " == *" $long="* ]]; then return 0 elif [[ " $@ " == *" $short "* ]] || [[ " $@ " == *" $short="* ]]; then return 0 fi return 1 } # Parses and echos value of specified argument. # # Example: # # $ parse-argument file f -t none --file /tmp/foobar.txt # /tmp/foobar.txt # $ parse-argument file f -t none --file="/tmp/foo bar.txt" # /tmp/foo bar.txt # $ parse-argument file f -t none -f /tmp/foobar.txt # /tmp/foobar.txt # $ parse-argument file f -t none -f=/tmp/foo\ bar.txt # /tmp/foo bar.txt # # Returns 0 and echos value if argument was found, returns 1 otherwise. parse-argument() { local long short arg next_arg long="--$1" short="-$2" shift 2 for arg in "$@"; do if [ -n "$next_arg" ]; then echo "$arg" return 0 elif [[ " $arg " == *" $long "* ]] || [[ " $arg " == *" $short "* ]]; then next_arg="yes" elif [[ " $arg " == *" $long="* ]]; then arg="${arg/#$long=/}" echo "$arg" return 0 elif [[ " $arg " == *" $short="* ]]; then arg="${arg/#$short=/}" echo "$arg" return 0 fi done return 1 } # Trim leading and trailing whitespace. # # Example: # # $ trim " foo bar " # foo bar # trim() { local string="$@" string="${string#"${string%%[![:space:]]*}"}" string="${string%"${string##*[![:space:]]}"}" echo -n "$string" } # # Internal functions # compile-dotfile() { local dotfile="$1" if [ -z "$dotfile" ]; then dotfile="$DOTFILE"; fi if [ ! -f "$dotfile" ]; then echo "ERROR: \"$dotfile\" does not exist." >&2 return 1 fi local output="" local line="" while IFS= read line; do # Ignore comments and blank lines. if [[ "$line" =~ ^(\ *\#.*|\ *)$ ]]; then continue # Parse ": -> " lines. elif [[ "$line" =~ ^(\ +)?([a-zA-Z0-9_-]+):\ (.+)\ +-[\>]\ +(.+)$ ]]; then output="${output}${BASH_REMATCH[1]}dotify-action ${BASH_REMATCH[2]} " output="${output}$(trim "${BASH_REMATCH[3]}") " output="${output}$(trim "${BASH_REMATCH[4]}")\n" # Parse " -> " lines. elif [[ "$line" =~ ^(\ +)?(.+)\ -[\>]\ (.+)$ ]]; then output="${output}${BASH_REMATCH[1]}dotify-action default " output="${output}$(trim "${BASH_REMATCH[2]}") " output="${output}$(trim "${BASH_REMATCH[3]}")\n" # Append line without modifications. else output="${output}${line}\n" fi done < "$dotfile" echo -e "$output" } create-symlink() { local source="$1" local target="$2" if [ ! -e "$target" ] && [ ! -h "$target" ]; then ln -s "$source" "$target" return 0 elif [ -h "$target" ]; then if [ "$(readlink "$target")" != "$source" ]; then echo "ERROR: \"$target\" exists, is a symlink to:" \ "$(readlink "$target")" >&2 return 1 fi else echo "ERROR: \"$target\" exists" >&2 return 1 fi } execute-dotfile() { local dotfile_source="$(dotify-compile)" locate-target if [ "$?" != "0" ]; then return 1; fi ROOT_DIR="$(dirname "$DOTFILE")" eval "$dotfile_source" return $? } locate-dotfile() { if [ -n "$DOTFILE" ]; then if [ ! -f "$DOTFILE" ]; then echo "ERROR: \"$DOTFILE\" does not exist." >&2 return 1 fi elif [ -f "$(pwd)/Dotfile" ]; then DOTFILE="$(pwd)/Dotfile" else echo "ERROR: \"$(pwd)\" does not have a Dotfile." >&2 return 1 fi } locate-target() { if [ -n "$TARGET" ]; then if [ ! -d "$TARGET" ]; then echo "ERROR: Target \"$TARGET\" is not a directory." >&2 return 1 fi elif [ -n "$HOME" ] && [ -d "$HOME" ]; then TARGET="$HOME" elif [ -d ~ ]; then TARGET=~ else echo "ERROR: Your \$HOME folder could not be found." >&2 return 1 fi } # # Command functions # dotify-action() { local action="$1" local source="$2" local target="$3" if [ "$action" == "default" ]; then action="$DOTIFY_OPT_DEFAULT_ACTION" fi ! valid_action="$(command -v "dotify-action-${action}")" if [ -z "$valid_action" ]; then echo "ERROR: \"$action\" is not a valid action." >&2 return 1 fi dotify-action-${action} "$source" "$target" } dotify-clean() { DOTIFY_RUN_MODE="clean" execute-dotfile return $? } dotify-compile() { locate-dotfile if [ "$?" != "0" ]; then return 1; fi compile-dotfile "$DOTFILE" return $? } dotify-help() { echo "$(dotify-print-version)" echo "usage: dotify []" } dotify-info() { locate-dotfile if [ "$?" != "0" ]; then return 1; fi locate-target if [ "$?" != "0" ]; then return 1; fi echo "$(dotify-print-version)" echo " Dotfile: $DOTFILE" echo " Root: $(dirname "$DOTFILE")" echo " Target: $TARGET" } dotify-install() { DOTIFY_RUN_MODE="install" execute-dotfile return $? } dotify-uninstall() { DOTIFY_RUN_MODE="uninstall" execute-dotfile return $? } dotify-version() { echo "0.0.1" } dotify-print-version() { echo "dotify $(dotify-version)" } # # Built-in Plugins # dotify-action-link() { if [ "$DOTIFY_RUN_MODE" == "install" ]; then dotify-action-link-install $@ elif [ "$DOTIFY_RUN_MODE" == "uninstall" ]; then dotify-action-link-uninstall $@ fi } dotify-action-link-install() { echo "link install: $@" } dotify-action-link-uninstall() { echo "link uninstall: $@" } dotify-action-git() { if [ "$DOTIFY_RUN_MODE" == "install" ]; then dotify-action-git-install $@ elif [ "$DOTIFY_RUN_MODE" == "uninstall" ]; then dotify-action-git-uninstall $@ fi } dotify-action-git-install() { echo "git install: $@" } dotify-action-git-uninstall() { echo "git uninstall: $@" } # # Dotfile commands # root_link () { DOTIFY_OPT_ROOT_LINK="$@" } default_action() { DOTIFY_OPT_DEFAULT_ACTION="$@" } include() { echo "include: $@" } # # Default Options # DOTIFY_OPT_ROOT_LINK=".dotfiles" DOTIFY_OPT_DEFAULT_ACTION="link" # # Argument Parsing # DOTFILE="" # --dotfile / -f TARGET="" # --target / -t DRY_RUN="" # --dry-run / -d HELP="" # --help / -h VERSION="" # --version / -v if has-argument dotfile f "$@"; then DOTFILE="$(parse-argument dotfile f "$@")" fi if has-argument target t "$@"; then TARGET="$(parse-argument target t "$@")" fi if has-argument dry-run d "$@"; then DRY_RUN="1" fi if has-argument help h "$@"; then HELP="1" fi if has-argument version v "$@"; then VERSION="1" fi # # Command Parsing # # Command is first argument that does not start with a dash or plus. for arg in "$@"; do if [ -n "$skip_next" ]; then skip_next= elif [[ "$arg" =~ ^(--dotfile|-f|--taraget|-t)$ ]]; then skip_next=1 elif [[ "$arg" != "-"* ]] && [[ "$arg" != "+"* ]]; then command="$arg" break fi done # Show help and exit if help arguments or command are given. if [ -n "$HELP" ] || [ "$command" == "help" ]; then dotify-help exit fi # Show version info and exit if version arguments or command are given. if [ -n "$VERSION" ] || [ "$command" == "version" ]; then dotify-help | head -1 exit fi # Deal with the commands. case "$command" in "info" ) dotify-info ;; "compile" ) dotify-compile ;; "" | "install" ) dotify-install ;; "uninstall" | "remove" ) dotify-uninstall ;; "clean" ) dotify-clean ;; esac exit $?