-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdotfile
executable file
·948 lines (752 loc) · 24.1 KB
/
dotfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
#!/usr/bin/env sh
# shellcheck shell=ash
# This script assumes at least `local` extension is enabled.
# External sources available for install
readonly DOTFILE_AVAILABLE_SOURCES="native pip cargo gem aur"
readonly DOTFILE_DEBUG=${DEBUG:-0}
readonly DOTFILE_LOG_LEVEL=${LOG_LEVEL:-2}
# XDG base directories
readonly XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache}
readonly XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
readonly XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share}
readonly DOTFILE_CONFIG_FILE="$XDG_CONFIG_HOME/dotfile.conf"
readonly DOTFILE_CACHE_DIR="$XDG_CACHE_HOME/dotfile"
readonly DOTFILE_DATA_DIR="$XDG_DATA_HOME/dotfile"
## Logging functions
################################
# Color constants
readonly BLACK=$(tput setaf 0)
readonly RED=$(tput setaf 1)
readonly GREEN=$(tput setaf 2)
readonly YELLOW=$(tput setaf 3)
readonly BLUE=$(tput setaf 4)
readonly MAGENTA=$(tput setaf 5)
readonly CYAN=$(tput setaf 6)
readonly WHITE=$(tput setaf 7)
# Text style constants
readonly BOLD=$(tput bold)
readonly UNDERLINE=$(tput smul)
readonly BLINK=$(tput blink)
# Reset test to normal style
readonly NORMAL=$(tput sgr0)
debug() {
[ "$DOTFILE_DEBUG" -eq 0 ] && [ "$DOTFILE_LOG_LEVEL" -lt 3 ] && return
printf "[${BOLD}${WHITE}DEBUG${NORMAL}] ${WHITE}%s${NORMAL}\n" "$@" >&2
}
info() {
[ "$DOTFILE_LOG_LEVEL" -lt 2 ] && return
printf "[${BOLD}${BLUE}INFO${NORMAL}] ${WHITE}%s${NORMAL}\n" "$@" >&2
}
warn() {
[ "$DOTFILE_LOG_LEVEL" -lt 1 ] && return
printf "[${BOLD}${YELLOW}WARNING${NORMAL}] ${WHITE}%s${NORMAL}\n" "$@" >&2
}
error() {
printf "[${BOLD}${RED}ERROR${NORMAL}] ${WHITE}%s${NORMAL}\n" "$@" >&2
}
## Utility functions
################################
## Push a new item to stack.
##
## Usage: stack_push <stack_id> <item>
stack_push() {
if [ "$#" -ne 2 ]; then
error "Usage: stack_push <stack_id> <item>"
return 1
fi
local stack_id item
stack_id="$1"
item="$2"
debug "stack_push: Pushing $item into $stack_id ($(stack_print $stack_id))"
# Make sure that all items in the stack are unique
if stack_contains "$stack_id" "$item"; then
debug "stack_push: Stack $stack_id already has $item."
return
fi
eval "stack_$stack_id=\"\${stack_$stack_id} $item\""
}
## Pop an item from stack to stdout.
##
## Usage: stack_pop <stack_id>
stack_pop() {
# Syntax: stack_pop <stack_id>
if [ "$#" -ne 1 ]; then
error "Usage: stack_pop <stack_id>"
return 1
fi
local stack_id result
stack_id="$1"
debug "stack_pop: Popping the last item from $stack_id ($(stack_print $stack_id))"
result=$(eval "echo \${stack_$stack_id}" | awk -F ' ' '{ print $NF }')
# Check if the result is empty
if [ -z "$result" ]; then
warn "Stack $stack_id is already empty."
return
fi
# Remove the item from the end
eval "stack_$stack_id=\$(echo \"\${stack_$stack_id}\" | sed -e 's/ $result$//')"
# Print the last element to stdout
echo $result
}
## Clears and unsets the stack variable.
##
## Usage: stack_clear <stack_id>
stack_clear() {
if [ "$#" -ne 1 ]; then
error "Usage: stack_clear <stack_id>"
return 1
fi
local stack_id
stack_id="$1"
unset $(stack_name $stack_id)
}
## Check if the stack has the item.
##
## Usage: stack_contains <stack_id> <item>
stack_contains() {
if [ "$#" -ne 2 ]; then
error "Usage: stack_contains <stack_id> <item>"
return 1
fi
local stack_id item occurenece
stack_id="$1"
item="$2"
debug "stack_contains: Checking $stack_id ($(stack_print $stack_id)) contains $item..."
occurenece=$(stack_print $stack_id | grep -c "$item")
[ $? -eq 0 ] && debug "stack_contains: Yes." || debug "stack_contains: No."
[ "$occurenece" -ne 0 ]
}
## Print the content of the stack.
##
## Usage: stack_print <stack_id>
stack_print() {
# Syntax: stack_print <stack_id>
if [ "$#" -ne 1 ]; then
error "Usage: stack_print <stack_id>"
return 1
fi
local stack_id
stack_id="$1"
eval "echo \${stack_$stack_id}"
}
## Print the name of the stack.
##
## Usage: stack_name <stack_id>
stack_name() {
# Syntax: stack_name <stack_id>
if [ "$#" -ne 1 ]; then
error "Usage: stack_name <stack_id>"
return 1
fi
local stack_id
stack_id="$1"
echo "stack_$stack_id"
}
## Ensure a command succeeds.
##
## Usage: ensure <command> <args...>
##
## If the command fails, print an error message and exit the script.
ensure() {
debug "ensure: Making sure that this command succeeds: $*"
if ! "$@"; then
error "Command failed: $*"
error "This command is required. Cannot proceed..."
exit 1
fi
}
## Maybe a command exists.
##
## Usage: maybe <command> <args...>
##
## If the command exists, run it with the arguments. Otherwise, do nothing.
maybe() {
debug "maybe: Trying this command: $*"
if is "$1" command; then
shift
"$@"
else
debug "maybe: No such command: $1"
fi
}
## Query and check information about the current system.
##
## Usage: is <subject> <complement>
##
## <subject> is the query to check. Its value varies depending on the complement
## specified below.
##
## <complement> can be one of the following:
## - platform
## - command
## - file
## - directory
## - symlink
## - installed
##
## This function can be used directly as a boolean value in shell.
##
## Example:
## if is arch platform; then
## echo "This is Arch Linux!"
## elif is debian platform; then
## echo "This is Debian or its derivatives!"
## elif is linux platform; then
## echo "This is some sort of Linux!"
## else
## echo "I'm not sure what this is!"
## fi
is() {
if [ "$#" -ne 2 ]; then
error "Usage: is <subject> <complement>"
return 1
fi
local subject complement
subject="$1"
complement="$2"
if ! _is_command "_is_$complement"; then
error "Unknown complement: $complement"
return 1
fi
"_is_$complement" "$subject"
}
## Check if the command is available.
##
## Usage: _is_command <command>
##
## This is the implementation of `is <command> command` syntax.
_is_command() {
command -v "$1" > /dev/null
}
## Check if the provided path is a file.
##
## Usage: _is_file <path>
##
## This is the implementation of `is <path> file` syntax.
_is_file() {
[ -f "$1" ]
}
## Check if the provided path is a directory.
##
## Usage: _is_directory <path>
##
## This is the implementation of `is <path> directory` syntax.
_is_directory() {
[ -d "$1" ]
}
## Check if the provided path is a symbolic link.
##
## Usage: _is_symlink <path>
##
## This is the implementation of `is <path> symlink` syntax.
_is_symlink() {
[ -L "$1" ]
}
## Check if the provided package is installed.
##
## Usage: _is_installed <package>
##
## This is the implementation of `is <package> installed` syntax.
_is_installed() {
[ -e "$DOTFILE_DATA_DIR/$1" ]
}
## Check if the current platform is compatible with the query.
##
## Usage: _is_platform <query>
##
## This is the implementation of `is <query> platform` syntax.
_is_platform() {
local query osname likename
query="$1"
osname="$(uname -s)"
# Clear the previous likename junk
stack_clear likename
# For Linux, query the distro name instead of the kernel
if [ "$osname" = "Linux" ]; then
if is "/etc/os-release" file; then
# Many distros have /etc/os-release file describing itself
. /etc/os-release
[ "$ID" ] && osname="$ID"
[ "$ID_LIKE" ] && likename="$ID_LIKE"
stack_push likename linux
elif [ "$(uname -o)" = "Android" ]; then
# Android is a whole different beast
osname=android
else
# No idea what it is, slap linux on it and call it a day
osname=linux
fi
fi
# Match the result
[ "$osname" = "$query" ] || stack_contains likename "$query"
}
## Dependency list creator
depends() {
# Syntax: depends <src> <packages>...
# <src> can be `native`, `dotfile`, `pip`, `gem`, etc.
# There must be a function called install_<src> to install the
# dependencies.
local src package arrayname
src="$1"
package="$2"
shift 2
# Allow packages from arbitrary sources
stack_push "depends_$src" "$package"
# If there are more than one packages, recursively add more
[ $# -ne 0 ] && depends "$src" "$@"
}
## Install native package dependencies
install_native() {
[ "$#" -eq 0 ] && return
info "Installing native packages: $@"
local sudo_command
# Check if sudo exists in the first place
if is sudo command; then
# Check if the user can access sudo
if ! sudo -v -p "Installing native packages requires sudo: "; then
is "$DOTFILE_CACHE_DIR" directory || mkdir -p "$DOTFILE_CACHE_DIR"
printf "%s\n" $@ >> $DOTFILE_CACHE_DIR/native_pkgs.txt
if ! [ "$dotfile_sudo_warned" ]; then
warn "This account doesn't seem to have sudo access!"
warn "You may have to install some packages yourself."
warn "Refer to the native_pkgs.txt in ${XDG_CACHE_HOME} directory"
warn "after this command exits."
dotfile_sudo_warned="yes"
fi
return
fi
sudo_command='sudo'
else
# sudo doesn't exist and user is not root
if [ "$(id -u)" -ne 0 ]; then
error "This script requires sudo to install packages."
error "It seems like I cannot get root privilege via sudo."
return 120
fi
fi
# TODO: support other platforms as well
if is debian platform; then
$sudo_command apt-get update && $sudo_command apt-get install -y "$@"
return
fi
if is arch platform; then
$sudo_command pacman -Sy --needed --noconfirm "$@"
return
fi
mkdir -p $DOTFILE_CACHE_DIR
printf "%n" $@ >> $DOTFILE_CACHE_DIR/native_pkgs.txt
if ! [ "$dotfile_unsupported_warned" ]; then
warn "This operating system is not supported by me!"
warn "You may have to install some packages yourself."
warn "Refer to the native_pkgs.txt in $DOTFILE_CACHE_DIR."
dotfile_unsupported_warned="yes"
fi
}
## Install PIP package dependencies
install_pip() {
[ "$#" -eq 0 ] && return
info "Installing PIP dependencies: $@"
if ! is pip command; then
info "pip is not found! Installing that first..."
install_native python python-pip
fi
pip install --user "$@"
}
## Install cargo dependencies
install_cargo() {
[ "$#" -eq 0 ] && return
info "Installing Cargo dependencies: $@"
if ! is cargo command; then
info "Cargo is not found! Installing that first..."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
fi
cargo install "$@"
}
## Install rubygem dependencies
install_gem() {
[ "$#" -eq 0 ] && return
info "Installing Ruby Gem dependencies: $@"
if ! is gem command; then
info "gem is not found! Installing that first..."
install_native ruby rubygems
fi
gem install "$@"
}
## Install Arch User Repository dependencies
install_aur() {
[ "$#" -eq 0 ] && return
if [ "$(id -u)" -eq 0 ]; then
error "This script cannot be run as root!"
error "Please make a user account to install packages from AUR."
exit 200
fi
install_native base-devel
if ! is arch platform; then
error "AUR dependency can be installed in Arch Linux and its derivatives."
error "Tapping out..."
error "You may have to check the package's information file to verify"
error "platform-check routines."
exit 1
fi
if ! is aur command; then
info "aurutils is not found! Installing that first..."
info "Creating a new group for AUR repo..."
sudo groupadd aur
info "Adding the current user to the new group..."
sudo usermod -aG aur "$USER"
info "Building aurutils from AUR..."
local aurdir prevdir
prevdir="$(pwd)"
aurdir="/tmp/aurutils-download"
if is "$aurdir" directory; then
rm -rf "$aurdir"
fi
mkdir "$aurdir"
cd "$aurdir"
git clone https://aur.archlinux.org/aurutils
cd aurutils
makepkg -si --noconfirm
cd "$prevdir"
rm -rf "$aurdir"
printf '\n[aur]\nSigLevel = Optional TrustAll\nServer = file:///home/aur' "$USER" \
| sudo tee -a /etc/pacman.conf
sudo install -d /home/aur -g aur
# Setting up setgid bit
sudo chmod 770 /home/aur
sudo chmod g+s /home/aur
sg aur "repo-add /home/aur/aur.db.tar"
sudo pacman -Sy vifm
fi
for package in $@; do
sg aur "aur sync $package"
done
install_native $@
}
## Install dotfile package dependencies
install_dotfile() {
[ "$#" -eq 0 ] && return
info "Installing dotfile dependencies..."
for pkg in $(stack_print depends_dotfile); do
dotfile_install "$pkg"
done
}
## User CLI interface handlers
################################
# Print the usage information
dotfile_usage() {
cat 1>&2 <<EOF
USAGE:
$(basename "$0") <command>
COMMANDS:
init Initialize this machine for installation.
install Install a package to this machine.
uninstall Uninstall a package from this machine.
update Update the repository to the most recent version.
help Display this help message.
shell Drop into a shell within this script. (DEBUG only)
EOF
}
## Initialize the dotfile repository
dotfile_init() {
# Initialization routine
# Things to do:
# 1. Create configuration file $DOTFILE_CONFIG_FILE
# 2. Create package folder $DOTFILE_DATA_DIR/
# 3. Verify all necessary binaries are here (git, stow, etc.)
# 4. Symbolic link itself to ~/.local/bin
# Create basic configuration file
debug "init: Checking for the configuration file..."
if ! is "$DOTFILE_CONFIG_FILE" file; then
info "Dotfile configuration file is not found. Creating..."
ensure mkdir -p "$XDG_CONFIG_HOME"
ensure touch "$DOTFILE_CONFIG_FILE"
info "Setting dotfile home directory as:"
echo "dotfile_home=$(dirname "$(realpath "$0")")" \
| ensure tee "$DOTFILE_CONFIG_FILE" \
1>&2
fi
# Create directory where package data will be stored
debug "init: Checking for the data storage..."
if ! is "$DOTFILE_DATA_DIR" directory; then
info "Dotfile package storage is not found. Creating..."
ensure mkdir -p "$DOTFILE_DATA_DIR"
fi
# git is required to update packages
debug "init: Checking for git command..."
if ! is git command; then
info "git does not exist. Attempting to install..."
ensure install_native git
fi
# stow is required to make symbolic links
debug "init: Checking for stow command..."
if ! is stow command; then
info "stow does not exist. Attempting to install..."
ensure install_native stow
fi
# This script is going to be installed in local PATH
debug "init: Checking for dotfile symlink in local binary directory..."
if ! is "$HOME/.local/bin/dotfile" symlink; then
info "Installing dotfile binary to local binary directory..."
ensure mkdir -p "$HOME/.local/bin"
ensure ln -s "$(realpath "$0")" "$HOME/.local/bin/dotfile"
fi
}
## Install a dotfile package
dotfile_install() {
if [ "$#" -ne 1 ]; then
error "You must provide one package!"
error "Usage: dotfile install <package>"
return 1
fi
local pkg
pkg="$1"
info "Installing dotfile package $pkg..."
# Check package sanity
debug "install: Checking $pkg is a directory..."
if ! is "$pkg" directory; then
error "Dotfile package $pkg is not found! Aborting..."
exit 2
fi
debug "install: Checking $pkg has a pkginfo definition..."
if ! is "$pkg/pkginfo" file; then
error "$pkg is not a valid dotfile package. Aborting..."
exit 2
fi
# Check if package is already installed
debug "install: Checking if package is already installed..."
if is "$pkg" installed; then
info "$pkg seems to be installed already! Skipping..."
return
fi
# Check circular dependency
debug "install: Checking for circular dependency..."
if stack_contains ancestors "$pkg"; then
warn "Circular dependency detected! Ignoring the last attempt to install $pkg..."
warn "You may want to fix the package description file to resolve this issue."
return
fi
# If already visited, then do nothing
debug "install: Checking if this package is marked to be installed already..."
if stack_contains visited "$pkg"; then
info "Package $pkg is already installed by other package. Skipping..."
return
fi
# Load dotfile description file
. "$pkg/pkginfo"
# Mark current package as visited
debug "install: Marking $pkg as visited..."
stack_push visited "$pkg"
# Find out the direct dependencies
debug "install: Finding out the direct dependency of $pkg..."
local difference
difference=$(stack_print depends_dotfile)
for visited_pkg in $(stack_print visited); do
debug "install: Removing $visited_pkg from difference array ($difference)..."
difference=$(echo "$difference" | sed -E -e "s/^$visited_pkg | $visited_pkg|$visited_pkg | $visited_pkg\$|^$visited_pkg\$//g")
done
# Install dependencies first
debug "install: Marking $pkg as the ancestor for the next dotfile..."
stack_push ancestors "$pkg"
for dependency in $difference; do
for src in $DOTFILE_AVAILABLE_SOURCES; do
stack_clear "depends_$src"
done
dotfile_install "$dependency"
done
stack_pop ancestors > /dev/null
# Unset pre- and post-installation hooks
unset -f pre_install post_install
# Source dotfile description file again to restore changed hooks
. "$pkg/pkginfo"
# Run the pre-installation hook
maybe pre_install
# Install external dependencies
for src in $DOTFILE_AVAILABLE_SOURCES; do
info "Installing dependencies from $src..."
eval "install_$src $(stack_print depends_$src)"
# Clear external dependency list after installation
stack_clear "depends_$src"
done
# Stow the directory
info "Symlinking the configuration files..."
ensure stow -S -t "$HOME" --ignore=".gitignore" --ignore="pkginfo" "$pkg"
# Copy description file to the data directory
ensure cp "$pkg/pkginfo" "$DOTFILE_DATA_DIR/$pkg"
# Run the post-installation hook
maybe post_install
info "$pkg package is installed!"
return
}
dotfile_uninstall() {
if [ "$#" -ne 1 ] && [ "$#" -ne 2 ]; then
error "You must provide one package!"
error "Usage: dotfile uninstall [--force] <package>"
return 1
fi
local force
if [ "$1" = "--force" ] || [ "$1" = "-f" ]; then
force=1
shift
fi
local pkg
pkg="$1"
info "Uninstalling dotfile package $pkg..."
# Package should be installed before uninstalling
if ! is "$DOTFILE_DATA_DIR/$pkg" file; then
error "Package '$pkg' is not installed currently."
error "Cannot uninstall a nonexistent package. Aborting..."
return 1
fi
# Check if any other package depends on this dotfile package
for installed in $(find "$DOTFILE_DATA_DIR" -type f); do
# Skip the package to uninstall
[ "$pkg" = "$(basename $installed)" ] && continue
# Source the package description file to load dependencies
. "$installed"
# If the package description has the package as dependency,
# the package will not work so mark it
if stack_contains depends_dotfile "$pkg"; then
warn "$installed depends on $pkg!"
stack_push broken_pkg "$installed"
fi
# Reset the dotfile dependencies
stack_clear depends_dotfile
done
# If there are to-be-broken packages, address them
if [ "$broken_pkg" ]; then
warn "Uninstalling this package will break dependencies for the following dotfiles:"
for broken in $broken_pkg; do
warn " - $(basename $broken)"
done
# If not force, then crash and burn
if ! [ "$force" ]; then
error "Cannot remove $pkg due to dependency check failure."
error "To ignore this, use --force option."
return 1
fi
fi
# Unset pre- and post-uninstallation hooks
unset -f pre_uninstall post_uninstall
# Source dotfile description file again to restore changed hooks
. "$DOTFILE_DATA_DIR/$pkg"
# Run the pre-uninstallation hook
maybe pre_uninstall
# Unstow the directory
info "Unstowing the configuration files..."
ensure stow -D "$pkg"
# Remove description file in the data directory
ensure rm -f "$DOTFILE_DATA_DIR/$pkg"
# Run the post-uninstallation hook
maybe post_uninstall
info "$pkg package is uninstalled!"
return
}
dotfile_update() {
# Fetch the changes from origin
git fetch origin
# Check which directories have changes
local changed_dirs
changed_dirs="$(git diff --name-only HEAD..origin/main | sed -e 's=^/==' -e 's=/.*==' | uniq)"
if [ -z "$changed_dirs" ]; then
info "Already up to date!"
return
fi
# Check if the directories are actually dotfile packages
local changed_pkgs
for candidate in $changed_dirs; do
if [ -e "$candidate/pkginfo" ]; then
info "Package $candidate has changes available."
stack_push changed_pkgs "$candidate"
fi
done
# Remove packages with changes available
local installed_pkgs
for pkg in $changed_pkgs; do
dotfile_uninstall --force "$pkg"
stack_push installed_pkgs "$pkg"
done
# Try updating the repository...
if ! git merge --ff-only origin/main; then
error "Unable to fast-forward the update! Tapping out..."
error "It seems you have unsaved changes in the dotfile repository."
exit 1
fi
# Reinstall the updated packages
for pkg in $installed_pkgs; do
dotfile_install pkg
done
info "Update completed!"
return
}
dotfile_shell() {
if [ "$DOTFILE_DEBUG" -eq 0 ]; then
error "Opening shell is not supported if DEBUG environment variable is not set!"
exit 1
fi
while true; do
read -r -p "dotfile> " line
[ "$line" = "exit" ] && break
eval "$line"
done
}
## Entrypoint
################################
# Take the first positional argument
action="$1"
if [ -z "$action" ]; then
error "You must supply at least one command!"
dotfile_usage
exit 1
fi
shift
# Display help message
if [ "$action" = "help" ]; then
echo "dotfile -- automatic dotfile installer, because I am lazy as fuck"
echo ""
dotfile_usage
exit
fi
# If there is no dotfile configuration file, instruct the user to run
# `dotfile init` first
if [ "$action" != "init" ] && ! is "$DOTFILE_CONFIG_FILE" file; then
error "Dotfile manager is not initialized yet!"
error "Please run \"dotfile init\" first."
exit 1
fi
# Go to the dotfile home directory by emulating `pushd`
if is "$DOTFILE_CONFIG_FILE" file; then
debug "entry: Found dotfile config. Sourcing..."
ensure . "$DOTFILE_CONFIG_FILE"
debug "entry: Changing directory to $dotfile_home..."
dotfile_old_pwd="$(pwd)"
cd "$dotfile_home"
fi
case "$action" in
init)
dotfile_init "$@"
;;
install)
dotfile_install "$@"
;;
uninstall)
dotfile_uninstall "$@"
;;
update)
dotfile_update "$@"
;;
remove)
dotfile_remove "$@"
;;
shell)
dotfile_shell "$@"
;;
*)
error "Unknown command: $action"
dotfile_usage
;;
esac
# Restore the directory
if [ "$dotfile_old_pwd" ]; then
debug "entry: Changing back to the original directory in $dotfile_old_pwd..."
cd "$dotfile_old_pwd"
fi