Browse Source

Replace old setup Python script with command

master
Noah Petherbridge 3 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 @@
backup/
.last-updated
__pycache__
*.pyc

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

@@ -2,6 +2,9 @@
# Common shell functions/aliases between bash and zsh.
###

# Notify to update the dotfiles.
dfm check-update

################################################################################
## Functions
################################################################################


+ 329
- 0
home/bin/dfm View File

@@ -0,0 +1,329 @@
#!/usr/bin/env perl

=head1 NAME

dfm: Dotfiles Manager.

=head1 DESCRIPTION

This is a custom script to help me manage my dotfiles. The C<setup> script
acts as an alias to C<dfm setup>.

This is written in Perl for maximum compatibility; the old C<setup> script that
this replaces was written in Python, and for the servers I work with I had to
make it simultaneously support Python 2.6, 2.7 and 3.x; Perl is simpler and
more universal.

=head1 USAGE

dfm <command> [options...]

=cut

use strict;
use warnings;

use Cwd qw(getcwd abs_path);
use Data::Dumper;
use File::Copy;
use FindBin;
use Getopt::Long;

# Our home folder and the root to the .dotfiles repo.
our $HOME = $ENV{HOME} || $ENV{USERPROFILE} || getcwd();
our $DOTFILES = abs_path("$FindBin::RealBin/../..");
our $BACKUP = "$DOTFILES/backup";

# Threshhold at which to start notifying about possible updates.
our $UPDATE_FILE = "$DOTFILES/.last-updated";
our $UPDATE_THRESHHOLD = 60*60*24*15;

=head1 OPTIONS

=over 4

=item --noop

Don't make any changes to the filesystem; just go through the motions.

=item --copy

Use file copies instead of symlinks, in case your host platform doesn't support
symlinks.

In C<--copy> mode, any existing symlinks to a dotfile will be deleted and
replaced with a normal file copy.

=item --force

Force certain subcommands to do as much work as possible.

In C<--force> mode, all existing symlinks to a dotfile will be deleted and
re-linked (or copied if used with C<--copy>).

When used with C<dfm check-update>, it always tells you the update notification
including how many days since you've last updated.

=item --help

Shows this help documentation.

=back

=cut

# Global command line arguments.
our $noop = 0;
our $copy = 0;
our $force = 0;

# Process command line arguments.
sub flags {
# Command line flags.
my $help = 0;
GetOptions(
"force" => \$force,
"noop" => \$noop,
"copy" => \$copy,
"help|?" => \$help,
);

if ($help) {
exec("perldoc", $0);
}
}

sub main {
flags();
if (scalar @ARGV == 0) {
die "Usage: dfm <command> [args...]\n";
}

# Handle commands.
my $command = lc(shift @ARGV);

if ($command eq "setup") {
setup();
system("dfm vim >/dev/null 2>&1 &");
} elsif ($command eq "vim") {
vim();
} elsif ($command eq "check-update") {
checkUpdate();
} elsif ($command eq "update") {
update();
}

return;
}

=head1 COMMANDS

=over 4

=item C<dfm setup>

Sets up and installs the dotfiles.

All files under the C<.dotfiles/home> directory are symlinked into their
respective paths in C<$HOME>.

If a file already exists in C<$HOME> and is not a link, the original file is
copied into C<.dotfiles/backup> and then deleted and replaced with a link.

Files that are already links are not modified, unless the C<--force> command
line flag is used. Then links will be removed and relinked.

At the end, this also calls C<dfm vim> in the background to handle Vim plugins.

=cut

sub setup {
crawl("$DOTFILES/home");

# Log the time we last ran this.
updated(time());
}

=item C<dfm vim>

Sets up my Vim plugins.

I use Git submodules for my Vim plugins, and these take several seconds to
download when setting up my dotfiles for the first time.

Calling C<dfm setup> will also call C<dfm vim> (in the background) at the end,
so that most of the dotfiles get linked immediately and then the Vim ones
follow shortly after, without making me wait for the submodules to download.

=cut

sub vim {
# Initialize the git submodules to pull down our Vim plugins.
print "Initializing git submodules...\n";
chdir($DOTFILES);
system("git submodule update --init");
chdir($HOME);
print "Submodules updated!\n\n";

# And re-run `dfm setup` to install the vim dotfiles.
setup();
}

=item C<dfm check-update>

Check if the dotfiles haven't been updated in a while.

This is intended to be called from your C<.bashrc> to notify that updates
may be available, when the dotfiles were last updated greater than 15 days ago.

If the last-update threshhold is greater than 15 days, it prints a message like:

It has been 15 days since you've updated your dotfiles (`dfm update`)

Otherwise it doesn't print anything. Use C<--force> to force it to print.

It only notifies about updates one time per day (by modifying the time stamp
on the C<.last-updated> file).

=cut

sub checkUpdate {
my $time = updated();

my ($mtime) = (stat($UPDATE_FILE))[9];
my $delta = time() - $time;
my $days = int($delta / (60*60*24));

# Need to notify? If the last-updated time is >15 days and the file
# itself was last modified more than 24h ago.
my $notify = (time() - $time > $UPDATE_THRESHHOLD) && (time() - $mtime > 60*60*24);

if ($force || $notify) {
print "It has been $days days since you've updated your dotfiles (" .
"`dfm update`)\n";
}

system("touch", $UPDATE_FILE);
exit 0;
}

=item C<update>

Update the dotfiles.

This will go into the git repo and do a C<git pull> and re-link any new files.

=cut

sub update {
chdir($DOTFILES);
system("git stash; git pull; git stash pop");
setup();
}

=back

=cut

sub crawl {
my ($directory) = @_;

opendir(my $dh, $directory);
foreach my $file (readdir($dh)) {
next if $file =~ /^\.+$/; # Skip . and ..

# Get the absolute path to this file in the dotfiles repo and its
# destination relative to $HOME.
my $source = "$directory/$file";
my $target = $source;
$target =~ s{^$DOTFILES/home}{$HOME}g;
my $backup = $source;
$backup =~ s{^$DOTFILES/home}{$BACKUP}g;

# If the source is a directory, make sure it exists relative to $HOME.
if (-d $source) {
if (!-d $target) {
print "Create directory: $target\n";
mkdir($target) unless $noop;
}
if (!-d $backup) {
mkdir($backup) unless $noop;
}

# Recursively descend into the directory.
crawl($source);
next;
}

# Besides directories we only care about normal files.
next unless -f $source;

# Existing non-link targets should be backed up.
if (-f $target && !-l $target) {
print "Back up existing file to: $backup\n";
copy($target, $backup) unless $noop;
unlink($target) unless $noop;
}

# If in `copy` or `force` mode, delete any existing symlink.
if (-l $target && ($copy || $force)) {
my $link = readlink($target);
my $label = $copy ? "copy" : "force";
print "[--$label] Delete existing link (was $target -> $link)\n";
unlink($target) unless $noop;
}

# Already linked?
if (-l $target || ($copy && -f $target)) {
my $link = readlink($target);
if ($link ne $source) {
print "Removing old link; wrong target (was $target -> $link)\n";
unlink($target) unless $noop;
} else {
# Already linked!
next;
}
}

# Link it.
print "Link: $target\n";
if ($copy) {
copy($source, $target) unless $noop;
} else {
symlink($source, $target) unless $noop;
}

# Fix permissions.
if ($source =~ m{.ssh/config$}) {
print "Fix permissions: .ssh/config\n";
chmod 0600, $target unless $noop;
}
}
}

# Get or set the 'last updated' time.
sub updated {
my ($setting) = @_;

if ($setting) {
open(my $fh, ">", $UPDATE_FILE);
print {$fh} Dumper({ time => $setting });
close($fh);
return $setting;
}

if (-f $UPDATE_FILE) {
my $data = do $UPDATE_FILE;
return $data->{time} || 0;
}

return 0;
}

main() unless caller;

=head1 AUTHOR

Noah Petherbridge, L<https://www.kirsle.net/>

=cut

+ 3
- 97
setup View File

@@ -1,97 +1,3 @@
#!/usr/bin/env python3

"""Initialize your dotfiles setup.

Usage: setup [--install]

This will create symlinks in $HOME for every file listed in ./home in this
repository. It will NOT delete existing files in $HOME; use the --install
option to delete existing files.

This script should work in any version of Python above v2.5"""

import sys
import os
import os.path
import shutil
import re
import subprocess
import stat

# Install? (deletes existing files in $HOME).
install = "--install" in sys.argv

# Get the path to the git repo.
basedir = os.path.abspath(os.path.dirname(__file__))
homedir = os.environ.get("HOME", ".")
source = os.path.join(basedir, "home")

print("Setting up .dotfiles")
print("====================")
print("")
if install:
print("* Install mode: will delete files in $HOME and set up symlinks!")

# Set up the submodules first.
print("Initializing git submodules...")
os.chdir(basedir)
subprocess.call(["git", "submodule", "init"])
subprocess.call(["git", "submodule", "update"])
os.chdir(homedir)
print("Submodules updated!")
print("")

def crawl(folder):
"""Recursively crawl a folder. Directories will be created relative to
$HOME, and files in those directories will be symlinked."""
for item in sorted(os.listdir(folder)):
# Resolve the path to this file relative to $HOME and the absolute
# path to the symlink target. First get the path to the target.
target = os.path.join(folder, item)

# Remove the source dir prefix from it to get the path relative
# to the "./home" folder, then use that for $HOME.
path = re.sub(r'^%s/' % source, '', target)
home = os.path.join(homedir, path)

# If the target is a directory, make sure it exists relative to $HOME.
if os.path.isdir(target):
if not os.path.isdir(home):
print("Create directory:", home)
os.mkdir(home)

# Recursively crawl it.
crawl(target)
continue

# Otherwise it's a file. In install mode, delete the existing file.
if os.path.exists(home) or os.path.islink(home):
if install:
print("Delete:", home)
os.unlink(home)
elif not os.path.islink(home):
print("Found existing (non-link file), but not in --install mode:", home)
continue

# Already linked?
if os.path.islink(home):
link = os.readlink(home)
if link == target:
print("Already linked:", home)
continue
else:
print("Delete existing link:", home)
os.unlink(home)

# Link it.
print("Link: %s -> %s" % (home, target))
os.symlink(target, home)

# Fix permissions.
if path == ".ssh/config":
print("chmod 600 .ssh/config")
os.chmod(home, stat.S_IRUSR | stat.S_IWUSR)

crawl(source)

# vim:expandtab
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
exec "$DIR/home/bin/dfm" setup $@

Loading…
Cancel
Save