diff --git a/markdownrunner.py b/markdownrunner.py index 77a019f..b2975ec 100644 --- a/markdownrunner.py +++ b/markdownrunner.py @@ -10,14 +10,25 @@ import sys # - Let the user decide, what to run / edit / split / skip # - Run the commands remotely -# Usage: $0 remote.host.name md1 md2 ... -# TODO: implement it +# Usage: $0 remote.host.name md1 ... +host: str + +def main(): + if len(sys.argv) < 3: + print(f"Usage: {sys.argv[0]} hostname file1.md ...") + exit(1) + # Here I would use Haskell's pointfree functions, so that host is implicitly passed... + # Never mind, it will be a global variable NOTE THIS!!! + global host + host = sys.argv[1] + for file in sys.argv[2:]: + process_file(file) @dataclass class Command: user: str - directory: str + directory: Any[str, None] command: str context: str @@ -40,54 +51,136 @@ def extract_snippets(pd: t.Pandoc) -> Sequence[Tuple[str, str]]: last_block = stringify(block) return result -def parse_snippet(s: Tuple[str, str]) -> Sequence[Sequence[Command]]: - # We need to preserve the groups of commands, so we return a list of groups (lists) of commands. +def parse_snippet(s: Tuple[str, str]) -> Sequence[Command]: # Will return always a list, even if it has only one element. # This is where we handle blank lines, comments, cd's and sudo's, so that the Commands in the output are "runnable" - context_prefix = s[1] - # TODO -def user_action(hunk: Sequence[Command]) -> Sequence[Command]: + # The original context should be mentioned for all hunks + context = s[0] + + + # We now check the separate lines and possibly tweak them (e.g. by removing sudo-s) + lines = s[1].split('\n') + directory = None # Don't cd by default + user = 'root' # Connect as root by default (it does not really matter, since the commands are run with sudo's) + result = [] + for line in lines: + line = line.strip() + if line.startswith('cd'): + parts = line.split() + assert len(parts) == 2 + directory = parts[1] + continue + elif line.startswith('sudo -u'): + parts = line.split(maxsplit=5) + assert parts[3] == '-H', "Insecure sudo?" + user = parts[2] + directory = f'~{user}' # This actually works with SSH. That is nice, since we don't need to know the mapping of + cmd = parts[4] + elif line.startswith('sudo'): + parts = line.split(maxsplit=2) + assert not parts[1].startswith('-'), "Weird sudo" + user = 'root' + cmd = parts[1] + elif line == '': + # Only add it to context for reference + context = f'{context}\n' + continue + else: + # No sudo or cd, just a command. + user = 'root' # Reset to default + cmd = line + result.append(Command(user=user,directory=directory,command=cmd,context=context)) + # Let's have the previously processed commands in the context + context = f"{context}\nCMD: {user}:{directory if directory else '??'} $ {cmd}" + return result # I miss the right feature of Nim lang... + + + +def user_action(hunk: Sequence[Command], context=None) -> Sequence[Command]: # Maybe modify the commands to run. # This should have a simple UI: show the hunk with context (ideally in grey), ask what to do, return the wanted command # Actions: yes(y): accept input; no(n): skip this command; edit(e): launch $EDITOR or vim on the hunk; quit(q): terminate; split(s): split hunks to individual lines and ask for each one. - # TODO - pass + + # FIXME: We don't implement splitting yet, because it's simple to comment / delete in the editor + # FIXME: We only show given context and don't care for the command's one; that's wrong. + + def stringify_hunk(hunk): + result = '' + for cmd in hunk: + global host + result += f'SSH: {cmd.user}@{host} {cmd.directory} $ {cmd.command}\n' + + + # TODO: Print context in color (gray?) + print('-------------------------------------------') + print(context) + print() + print(stringify_hunk(hunk)) + + # TODO: Print prompt in color (green?) + answer = input("What to do: edit(e), run(y), skip(n)?") + if answer.startswith(('e', 'E')): + import tempfile + with tempfile.NamedTemporaryFile() as tmpfile: + # Fill the file with data + for cmd in hunk: + tmpfile.file.write(f"{cmd.user}\t{cmd.directory}\t{cmd.command}\n") + tmpfile.file.flush() # Should not be needed, but better safe than sorry + # Let the user edit the file + import os + editor = os.getenv('EDITOR') + if editor is None: + # Fallback to vim + editor = 'vim' + import subprocess + subprocess.run([editor, tmpfile.name]) + # Read the file back + new_hunk = [] + for line in tmpfile.file.readlines(): + parts = line.split('\t', maxsplit=3) + new_hunk.append(Command(user=parts[0],directory=parts[1],command=parts[2],context=None) + # Python has no goto for restarting, but we can recurse + return user_action(new_hunk, context) + elif answer.startswith(('y', 'Y')): + return hunk + elif answer.startswith(('n', 'N')): + return [] + + def run_command(cmd: Command): # Generate and run the SSH command. + global host + local_command = ['ssh', host] + if cmd.user is not None: + local_command.extend(['-l', cmd.user]) + if cmd.directory is not None: + cmd.command += f'cd {cmd.directory}; {cmd.command}' + local_command.append(cmd.command) + + # Run + import subprocess + process = subprocess.run(local_command) + if process.returncode != 0: + # TODO: Print in color (red?) + answer = input(f"Command {process.args} failed with return code {process.returncode}. Continue? ") + if not answer.startswith(('Y', 'y', 'a', 'A')): # We are Czech, so "ano" is a valid answer :-) + print("Bailing out") + sys.exit(1) def process_file(fn: str): """ To be called upon each file on the command line. This function should perform all the steps needed in order to remotely run a parsed markdown, id est the three parts above. """ - # TODO - pass - - -# FIXME: tohle má být parametr -TARGET="nothing.nowhere.invalid" - -def format_command(command, user=None, directory=None, target=None): -> str - cmd = "ssh " - - if user is not None: - cmd += f"{user}@" - - if target is None: - global TARGET - cmd += TARGET - else: - cmd += target - - # Be safe: - cmd += " set -e ';' " - - if directory is not None - cmd += f"cd '{directory}' ';' " - - cmd += command - return cmd - - + pd = pandoc.read(open(fn)) + snippets = extract_snippets(pd) + for snip in snippets: + cmds = parse_snippet(snip) + cmds = user_action(cmds) + for cmd in cmds: + run_command(cmd) + +if __name__ == '__main__': + main()