diff --git a/bashf/bashfile.py b/bashf/bashfile.py index 454db1b..03b80d6 100644 --- a/bashf/bashfile.py +++ b/bashf/bashfile.py @@ -1,5 +1,9 @@ +import sys import os import json +from random import randint + +import click from .bash import Bash @@ -14,13 +18,60 @@ class TaskNotInBashfile(ValueError): pass -class FlagNotAvailable(ValueError): +class FilterNotAvailable(ValueError): pass -class Flag: - def __init__(self, name): - self.name = name +class TaskFilter: + def __init__(self, s): + self.source = s + + def __str__(self): + return f"{self.source!r}" + + @property + def name(self): + return self.source.split(":", 1)[0][len("@") :] + + @property + def args(self): + args = {} + + try: + + for arg in self.source.split(":", 1)[1].split(":"): + split = arg.split("=", 1) + + key = split[0] + value = split[1] if len(split) == 2 else True + + args[key] = value + except IndexError: + pass + + return args + + def depends_on(self, **kwargs): + return [] + + @staticmethod + def execute_confirm(*, prompt=False, yes=False, secure=False, **kwargs): + if not yes: + if secure: + int1 = randint(0, 12) + int2 = randint(0, 12) + + user_value = click.prompt(f" What is {int1} times {int2}?") + + if int(user_value) != int1 * int2: + sys.exit(1) + + else: + click.confirm(" Do you want to continue?", abort=True) + + def execute(self, yes=False, **kwargs): + if self.name == "confirm": + self.execute_confirm(yes=yes, **self.args) class TaskScript: @@ -34,6 +85,9 @@ class TaskScript: def __repr__(self): return f"" + def __str__(self): + return f"{self.name!r}" + @property def declaration_line(self): for line in self.bashfile.source_lines: @@ -41,21 +95,36 @@ class TaskScript: return line def depends_on(self, *, reverse=False, recursive=False): - def gen_tasks(): - task_names = self.declaration_line.split(":")[1].split() - task_indexes = [self.bashfile.find_chunk(task_name=n) for n in task_names] - for i in task_indexes: - yield TaskScript(bashfile=self.bashfile, chunk_index=i) + def gen_actions(): + task_strings = self.declaration_line.split(":", 1)[1].split() + # Filter out filters. + # filters = [t for t in task_names if t.startswith("@")] - tasks = [t for t in gen_tasks()] + # for filter in filters: + # del task_names[task_names.index(filter)] + + task_name_index_tuples = [ + (self.bashfile.find_chunk(task_name=s), s) for s in task_strings + ] + + for i, task_string in task_name_index_tuples: + + if i is None: + # Create the filter. + yield TaskFilter(task_string) + else: + # Otherwise, create the task. + yield TaskScript(bashfile=self.bashfile, chunk_index=i) + + actions = [t for t in gen_actions()] if recursive: - for i, task in enumerate(tasks[:]): + for i, task in enumerate(actions[:]): for t in reversed(task.depends_on()): - if t.name not in [task.name for task in tasks]: - tasks.insert(i + 1, t) + if t.name not in [task.name for task in actions]: + actions.insert(i + 1, t) - return tasks + return actions @classmethod def _from_chunk_index(Class, bashfile, *, i): @@ -68,7 +137,7 @@ class TaskScript: if line.startswith(indent_style): return line[len(indent_style) :] - def execute(self, blocking=False): + def execute(self, *, blocking=False, **kwargs): bash = Bash(environ=self.bashfile.environ) return bash.command(self.source, blocking=False) @@ -100,7 +169,7 @@ class TaskScript: class Bashfile: def __init__(self, *, path): self.path = path - self.environ = {} + self.environ = os.environ self._chunks = [] if not os.path.exists(path): diff --git a/bashf/cli.py b/bashf/cli.py index 18c45ca..066b6c4 100644 --- a/bashf/cli.py +++ b/bashf/cli.py @@ -3,6 +3,8 @@ import click import crayons from .bashfile import Bashfile +SAFE_ENVIRONS = ["HOME"] + def indent(line): return f'{" " * 4}{line}' @@ -35,6 +37,7 @@ def indent(line): multiple=True, help="task environment variable (can be passed multiple times).", ) +@click.option("--yes", is_flag=True, help="Set prompts to yes.") @click.option( "--fail", "-x", @@ -42,6 +45,13 @@ def indent(line): type=click.BOOL, help="Fail immediately, if any task fails.", ) +@click.option( + "--secure", + "-s", + is_flag=True, + type=click.BOOL, + help="Ignore parent shell's environment variables.", +) @click.option( "--arg", "-a", @@ -59,7 +69,19 @@ def indent(line): help="environment variables, in JSON format.", ) def task( - *, task, bashfile, arg, _list, environ, fail, environ_json, shellcheck, debug, quiet + *, + task, + bashfile, + arg, + _list, + environ, + fail, + environ_json, + shellcheck, + debug, + quiet, + secure, + yes, ): """bashf — Bashfile runner (the familiar Bash/Make hybrid).""" # Default to list behavior, when no task is provided. @@ -70,6 +92,10 @@ def task( if bashfile == "__BASHFILE__": bashfile = Bashfile.find(root=".") + if secure: + for key in bashfile.environ: + if key not in SAFE_ENVIRONS: + del bashfile.environ[key] if environ_json: bashfile.add_environ_json(environ_json) @@ -92,27 +118,33 @@ def task( try: task = bashfile[task] except KeyError: - click.echo(crayons.red(f"Task {task!r} does not exist!")) + click.echo(crayons.red(f"Task {task} does not exist!")) sys.exit(1) - def execute_task(task): + def execute_task(task, *, next_task=None): if not quiet: click.echo( crayons.white(" · ") - + crayons.yellow(f"Executing task {task.name!r}") - + crayons.white("…") + + crayons.yellow(f"Executing {task}") + + crayons.white(" · ") ) - return_code = task.execute() + return_code = task.execute(yes=yes, next_task=next_task) if fail: if not return_code == 0: - click.echo(f"Task {task.name!r} failed!") + click.echo(f"Task {task} failed!") sys.exit(return_code) - for task in task.depends_on(recursive=True) + [task]: - execute_task(task) + tasks = task.depends_on(recursive=True) + [task] + for task in tasks: + try: + next_task = tasks[tasks.index(task) + 1] + except IndexError: + next_task = None - click.echo(crayons.white(" · ") + crayons.green("Done") + crayons.white("!")) + execute_task(task, next_task=next_task) + + click.echo(crayons.white(" · ") + crayons.green("Done") + crayons.white(" · ")) sys.exit(0) diff --git a/bashf/environment.py b/bashf/environment.py new file mode 100644 index 0000000..e69de29 diff --git a/bashf/scripts/stdlib.sh b/bashf/scripts/stdlib.sh index 1e82d39..3106096 100644 --- a/bashf/scripts/stdlib.sh +++ b/bashf/scripts/stdlib.sh @@ -10,3 +10,37 @@ fi bashf-indent() { bashf-sed "s/^/ /" } + +# --------------------- +# From: https://github.com/heroku/buildpack-stdlib/blob/master/stdlib.sh + +# Buildpack Steps. +puts_step() { + if [[ "$*" == "-" ]]; then + read -r output + else + output=$* + fi + echo -e "\\e[1m\\e[36m=== $output\\e[0m" + unset output +} + +# Buildpack Error. +puts_error() { + if [[ "$*" == "-" ]]; then + read -r output + else + output=$* + fi + echo -e "\\e[1m\\e[31m=!= $output\\e[0m" +} + +# Buildpack Warning. +puts_warn() { + if [[ "$*" == "-" ]]; then + read -r output + else + output=$* + fi + echo -e "\\e[1m\\e[33m=!= $output\\e[0m" +}