My Unix config files and shell scripts, optimized for Fedora, Debian, macOS and Windows (in that order).
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

339 lines
8.1 KiB

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