├── LICENSE ├── README.md └── dm /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Felix Hanley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dm: Dotfile Manager 2 | 3 | A single-file "dotfile" manager written in POSIX shell. It creates and 4 | synchronises symlinks in your home directory to a central dotfile 5 | source. 6 | 7 | ## Usage 8 | 9 | The script expects your dotfiles master to be in `~/.dotfiles` or have the ENV 10 | variable `DOTFILES` set to the path. This master path can then be kept in 11 | revision control and be kept clean. The script will symbolically link files 12 | from the master path to your home directory. 13 | 14 | `dm check` will list all files needing linking. 15 | 16 | `dm sync` will link all files to your home directory. 17 | 18 | `dm add ` will move the file into the master and then link it. 19 | 20 | Each command has optional flags which modify the default behaviour as the usage 21 | help describes below: 22 | 23 | Options: 24 | -v Be noisy 25 | -s Specify dotfile path (default: ~/.dotfiles) 26 | -f Force. Replace symlinks and no backups (sync) 27 | -h This help 28 | 29 | ## FAQs 30 | 31 | Q: What about deeply nested files? 32 | 33 | A: All parent directories that do not exist will be created in your home 34 | directory. This enables linking only files. For example: 35 | 36 | 37 | ~ 38 | |-- blah 39 | \-- bin 40 | \-- nested 41 | \-- foo -> ~/.dotfiles/bin/nested/foo 42 | 43 | The `nested` and `foo` directories above will be created if need be. 44 | 45 | Q: How do I clean up old symlinks? 46 | 47 | A: Manually. I have not yet had the time/motivation to work out how to see if 48 | the broken symlink is pointing to a missing file in the dotfiles source. 49 | 50 | Q: What about host specific files? 51 | 52 | A: By creating a file in your dotfiles with the suffix `.__$(hostname -s)` then 53 | dm will use it in place of the general version. To exclude a file for a 54 | particular host then append yet another suffix of `!` like this (for the host 55 | "acme"): 56 | 57 | 58 | ~ 59 | |-- blah 60 | \-- bin 61 | |-- somefile 62 | |-- somefile.__acme -> ~/.dotfiles/bin/somefile 63 | |-- anotherfile 64 | \-- anotherfile.__acme! 65 | 66 | ## Author 67 | 68 | Felix Hanley 69 | 70 | ## License 71 | 72 | MIT 73 | -------------------------------------------------------------------------------- /dm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Dotfile Manager 4 | # 5 | # POSIX shell script to keep a central repo of dotfiles 6 | # and link them into your home folder. 7 | 8 | # Author: Felix Hanley 9 | 10 | # Some things we need 11 | readlink=$(command -v readlink) 12 | if [ -z "$readlink" ]; then 13 | echo "Missing readlink, cannot continue." 14 | exit 1 15 | fi 16 | dirname=$(command -v dirname) 17 | if [ -z "$dirname" ]; then 18 | echo "Missing dirname, cannot continue." 19 | exit 1 20 | fi 21 | find=$(command -v find) 22 | if [ -z "$find" ]; then 23 | echo "Missing find, cannot continue." 24 | exit 1 25 | fi 26 | realpath=$(command -v realpath) 27 | if [ -z "$realpath" ]; then 28 | # Provide a realpath implementation 29 | realpath() { 30 | canonicalize_path "$(resolve_symlinks "$1")" 31 | } 32 | fi 33 | hostn="$(hostname |cut -d. -f1)" 34 | 35 | # Show usage 36 | usage() { 37 | printf 'Manage your dotfiles\n' 38 | printf 'usage: dm [opts] [check|add|clean]\n' 39 | printf '\n' 40 | printf ' add\t\tadd a file to your dotfile repo\n' 41 | printf ' check\t\tlist files needing linking (same is -n flag)\n' 42 | printf ' clean\t\tlist and prompt to delete broken symlinks\n' 43 | printf ' pull\t\tpull dotfiles from git\n' 44 | printf '\n' 45 | printf 'Options:\n' 46 | printf '\t-s Specify dotfile path (default: %s)\n' "$DOTFILES" 47 | printf '\t-n Dry run. no changes (same as check command)\n' 48 | printf '\t-f Force. Replace symlinks and no backups\n' 49 | printf '\t-h This help\n' 50 | } 51 | 52 | resolve_symlinks() { 53 | if path=$($readlink -- "$1"); then 54 | dir_context=$($dirname -- "$1") 55 | resolve_symlinks "$(_prepend_path_if_relative "$dir_context" "$path")" 56 | else 57 | printf '%s\n' "$1" 58 | fi 59 | } 60 | _prepend_path_if_relative() { 61 | case "$2" in 62 | /* ) printf '%s\n' "$2" ;; 63 | * ) printf '%s\n' "$1/$2" ;; 64 | esac 65 | } 66 | canonicalize_path() { 67 | if [ -d "$1" ]; then 68 | # Canonicalize dir path 69 | (cd "$1" 2>/dev/null && pwd -P) 70 | else 71 | # Canonicalize file path 72 | dir=$("$dirname" -- "$1") 73 | file=$(basename -- "$1") 74 | (cd "$dir" 2>/dev/null && printf '%s/%s\n' "$(pwd -P)" "$file") 75 | fi 76 | } 77 | 78 | backup() { 79 | [ ! -e "$1" ] && return 80 | printf 'backing up %s\n' "$1" 81 | [ -n "$DRYRUN" ] && return 82 | if ! cp -f "$1" "$1.dm-backup"; then 83 | printf 'failed to backup %s\n' "$1" 84 | exit 1 85 | fi 86 | } 87 | 88 | remove() { 89 | [ -z "$DRYRUN" ] && [ -f "$1" ] && rm "$1" 90 | } 91 | 92 | # Perform the actual link creation 93 | create_link() { 94 | src=$1; shift; 95 | dest=$1; shift; 96 | 97 | if [ -e "$dest" ] && [ -n "$BACKUP" ]; then 98 | backup "$dest" 99 | fi 100 | 101 | remove "$dest" 102 | 103 | if [ -L "$src" ]; then 104 | # The dotfile itself is a link, copy it 105 | src="$REALHOME/$($readlink -n "$src")" 106 | fi 107 | # Symbolic link command 108 | linkcmd="ln -s" 109 | if [ -z "$BACKUP" ]; then 110 | linkcmd="$linkcmd -f" 111 | fi 112 | printf 'linking %s\n' "$dest" 113 | [ -z "$DRYRUN" ] && $linkcmd "$src" "$dest" 114 | } 115 | 116 | ensure_path() { 117 | directory=$("$dirname" "$1") 118 | if [ ! -d "$directory" ]; then 119 | printf 'creating path %s\n' "$directory" 120 | [ -z "$DRYRUN" ] && mkdir -p "$($dirname "$1")" > /dev/null 121 | fi 122 | } 123 | 124 | scan() { 125 | # Each file and link in DOTFILES, excluding VCS 126 | # TODO enable configurable excludes 127 | filelist=$(find "$DOTFILES" \( -name .git -o -name .hg -o -name '*.swp' -o -name '*.__*' \) -prune -o \( -type f -print \) -o \( -type l -print \)) 128 | for file in $filelist; do 129 | process "$file" 130 | done 131 | } 132 | 133 | add() { 134 | file=$1; shift 135 | if [ "$file" = "." ] || [ "$file" = ".." ]; then 136 | return 137 | fi 138 | relative=${file#${DOTFILES}/} 139 | # Note these are in 'sync' order 140 | dest=$REALHOME/$relative 141 | src=$DOTFILES/$relative 142 | 143 | # Nothing to copy 144 | if [ ! -e "$dest" ]; then 145 | printf 'Cannot find %s\n' "$dest" 146 | return 1 147 | fi 148 | # Dotfile exists 149 | if [ -f "$src" ]; then 150 | printf '%s is already managed\n' "$dest" 151 | return 1 152 | fi 153 | # De-reference home version 154 | if [ -L "$dest" ]; then 155 | dest=$(realpath "$dest") 156 | fi 157 | ensure_path "$src" 158 | mv "$dest" "$src" && create_link "$src" "$dest" 159 | return $? 160 | } 161 | 162 | # Updates a link from the dotfiles src to the home directory 163 | # $1 is the file within the dotfile directory 164 | process() { 165 | file=$1; shift 166 | if [ "$file" = "." ] || [ "$file" = ".." ]; then 167 | return 168 | fi 169 | relative=${file#${DOTFILES}/} 170 | dest=$REALHOME/$relative 171 | src=$DOTFILES/$relative 172 | 173 | # check for host specific version 174 | if [ -f "$src.__$hostn!" ]; then 175 | backup "$dest" && remove "$dest" 176 | return 177 | fi 178 | if [ -f "$src.__$hostn" ]; then 179 | src="$src.__$hostn" 180 | fi 181 | 182 | #printf 'src=%s dest=%s relative=%s\n' "$src" "$dest" "$relative" 183 | 184 | ensure_path "$dest" 185 | 186 | # missing -> link 187 | if [ ! -e "$dest" ]; then 188 | create_link "$src" "$dest" 189 | return 0 190 | fi 191 | 192 | # symlink -> relink 193 | if [ -L "$dest" ]; then 194 | destlink=$(realpath "$($readlink -n "$dest")") 195 | srclink=$(realpath "$src") 196 | # Src is also a link 197 | # if [ -L "$src" ]; then 198 | # FIXME 199 | # Need to determine relative links 200 | # srclink=$(realpath "$($readlink -n "$src")") 201 | # fi 202 | if [ "$destlink" != "$srclink" ]; then 203 | create_link "$src" "$dest" 204 | fi 205 | return 0 206 | fi 207 | 208 | # regular file exists 209 | if [ -f "$dest" ]; then 210 | create_link "$src" "$dest" 211 | return 0 212 | fi 213 | 214 | printf 'unknown type %s\n' "$dest" 215 | return 1 216 | } 217 | 218 | main() { 219 | while getopts ":bns:" opt; do 220 | case $opt in 221 | b) BACKUP=true ;; 222 | n) DRYRUN=true ;; 223 | s) DOTFILES=$OPTARG ;; 224 | ?) 225 | usage 226 | exit 0 227 | ;; 228 | esac 229 | done 230 | 231 | # Shift the rest 232 | shift $((OPTIND - 1)) 233 | 234 | REALHOME=$(realpath "$HOME") 235 | # Default dotfiles path 236 | DOTFILES=$(realpath "${DOTFILES:-$REALHOME/.dotfiles}/") 237 | 238 | case "$1" in 239 | check) 240 | DRYRUN=true 241 | scan 242 | ;; 243 | add) 244 | if [ -z "$2" ]; then 245 | echo "Missing required path" 246 | usage 247 | return 1 248 | fi 249 | add "$2" 250 | ;; 251 | clean) 252 | $find "$REALHOME" -type l -exec test ! -e '{}' \; -exec rm -i '{}' \; 253 | ;; 254 | pull) 255 | oldpwd=$PWD 256 | cd $DOTFILES && git pull 257 | cd $oldpwd 258 | ;; 259 | *) 260 | scan 261 | ;; 262 | esac 263 | return $? 264 | } 265 | 266 | main "$@" 267 | 268 | # vim: ft=sh 269 | --------------------------------------------------------------------------------