Package: dfm
dfmA dotfile manager for lazy people and pair programmers.dfm doesn't require that the dotfiles in your repository start with dots
though it handles either case equally well.Table of ContentsFeaturesMultiple dotfile profilesProfile modulesPre and post command hooksRespects$XDG_CONFIG_HOMESkips relevant filesConfigurable mappingsEncrypted DotfilesInstallationUpdatingUsageGit quick startExisting dotfiles repositoryNo existing dotfiles repositoryConfigurationContributingLicenseFeaturesdfm supports these features that I was unable to find in other Dotfile
Management solutions.Multiple dotfile profilesdfm's core feature is the idea of profiles. Profiles are simply a
collection of dotfiles that dfm manages and links in the$HOMEdirectory or configuration directories. This means that you can have
multiple profiles and overlap them.This feature is hard to describe, so I will illustrate it's usefulness
with two use cases:The work profileI use one laptop for work and personal projects in my dfm profiles I have my
personal profilechasinglogicwhich contains all my dotfiles for Emacs, git,
etc. and a "work" profile which only has a.gitconfigthat has my work email
in it. So my profile directory looks like this:profiles/
├── chasinglogic
│   ├── agignore
│   ├── bash
│   ├── bashrc
│   ├── gitconfig
│   ├── gnupg
│   ├── password-store
│   ├── pypirc
│   ├── spacemacs.d
│   └── tmux.conf
└── work
    └── gitconfigSince dfm when linking only overwrites the files which are in the new
profile, I can rundfm link workand still have access to my emacs
configuration but mygitconfighas been updated to use my work
email. Similarly when I leave work I justdfm link chasinglogicto
switch back.Seeprofile modulesfor an even better solution to this
particular use case.Pair programmingThe original inspiration for this tool was pair programming with my
friendlionize. lionize has a dotfiles
repository so I can clone it using the git backend for dfm withdfm clone --name lionize https://github.com/lionize/dotfiles.Now our profile directory looks like:profiles/
├── chasinglogic
│   ├── .dfm.yml
│   ├── .git
│   ├── .gitignore
│   ├── agignore
│   ├── bash
│   ├── bashrc
│   ├── gitconfig
│   ├── gnupg
│   ├── password-store
│   ├── pypirc
│   ├── spacemacs.d
│   └── tmux.conf
├── lionize
│   ├── .agignore
│   ├── .git
│   ├── .gitconfig
│   ├── .gitignore_global
│   ├── .gitmessage
│   ├── .scripts
│   ├── .tmux.conf
│   ├── .vim
│   ├── .vimrc -> ./.vim/init.vim
│   └── .zshrc
└── work
    ├── .git
    └── gitconfigNow when I'm driving I simplydfm link chasinglogicand when passing back to
lionize he runsdfm link lionizeand we don't have to mess with multiple
machines vice versa.Profile modulesdfm supports profile modules which can be either additional dotfiles profiles as
accepted by thedfm clonecommand or can be any git repository such asSpacemacs. You can get more info about
how to use them and configure them inConfigurationPre and Post command hooksdfm supports pre and post command hooks. that allows you to specify before and
after command scripts to run. For example, I use a profile module to keep
certain ssh keys in an encrypted git repository. Whenever I run thedfm synccommand
I have hooks which fix the permissions of the keys and ssh-add them to my ssh
agent. You can read about how to write your own hooks inConfigurationRespects$XDG_CONFIG_HOMEdfm respects dotfiles which exist in the$XDG_CONFIG_HOMEdirectory,
meaning if in your repository you have a folder namedconfigor.configit'll translate those into the$XDG_CONFIG_HOMEdirectory automatically. Similarly when usingdfm addif inside your$XDG_CONFIG_HOMEor $HOME/.configuration directories it'll add those to
the repository appropriately.Skips relevant filesdfm by default will skip multiple relevant files..gitdfm will skip the .git directory so your$HOMEdirectory isn't
turned into a git repository..gitignoreIf you would like to store a global.gitignorefile you can either omit the
leading dot (so justgitignore) or name the global one.ggitignoreand dfm
will translate the name for you. Otherwise it assumes that.gitignoreis the
gitignore for the profile's repository and so skips it.READMEWant to make a README for your dotfiles? Go ahead! As long as the file name
starts with README dfm will ignore it. SoREADME.txtREADME.mdandREADME.rstor whatever other permutations you can dream up all work.LICENSEYou should put a LICENSE on all code you put on the internet and some dotfiles /
configurations are actual code (See: Emacs). If you put a LICENSE in your
profile dfm will respect you being a good open source citizen and not clutter your$HOMEdirectory..dfm.ymlThis is a special dfm file used for hooks today and in the future for other ways
to extend dfm. As such dfm doesn't put it in your$HOMEdirectory.Custom mappingsThe above ignores are implemented as a dfm feature called
Mappings. You can write your own mappings to either skip, skip based
on platform or translate files to different locations than dfm would
normally place them. You can read how to configure your own mappings
inConfigurationEncrypted DotfilesUsing hooks and mappings you can integrate GPG with DFM to have an encrypted
dotfiles repository.If you add the following.dfm.ymlto your repository per theConfigurationdocumentation:---mappings:-match:'.*.gpg'skip:truehooks:before_sync:-interpreter:/bin/bash -cscript:|echo "encrypting files..."for file in $(find . -not -name '*.gpg' -not -name '.dfm.yml' -not -name '.gitignore' -not -path './.git/*'); doecho "Encrypting $file to ${file/.gpg/}"gpg --batch --yes --encrypt ${file/.gpg/}doneafter_sync:-interpreter:/bin/bash -cscript:|for file in $(git ls-files | grep -v .dfm.yml | grep -v .gitignore); dogpg --batch --yes --decrypt -o ${file/.gpg/} $filedoneAnd the following.gitignorefile:*
!*/
!.gitignore
!.dfm.yml
!*.gpgThen when runningdfm syncDFM will run the gpg command to encrypt all your
files, then git will ignore all non-GPG encrypted files (due to the.gitignore), and after syncing DFM will decrypt all the GPG encrypted files.This all happens before linking, when you rundfm linkDFM will ignore all gpg
encrypted files due to themappingconfiguration. It will then only link the
unencrypted versions into your home directory.InstallationInstall from Releasedfm is available on PyPi and should be installed from there:$ pip3 install dfmdfm supports Python 3+.Install from SourceClone the repository and runmake install:gitclonehttps://github.com/chasinglogic/dfmcddfm
makeinstallIt's possible that for your system you will need to run the make
command with sudo.UsageUsage:
    dfm [options] <command> [<args>...]
    dfm help
    dfm sync
    dfm link <profile>

Dotfile management written for pair programmers. Examples on getting
started with dfm are avialable at https://github.com/chasinglogic/dfm

Options:
    -v, --verbose  If provided print more logging info
    --debug        If provided print debug level logging info
    -h, --help     Print this help information

Commands:
    help           Print usage information about dfm commands
    sync (s)       Sync your dotfiles
    add (a)        Add the file to the current dotfile profile
    clean (x)      Clean dead symlinks
    clone (c)      Use git clone to download an existing profile
    git (g)        Run the given git command on the current profile
    init (i)       Create a new profile
    link (l)       Create links for a profile
    list (ls)      List available profiles
    remove (rm)    Remove a profile
    run-hook (rh)  Run dfm hooks without using normal commands
    where (w)      Prints the location of the current dotfile profile

See 'dfm help <command>' for more information on a specific command.Quick startQuick start (Existing dotfiles repository)If you already have a dotfiles repository you can start by cloning it using the clone
command.SSH URLs will work as well.dfmclonehttps://github.com/chasinglogic/dotfilesIf you're using GitHub you can shortcut the domain:dfmclonechasinglogic/dotfilesIf you want to clone and link the dotfiles in one command:dfmclone--linkchasinglogic/dotfilesYou may have to use--overwriteas well if you have existing non-symlinked
versions of your dotfilesOnce you have multiple profiles you can switch between them usingdfm linkdfmlinksome-other-profileSee the Usage Notes below for some quick info on what to expect from other dfm
commands.Quick Start (No existing dotfiles repository)If you don't have a dotfiles repository the best place to start is withdfm initdfminitmy-new-profileThen rundfm linkto set it as the active profile, this is also how you switch
profilesdfmlinkmy-new-profileOnce that's done you can start adding your dotfilesdfmadd~/.bashrcAlternatively you can add multiple files at oncedfmadd~/.bashrc~/.vimrc~/.vim~/.emacs.dThen create your dotfiles repository on GitHub. Instructions for how to do that can be
foundhere. Once that's done
get the "clone" URL for your new repository and set it as origin for the profile:Note:When creating the remote repository don't choose any options such as
"initialize this repository with a README" otherwise git'll get cranky when you add
the remote because of a recent git update and how it handlesunrelated
historiesif you do don't worry the linked post explains how to get past it.dfmgitremoteaddorigin<yourcloneURL>Then simply rundfm syncto sync your dotfiles to the remotedfmsyncNow you're done!Configurationdfm supports a.dfm.ymlfile in the root of your repository that
changes dfm's behavior when syncing and linking your profile. This
file will be ignored when doing adfm linkso won't end up in
your home directory. The.dfm.ymlcan be used to configure these
features:ModulesMappingsHooksModulesModules in dfm are sub profiles. They're git repositories that are cloned into a
a special directory:$XDG_CONFIG_HOME/dfm/modules. They're shared across
profiles so if two dotfile profiles have the same module they'll share that
module.The syntax for defining a minimum module is as follows:modules:-repository:git@github.com:chasinglogic/dotfilesThis would clone my dotfiles repository as a module into$XDG_CONFIG_HOME/dfm/modules/chasinglogic. If I wanted to use a unique name or
some other folder name so it wouldn't be shared you can specify an additional
optionname:modules:-repository:git@github.com:chasinglogic/dotfilesname:chasinglogic-dotfilesWhich would instead clone into$XDG_CONFIG_HOME/dfm/modules/chasinglogic-dotfiles. You can define multiple
modules:modules:-repository:git@github.com:chasinglogic/dotfilesname:chasinglogic-dotfiles-repository:git@github.com:lionize/dotfilesMake sure that you specify a name if the resulting clone location as defined by
git would conflict as we see here. Both of these would have been cloned into
dotfiles which would cause the clone to fail for the second module if we didn't
specify name for chasinglogic's dotfiles.An additional use for modules is that of a git repository you want to clone but not
link. An example use would be for downloadingSpacemacsor any such community
configuration like oh-my-zsh, etc.modules:-repo:git@github.com:syl20bnr/spacemacslink:nonepull_only:truelocation:~/.emacs.dHere we specify a few extra keys. There purpose should be self explanatory but
if you're curiousbelowis a detailed explanation of all keys
that each module configuration supports.Modules work just like any other dfm profile so if a module you're
pulling in has a.dfm.ymlin it that will be loaded and executed
accordingly. Including pulling down any modules it defines.Available keysreponamelocationlinkpull_onlymappingsrepoRequired, this is the git repository to clone for the module.nameThis changes the cloned name. This only has an effect if location isn't
provided. Normally a git repository would be cloned into$XDG_CONFIG_HOME/dfm/modulesand the resulting folder would be named whatever
git decides it should be based on the git URL. If this is provided it'll be
cloned into the modules directory with the specified name. This is useful if
multiple profiles use the same module.locationIf provided module will be cloned into the specified location. You can use the~bash expansion here to represent$HOME. No other expansions are available.
This option is useful for cloning community configurations like oh-my-zsh or
spacemacs.linkDetermines when to link the module. Link in this context means that it'll be
treated like a normal dotfile profile, so all files will go through the same
translation rules as a regular profile and be linked accordingly. Available
values arepost,pre, andnone.postis the default and means that the
module will be linked after the parent profile. "pre" means this will be linked
before the parent profile, use this if for instance you want to use most files
from this profile and override a few files with those from the parent file since
dfm will overwrite the links with the last one found. "none" means the module is
not a dotfiles profile and shouldn't be linked at all, an example being
community configuration repositories like oh-my-zsh or spacemacs.pull_onlyIf set totruewon't attempt to push any changes. It's important to
know that dfm always tries to push to origin master, so if you don't
have write access to the repository or don't want it to automatically
push to master then you should set this to true. This is useful for
community configuration repositories.mappingsA list of file mappings as described below inMappings. Modules do
not inherit parent mappings, they do however inherit the default mappings as
described inSkips Relevant FilesMappingsMappings are a way of defining custom file locations. To understand
mappings one must understand dfm's default link behavior:Default behaviorFor an example let's say you have a file namedmy_config.txtin your
dotfile repository. dfm will try and translate that to a new location
of$HOME/.my_config.txt. It'll then create a symlink at that location
pointing tomy_config.txtin your dotfile repository.Using mappingsWith mappings you can replace this behavior and make it so dfm will
linkmy_configwherever you wish. This is useful if you need to
store config files that are actually global. Such as configuration
files that would go into/etc/or if you want to sync some files in
your repo but not link them.Here is a simple example:mappings:-match:.config/some-dirlink_as_dir:true-match:my_global_etc_filestarget_dir:/etc/-match:something_want_to_skip_but_syncskip:true-match:something_only_for_macostarget_os:"Darwin"-match:some_file_for_mac_and_linux_onlytarget_os:-"Linux"-"Darwin"-match:some_specific_translation_for_macdest:~/.mac_os_dotfiletarget_os:-"Darwin"Here dfm uses the match as a regular expression to match the file
paths in your dotfile repository. When it finds a path which matches
the regular expression it adds an alternative linking behavior. For
anything whereskipis true it simply skips linking. For anything
withtarget_dirthat value will override$HOMEwhen linking. For
anything with atarget_osvalue the file will only be linked if dfm
is being run on the given os.Link as Dir MappingsAbove you can see a mapping using thelink_as_diroption. When this is set totruefor a mapping thematch:value will be used as a directory relative to the root of the
dotfile repo and will be linked as a directory. Normally DFM only links files, this can
cause issues with some types of configuration where you regularly generate files like
snippet tools. Consider the following dotfiles in a dotfile repository:$REPO/.config/nvim
├── UltiSnips
│   ├── gitcommit.snippets
│   └── python.snippetsThat would produce the following links in$HOME/.config/nvim:$HOME/.config/nvim
├── UltiSnips
│   ├── gitcommit.snippets -> $HOME/.config/dfm/profiles/chasinglogic/.config/nvim/UltiSnips/gitcommit.snippets
│   └── python.snippets -> $HOME/.config/dfm/profiles/chasinglogic/.config/nvim/UltiSnips/python.snippetsEvery time you used:UltiSnipsEditto create a new snippet file type you'd have to
then remember to manually move that into your dotfile repository and re-rundfm link.
To solve this problem you can use the following mapping in your.dfm.ymlyou can
instead linkUltiSnipsthe directory instead of it's files:mappings:
  - match: .config/nvim/UltiSnips
    link_as_dir: trueNow DFM links the$HOME/.config/nvim/UltiSnipsdirectory to the$REPO/.config/nvim/UltiSnips:$HOME/.config/nvim
├── UltiSnips -> $HOME/.config/dfm/profiles/chasinglogic/.config/nvim/UltiSnipsAvailable configurationMappings support the following configuration options:matchskipdesttarget_dirtarget_osmatchMatch is a regular expression used to match the file path of any files
in your dotfile repository. This is used to determine if the custom
linking behavior for a file should be used.These are python style regular expressions and are matched using there.findallmethod so are by default fuzzy matching.skipIf provided the file/s will not be linked.destThe new full path to the file. This can be used to completely change a file's
name or put it in a wholly new location. This is more explicity thantarget_dirand covers cases thattarget_diris not suited for (for example
if a file is a dotfile on one OS but not on another.)target_dirWhere to link the file to. The~expansion for$HOMEis supported
here but no other expansions are available. It is worth noting that if
you're using~in your target_dir then you should probably just
create the directory structure in your git repo.target_osA string or list of strings matching the OS's to link this file
on. A non-exhaustive list of common values are:Linux,Darwin, orWindows. This matches the string returned byPython'splatform.system()function.HooksHooks in dfm are used for those few extra tasks that you need to do whenever
your dotfiles are synced or linked.An example from my personal dotfiles is running an Ansible playbook
whenever I sync my dotfiles. To accomplish this I wrote anafter_synchook as follows:hooks:after_sync:-ansible-playbook ansible/dev-mac.ymlNow whenever I sync my dotfiles Ansible will run mydev-macplaybook to make
sure that my packages etc are also in sync!The hooks option is just a YAML map which supports the following keys:after_link,before_link,after_sync, andbefore_sync. The
values of any of those keys is a YAML list of strings which will be
executed in a shell via/bin/sh -c '$YOUR COMMAND'. An example would
be:hooks:after_link:-ls -l-whoami-echo "All done!"All commands are ran with a working directory of your dotfile
repository and the current process environment is passed down to the
process so you can use$HOMEetc environment variables in your
commands.By default the comamnds will run with the interpreter/bin/sh -c. So the
expanded comamnd line for the first hook above would be:/bin/sh -c 'ls -l'If you want to use a different interpreter you can use instead use this hook
format:hooks:
  after_link:
    - interpreter: python -c
      script: |
        print("hello world from Python")You may want to do this in cases where you need complex logic (like that which
should live in a Python script) or for example on Debian based systems which
use dash instead of bash as the /bin/sh interpreter and so have a very limited
expansion feature set.ContributingFork it!Create your feature branch:git checkout -b my-new-featureCommit your changes:git commit -am 'Add some feature'Push to the branch:git push origin my-new-feature:fire: Submit a pull request :D :fire:All pull requests should go to the develop branch not master. Thanks!LicenseThis code is distributed under the GNU General Public LicenseCopyright (C) 2018 Mathew Robinson

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.