#!/usr/bin/env python from __future__ import print_function, unicode_literals import argparse import io import logging import os import re import sys _DESCRIPTION = """\ Migrate arbitrary `.go` sources (mostly) from the v1 to v2 API. """ _MIGRATORS = [] def main(sysargs=sys.argv[:]): parser = argparse.ArgumentParser( description=_DESCRIPTION, formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('path', nargs='*', type=os.path.abspath, default=os.getcwd()) parser.add_argument('-w', '--write', help='write changes back to file', action='store_true', default=False) parser.add_argument('-q', '--quiet', help='quiet down the logging', action='store_true', default=False) parser.add_argument('-D', '--debug', help='debug up the logging', action='store_true', default=(os.environ.get('DEBUG') != '')) parser.add_argument('--selftest', help='run internal tests', action='store_true', default=False) args = parser.parse_args(sysargs[1:]) if args.selftest: logging.basicConfig( level=logging.WARN, format='selftest: %(message)s' ) test_migrators() return 0 level = logging.FATAL if args.quiet else logging.INFO level = logging.DEBUG if args.debug else level logging.basicConfig(level=level, format='%(message)s') paths = args.path if len(paths) == 0: paths = ['.'] for filepath in _find_candidate_files(paths): updated_source = _update_filepath(filepath) if args.write: logging.info('Updating %s', filepath) with io.open(filepath, 'w', encoding='utf-8') as outfile: outfile.write(updated_source) else: logging.info('// Updated %s:', filepath) print(updated_source) return 0 def _find_candidate_files(paths): for path in paths: if not os.path.isdir(path): yield path continue for curdir, dirs, files in os.walk(path): for i, dirname in enumerate(dirs[:]): if dirname.startswith('.'): dirs.pop(i) for filename in files: if not filename.decode('utf-8').endswith('.go'): continue filepath = os.path.join(curdir, filename) if not os.access(filepath, os.R_OK | os.W_OK): continue yield filepath def _update_filepath(filepath): with io.open(filepath, encoding='utf-8') as infile: return _update_source(infile.read()) def _update_source(source): for migrator, func in _MIGRATORS: logging.debug('Running %s migrator', migrator) source = func(source) return source def _subfmt(pattern, replfmt, source, flags=re.UNICODE): def repl(match): return replfmt.format(**match.groupdict()) return re.sub(pattern, repl, source, flags=flags) def _migrator(func): _MIGRATORS.append((func.__name__.strip('_'), func)) return func @_migrator def _slice_pointer_types(source): return _subfmt( '(?P\\[\\])cli\\.(?PCommand|Author){', '{prefix}*cli.{type}{{', source ) @_migrator def _pointer_type_literal(source): return _subfmt( '(?P\\s+)cli\\.(?PCommand|Author){', '{prefix}&cli.{type}{{', source ) @_migrator def _slice_types(source): return _subfmt( '&cli\\.(?PIntSlice|StringSlice){(?P[^}]*)}', 'cli.New{type}({args})', source, flags=re.DOTALL | re.UNICODE ) @_migrator def _flag_literals(source): return _subfmt( '(?P\\s+)cli\\.(?P\\w+)Flag{', '{prefix}&cli.{type}Flag{{', source ) @_migrator def _v1_imports(source): return re.sub( '"(?:github\\.com|gopkg\\.in)/(?:codegangsta|urfave)/cli(?:\\.v1|)"', '"gopkg.in/urfave/cli.v2"', source, flags=re.UNICODE ) @_migrator def _new_exit_error(source): return re.sub('cli\\.NewExitError', 'cli.Exit', source, flags=re.UNICODE) @_migrator def _bool_t_flag(source): return _subfmt( 'cli\\.BoolTFlag{(?P[^}]*)}', 'cli.BoolFlag{{Value: true,{args}}}', source, flags=re.DOTALL | re.UNICODE ) @_migrator def _context_args_len(source): return _subfmt( 'len\\((?P\\S+)\\.Args\\(\\)\\)', '{prefix}.Args().Len()', source ) @_migrator def _context_args_index(source): return _subfmt( '\\.Args\\(\\)\\[(?P\\d+)\\]', '.Args().Get({index})', source ) @_migrator def _envvar_string(source): return re.sub( 'EnvVar:(?P\\s+)"(?P[^"]+)"', _envvar_string_repl, source, flags=re.UNICODE ) def _envvar_string_repl(match): return 'EnvVars:{ws}[]string{{{value}}}'.format( value=', '.join([ '"{}"'.format(s) for s in re.split( '\\s*,\\s*', match.groupdict()['string'], flags=re.UNICODE ) ]), **match.groupdict() ) @_migrator def _flag_name_stringly(source): return re.sub( '(?P\\s+)Name:(?P\\s+)"(?P[^"]+)"', _flag_name_stringly_repl, source, flags=re.UNICODE ) def _flag_name_stringly_repl(match): revars = dict(match.groupdict()) string = revars['string'] parts = list( reversed( sorted( filter(lambda s: len(s.strip()) > 0, [ part.strip() for part in string.split(',') ]), key=len ) ) ) if len(parts) == 1: return '{prefix}Name:{ws}"{string}"'.format(**revars) return ( '{prefix}Name:{ws}"{name}", Aliases: []string{{{aliases}}}' ).format( name=parts[0], aliases=', '.join(['"{}"'.format(s) for s in parts[1:]]), **revars ) @_migrator def _commands_opaque_type(source): return _subfmt( 'cli\\.Commands(?P[^B])', '[]*cli.Command{suffix}', source ) @_migrator def _flag_names(source): return re.sub('\\.GetName\\(\\)', '.Names()[0]', source, flags=re.UNICODE) @_migrator def _app_categories(source): source = _subfmt( '(?Prange\\s+\\S+)\\.App\\.Categories\\(\\)', '{prefix}.App.Categories.Categories()', source ) return re.sub( '\\.App\\.Categories\\(\\)', '.App.Categories', source, flags=re.UNICODE ) @_migrator def _command_category_commands(source): # XXX: brittle return _subfmt( '(?P\\s+category\\.)Commands(?P[^(])', '{prefix}VisibleCommands(){suffix}', source ) @_migrator def _context_bool_t(source): # XXX: probably brittle return _subfmt( '(?P\\S+)(?:Global|)BoolT\\(', '!{prefix}Bool(', source ) @_migrator def _context_global_methods(source): return _subfmt( '\\.Global(?P' 'Bool|Duration|Float64|Generic|Int|IntSlice|String|StringSlice|' 'FlagNames|IsSet|Set' ')\\(', '.{method}(', source ) @_migrator def _context_parent(source): # XXX: brittle return re.sub('\\.Parent\\(\\)', '.Lineage()[1]', source, flags=re.UNICODE) @_migrator def _app_init(source): return re.sub( 'cli\\.NewApp\\(\\)', '(&cli.App{})', source, flags=re.UNICODE ) @_migrator def _bash_complete(source): return re.sub( 'BashComplete:', 'ShellComplete:', re.sub('\\.BashComplete', '.ShellComplete', source, flags=re.UNICODE)) @_migrator def _enable_bash_completion(source): return re.sub( '\\.EnableBashCompletion', '.EnableShellCompletion', source, flags=re.UNICODE ) @_migrator def _bash_completion_flag(source): return re.sub( 'cli\\.BashCompletionFlag', 'cli.GenerateCompletionFlag', source, flags=re.UNICODE ) def test_migrators(): import difflib for i, (source, expected) in enumerate(_MIGRATOR_TESTS): actual = _update_source(source) if expected != actual: udiff = difflib.unified_diff( expected.splitlines(), actual.splitlines(), fromfile='a/source.go', tofile='b/source.go', lineterm='' ) for line in udiff: print(line) raise AssertionError('migrated source does not match expected') logging.warn('Test case %d/%d OK', i+1, len(_MIGRATOR_TESTS)) _MIGRATOR_TESTS = ( (""" \t\t\t&cli.StringSlice{"a", "b", "c"}, """, """ \t\t\tcli.NewStringSlice("a", "b", "c"), """), (""" \t\tcli.IntFlag{ \t\t\tName: "yep", \t\t\tValue: 3, \t\t} """, """ \t\t&cli.IntFlag{ \t\t\tName: "yep", \t\t\tValue: 3, \t\t} """), (""" \t\tapp.Commands = []cli.Command{ \t\t\t{ \t\t\t\tName: "whatebbs", \t\t\t}, \t\t} """, """ \t\tapp.Commands = []*cli.Command{ \t\t\t{ \t\t\t\tName: "whatebbs", \t\t\t}, \t\t} """), (""" \t\tapp.Commands = []cli.Command{ \t\t\tcli.Command{ \t\t\t\tName: "whatebbs", \t\t\t}, \t\t} """, """ \t\tapp.Commands = []*cli.Command{ \t\t\t&cli.Command{ \t\t\t\tName: "whatebbs", \t\t\t}, \t\t} """), (""" \t"github.com/codegangsta/cli" \t"github.com/urfave/cli" \t"gopkg.in/codegangsta/cli" \t"gopkg.in/codegangsta/cli.v1" \t"gopkg.in/urfave/cli" \t"gopkg.in/urfave/cli.v1" """, """ \t"gopkg.in/urfave/cli.v2" \t"gopkg.in/urfave/cli.v2" \t"gopkg.in/urfave/cli.v2" \t"gopkg.in/urfave/cli.v2" \t"gopkg.in/urfave/cli.v2" \t"gopkg.in/urfave/cli.v2" """), (""" \t\t\t\treturn cli.NewExitError("foo whatebber", 9) """, """ \t\t\t\treturn cli.Exit("foo whatebber", 9) """), (""" \t\t\tapp.Flags = []cli.Flag{ \t\t\t\tcli.StringFlag{ \t\t\t\t\tName: "aha", \t\t\t\t}, \t\t\t\tcli.BoolTFlag{ \t\t\t\t\tName: "blurp", \t\t\t\t}, \t\t\t} """, """ \t\t\tapp.Flags = []cli.Flag{ \t\t\t\t&cli.StringFlag{ \t\t\t\t\tName: "aha", \t\t\t\t}, \t\t\t\t&cli.BoolFlag{Value: true, \t\t\t\t\tName: "blurp", \t\t\t\t}, \t\t\t} """), (""" \t\t\tAction = func(c *cli.Context) error { \t\t\t\tif c.Args()[4] == "meep" { \t\t\t\t\treturn nil \t\t\t\t} \t\t\t\treturn errors.New("mope") \t\t\t} """, """ \t\t\tAction = func(c *cli.Context) error { \t\t\t\tif c.Args().Get(4) == "meep" { \t\t\t\t\treturn nil \t\t\t\t} \t\t\t\treturn errors.New("mope") \t\t\t} """), (""" \t\tapp.Flags = []cli.Flag{ \t\t\tcli.StringFlag{ \t\t\t\tName: "toots", \t\t\t\tEnvVar: "TOOTS,TOOTERS", \t\t\t}, \t\t} """, """ \t\tapp.Flags = []cli.Flag{ \t\t\t&cli.StringFlag{ \t\t\t\tName: "toots", \t\t\t\tEnvVars: []string{"TOOTS", "TOOTERS"}, \t\t\t}, \t\t} """), (""" \t\tapp.Flags = []cli.Flag{ \t\t\tcli.StringFlag{ \t\t\t\tName: "t, tootles, toots", \t\t\t}, \t\t} """, """ \t\tapp.Flags = []cli.Flag{ \t\t\t&cli.StringFlag{ \t\t\t\tName: "tootles", Aliases: []string{"toots", "t"}, \t\t\t}, \t\t} """), (""" \t\tapp := cli.NewApp() \t\tapp.HideHelp = true """, """ \t\tapp := (&cli.App{}) \t\tapp.HideHelp = true """) ) if __name__ == '__main__': sys.exit(main())