dotfm

My dotfile manager
git clone git://src.gearsix.net/dotfm
Log | Files | Refs | Atom | README | LICENSE

commit 903850faaa6a377ce75031b54171f7371bdd2ee3
parent f8765f7e6674b0046f27c9d540330dc4c5d7d688
Author: gearsix <gearsix@tuta.io>
Date:   Thu, 22 Apr 2021 00:22:45 +0100

1/? total refactor to tidyup everything and make it more maintainable

Diffstat:
Msrc/dotfm.py | 272++++++++++++++++++++++++++++++++++++++-----------------------------------------
1 file changed, 130 insertions(+), 142 deletions(-)

diff --git a/src/dotfm.py b/src/dotfm.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 -#========================== -# dotfm - dot file manager -#========================== +#========================= +# dotfm - dotfile manager +#========================= # authors: gearsix # created: 2020-01-15 -# updated: 2020-11-25 -# notes: +# updated: 2021-04-22 #--------- # IMPORTS @@ -21,16 +20,17 @@ import argparse #--------- # GLOBALS #--------- -NAME = os.path.basename(__file__) # program name -HOME = os.getenv('HOME') # $HOME (where user's dotfiles are stored) -USER = os.getenv('USER') # $USER calling dotfm -ARGS = sys.argv # parsed arguments -EDITOR = os.getenv('EDITOR') or 'nano' # text editor to modify dotfiles with -VERSION = 'v2.1.2' -DOTFM_CSV_FILE = '/home/{}/.local/share/dotfm/installed.csv'.format(USER) -KNOWN_DOTFILES = [ # dotfiles that dotfm knows by default +NAME = os.path.basename(__file__) +HOME = os.getenv('HOME') +USER = os.getenv('USER') +ARGS = [] +EDITOR = os.getenv('EDITOR') or 'nano' +VERSION = 'v2.2.0' +INSTALLED = [] +INSTALLED_FILE = '/home/{}/.local/share/dotfm/installed.csv'.format(USER) +KNOWN = [ # dotfiles that dotfm knows by default # install location, aliases... - [DOTFM_CSV_FILE, 'dotfm', 'dotfm.csv'], + [INSTALLED_FILE, 'dotfm'], ['{}/.bashrc'.format(HOME), '.bashrc', 'bashrc'], ['{}/.bash_profile'.format(HOME), '.bash_profile', 'bash_profile'], ['{}/.profile'.format(HOME), '.profile', 'profile'], @@ -56,29 +56,31 @@ KNOWN_DOTFILES = [ # dotfiles that dotfm knows by default ['{}/.inputrc'.format(HOME), '.inputrc', 'inputrc'], ['{}/.sfeed'.format(HOME), '.sfeedrc', 'sfeedrc'], ] -INSTALLED_DOTFILES = [] # appended to during dotfm_init #----------- # FUNCTIONS #----------- -def log_info(info): +# utilities +def debug(message): + if ARGS.debug == True: + print(message) + +def info(message): if ARGS.quiet == False: - LOGGER.info(info) + print(message) + +def warn(message): + input('{}, press enter to continue.'.format(message)) -def error_exit(message): - LOGGER.error(message) +def fatal(message): + print(message) sys.exit() -def parse_arguments(): - global ARGS +# main +def parseargs(): valid_commands = ['install', 'update', 'remove', 'edit', 'list'] - - parser = argparse.ArgumentParser( - description='a simple tool to help you manage your dot files.') - parser.add_argument('cmd', metavar='COMMAND', choices=valid_commands, - help='the dotfm COMMAND to execute: {}'.format(valid_commands)) - parser.add_argument('dotfile', metavar='DOTFILE', nargs=argparse.REMAINDER, - help='the target dotfile to execute COMMAND on') + parser = argparse.ArgumentParser(description='a simple tool to help you manage your dotfile symlinks.') + # OPTIONS parser.add_argument('-s', '--skip', action='store_true', help='skip any user prompts and use default values where possible') parser.add_argument('-d', '--debug', action='store_true', @@ -86,89 +88,82 @@ def parse_arguments(): parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(VERSION)) parser.add_argument('-q', '--quiet', action='store_true', - help='tell dotfm to shutup') - ARGS = parser.parse_args() - -def dotfm_init(): - """ If DOTFM_CSV_FILE does not exist, create it. If it does, load it's - values into KNOWN_DOTFILES. - Will prompt the user where they want the file to be created at, - if that location does not match DOTFM_CSV_FILE, DOTFM_CSV_FILE - will be a symbolic link to that location. - """ - LOGGER.debug('loading dotfile locations...') - - if not os.path.exists(DOTFM_CSV_FILE): - LOGGER.warning('{} not found'.format(DOTFM_CSV_FILE)) - - # get location to create dotfm.csv at - location = -1 - while location == -1: - location = input('where would you like to store the ' \ - 'dotfm csv file (default: {})? '.format(DOTFM_CSV_FILE)) - # prompt user to overwrite existing dotfm.csv file - if len(location) > 0 and os.path.exists(location): - yn = '' - while yn == '': - yn = input('{} already exists, ' \ - 'overwrite (y/n)? '.format(location)) - if yn[0] == 'y': - log_info('overwriting {}'.format(location)) - elif yn[0] == 'n': - log_info('{} already exists, using default ' \ - 'location ({})'.format(DOTFILE_LOCATION)) - location = DOTFM_CSV_FILE - else: - yn = '' - # use default location - elif len(location) == 0: - location = DOTFM_CSV_FILE - # ask again - else: - location = -1 - - # write dotfm dotfile to csv - log_info('creating dotfm_csv_file at {}'.format(location)) - os.system('mkdir -p {}'.format(os.path.dirname(location))) - dotfm_csv = open(location, "w") - for i, dfl in enumerate(KNOWN_DOTFILES[0]): - if i == 0: - dotfm_csv.write(dfl) - else: - dotfm_csv.write(',{}'.format(dfl)) - dotfm_csv.write('\n') - dotfm_csv.close() - - # create dotfm.csv symbolic link - os.system('mkdir -p {}'.format(os.path.dirname(DOTFM_CSV_FILE))) - if os.path.abspath(location) != DOTFM_CSV_FILE: - os.system('ln -isv {} {}'.format(os.path.abspath(location), - DOTFM_CSV_FILE)) - - # append to INSTALLED_DOTFILES - INSTALLED_DOTFILES.append(KNOWN_DOTFILES[0]) - else: # load existing values into INSTALLED_DOTFILES - dotfm_csv = open(DOTFM_CSV_FILE, "r") - dotfm_csv_reader = csv.reader(dotfm_csv) - for dfl in dotfm_csv_reader: - INSTALLED_DOTFILES.append(dfl) - dotfm_csv.close() - + help='mute dotfm info logs') + # POSITIONAL + parser.add_argument('cmd', metavar='COMMAND', choices=valid_commands, + help='the dotfm COMMAND to execute: {}'.format(valid_commands)) + parser.add_argument('dotfile', metavar='DOTFILE', nargs=argparse.REMAINDER, + help='the target dotfile to execute COMMAND on') + return parser.parse_args() + +# main/init +def init(): + debug('init: loading dotfile locations') + if not os.path.exists(INSTALLED_FILE): + debug(INSTALLED_FILE, 'not found') + init_createcsv() + INSTALLED = init_loadcsv(INSTALLED_FILE) + INSTALLED = init_cleandups(INSTALLED, 0) + +def init_createcsv(default_location): + location = default_location + if ARGS.skip == False: + location = input('dotfm csv file location (default: {})? '.format(default_location)) + if len(location) == 0: + location = default_location + if os.path.exists(location): + debug(location, 'already exists') + on = input('[o]verwrite or [u]se {}? '.format(location)) + if len(on) > 0: + if on[0] == 'o': # create file at location & write KNOWN[0] to it + warn('overwriting {}, all existing data in this file will be lost') + os.system('mkdir -p', os.path.dirname(location)) + dotfm_csv = open(location, "w") + for i, dfl in enumerate(KNOWN[0]): + dotfm_csv.write(dfl if i == 0 else ',{}'.format(dfl)) + dotfm_csv.write('\n') + dotfm_csv.close() + elif on[0] == 'u': + debug('', location) + sys.exit() + # create default_location symlink + if os.path.abspath(location) != os.path.abspath(default_location): + debug('creating dotfm csv file symlink') + os.system('mkdir -p', os.path.dirname(default_location)) + os.system('ln -isv', os.path.abspath(location), default_location) + +def init_loadcsv(location): + data = [] + dotfm_csv = csv.reader(open(location, "r")) + for dfl in dotfm_csv: + data.append(dfl) + dotfm_csv.close() + return data + +def init_cleandups(data, id_index): + unique = [] + for d in data: + for i, u in enumerate(unique): + if d[id_index] == u[id_index]: + unique[i] = d # assume later entry = more recent + return unique + +# main/install def dotfm_install(dotfile_source): - """ Check "KNOWN_DOTFILES" to see if an alias matches "dotfile" basename, + """ Check "KNOWN" to see if an alias matches "dotfile" basename, if it does create a symbolic link from "dotfile" to the matching - "KNOWN_DOTFILES" location (index 0). + "KNOWN" location (index 0). - If file at matching "KNOWN_DOTFILES" location exists, prompt user to + If file at matching "KNOWN" location exists, prompt user to overwrite it. @param dotfile = filepath to the dotfile to install """ log_info('installing {}...'.format(dotfile_source)) - # check if dotfile matches an alias in KNOWN_DOTFILES + # check if dotfile matches an alias in KNOWN found = -1 - for d, dfl in enumerate(KNOWN_DOTFILES): + for d, dfl in enumerate(KNOWN): for a, alias in enumerate(dfl[1:]): if os.path.basename(dotfile_source) == alias: found = d @@ -176,7 +171,7 @@ def dotfm_install(dotfile_source): # prompt for location dest = '' if found != -1: - dest = os.path.abspath(KNOWN_DOTFILES[found][0]) + dest = os.path.abspath(KNOWN[found][0]) if len(dest) == 0 or ARGS.skip == False: default = dest dest = '' @@ -189,7 +184,7 @@ def dotfm_install(dotfile_source): # prompt for aliases aliases = [] if found != -1: - aliases = KNOWN_DOTFILES[found][1:] + aliases = KNOWN[found][1:] if len(aliases) == 0 or ARGS.skip == False: default = aliases aliases = [] @@ -221,22 +216,23 @@ def dotfm_install(dotfile_source): oca = '' # create symbolic link to dotfile os.system('ln -vs {} {}'.format(dotfile_source, dest)) - # append to DOTFILE_CSV_FILE and INSTALLED_DOTFILES + # append to DOTFILE_CSV_FILE and INSTALLED log_info('appending to installed dotfiles...') dfl = aliases dfl.insert(0, dest) - with open(DOTFM_CSV_FILE, "a") as dotfm_csv_file: + with open(INSTALLED_FILE, "a") as dotfm_csv_file: dotfm_csv = csv.writer(dotfm_csv_file, lineterminator='\n') dotfm_csv.writerow(dfl) dotfm_csv_file.close() - INSTALLED_DOTFILES.append(dfl) + INSTALLED.append(dfl) log_info('success - you might need to re-open the terminal to see changes take effect') +# main/update def dotfm_update(dotfile_alias, new_source): """ Update the source location that the dotfile symlink of an already installed dotfile points to. If the dotfile_alias does not exist in - DOTFM_CSV_FILE, dotfm_install is called instead. + INSTALLED_FILE, dotfm_install is called instead. @param alias = an alias matching the known aliases of the dotfile to update @@ -246,7 +242,7 @@ def dotfm_update(dotfile_alias, new_source): log_info('updating {} -> {}...'.format(dotfile_alias, new_source)) found = -1 - for i, dfl in enumerate(INSTALLED_DOTFILES): + for i, dfl in enumerate(INSTALLED): if dotfile_alias in dfl: found = i break @@ -261,9 +257,10 @@ def dotfm_update(dotfile_alias, new_source): log_info('could not find dotfile matching alias "{}"'.format( dotfile_alias)) +# main/remove def dotfm_remove(alias): """ Remove a dotfile (from it's known location) and remove it from - DOTFM_CSV_FILE + INSTALLED_FILE @param alias = an alias matching the known aliases of the dotfile to remove @@ -271,7 +268,7 @@ def dotfm_remove(alias): log_info('removing {}...'.format(alias)) found = -1 - for i, dfl in enumerate(INSTALLED_DOTFILES): + for i, dfl in enumerate(INSTALLED): if alias in dfl: found = i break @@ -281,14 +278,15 @@ def dotfm_remove(alias): target = '{}'.format(os.path.abspath(dfl[0])) os.system('rm -iv {}'.format(target)) # remove from installed - del INSTALLED_DOTFILES[found] - with open(DOTFM_CSV_FILE, 'w') as dotfm_file: + del INSTALLED[found] + with open(INSTALLED_FILE, 'w') as dotfm_file: dotfm_csv = csv.writer(dotfm_file) - dotfm_csv.writerows(INSTALLED_DOTFILES) + dotfm_csv.writerows(INSTALLED) dotfm_file.close() else: log_info('could not find dotfile matching alias "{}"'.format(alias)) +# main/edit def dotfm_edit(dotfile_alias): """ Open dotfile with alias matching "dotfm_alias" in EDITOR @param dotfile_alias = an alias of the dotfile to open @@ -296,7 +294,7 @@ def dotfm_edit(dotfile_alias): log_info('editing {}...'.format(dotfile_alias)) target = '' - for dfl in INSTALLED_DOTFILES: + for dfl in INSTALLED: if dotfile_alias in dfl: target = '{}'.format(dfl[0]) os.system('{} {}'.format(EDITOR, target)) @@ -317,6 +315,7 @@ def dotfm_edit(dotfile_alias): error_exit('could not find alias {} in installed.csv'.format( os.path.basename(dotfile_alias))) +# main/list def dotfm_list(dotfile_aliases): """ List specified dotfile aliases and install location (displays all if none are specified). @@ -328,11 +327,11 @@ def dotfm_list(dotfile_aliases): if len(dotfile_aliases) == 0: os.system('printf "LOCATION,ALIASES...\n$(cat {})" | ' \ - 'column -t -s,'.format(DOTFM_CSV_FILE)) + 'column -t -s,'.format(INSTALLED_FILE)) else: dotfiles = '' for alias in dotfile_aliases: - for dfl in INSTALLED_DOTFILES: + for dfl in INSTALLED: if alias in dfl: dotfiles += dfl[0] for i, a in enumerate(dfl[1:]): @@ -346,35 +345,24 @@ def dotfm_list(dotfile_aliases): # MAIN #------ if __name__ == '__main__': - # parse args - parse_arguments() - command = ARGS.cmd - dotfile = ARGS.dotfile - - # init LOGGER - log_lvl = logging.INFO - log_fmt = '%(levelname)-7s | %(message)s' + ARGS = parseargs() if ARGS.debug == True: - LOGGER.debug('displaying debug logs') - log_lvl = logging.DEBUG - logging.basicConfig(level=log_lvl, format=log_fmt) - LOGGER = logging.getLogger(__name__) - - # load dotfile locations - dotfm_init() + debug('printing debug logs') + if ARGS.quiet == True: + debug('muting info logs') - # run command - if command == 'install': - for d in dotfile: + init() + if ARGS.cmd == 'install': + for d in ARGS.dotfile: dotfm_install(os.path.abspath(d)) - elif command == 'update': - dotfm_update(dotfile[0], dotfile[1]) - elif command == 'remove': - for d in dotfile: + elif ARGS.cmd == 'update': + dotfm_update(ARGS.dotfile[0], ARGS.dotfile[1]) + elif ARGS.cmd == 'remove': + for d in ARGS.dotfile: dotfm_remove(d) - elif command == 'edit': - for d in dotfile: + elif ARGS.cmd == 'edit': + for d in ARGS.dotfile: dotfm_edit(d) - elif command == 'list': - dotfm_list(dotfile) + elif ARGS.cmd == 'list': + dotfm_list(ARGS.dotfile)