diff options
author | William Carroll <wpcarro@gmail.com> | 2020-01-31T23·16+0000 |
---|---|---|
committer | William Carroll <wpcarro@gmail.com> | 2020-02-02T18·31+0000 |
commit | 6fd9947ec8f5ba1aa0af8ccc8f33dfee23bb7810 (patch) | |
tree | 2f2704645c6b0ae9d7d87f0a95fbb56efd3e5b21 | |
parent | e8b47d5030801c0249a8e9741df3ead0b0d374de (diff) |
Re-write delete_dotfile_symlinks in golang
I'm currently quite unfamiliar with golang. As an exercise to help me onboard onto golang, and as a proof-of-concept to see if golang is a viable substitute for Python as a scripting language, I decided to port my delete_dotfile_symlinks to golang. In the process, renamed ./python -> ./scripts, which is a more accommodating name for a directory.
-rwxr-xr-x | python/delete_dotfile_symlinks.py | 88 | ||||
-rw-r--r-- | scripts/delete_dotfile_symlinks.go | 117 |
2 files changed, 117 insertions, 88 deletions
diff --git a/python/delete_dotfile_symlinks.py b/python/delete_dotfile_symlinks.py deleted file mode 100755 index c2048120829f..000000000000 --- a/python/delete_dotfile_symlinks.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python3 - -# TODO: Prefer a proper Python module doc here. -# Find and delete all symlinks to the dotfiles defined in $BRIEFCASE. -# -# Oftentimes I corrupt the state of my dotfiles. The intention with this script -# is to write some tooling to help me better manage my dotfile cleanliness. An -# example workflow might look like: -# -# ```shell -# > ./delete_dotfile_symlinks.py --audit -# > ./delete_dotfile_symlinks.py --seriously -# > cd .. -# > make install -# ``` - -################################################################################ -# Dependencies -################################################################################ - -import argparse -import os -import sys - -from os.path import expanduser - -################################################################################ -# Library -################################################################################ - -def homify(path): - """Prefer ~ instead of the absolute `path`.""" - home = expanduser('~') - return path.replace(home, '~') - -def main(): - parser = argparse.ArgumentParser(description='Remove symlinks to my managed dotfiles.') - parser.add_argument('--audit', dest='audit', action='store_true', help='Output all symlinks that would be deleted. This is the default behavior. This option is mutually exclusive with the --seriously option.') - parser.add_argument('--seriously', dest='seriously', action='store_true', help='Actually delete the symlinks. This option is mutually exclusive with the --audit option.') - parser.add_argument('--repo-name', dest='name', default='briefcase', help='The name of the repository. Usually "briefcase" or "dotfiles".') - parser.add_argument('--device-only', dest='device_only', action='store_true', help='Only output the device-specific dotfiles.') - args = parser.parse_args() - - # TODO: Instead of doing this manually, is this something that argparse supports? - if not args.audit and not args.seriously: - print('Either --audit or --seriously must be passed. See --help for more information.') - # TODO: What's the proper exit code here? - sys.exit(1) - - # These two options are mutually exclusive. - assert args.audit != args.seriously - - count = 0 - - for cwd, dirs, files in os.walk(expanduser("~")): - # Skip this repository. - if args.name in cwd: - continue - for file in files: - source = os.path.join(cwd, file) - if os.path.islink(source): - dest = os.readlink(source) - # We won't know certainly if the link points to a dotfile that I manage - # with this simple test. - if args.device_only: - # TODO: Change 'desktop' with a lookup of hostname to device name. - if 'configs/desktop' in dest: - if args.audit: - print('{} -> {}'.format(homify(source), homify(dest))) - elif args.seriously: - print('rm {}'.format(source)) - os.remove(source) - count += 1 - elif args.name in dest: - if args.audit: - print('{} -> {}'.format(homify(source), homify(dest))) - elif args.seriously: - print('rm {}'.format(source)) - os.remove(source) - count += 1 - - if args.audit: - print('Would have deleted {} symlinks.'.format(count)) - elif args.seriously: - print('Successfully deleted {} symlinks.'.format(count)) - -if __name__ == '__main__': - main() diff --git a/scripts/delete_dotfile_symlinks.go b/scripts/delete_dotfile_symlinks.go new file mode 100644 index 000000000000..497b2d57b8c1 --- /dev/null +++ b/scripts/delete_dotfile_symlinks.go @@ -0,0 +1,117 @@ +// Find and delete all symlinks to the dotfiles defined in $BRIEFCASE. +// +// Oftentimes I corrupt the state of my dotfiles. The intention with this script +// is to write some tooling to help me better manage my dotfile cleanliness. An +// example workflow might look like: +// +// ```shell +// > go run delete_dotfile_symlinks.go --audit +// > go run delete_dotfile_symlinks.go --seriously +// > cd .. +// > make install +// ``` +// +// Outstanding TODOs: +// - Package this with <depot>buildGo.nix. +// - How can this be run as script without `go run`? She-bang at the top? +// - See TODOs within this package. + +package main + +import ( + "errors" + "flag" + "fmt" + "log" + "os" + "path/filepath" + "strings" +) + +// Wanted for go tooling: +// 1. jump-to-def +// 2. documentation at point +// 3. autocompletion + +// TODO: Consider adding this to a utils.go package. +func failOn(err error) { + if err != nil { + log.Fatal(err) + } +} + +// TODO: Consider adding this to a utils.go package. +func isSymlink(m os.FileMode) bool { + return m&os.ModeSymlink != 0 +} + +var hostnames = map[string]string{ + os.Getenv("DESKTOP"): "desktop", + os.Getenv("LAPTOP"): "work_laptop", + os.Getenv("CLOUDTOP"): "cloudtop", +} + +func main() { + audit := flag.Bool("audit", false, "Output all symlinks that would be deleted. This is the default behavior. This option is mutually exclusive with the --seriously option.") + seriously := flag.Bool("seriously", false, "Actually delete the symlinks. This option is mutually exclusive with the --audit option.") + repoName := flag.String("repo-name", "briefcase", "The name of the repository.") + deviceOnly := flag.Bool("device-only", false, "Only output the device-specific dotfiles.") + flag.Parse() + + if !*audit && !*seriously { + log.Fatal(errors.New("Either -audit or -seriously needs to be set.")) + } + if *audit == *seriously { + log.Fatal(errors.New("Arguments -audit and -seriously are mutually exclusive")) + } + + home, err := os.UserHomeDir() + failOn(err) + count := 0 + + err = filepath.Walk(home, func(path string, info os.FileInfo, err error) error { + if isSymlink(info.Mode()) { + dest, err := os.Readlink(path) + failOn(err) + + var predicate func(string) bool + + if *deviceOnly { + predicate = func(dest string) bool { + var hostname string + hostname, err = os.Hostname() + failOn(err) + seeking, ok := hostnames[hostname] + if !ok { + log.Fatal(fmt.Sprintf("Hostname \"%s\" not supported in the hostnames map.", hostname)) + } + return strings.Contains(dest, *repoName) && strings.Contains(dest, seeking) + } + } else { + predicate = func(dest string) bool { + return strings.Contains(dest, *repoName) + } + } + + if predicate(dest) { + if *audit { + fmt.Printf("%s -> %s\n", path, dest) + } else if *seriously { + fmt.Printf("rm %s\n", path) + err = os.Remove(path) + failOn(err) + } + count += 1 + } + } + return nil + }) + failOn(err) + if *audit { + fmt.Printf("Would have deleted %d symlinks.\n", count) + } else if *seriously { + fmt.Printf("Successfully deleted %d symlinks.\n", count) + } + + os.Exit(0) +} |