Browse Source

Replace old setup Python script with command

master
Noah Petherbridge 2 years ago
parent
commit
ca25d0022e
4 changed files with 337 additions and 97 deletions
  1. 2
    0
      .gitignore
  2. 3
    0
      home/.common.sh
  3. 329
    0
      home/bin/dfm
  4. 3
    97
      setup

+ 2
- 0
.gitignore View File

@@ -1,2 +1,4 @@
1
+backup/
2
+.last-updated
1 3
 __pycache__
2 4
 *.pyc

+ 3
- 0
home/.common.sh View File

@@ -2,6 +2,9 @@
2 2
 # Common shell functions/aliases between bash and zsh.
3 3
 ###
4 4
 
5
+# Notify to update the dotfiles.
6
+dfm check-update
7
+
5 8
 ################################################################################
6 9
 ## Functions
7 10
 ################################################################################

+ 329
- 0
home/bin/dfm View File

@@ -0,0 +1,329 @@
1
+#!/usr/bin/env perl
2
+
3
+=head1 NAME
4
+
5
+dfm: Dotfiles Manager.
6
+
7
+=head1 DESCRIPTION
8
+
9
+This is a custom script to help me manage my dotfiles. The C<setup> script
10
+acts as an alias to C<dfm setup>.
11
+
12
+This is written in Perl for maximum compatibility; the old C<setup> script that
13
+this replaces was written in Python, and for the servers I work with I had to
14
+make it simultaneously support Python 2.6, 2.7 and 3.x; Perl is simpler and
15
+more universal.
16
+
17
+=head1 USAGE
18
+
19
+  dfm <command> [options...]
20
+
21
+=cut
22
+
23
+use strict;
24
+use warnings;
25
+
26
+use Cwd qw(getcwd abs_path);
27
+use Data::Dumper;
28
+use File::Copy;
29
+use FindBin;
30
+use Getopt::Long;
31
+
32
+# Our home folder and the root to the .dotfiles repo.
33
+our $HOME     = $ENV{HOME} || $ENV{USERPROFILE} || getcwd();
34
+our $DOTFILES = abs_path("$FindBin::RealBin/../..");
35
+our $BACKUP   = "$DOTFILES/backup";
36
+
37
+# Threshhold at which to start notifying about possible updates.
38
+our $UPDATE_FILE = "$DOTFILES/.last-updated";
39
+our $UPDATE_THRESHHOLD = 60*60*24*15;
40
+
41
+=head1 OPTIONS
42
+
43
+=over 4
44
+
45
+=item --noop
46
+
47
+Don't make any changes to the filesystem; just go through the motions.
48
+
49
+=item --copy
50
+
51
+Use file copies instead of symlinks, in case your host platform doesn't support
52
+symlinks.
53
+
54
+In C<--copy> mode, any existing symlinks to a dotfile will be deleted and
55
+replaced with a normal file copy.
56
+
57
+=item --force
58
+
59
+Force certain subcommands to do as much work as possible.
60
+
61
+In C<--force> mode, all existing symlinks to a dotfile will be deleted and
62
+re-linked (or copied if used with C<--copy>).
63
+
64
+When used with C<dfm check-update>, it always tells you the update notification
65
+including how many days since you've last updated.
66
+
67
+=item --help
68
+
69
+Shows this help documentation.
70
+
71
+=back
72
+
73
+=cut
74
+
75
+# Global command line arguments.
76
+our $noop  = 0;
77
+our $copy  = 0;
78
+our $force = 0;
79
+
80
+# Process command line arguments.
81
+sub flags {
82
+    # Command line flags.
83
+    my $help = 0;
84
+    GetOptions(
85
+        "force"  => \$force,
86
+        "noop"   => \$noop,
87
+        "copy"   => \$copy,
88
+        "help|?" => \$help,
89
+    );
90
+
91
+    if ($help) {
92
+        exec("perldoc", $0);
93
+    }
94
+}
95
+
96
+sub main {
97
+    flags();
98
+    if (scalar @ARGV == 0) {
99
+        die "Usage: dfm <command> [args...]\n";
100
+    }
101
+
102
+    # Handle commands.
103
+    my $command = lc(shift @ARGV);
104
+
105
+    if ($command eq "setup") {
106
+        setup();
107
+        system("dfm vim >/dev/null 2>&1 &");
108
+    } elsif ($command eq "vim") {
109
+        vim();
110
+    } elsif ($command eq "check-update") {
111
+        checkUpdate();
112
+    } elsif ($command eq "update") {
113
+        update();
114
+    }
115
+
116
+    return;
117
+}
118
+
119
+=head1 COMMANDS
120
+
121
+=over 4
122
+
123
+=item C<dfm setup>
124
+
125
+Sets up and installs the dotfiles.
126
+
127
+All files under the C<.dotfiles/home> directory are symlinked into their
128
+respective paths in C<$HOME>.
129
+
130
+If a file already exists in C<$HOME> and is not a link, the original file is
131
+copied into C<.dotfiles/backup> and then deleted and replaced with a link.
132
+
133
+Files that are already links are not modified, unless the C<--force> command
134
+line flag is used. Then links will be removed and relinked.
135
+
136
+At the end, this also calls C<dfm vim> in the background to handle Vim plugins.
137
+
138
+=cut
139
+
140
+sub setup {
141
+    crawl("$DOTFILES/home");
142
+
143
+    # Log the time we last ran this.
144
+    updated(time());
145
+}
146
+
147
+=item C<dfm vim>
148
+
149
+Sets up my Vim plugins.
150
+
151
+I use Git submodules for my Vim plugins, and these take several seconds to
152
+download when setting up my dotfiles for the first time.
153
+
154
+Calling C<dfm setup> will also call C<dfm vim> (in the background) at the end,
155
+so that most of the dotfiles get linked immediately and then the Vim ones
156
+follow shortly after, without making me wait for the submodules to download.
157
+
158
+=cut
159
+
160
+sub vim {
161
+    # Initialize the git submodules to pull down our Vim plugins.
162
+    print "Initializing git submodules...\n";
163
+    chdir($DOTFILES);
164
+    system("git submodule update --init");
165
+    chdir($HOME);
166
+    print "Submodules updated!\n\n";
167
+
168
+    # And re-run `dfm setup` to install the vim dotfiles.
169
+    setup();
170
+}
171
+
172
+=item C<dfm check-update>
173
+
174
+Check if the dotfiles haven't been updated in a while.
175
+
176
+This is intended to be called from your C<.bashrc> to notify that updates
177
+may be available, when the dotfiles were last updated greater than 15 days ago.
178
+
179
+If the last-update threshhold is greater than 15 days, it prints a message like:
180
+
181
+  It has been 15 days since you've updated your dotfiles (`dfm update`)
182
+
183
+Otherwise it doesn't print anything. Use C<--force> to force it to print.
184
+
185
+It only notifies about updates one time per day (by modifying the time stamp
186
+on the C<.last-updated> file).
187
+
188
+=cut
189
+
190
+sub checkUpdate {
191
+    my $time = updated();
192
+
193
+    my ($mtime) = (stat($UPDATE_FILE))[9];
194
+    my $delta = time() - $time;
195
+    my $days = int($delta / (60*60*24));
196
+
197
+    # Need to notify? If the last-updated time is >15 days and the file
198
+    # itself was last modified more than 24h ago.
199
+    my $notify = (time() - $time > $UPDATE_THRESHHOLD) && (time() - $mtime > 60*60*24);
200
+
201
+    if ($force || $notify) {
202
+        print "It has been $days days since you've updated your dotfiles (" .
203
+            "`dfm update`)\n";
204
+    }
205
+
206
+    system("touch", $UPDATE_FILE);
207
+    exit 0;
208
+}
209
+
210
+=item C<update>
211
+
212
+Update the dotfiles.
213
+
214
+This will go into the git repo and do a C<git pull> and re-link any new files.
215
+
216
+=cut
217
+
218
+sub update {
219
+    chdir($DOTFILES);
220
+    system("git stash; git pull; git stash pop");
221
+    setup();
222
+}
223
+
224
+=back
225
+
226
+=cut
227
+
228
+sub crawl {
229
+    my ($directory) = @_;
230
+
231
+    opendir(my $dh, $directory);
232
+    foreach my $file (readdir($dh)) {
233
+        next if $file =~ /^\.+$/; # Skip . and ..
234
+
235
+        # Get the absolute path to this file in the dotfiles repo and its
236
+        # destination relative to $HOME.
237
+        my $source = "$directory/$file";
238
+        my $target = $source;
239
+        $target =~ s{^$DOTFILES/home}{$HOME}g;
240
+        my $backup = $source;
241
+        $backup =~ s{^$DOTFILES/home}{$BACKUP}g;
242
+
243
+        # If the source is a directory, make sure it exists relative to $HOME.
244
+        if (-d $source) {
245
+            if (!-d $target) {
246
+                print "Create directory: $target\n";
247
+                mkdir($target) unless $noop;
248
+            }
249
+            if (!-d $backup) {
250
+                mkdir($backup) unless $noop;
251
+            }
252
+
253
+            # Recursively descend into the directory.
254
+            crawl($source);
255
+            next;
256
+        }
257
+
258
+        # Besides directories we only care about normal files.
259
+        next unless -f $source;
260
+
261
+        # Existing non-link targets should be backed up.
262
+        if (-f $target && !-l $target) {
263
+            print "Back up existing file to: $backup\n";
264
+            copy($target, $backup) unless $noop;
265
+            unlink($target) unless $noop;
266
+        }
267
+
268
+        # If in `copy` or `force` mode, delete any existing symlink.
269
+        if (-l $target && ($copy || $force)) {
270
+            my $link = readlink($target);
271
+            my $label = $copy ? "copy" : "force";
272
+            print "[--$label] Delete existing link (was $target -> $link)\n";
273
+            unlink($target) unless $noop;
274
+        }
275
+
276
+        # Already linked?
277
+        if (-l $target || ($copy && -f $target)) {
278
+            my $link = readlink($target);
279
+            if ($link ne $source) {
280
+                print "Removing old link; wrong target (was $target -> $link)\n";
281
+                unlink($target) unless $noop;
282
+            } else {
283
+                # Already linked!
284
+                next;
285
+            }
286
+        }
287
+
288
+        # Link it.
289
+        print "Link: $target\n";
290
+        if ($copy) {
291
+            copy($source, $target) unless $noop;
292
+        } else {
293
+            symlink($source, $target) unless $noop;
294
+        }
295
+
296
+        # Fix permissions.
297
+        if ($source =~ m{.ssh/config$}) {
298
+            print "Fix permissions: .ssh/config\n";
299
+            chmod 0600, $target unless $noop;
300
+        }
301
+    }
302
+}
303
+
304
+# Get or set the 'last updated' time.
305
+sub updated {
306
+    my ($setting) = @_;
307
+
308
+    if ($setting) {
309
+        open(my $fh, ">", $UPDATE_FILE);
310
+        print {$fh} Dumper({ time => $setting });
311
+        close($fh);
312
+        return $setting;
313
+    }
314
+
315
+    if (-f $UPDATE_FILE) {
316
+        my $data = do $UPDATE_FILE;
317
+        return $data->{time} || 0;
318
+    }
319
+
320
+    return 0;
321
+}
322
+
323
+main() unless caller;
324
+
325
+=head1 AUTHOR
326
+
327
+Noah Petherbridge, L<https://www.kirsle.net/>
328
+
329
+=cut

+ 3
- 97
setup View File

@@ -1,97 +1,3 @@
1
-#!/usr/bin/env python3
2
-
3
-"""Initialize your dotfiles setup.
4
-
5
-Usage: setup [--install]
6
-
7
-This will create symlinks in $HOME for every file listed in ./home in this
8
-repository. It will NOT delete existing files in $HOME; use the --install
9
-option to delete existing files.
10
-
11
-This script should work in any version of Python above v2.5"""
12
-
13
-import sys
14
-import os
15
-import os.path
16
-import shutil
17
-import re
18
-import subprocess
19
-import stat
20
-
21
-# Install? (deletes existing files in $HOME).
22
-install = "--install" in sys.argv
23
-
24
-# Get the path to the git repo.
25
-basedir = os.path.abspath(os.path.dirname(__file__))
26
-homedir = os.environ.get("HOME", ".")
27
-source  = os.path.join(basedir, "home")
28
-
29
-print("Setting up .dotfiles")
30
-print("====================")
31
-print("")
32
-if install:
33
-    print("* Install mode: will delete files in $HOME and set up symlinks!")
34
-
35
-# Set up the submodules first.
36
-print("Initializing git submodules...")
37
-os.chdir(basedir)
38
-subprocess.call(["git", "submodule", "init"])
39
-subprocess.call(["git", "submodule", "update"])
40
-os.chdir(homedir)
41
-print("Submodules updated!")
42
-print("")
43
-
44
-def crawl(folder):
45
-    """Recursively crawl a folder. Directories will be created relative to
46
-    $HOME, and files in those directories will be symlinked."""
47
-    for item in sorted(os.listdir(folder)):
48
-        # Resolve the path to this file relative to $HOME and the absolute
49
-        # path to the symlink target. First get the path to the target.
50
-        target = os.path.join(folder, item)
51
-
52
-        # Remove the source dir prefix from it to get the path relative
53
-        # to the "./home" folder, then use that for $HOME.
54
-        path   = re.sub(r'^%s/' % source, '', target)
55
-        home   = os.path.join(homedir, path)
56
-
57
-        # If the target is a directory, make sure it exists relative to $HOME.
58
-        if os.path.isdir(target):
59
-            if not os.path.isdir(home):
60
-                print("Create directory:", home)
61
-                os.mkdir(home)
62
-
63
-            # Recursively crawl it.
64
-            crawl(target)
65
-            continue
66
-
67
-        # Otherwise it's a file. In install mode, delete the existing file.
68
-        if os.path.exists(home) or os.path.islink(home):
69
-            if install:
70
-                print("Delete:", home)
71
-                os.unlink(home)
72
-            elif not os.path.islink(home):
73
-                print("Found existing (non-link file), but not in --install mode:", home)
74
-                continue
75
-
76
-        # Already linked?
77
-        if os.path.islink(home):
78
-            link = os.readlink(home)
79
-            if link == target:
80
-                print("Already linked:", home)
81
-                continue
82
-            else:
83
-                print("Delete existing link:", home)
84
-                os.unlink(home)
85
-
86
-        # Link it.
87
-        print("Link: %s -> %s" % (home, target))
88
-        os.symlink(target, home)
89
-
90
-        # Fix permissions.
91
-        if path == ".ssh/config":
92
-            print("chmod 600 .ssh/config")
93
-            os.chmod(home, stat.S_IRUSR | stat.S_IWUSR)
94
-
95
-crawl(source)
96
-
97
-# vim:expandtab
1
+#!/bin/bash
2
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
3
+exec "$DIR/home/bin/dfm" setup $@

Loading…
Cancel
Save