urfave-cli/cli-v1-to-v2
Dan Buch cbd0e8231b
Add migrator for stringly values of flag "Name"
and reorganize things a bit for less jumpy editing
2016-05-31 20:58:12 -04:00

383 lines
8.7 KiB
Python
Executable File

#!/usr/bin/env python
from __future__ import print_function, unicode_literals
import argparse
import logging
import os
import re
import sys
_DESCRIPTION = """\
Migrate arbitrary `.go` sources (mostly) from the v1 to v2 API.
"""
_V2_IMPORT = '"gopkg.in/urfave/cli.v2"'
_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('--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
logging.basicConfig(
level=(logging.FATAL if args.quiet else logging.INFO),
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 open(filepath, 'w') 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 open(filepath) as infile:
return _update_source(infile.read())
def _update_source(source):
for migrator, func in _MIGRATORS.items():
logging.info('Running %s migrator', migrator)
source = func(source)
return source
def _migrator(func):
_MIGRATORS[func.__name__.replace('_migrate_', '')] = func
return func
@_migrator
def _migrate_command_slice(source):
return re.sub(
'(?P<prefix>\\[\\])cli\\.Command{',
_command_slice_repl, source
)
def _command_slice_repl(match):
return '{prefix}*cli.Command{{'.format(**match.groupdict())
@_migrator
def _migrate_command_literal(source):
return re.sub(
'(?P<prefix>\\s+)cli\\.Command{',
_command_literal_repl, source
)
def _command_literal_repl(match):
return '{prefix}&cli.Command{{'.format(**match.groupdict())
@_migrator
def _migrate_slice_types(source):
return re.sub(
'&cli\\.(?P<type>IntSlice|StringSlice){(?P<args>[^}]*)}',
_slice_type_repl, source, flags=re.DOTALL
)
def _slice_type_repl(match):
return 'cli.New{type}({args})'.format(**match.groupdict())
@_migrator
def _migrate_flag_literals(source):
return re.sub(
'(?P<prefix>\\s+)cli\\.(?P<type>\\w+)Flag{',
_flag_literal_repl, source
)
def _flag_literal_repl(match):
return '{prefix}&cli.{type}Flag{{'.format(**match.groupdict())
@_migrator
def _migrate_v1_imports(source):
return re.sub(
'"(?:github\\.com|gopkg\\.in)/(?:codegangsta|urfave)/cli(?:\\.v1|)"',
_V2_IMPORT, source
)
@_migrator
def _migrate_new_exit_error(source):
return re.sub('cli\\.NewExitError', 'cli.Exit', source)
@_migrator
def _migrate_bool_t_flag(source):
return re.sub(
'cli\\.BoolTFlag{(?P<args>[^}]*)}',
_bool_t_flag_repl, source, flags=re.DOTALL
)
def _bool_t_flag_repl(match):
return 'cli.BoolFlag{{Value: true,{args}}}'.format(**match.groupdict())
@_migrator
def _migrate_context_args_index(source):
return re.sub(
'\\.Args\\(\\)\\[(?P<index>\\d+)\\]',
_context_args_index_repl, source
)
def _context_args_index_repl(match):
return '.Args().Get({index})'.format(**match.groupdict())
@_migrator
def _migrate_envvar_string(source):
return re.sub('EnvVar:(?P<ws>\\s+)"(?P<string>[^"]+)"', _envvar_string_repl, source)
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'])
]),
**match.groupdict()
)
@_migrator
def _migrate_flag_name_stringly(source):
return re.sub(
'(?P<prefix>\\s+)Name:(?P<ws>\\s+)"(?P<string>[^"]+)"',
_flag_name_stringly_repl, source
)
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
)
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}
""")
)
if __name__ == '__main__':
sys.exit(main())