"""Cliget - install cli tools in you user profile Usage: cliget [-v] [-c URL] list cliget [-v] [-c URL] search PAT cliget [-v] [-c URL] install TOOL ... cliget [-v] [-c URL] update [--all] [TOOL ...] list list all installed tools search search for tools in the catalog with the given pattern install TOOL install some tools update list all updatable tools update TOOL update tools update --all update all updatable tools Options: -h, --help -c, --catalog URL -v, --verbose """ import logging import sys, os from docopt import docopt from yaml import load, SafeLoader from semver import VersionInfo import re from subprocess import run, CalledProcessError, TimeoutExpired from fuzzywuzzy import fuzz import requests def trace(*mess): if "TRACE" in os.environ: print("TRACE", mess) def warn(*mess): print(f"WARN {mess[0]}") class DotDict(dict): def __getattr__(self, name): return self[name] if name in self else None def loadcatalog(options)->dict: catalog=options.get('__catalog', 'catalog.yaml') o = load(open(catalog), SafeLoader) trace(o) return { k:DotDict(v) for k,v in o.items()} def find_semver(s:str) -> VersionInfo: ver = VersionInfo(0,0,0) try: ver = VersionInfo.parse(s) except ValueError: try: ver = VersionInfo(*list(i.group(0) for i in re.finditer('\d+', s))[:3]) except Exception as e: trace("parse error", e) return ver _version = lambda cmd: run([cmd, '--version'], input='', text=True, capture_output=True, check=True, timeout=0.1).stdout.split('\n')[0] def _internal_list(options) -> tuple[str,VersionInfo]: """list installed tools and their version""" ctl = loadcatalog(options) for cli, props in ctl.items(): # search in path try: vers = _version(cli) trace(cli, vers) yield cli, props, find_semver(vers) except CalledProcessError: trace(cli, "call error") except TimeoutExpired: trace(cli, "timeout") except FileNotFoundError: trace(cli, "not found") def dolist(options): for (cli, _, ver) in _internal_list(options): print(cli, ver) def dosearch(options): pat = options.PAT ctl = loadcatalog(options) L = [] for cli, props in ctl.items(): trace(cli, props) rtitle = fuzz.ratio(cli, pat) rdesc = fuzz.partial_ratio(props['desc'], pat) score = 2 * rtitle + rdesc L.append((cli, props['desc'], score)) L = sorted(L, key=lambda x: -x[-1]) # TODO format a as table print("\n".join("|".join(map(str,l)) for l in L[:10])) def dolistupdate(options): print("look for updatables") for (cli, props, ver) in _internal_list(options): # get last version online if props.github: pass pass def _gh_version(repo:str) -> [VersionInfo|None]: [owner, repo] = repo.split("/") url = f'https://api.github.com/repos/{owner}/{repo}/releases/latest' response = requests.get(url) return response.json().get("name") def doinstall(options): tools = options.TOOL ctl = loadcatalog(options) for tool in tools: if tool in ctl: props = ctl[tool] if props.github: vers = _gh_version(props.github) trace(vers) else: warn(f'{tool} not in catalog') if __name__ == '__main__': if "DEBUG" in os.environ: logging.basicConfig(level=logging.DEBUG) logging.debug("debug is on") else: logging.info("not in debug") options = docopt(__doc__, version='Cliget 0.1.0') options = DotDict({k.replace('-','_'):v for (k,v) in options.items() if v is not None}) trace(options) if options.list: dolist(options) elif options.search: dosearch(options) elif options.install or options.update: if not options.__all and len(options.TOOL)==0: dolistupdate(options) elif len(options.TOOL)>0: doinstall(options) else: print("not implemented")