Replace old setup Python script with command
This commit is contained in:
parent
3efed09937
commit
ca25d0022e
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
||||||
|
backup/
|
||||||
|
.last-updated
|
||||||
__pycache__
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
# Common shell functions/aliases between bash and zsh.
|
# Common shell functions/aliases between bash and zsh.
|
||||||
###
|
###
|
||||||
|
|
||||||
|
# Notify to update the dotfiles.
|
||||||
|
dfm check-update
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
## Functions
|
## Functions
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
329
home/bin/dfm
Executable file
329
home/bin/dfm
Executable 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
|
100
setup
100
setup
|
@ -1,97 +1,3 @@
|
||||||
#!/usr/bin/env python3
|
#!/bin/bash
|
||||||
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
"""Initialize your dotfiles setup.
|
exec "$DIR/home/bin/dfm" 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
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user