about summary refs log tree commit diff
diff options
context:
space:
mode:
authorWilliam Carroll <wpcarro@gmail.com>2020-01-31T23·16+0000
committerWilliam Carroll <wpcarro@gmail.com>2020-02-02T18·31+0000
commit6fd9947ec8f5ba1aa0af8ccc8f33dfee23bb7810 (patch)
tree2f2704645c6b0ae9d7d87f0a95fbb56efd3e5b21
parente8b47d5030801c0249a8e9741df3ead0b0d374de (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-xpython/delete_dotfile_symlinks.py88
-rw-r--r--scripts/delete_dotfile_symlinks.go117
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)
+}