bootstrap.sh 6.85 KB
Newer Older
1
2
3
4
#!/usr/bin/env bash

# Setup script for the dotfiles
#
5
# Dotfiles are organized into directories by topic.
6
7
#
# If topic directory contains a file named `dotfiles.meta`, it will be sourced
8
# upon installing that topic. Use it to specify non-standard target directory
9
# for dotfiles (PREFIX) instead of $HOME or to mark the topic that requires
10
11
12
13
14
# root privileges (SCOPE=system). To avoid adding a dot to the dotfile name
# when installing it, add explicit PREFIX="$HOME" (or other directory) to
# `dotfiles.meta`. This file may also be used to inject arbitrary shell code
# into topic installation process - use that feature with care and NEVER
# install dotfiles from untrusted sources.
15
16
17
18
19
#
# File suffixes correspond to bootstrap actions:
#     .copy - for files to be copied over to new location
#     .link - for files to be linked to from new location
#     .append - for files to be appended to the target file
20
21
22
# All other files and directories are ignored.
#
# Example:
23
24
25
26
#     topic-foo/vimrc.link:
#        Will be symlinked from ~/.vimrc
#     topic-bar/bashrc.copy
#        Will be copied over to ~/.bashrc
Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
27
#     topic-baz/default/keyboard.copy with PREFIX=/etc
28
#        Will be copied to /etc/default/keyboard
29
#     topic-baz/file/without/valid/suffix
30
#        Will be ignored
31
#
32
# The script depends on the following tools:
33
#     - GNU coreutils: fmt, ln, mkdir, cp, readlink, tr (and others)
34
#     - GNU find
35
#     - GNU grep
36
37
38
39


# Fail loudly on any error
set -e
40
set -o pipefail
41
42
43


# Global parameters
44
45
[[ -z "$DOTFILES" ]] && DOTFILES=$(readlink -m "$(dirname "$0")")
[[ -z "$DOTFILES_BACKUP" ]] && DOTFILES_BACKUP="$HOME/.dotfiles-overwritten/"
46
SUFFIXES="link|append|copy"
47
COLUMNS=$(tput cols||echo 80)
Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
48
49
50


main() {
51
    local topic item retcode
52
    if [[ -f "$1" ]]  # topic lists
Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
53
    then
54
        for item in "$@"
Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
55
        do
56
57
58
59
            grep -Ev '^\s*#|^\s*$' "$item" | while read -r topic
            do
                install_topic "$topic"
            done
Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
60
        done
61
    elif [[ ! -z "$1" && -d "$DOTFILES/$1" ]]  # topic names
Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
62
    then
63
64
65
66
        for item in "$@"
        do
            install_topic "$item"
        done
Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
67
    else
68
69
70
71
72
73
74
        if [[ ! -z "$*" && ! "$*" =~ ^(-h|--help|--usage)$ ]]
        then
            printf "Invalid command line arguments: $*\n\n" >&2
            retcode=1
        else
            retcode=0
        fi
Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
75
        printf "%s\n" \
76
77
78
79
80
81
            "Usage: $(basename "$0") TOPIC [TOPIC2 ...] or" \
            "       $(basename "$0") FILENAME [FILENAME2 ...]" \
            "" \
            "Bootstrap script for $DOTFILES" \
            "" \
            "Install dotfiles either for individual TOPICs provided as arguments" \
82
            "or for several topics listed in FILENAMEs (one topic per line, blank"\
83
            "lines and lines starting with hash symbol are ignored)" \
Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
84
            "" \
85
            "All arguments have to be either TOPICs or FILENAMEs. Mixing argument" \
86
87
88
89
90
91
92
93
94
            "types is not supported and will lead to an error" \
            "" \
            "Existing files in the destination directories will be overwritten" \
            "after being backed up to $DOTFILES_BACKUP" \
            "" \
            "Default locations for dotfiles source and backup directories may be" \
            "overriden using following environment variables:" \
            "    \$DOTFILES=$DOTFILES" \
            "    \$DOTFILES_BACKUP=$DOTFILES_BACKUP"
95
        return "$retcode"
Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
96
97
    fi
}
98
99
100


install_topic() {
101
    local topic="${1//[$'\r\t\n']/}"
102
    local file files ifs_backup meta
103

104
105
    if [[ -z "$topic" ]]
    then
Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
106
        echo "$FUNCNAME() requires topic name as an argument" >&2
107
108
109
        return 1
    fi

110
111
112
113
    # Notify user what's happening
    echo -e "\nCONFIGURING TOPIC: $topic"

    # Locate relevant files
114
115
116
117
118
119
    files=$( \
        find \
            "$DOTFILES/$topic" \
            -regextype posix-egrep \
            -regex ".*\.($SUFFIXES)" \
    )
120
    [[ -z "$files" ]] && return
121

122
    # Initialize extra metadata
123
    local PREFIX=""
124
125
126
127
128
129
    local SCOPE="user"

    # Load custom metadata
    meta="$DOTFILES/$topic/dotfiles.meta"
    [[ -s "$meta" ]] && source "$meta"

130
    # Handle system-wide actions
131
    if [[ "$SCOPE" == "system" && "$EUID" != "0" ]]
132
133
134
135
136
    then
        echo "Must be root to change system configuration" >&2
        return 1
    fi

137
    # Install all files
138
139
140
141
142
143
144
145
146
147
148
149
    ifs_backup="$IFS"
    IFS=$'\n'
    for file in $files
    do
        install_file "$file"
    done
    IFS="$ifs_backup"
}


install_file() {
    local file="$1"
150
    local action target destination overwritten output
151

152
153
    if [[ -z "$file" ]]
    then
Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
154
        echo "$FUNCNAME() requires file name as an argument" >&2
155
156
157
        return 1
    fi

158
159
160
    action=$(get_action "$file")
    target=$(get_target "$file")

161
    # Calculate destination
162
    if [[ -z "$PREFIX" ]] # PREFIX value is set in install_topic()
163
164
    then
        destination="$HOME/.$target"
165
166
    else
        destination="$PREFIX/$target"
167
168
169
170
171
172
    fi

    # Backup existing files before overwriting
    if [[ -e "$destination" ]]
    then
        mkdir -p "$DOTFILES_BACKUP"
173
        cp --backup=numbered --parents "$destination" "$DOTFILES_BACKUP"
174
175
176
177
178
179
180
181
182
183
184
185
186
        overwritten=", old file backed up"
    else
        overwritten=""
    fi

    # Perform actual action
    execute_action() {
        case "$action" in
        link)
            ln -sfv "$file" "$destination" ;;
        copy)
            cp -v "$file" "$destination" ;;
        append)
187
188
189
190
191
192
193
194
195
196
            local newline="^"
            local pattern=$(tr '\n' "$newline" < "$file")
            touch "$destination"
            if tr '\n' "$newline" < "$destination" | grep -qF "$pattern"
            then
                :  # already appended
            else
                cat "$file" >> "$destination"
            fi
            echo "'$file' -> '$destination'"
197
198
199
            ;;
        esac
    }
200
201
202

    # Execute verbosely
    echo -e "\n  [$action$overwritten]"
203
    mkdir -p "$(dirname "$destination")"
204
205
    output=$(execute_action)  # separate statement to propagate errors properly
    echo "    $output" | fmt -c -w "$COLUMNS"
206
207
208
209
210
}


get_target() {
    local dotfile reply
Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
211
    dotfile=$(relative_dotfile_path "$1")
212
213
214
215
216
217
218
219

    reply="${dotfile%.*}"  # remove action suffix
    reply="${reply#*/}"  # remove topic
    echo "$reply"
}


get_action() {
220
    local dotfile suffix reply
Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
221
    dotfile=$(relative_dotfile_path "$1")
222
223
224
225
    suffix="${dotfile##*.}"

    if [[ $suffix =~ ^($SUFFIXES)$ ]]
    then
226
        reply="$suffix"
227
228
229
230
    else
        echo "Invalid dotfile suffix: $suffix ($dotfile)" >&2
        return 1
    fi
231
232
233
234
235
236
237

    if [[ "$OSTYPE" == "msys" && "$reply" == "link" ]]
    then
        reply="copy"  # Windows can't handle symlinks easily
    fi

    echo "$reply"
238
239
240
}


Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
241
relative_dotfile_path() {
242
243
244
    local absolute=$(readlink -m "$1")
    echo "${absolute/#$DOTFILES\//}"
}
Vitaly Potyarkin's avatar
Vitaly Potyarkin committed
245
246
247


# Invoke commandline interface
248
249
250
251
252
253
# if __name__ == '__main__'   //  <https://stackoverflow.com/a/45988155>
get_caller() { echo "${FUNCNAME[1]}"; }
if [[ "$(get_caller)" == "main" ]]
then
    main "$@"
fi