201 lines
5.3 KiB
Python
201 lines
5.3 KiB
Python
"""Cliget - install cli tools in you user profile
|
|
|
|
Usage:
|
|
cliget [-v] [-c URL] list
|
|
cliget [-v] [-c URL] search PAT
|
|
cliget [-v] [-c URL] versions TOOLS ...
|
|
cliget [-v] [-c URL] allversions TOOL
|
|
cliget [-v] [-c URL] install TOOLS ...
|
|
cliget [-v] [-c URL] update [--all] [TOOLS ...]
|
|
|
|
list list all installed tools
|
|
search search catalog for tools with the given pattern
|
|
versions TOOLS list current and latest versions of some tools
|
|
allversions TOOL list all versions of a tool
|
|
install TOOLS install some tools
|
|
update list all updatable tools
|
|
update TOOLS 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 _load_catalog(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
|
|
|
|
|
|
def _local_version(cmd):
|
|
ver = VersionInfo(0)
|
|
try:
|
|
first_line = run([cmd, '--version'], input='', text=True, capture_output=True, check=True, timeout=0.1).stdout.split('\n')[0]
|
|
ver = _find_semver(first_line)
|
|
except Exception as e:
|
|
trace("run error", e)
|
|
return ver
|
|
|
|
def _internal_list(options) -> tuple[str,VersionInfo]:
|
|
"""list installed tools and their version"""
|
|
ctl = _load_catalog(options)
|
|
for cli, props in ctl.items():
|
|
# search in path
|
|
try:
|
|
vers = _local_version(cli)
|
|
trace(cli, vers)
|
|
yield cli, props, 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 = _load_catalog(options)
|
|
L = []
|
|
for cli, props in ctl.items():
|
|
trace(cli, props)
|
|
rtitle = fuzz.ratio(cli, pat)
|
|
rdesc = fuzz.partial_token_set_ratio(props['desc'], pat)
|
|
score = 0.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_versions(repo:str) -> [VersionInfo|None]:
|
|
[owner, repo] = repo.split("/")
|
|
url = f'https://api.github.com/repos/{owner}/{repo}/releases'
|
|
response = requests.get(url)
|
|
return [ _find_semver(o.get("tag_name")) for o in response.json()]
|
|
|
|
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 _find_semver(response.json().get("tag_name"))
|
|
|
|
def doversions(options):
|
|
tools = options.TOOLS
|
|
ctl = _load_catalog(options)
|
|
for cli in tools:
|
|
if cli in ctl:
|
|
props = ctl[cli]
|
|
rver = _gh_version(props.github) if props.github else VersionInfo(0)
|
|
lver = _local_version(cli)
|
|
trace(lver)
|
|
trace(rver)
|
|
print(f"{cli} | {lver} | {rver}")
|
|
else:
|
|
warn(f'{tool} not in catalog')
|
|
|
|
def doallversions(options):
|
|
tool = options.TOOL
|
|
ctl = _load_catalog(options)
|
|
if tool in ctl:
|
|
props = ctl[tool]
|
|
if props.github:
|
|
vers = _gh_versions(props.github)
|
|
trace(vers)
|
|
print("\n".join(vers))
|
|
else:
|
|
warn(f'{tool} not in catalog')
|
|
|
|
|
|
def doinstall(options):
|
|
tools = options.TOOLS
|
|
ctl = _load_catalog(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')
|
|
|
|
def perform_install(cli, repo):
|
|
# get arch and os
|
|
# dl asset
|
|
# mkdirs
|
|
# unpack
|
|
# symlink
|
|
|
|
|
|
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.versions:
|
|
doversions(options)
|
|
elif options.allversions(options):
|
|
doallversions(options)
|
|
elif options.install or options.update:
|
|
if not options.__all and len(options.TOOLS)==0:
|
|
dolistupdate(options)
|
|
elif len(options.TOOLS)>0:
|
|
doinstall(options)
|
|
else:
|
|
print("not implemented")
|