Files
bake/bake/cli.py
T
2019-09-16 11:37:52 -04:00

368 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import sys
import click
import json
from .bakefile import Bakefile, NoBakefileFound
from .clint import eng_join
import pygments
import pygments.lexers
import pygments.formatters
SAFE_ENVIRONS = [
"HOME",
"PATH",
"LANG",
"LOCALE",
"TERM",
"VIRTUAL_ENV",
"BAKEFILE_PATH",
]
def indent(line):
return f'{" " * 4}{line}'
def do_help(exit=0):
with click.Context(entrypoint) as ctx:
help = entrypoint.get_help(ctx)
help = help.replace(
" bake",
str(click.style(" $ ", fg="green", bold=True))
+ str(click.style("bake", fg="yellow", bold=True)),
)
help = help.replace(
"the strangely familiar taskrunner",
str(
click.style("the strangely familiar taskrunner", fg="white", bold=True)
),
)
help = help.replace(
"Options", str(click.style("Options", fg="white", bold=True))
)
help = help.replace(
"--insecure", str(click.style("--insecure", fg="red", bold=True))
)
help = help.replace("--yes", str(click.style("--yes", fg="red", bold=True)))
help = help.replace(
"--allow", str(click.style("--allow", fg="green", bold=True))
)
help = help.replace(
"--no-deps", str(click.style("--no-deps", fg="yellow", bold=True))
)
help = help.replace(
"--continue", str(click.style("--continue", fg="red", bold=True))
)
help = help.replace(
"--environ-json", str(click.style("--environ-json", fg="green", bold=True))
)
help = help.replace("-e,", str(click.style("-e", fg="green", bold=True) + ","))
help = help.replace(
"--shellcheck", str(click.style("--shellcheck", fg="magenta", bold=True))
)
click.echo(help, err=True)
sys.exit(exit)
def echo_json(obj):
_json = json.dumps(obj, indent=2)
if sys.stdin.isatty():
_json = pygments.highlight(
_json, pygments.lexers.JsonLexer(), pygments.formatters.TerminalFormatter()
)
click.echo(_json, err=False)
@click.command(context_settings=dict(help_option_names=["-h", "--help"]))
@click.argument(
"task",
type=click.STRING,
default="__LIST_ALL__",
envvar="BAKE_TASK",
# required=False,
)
@click.option(
"--bakefile",
"-b",
default="__BAKEFILE__",
envvar="BAKEFILE_PATH",
nargs=1,
type=click.Path(),
help="The Bakefile to use.",
)
@click.option(
"--list",
"-l",
"_list",
default=False,
is_flag=True,
help="Lists available tasks (and their dependencies).",
)
@click.option(
"--help", "-h", default=False, is_flag=True, help="Show this message and exit."
)
@click.option(
"--skip-done", default=False, is_flag=True, hidden=True, envvar="BAKE_SKIP_DONE"
)
@click.option("--debug", default=False, is_flag=True, hidden=True)
@click.option(
"--shellcheck",
default=False,
is_flag=True,
hidden=False,
help="Run shellcheck on Bakefile.",
)
@click.option(
"--allow",
default=False,
nargs=1,
multiple=True,
hidden=False,
help="Whitelist an environment variable for use.",
)
@click.option(
"--no-deps",
default=False,
is_flag=True,
hidden=False,
help="Do not run dependent tasks.",
)
@click.option("--yes", is_flag=True, help="Set mediumsecurity prompts to yes.")
@click.option(
"--continue",
"_continue",
is_flag=True,
type=click.BOOL,
help="Continue, if a task fails.",
)
@click.option(
"--insecure",
is_flag=True,
type=click.BOOL,
help="Inherit parent shell's environment variables.",
)
@click.argument(
"arguments",
nargs=-1,
type=click.STRING,
# multiple=True,
# help="task ARGV argument (can be passed multiple times).",
)
@click.option("--silent", "-s", is_flag=True, type=click.BOOL, help="Reduce output.")
@click.option(
"--environ-json",
"-e",
nargs=1,
type=click.STRING,
help="Provide environment variables via JSON.",
)
@click.option(
"--json",
"-j",
"_json",
is_flag=True,
type=click.BOOL,
help="Output in JSON format (stdout).",
)
def entrypoint(
*,
task,
bakefile,
arguments,
_list,
_continue,
environ_json,
shellcheck,
debug,
silent,
insecure,
allow,
_json,
skip_done,
no_deps,
yes,
help,
):
"""bake — the strangely familiar taskrunner."""
if help:
do_help(0)
# Default to list behavior, when no task is provided.
if _json:
silent = True
SAFE_ENVIRONS.extend(allow)
if task == "__LIST_ALL__":
_list = True
task = None
try:
if bakefile == "__BAKEFILE__":
bakefile = Bakefile.find(root=".", filename="Bakefile")
else:
bakefile = Bakefile(path=bakefile)
except NoBakefileFound:
click.echo(click.style("No Bakefile found!", fg="red"), err=True)
do_help(1)
if not insecure:
for key in bakefile.environ:
if key not in SAFE_ENVIRONS:
del bakefile.environ[key]
if environ_json:
bakefile.add_environ_json(environ_json)
if shellcheck:
pass
argv = []
environ = []
for i, argument in enumerate(arguments[:]):
if "=" in argument:
key, value = argument.split("=", 1)
environ.append((key, value))
else:
argv.append(argument)
if debug:
click.echo(f" + argv: {argv!r}", err=True)
click.echo(f" + environ: {environ!r}", err=True)
for env in environ:
key, value = env
if debug:
click.echo(
f" + Setting environ: {click.style(key, fg='red')} {click.style('=', fg='white')} {value}.",
err=True,
)
bakefile.add_environ(key, value)
bakefile.add_args(*argv)
if _list and not shellcheck:
__list_json = {"tasks": {}}
for _task in bakefile.tasks:
depends_on = bakefile[_task].depends_on(recursive=True)
if no_deps:
depends_on = ()
if depends_on:
deps = []
for dep in depends_on:
if dep.is_filter:
dep = click.style(str(dep), fg="yellow")
deps.append(str(dep))
deps = f"\n {click.style('+', fg='yellow', bold=True)} {eng_join(deps)}."
else:
deps = ""
colon = "" if not deps else ""
__list_json["tasks"].update(
{_task: {"depends_on": [str(d) for d in depends_on]}}
)
if not silent:
click.echo(
f" {click.style('-', fg='green', bold=True)} {click.style(_task, bold=True)}{colon}{deps}",
err=True,
)
if _json:
echo_json(__list_json)
sys.exit(0)
if shellcheck:
__shellcheck_statuses = []
for _task in bakefile.tasks:
_task = bakefile[_task]
c = _task.shellcheck(silent=silent, debug=debug)
__shellcheck_statuses.append(c.return_code)
if not c.ok:
# click.echo(click.style("Shellcheck failed!", fg="red"), err=True)
if _json:
echo_json(json.loads(c.out))
for report in json.loads(c.out):
level = report["level"]
code = report["code"]
message = report["message"]
line = report["line"]
column = (report["column"], report["endColumn"])
colored_line_n = click.style(
str(line).zfill(3), bg="black", fg="cyan", bold=True
)
colored_task = click.style(str(_task), fg="yellow", bold=True)
actual_line = _task.source_lines[line - 1]
click.echo(f"In {colored_task} line {line}:", err=True)
click.echo(f"{colored_line_n} {actual_line}", err=True)
length = column[1] - column[0]
if not length:
length = len(actual_line)
underline = (" " * 4) + (" " * column[0]) + "^" + "-" * (length - 1)
underline += f"SC{code}: {message}"
underline = click.style(level, fg="magenta") + click.style(
underline[len(level) :], fg="red", bold=True
)
click.echo(underline, err=True)
sys.exit(max(__shellcheck_statuses))
if task:
try:
task = bakefile[task]
except KeyError:
click.echo(click.style(f"Task {task} does not exist!", fg="red"))
sys.exit(1)
def execute_task(task, *, silent=False):
if not silent:
click.echo(
click.style(" + ", fg="white")
+ click.style(f"Executing {task}", fg="yellow")
+ click.style(":", fg="white"),
err=True,
)
return_code = task.execute(yes=yes, debug=debug, silent=silent)
if not _continue:
if (not return_code == 0) and (return_code is not None):
click.echo(click.style(f"Task {task} failed!", fg="red"), err=True)
sys.exit(return_code)
if not no_deps:
tasks = task.depends_on(recursive=True) + [task]
else:
tasks = [task]
for task in tasks:
execute_task(task, silent=silent)
if not silent and not skip_done:
click.echo(
click.style(" + ", fg="white")
+ click.style("Done", fg="green")
+ click.style(".", fg="white"),
err=True,
)
sys.exit(0)
if __name__ == "__main__":
entrypoint()