commit 85f6059b50bb58b7ed5312dc3fa6b408d0a25add
Author: Alex Balgavy <a.balgavy@gmail.com>
Date: Fri, 31 Jan 2020 21:27:50 +0100
Initial commit
Diffstat:
A | .gitignore | | | 70 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | README.md | | | 91 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | conf | | | 445 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | man/conf.1 | | | 248 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4 files changed, 854 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,70 @@
+
+# Created by https://www.gitignore.io/api/macos,perl
+# Edit at https://www.gitignore.io/?templates=macos,perl
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Perl ###
+!Build/
+.last_cover_stats
+/META.yml
+/META.json
+/MYMETA.*
+*.o
+*.pm.tdy
+*.bs
+
+# Devel::Cover
+cover_db/
+
+# Devel::NYTProf
+nytprof.out
+
+# Dizt::Zilla
+/.build/
+
+# Module::Build
+_build/
+Build
+Build.bat
+
+# Module::Install
+inc/
+
+# ExtUtils::MakeMaker
+/blib/
+/_eumm/
+/*.gz
+/Makefile
+/Makefile.old
+/MANIFEST.bak
+/pm_to_blib
+/*.zip
+
+# End of https://www.gitignore.io/api/macos,perl
diff --git a/README.md b/README.md
@@ -0,0 +1,91 @@
+# conf - a personal configuration manager
+## Installation
+1. Download the `conf` script, make it executable, and put it in your `$PATH`.
+2. Set value of `$DOTFILES` to the location of your dotfiles, either in the environment, or in the `conf` script itself.
+3. Set up your dotfiles hierarchy in any way you want.
+3. Put a map file in the root of $DOTFILES. This file defines how your dotfiles/folders map to other locations in your filesystem.
+4. Write the name of your map file in the `conf` script.
+5. Use `conf`, and profit, maybe. Run `conf -h` or `conf -man` to get help.
+
+## FAQ
+### How do you write a map file?
+This is easier by example.
+
+If you have a dotfile directory hierarchy that looks like this:
+
+```
+~/dotfiles
+├── dot.map
+├── shell
+│ ├── zprofile
+│ └── zshrc
+└── vim
+│ ├── after
+│ │ └── ftplugin
+│ ├── autoload
+│ └── vimrc
+└── tools
+ ├── lfrc
+ └── polybar
+ └── config
+
+```
+
+Then the `dot.map` file listed in the `dotfiles` directory might look like this:
+
+```map
+# All file/directory names are relative to the value of the $DOTFILES variable, which is set in the environment
+# or explicitly inside the `conf` script itself.
+shell:
+- zprofile: ~/.zprofile
+- zshrc: ~/.zshrc
+
+vim:
+- after/ftplugin: ~/.vim/after/ftplugin
+- autoload: ~/.vim/autoload
+- vimrc: ~/.vimrc
+
+tools/lfrc: ~/.config/lf/lfrc
+tools:
+- polybar:
+ -- config: ~/.config/polybar/config
+```
+
+And would result in the following symbolic links:
+
+* `~/.zprofile` pointing to `~/dotfiles/shell/zprofile`
+* `~/.zshrc` pointing to `~/dotfiles/shell/zshrc`
+* `~/.vim/after/ftplugin` pointing to `~/dotfiles/vim/after/ftplugin`
+* `~/.vim/autoload` pointing to `~/dotfiles/vim/autoload`
+* `~/.vimrc` pointing to `~/dotfiles/vimrc`
+* `~/.config/lf/lfrc` pointing to `~/dotfiles/tools/lfrc`
+* `~/.config/polybar/config` pointing to `~/dotfiles/tools/polybar/config`
+
+### Why would you make _another_ dotfiles manager?
+Valid question, there's so many dotfile managers already available.
+Why do I think I'm so special that none of the existing ones are good enough?
+
+Well, a few points about this manager:
+
+* It manages your dotfiles, and does nothing else.
+ It doesn't have git integration, it doesn't do any smart tracking, it doesn't replace your toilet paper - it just manages links between your configuration and the various configuration directories around your system.
+ You want git actions? Use git.
+* It reads its configuration from a single, plain-text file that can be named whatever you want.
+* It uses a configuration syntax that's intuitive _for me_.
+* It leverages the filesystem structure for simpler configuration.
+* It essentially runs anywhere out of the box.
+
+All this to make a dotfiles manager that works exactly the way I want it to work.
+
+### Why Perl?
+Because:
+
+* It's ubiquitous.
+* It's a step up from shell script in data structures, readability, maintainability, abstractions, etc.
+* It's still low level enough to work with the shell and filesystem, unlike e.g. Python.
+ If I want to test for a file's existence, I can use `-e`, like in Bash.
+ Symlink functions are available without having to import anything.
+* It's powerful for text manipulation, because it has regex built straight into the language.
+* It's still quite fast.
+ In fact, it's super fast, compared to my previous version written in Bash.
+ On my computer, the "list all" operation is at least five times faster, and "check all" is at least seven times faster.
diff --git a/conf b/conf
@@ -0,0 +1,445 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use Getopt::Long 'GetOptions';
+use Pod::Usage 'pod2usage';
+use Cwd 'realpath';
+use File::Copy 'move';
+use File::Path 'make_path';
+
+# Set these however you want
+my $DOTFILES = $ENV{'DOTFILES'};
+my $MAPFILE = $DOTFILES."/dot.map";
+
+=head1 NAME
+
+conf - Manage your dotfiles.
+
+=head1 SYNOPSIS
+
+ conf [options] command [entry_1 [entry_2 ...]]
+
+ Help Options:
+ --help, -h Show this script's help information.
+ --manual, -man Read this script's manual.
+
+ Arguments:
+ link Link an entry.
+ unlink Unlink an entry.
+ check Check an entry.
+ list List entries.
+ edit Edit the map file.
+
+=cut
+
+# Some preliminary checks
+die "\$DOTFILES not set." if not $DOTFILES;
+chdir "$DOTFILES" or die "Cannot cd into $DOTFILES.";
+(-e $MAPFILE) or die "Mapfile $MAPFILE does not exist.";
+
+# Print with a newline
+sub puts {
+ print $_[0]."\n";
+}
+
+# Ask a confirmation question, return the boolean answer
+sub confirm {
+ # Print the question
+ my $question = $_[0];
+ print $question." [Y/n] ";
+
+ # Obtain and clean the response
+ my $response = <>;
+ chomp $response;
+
+ # Check if the response is a "yes", return a boolean
+ return ($response =~ /^\s*([Yy]|[Yy][Ee][Ss])\b/);
+}
+
+# Print a message to stderr and exit
+sub Die {
+ print STDERR $_[0]."\n";
+ exit 1;
+}
+
+# Does a line contain a mapping?
+sub IsMapLine {
+ # It's a mapping if it's not blank or a comment
+ return ($_[0] !~ /^\s*$/ and $_[0] !~ /^\s*\#/ and $_[0] !~ /^$/)
+}
+
+# Parse a single map line into a standalone key-value mapping
+sub ParseLine {
+ # Split the line on colons
+ my @mapline = split(':', $_[0]);
+
+ # Grab the home directory
+ my $homedir = $ENV{'HOME'};
+
+ # Remove leading/trailing whitespace for both parts of the mapline
+ s/^\s*|\s*$//g for @mapline;
+
+ # Replace '~' with the full path of $HOME
+ $mapline[1] =~ s/\~/$homedir/;
+
+ # Return a reference to the parsed mapline
+ return \@mapline;
+}
+
+# Parse the whole mapfile, return a reference to the key-value hash
+sub ParseMapfile {
+ my (@nestdir, %mappings);
+ my $lineno = 1;
+
+ # Open up the mapfile for reading, using the handle MAP
+ open(MAP, "<$_[0]") or Die("Couldn't open the file $_[0], $!");
+
+ # While not EOF
+ while(my $line = <MAP>) {
+ if (IsMapLine($line)) {
+ # Parse the mapline
+ my ($src, $dst) = @{ParseLine($line)};
+
+ # If top level mapping
+ if ($src !~ /^-+/) {
+ # Zero out the nesting directory
+ @nestdir = ();
+
+ # If the line defines a new nesting dir, remember it
+ if (not defined $dst or $dst eq '') {
+ push @nestdir, $src
+ }
+
+ # Otherwise, it's a one-off mapping (doesn't have related nested mappings below)
+ else {
+ # If the source doesn't exist, can't map
+ if (not -e $src) {
+ Die("error in mapfile: $src does not exist (line $lineno)");
+ }
+ # If it does, add it as a mapping
+ else {
+ $mappings{"$src"} = $dst;
+ }
+ }
+ }
+
+ # Nested mappings
+ else {
+ # Number of dashes == number of levels
+ my $levels = length( ($src =~ /^(-+)/)[0] );
+
+ # Remove the punctuation to get the actual source
+ $src =~ s/(^-* *)|://g;
+
+ # If/while the mapping is at a higher level than the previous, unnest
+ if ($levels < scalar @nestdir) {
+ pop @nestdir while ($levels < scalar @nestdir);
+ }
+
+ # If the mapping's another nesting dir, add it
+ if (not defined $dst or $dst eq '') {
+ push @nestdir, $src
+ }
+ # Otherwise, if it's a mapping, save it with the full path
+ else {
+ my $fullsrc = join("/", @nestdir)."/$src";
+ # If it doesn't exist, can't map
+ if (not -e $fullsrc) {
+ Die("error in mapfile: $fullsrc does not exist (line $lineno)");
+ }
+ # Otherwise, add the mapping
+ else {
+ $mappings{"$fullsrc"} = $dst;
+ }
+ }
+ }
+ }
+ # Increase the line number to keep track of where we are in the mapfile (error reporting)
+ $lineno++;
+ }
+
+ # Return a ref to the hash of mappings
+ return \%mappings;
+}
+
+# Dump a hash, for debugging purposes
+sub PrettyPrint {
+ puts "hash length: ".keys(%{$_[0]});
+ use Data::Dumper;
+ print Dumper $_[0];
+}
+
+# Link to a source path at a destination path
+sub MakeLinkOp {
+ my ($src, $dst) = @_;
+
+ # If the destination exists
+ if (-e $dst) {
+ # If it's a link and points to the source, nothing to do here
+ if (-l $dst and realpath($dst) eq $DOTFILES."/".$src) {
+ puts "$dst is already linked to $src";
+ return;
+ }
+ # Otherwise, if it's not a link, back it up
+ else {
+ puts "$dst already exists, renaming to $dst.bak";
+ move $dst, $dst.".bak";
+ }
+ }
+
+ # Create the intermediary dirs if they don't exist
+ (my $linkpath = $dst) =~ s:/[^/]*$::;
+ if (! -d $linkpath) {
+ make_path($linkpath) or die "Couldn't make path $linkpath, $!";
+ }
+
+ # Print a message and link to the source at the destination
+ puts "$src ==> $dst";
+ symlink($DOTFILES."/".$src, $dst) or die "Couldn't symlink, $!";
+}
+
+# Remove a linked source path
+sub RmLinkOp {
+ my ($src, $dst) = @_;
+
+ # If the destination doesn't exist, nothing to do here.
+ if (! -e "$dst") {
+ puts "$dst does not exist.";
+ }
+ # If it exists, but it's not a link, don't do anything.
+ elsif (! -l "$dst") {
+ puts "$dst is not a link, not removing.";
+ }
+ # If it's a link, but doesn't point to the config file, don't do anything.
+ elsif (not realpath($dst) eq $DOTFILES."/".$src) {
+ puts "$dst does not point to $src, not removing.";
+ }
+ # Otherwise, it's a link that points to the config file, so remove it.
+ else {
+ puts "Removing link: $dst";
+ unlink "$dst" or die "Failed to remove file $dst: $!\n";
+ }
+}
+
+# Check whether a link points to the config file
+sub CheckLinkOp {
+ my ($src, $dst) = @_;
+ if (-e $dst) {
+ if (-l $_[1] and realpath($_[1]) eq $DOTFILES."/".$_[0]) {
+ puts "[ OK ] $src is linked at $dst.";
+ }
+ elsif (-l $dst) {
+ puts "[ XX ] $dst is a link but does not point to $src.";
+ }
+ else {
+ puts "[ XX ] $dst exists but does not point to $src.";
+ }
+ }
+ else {
+ puts "[ XX ] $src is not linked.";
+ }
+}
+
+# Execute a link operation on maps in ARGV
+sub ExecLinkOp {
+ # Retrieve the reference to map hash and the function to run
+ my ($maps, $opref) = @_;
+
+ # If operation should be on all tracked files
+ if (not @ARGV) {
+ keys %$maps; # reset `each` iterator
+ foreach my $src (keys %$maps) {
+ $opref->($src, $maps->{$src});
+ }
+ }
+ # If the user specified what to operate on
+ else {
+ while (@ARGV) {
+ my $src_part = shift @ARGV;
+
+ # if there's a mapping that matches exactly, operate on that
+ if (exists $maps->{$src_part}) {
+ $opref->($src_part, $maps->{$src_part});
+ }
+ # otherwise, operate on everything starting with whatever's passed
+ elsif ( my @matching = grep(/^$src_part/, keys %$maps) ) {
+ foreach my $src(@matching) {
+ $opref->($src, $maps->{$src});
+ }
+ }
+ # if nothing matches, fail
+ else {
+ my %opnametab = (\&MakeLinkOp => 'link', \&RmLinkOp => 'unlink', \&CheckLinkOp => 'check');
+ Die "Error: $src_part not present in mapfile, don't know how to ".$opnametab{$opref}.'.';
+ }
+ }
+ }
+}
+
+# Link command
+sub LinkCmd {
+ # If no args, default to linking all but confirm with user
+ if (not @ARGV and !confirm("Link all?")) {
+ Die "User cancelled.";
+ }
+
+ ExecLinkOp($_[0], \&MakeLinkOp);
+}
+
+# Unlink command
+sub UnlinkCmd {
+ # If no args, default to unlinking all but confirm with user
+ if (not @ARGV and !confirm("Unlink all?")) {
+ Die "User cancelled";
+ }
+ ExecLinkOp($_[0], \&RmLinkOp);
+}
+
+# Edit command
+sub EditCmd {
+ # Set a default editor value
+ $ENV{EDITOR} ||= 'vim';
+
+ # system() doesn't capture stdout
+ system "$ENV{EDITOR} $MAPFILE";
+}
+
+sub CheckCmd {
+ ExecLinkOp($_[0], \&CheckLinkOp);
+}
+
+sub ListCmd {
+ puts "Mappings (from $MAPFILE), paths relative to $DOTFILES";
+ puts "(format: source => name_of_symlink)\n";
+
+ my $hash = $_[0];
+ my @output = ();
+ foreach my $k (keys %$hash) {
+ push @output, "$k => $hash->{$k}";
+ }
+ foreach my $l (sort @output) {
+ puts $l;
+ }
+}
+
+# Run a subcommand based on string
+sub RunSubcommand {
+ # Dispatch table
+ my %subcommands = (
+ link => \&LinkCmd,
+ unlink => \&UnlinkCmd,
+ edit => \&EditCmd,
+ list => \&ListCmd,
+ check => \&CheckCmd
+ );
+
+ # If the command isn't in the table, exit and print usage
+ $subcommands{$_[0]} or pod2usage(2);
+
+ # Parse the file
+ my $mappings = ParseMapfile($MAPFILE);
+ # Then execute the command on the extracted mappings
+ $subcommands{$_[0]}->($mappings);
+}
+
+# If no arguments, show usage
+scalar @ARGV > 0 or pod2usage(2);
+
+# Get the commandline options, only recognise help/manual, everything else gets sent to dispatch subroutine
+my ($help, $manual);
+my %optctl = (help => \$help, manual => \$manual, '<>' => \&RunSubcommand);
+GetOptions(\%optctl, 'help|h', 'manual|man', '<>') or pod2usage(2);
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<--help>
+Show the brief help information.
+
+=item B<--manual>
+Read the manual, with examples.
+
+=back
+=cut
+
+# If help set, show usage
+pod2usage(2) if $help;
+# If manual set, show full documentation
+pod2usage({ -verbose => 2, -exitval => 1}) if $manual;
+
+=head1 DESCRIPTION
+
+This is a script to manage filesystem-wide symbolic links to your dotfiles (configuration), based on definitions in the map file.
+The location of the map file is set in the script itself, using the $MAPFILE variable.
+The location of your dotfiles is set either using the $DOTFILES environment variable, or inside the script itself.
+
+=head1 ARGUMENTS
+
+=over 4
+
+=item * B<link [entry1 [entry2...]]>
+
+Link entries according to the map file.
+With no arguments, links all entries.
+
+=item * B<unlink [entry1 [entry2...]]>
+
+Unlink entries according to the map file.
+With no arguments, unlinks all entries.
+
+=item * B<check [entry1 [entry2...]]>
+
+Check that entries are linked accordint to the map file.
+With no arguments, checks all entries.
+
+=item * B<edit>
+
+Edit the map file with whatever you set as $EDITOR
+
+=item * B<list>
+
+List the current mappings.
+
+
+=back
+
+=head1 EXAMPLES
+
+Link everything in the mapfile:
+
+ conf link
+
+Link all files in the "vim" directory:
+
+ conf link vim
+
+Link specifically the shell/bashrc file:
+
+ conf link shell/bashrc
+
+Link the shell/zprofile file and the vim/autoload directory:
+
+ conf link shell/zprofile vim/autoload
+
+Check if everything is linked:
+
+ conf check
+
+Check if everything in the "vim" directory is linked:
+
+ conf check vim
+
+Remove the link to the "lf" directory:
+
+ conf unlink lf
+
+Remove all links defined in the mapfile:
+
+ conf unlink
+
+=head1 AUTHOR
+
+Alexander Balgavy (thezeroalpha), L<https://github.com/thezeroalpha>.
+
+=cut
diff --git a/man/conf.1 b/man/conf.1
@@ -0,0 +1,248 @@
+.\" Automatically generated by Pod::Man 4.11 (Pod::Simple 3.35)
+.\"
+.\" Standard preamble:
+.\" ========================================================================
+.de Sp \" Vertical space (when we can't use .PP)
+.if t .sp .5v
+.if n .sp
+..
+.de Vb \" Begin verbatim text
+.ft CW
+.nf
+.ne \\$1
+..
+.de Ve \" End verbatim text
+.ft R
+.fi
+..
+.\" Set up some character translations and predefined strings. \*(-- will
+.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left
+.\" double quote, and \*(R" will give a right double quote. \*(C+ will
+.\" give a nicer C++. Capital omega is used to do unbreakable dashes and
+.\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff,
+.\" nothing in troff, for use with C<>.
+.tr \(*W-
+.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p'
+.ie n \{\
+. ds -- \(*W-
+. ds PI pi
+. if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch
+. if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch
+. ds L" ""
+. ds R" ""
+. ds C` ""
+. ds C' ""
+'br\}
+.el\{\
+. ds -- \|\(em\|
+. ds PI \(*p
+. ds L" ``
+. ds R" ''
+. ds C`
+. ds C'
+'br\}
+.\"
+.\" Escape single quotes in literal strings from groff's Unicode transform.
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.\"
+.\" If the F register is >0, we'll generate index entries on stderr for
+.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index
+.\" entries marked with X<> in POD. Of course, you'll have to process the
+.\" output yourself in some meaningful fashion.
+.\"
+.\" Avoid warning from groff about undefined register 'F'.
+.de IX
+..
+.nr rF 0
+.if \n(.g .if rF .nr rF 1
+.if (\n(rF:(\n(.g==0)) \{\
+. if \nF \{\
+. de IX
+. tm Index:\\$1\t\\n%\t"\\$2"
+..
+. if !\nF==2 \{\
+. nr % 0
+. nr F 2
+. \}
+. \}
+.\}
+.rr rF
+.\"
+.\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2).
+.\" Fear. Run. Save yourself. No user-serviceable parts.
+. \" fudge factors for nroff and troff
+.if n \{\
+. ds #H 0
+. ds #V .8m
+. ds #F .3m
+. ds #[ \f1
+. ds #] \fP
+.\}
+.if t \{\
+. ds #H ((1u-(\\\\n(.fu%2u))*.13m)
+. ds #V .6m
+. ds #F 0
+. ds #[ \&
+. ds #] \&
+.\}
+. \" simple accents for nroff and troff
+.if n \{\
+. ds ' \&
+. ds ` \&
+. ds ^ \&
+. ds , \&
+. ds ~ ~
+. ds /
+.\}
+.if t \{\
+. ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u"
+. ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u'
+. ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u'
+. ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u'
+. ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u'
+. ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u'
+.\}
+. \" troff and (daisy-wheel) nroff accents
+.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V'
+.ds 8 \h'\*(#H'\(*b\h'-\*(#H'
+.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#]
+.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H'
+.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u'
+.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#]
+.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#]
+.ds ae a\h'-(\w'a'u*4/10)'e
+.ds Ae A\h'-(\w'A'u*4/10)'E
+. \" corrections for vroff
+.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u'
+.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u'
+. \" for low resolution devices (crt and lpr)
+.if \n(.H>23 .if \n(.V>19 \
+\{\
+. ds : e
+. ds 8 ss
+. ds o a
+. ds d- d\h'-1'\(ga
+. ds D- D\h'-1'\(hy
+. ds th \o'bp'
+. ds Th \o'LP'
+. ds ae ae
+. ds Ae AE
+.\}
+.rm #[ #] #H #V #F C
+.\" ========================================================================
+.\"
+.IX Title "CONF 1"
+.TH CONF 1 "2020-01-31" "perl v5.30.1" "User Contributed Perl Documentation"
+.\" For nroff, turn off justification. Always turn off hyphenation; it makes
+.\" way too many mistakes in technical documents.
+.if n .ad l
+.nh
+.SH "NAME"
+conf \- Manage your dotfiles.
+.SH "SYNOPSIS"
+.IX Header "SYNOPSIS"
+.Vb 1
+\& conf [options] command [entry_1 [entry_2 ...]]
+\&
+\& Help Options:
+\& \-\-help, \-h Show this script\*(Aqs help information.
+\& \-\-manual, \-man Read this script\*(Aqs manual.
+\&
+\& Arguments:
+\& link Link an entry.
+\& unlink Unlink an entry.
+\& check Check an entry.
+\& list List entries.
+\& edit Edit the map file.
+.Ve
+.SH "OPTIONS"
+.IX Header "OPTIONS"
+.IP "\fB\-\-help\fR Show the brief help information." 4
+.IX Item "--help Show the brief help information."
+.PD 0
+.IP "\fB\-\-manual\fR Read the manual, with examples." 4
+.IX Item "--manual Read the manual, with examples."
+.PD
+.SH "DESCRIPTION"
+.IX Header "DESCRIPTION"
+This is a script to manage filesystem-wide symbolic links to your dotfiles (configuration), based on definitions in the map file.
+The location of the map file is set in the script itself, using the \f(CW$MAPFILE\fR variable.
+The location of your dotfiles is set either using the \f(CW$DOTFILES\fR environment variable, or inside the script itself.
+.SH "ARGUMENTS"
+.IX Header "ARGUMENTS"
+.IP "\(bu" 4
+\&\fBlink [entry1 [entry2...]]\fR
+.Sp
+Link entries according to the map file.
+With no arguments, links all entries.
+.IP "\(bu" 4
+\&\fBunlink [entry1 [entry2...]]\fR
+.Sp
+Unlink entries according to the map file.
+With no arguments, unlinks all entries.
+.IP "\(bu" 4
+\&\fBcheck [entry1 [entry2...]]\fR
+.Sp
+Check that entries are linked accordint to the map file.
+With no arguments, checks all entries.
+.IP "\(bu" 4
+\&\fBedit\fR
+.Sp
+Edit the map file with whatever you set as \f(CW$EDITOR\fR
+.IP "\(bu" 4
+\&\fBlist\fR
+.Sp
+List the current mappings.
+.SH "EXAMPLES"
+.IX Header "EXAMPLES"
+Link everything in the mapfile:
+.PP
+.Vb 1
+\& conf link
+.Ve
+.PP
+Link all files in the \*(L"vim\*(R" directory:
+.PP
+.Vb 1
+\& conf link vim
+.Ve
+.PP
+Link specifically the shell/bashrc file:
+.PP
+.Vb 1
+\& conf link shell/bashrc
+.Ve
+.PP
+Link the shell/zprofile file and the vim/autoload directory:
+.PP
+.Vb 1
+\& conf link shell/zprofile vim/autoload
+.Ve
+.PP
+Check if everything is linked:
+.PP
+.Vb 1
+\& conf check
+.Ve
+.PP
+Check if everything in the \*(L"vim\*(R" directory is linked:
+.PP
+.Vb 1
+\& conf check vim
+.Ve
+.PP
+Remove the link to the \*(L"lf\*(R" directory:
+.PP
+.Vb 1
+\& conf unlink lf
+.Ve
+.PP
+Remove all links defined in the mapfile:
+.PP
+.Vb 1
+\& conf unlink
+.Ve
+.SH "AUTHOR"
+.IX Header "AUTHOR"
+Alexander Balgavy (thezeroalpha), <https://github.com/thezeroalpha>.