Add the separate prepare and run scripts
They are just copied original with minor changes, which is awful from the programming point of view, but works.master
parent
4c7a88bf00
commit
5f60ef7ca3
@ -0,0 +1,196 @@
|
||||
#!/bin/python3
|
||||
|
||||
import pandoc
|
||||
import pandoc.types as t
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import *
|
||||
|
||||
# There are three parts:
|
||||
# - Parse a given markdown and extract the commands
|
||||
# - Also: keep track of the groups / ...
|
||||
# - Let the user decide, what to run / edit / split / skip
|
||||
# - Save the commands for later
|
||||
|
||||
# Usage: $0 target_filename md1 ...
|
||||
|
||||
target_filename: str
|
||||
directory: str = None
|
||||
default_user = 'root'
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print(f"Usage: {sys.argv[0]} filename file1.md ...")
|
||||
exit(1)
|
||||
# Here I would use Haskell's pointfree functions, so that file is implicitly passed...
|
||||
# Never mind, it will be a global variable NOTE THIS!!!
|
||||
global target_filename
|
||||
target_filename = sys.argv[1]
|
||||
for file in sys.argv[2:]:
|
||||
process_file(file)
|
||||
|
||||
@dataclass
|
||||
class Command:
|
||||
user: str
|
||||
directory: Union[str, None]
|
||||
command: str
|
||||
context: str
|
||||
|
||||
def stringify(block) -> str:
|
||||
"""
|
||||
Take an object of some of pandoc.types and make the best of it.
|
||||
"""
|
||||
# The pandoc library has a fancy function for that...
|
||||
return pandoc.write(block)
|
||||
|
||||
def extract_snippets(pd: t.Pandoc) -> Sequence[Tuple[str, str]]:
|
||||
"""
|
||||
This extracts snippets from the code. The problem is, that we want context with it, so we return a list of tuples (previous_block_stringified, code_snippet).
|
||||
"""
|
||||
result = []
|
||||
last_block = ""
|
||||
for block in pd[1]:
|
||||
if isinstance(block, t.CodeBlock):
|
||||
result.append((last_block, block[1]))
|
||||
last_block = stringify(block)
|
||||
return result
|
||||
|
||||
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"
|
||||
|
||||
# The original context should be mentioned for all hunks
|
||||
context = s[0]
|
||||
|
||||
snippet = s[1]
|
||||
|
||||
# Fix line-breaks
|
||||
# FIXME: nonsystematic and ad hoc
|
||||
snippet = snippet.replace('\\\n', '')
|
||||
|
||||
# We now check the separate lines and possibly tweak them (e.g. by removing sudo-s)
|
||||
lines = snippet.split('\n')
|
||||
global directory
|
||||
result = []
|
||||
for line in lines:
|
||||
user = None # This allows (in conjunction with the right order of arguments to 'ssh') to specify the default user in the hostname (as 'user@host.name')
|
||||
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=4)
|
||||
assert parts[3] == '-H', "Insecure sudo?"
|
||||
user = parts[2]
|
||||
cmd = parts[4]
|
||||
elif line.startswith('sudo'):
|
||||
parts = line.split(maxsplit=1)
|
||||
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.
|
||||
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.
|
||||
|
||||
# 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'
|
||||
return result
|
||||
|
||||
|
||||
# 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, default), skip(n)?")
|
||||
if answer is None or answer == '':
|
||||
# Default is run, for convenience
|
||||
answer = 'Y'
|
||||
if answer.startswith(('e', 'E')):
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile(mode='r+') as tmpfile:
|
||||
# Fill the file with data
|
||||
for cmd in hunk:
|
||||
tmpfile.file.write(f"{cmd.user if cmd.user else ''}\t{cmd.directory if cmd.directory else ''}\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
|
||||
tmpfile.file.seek(0)
|
||||
new_hunk = []
|
||||
for line in tmpfile.file.readlines():
|
||||
if line == '':
|
||||
continue
|
||||
line = line.rstrip('\n')
|
||||
parts = line.split('\t', maxsplit=2)
|
||||
# Change user to None if it is not specified
|
||||
parts[0] = parts[0].strip() if parts[0].strip() != '' else None
|
||||
# Change directory to None if it is not specified
|
||||
parts[1] = parts[1].strip() if parts[1].strip() != '' else None
|
||||
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 []
|
||||
else:
|
||||
# Bad choice or error. Notify user and try again
|
||||
print("Bad choice, or maybe bug.")
|
||||
# No goto, use recursion
|
||||
return user_action(hunk, context)
|
||||
|
||||
|
||||
|
||||
def run_command(cmd: Command):
|
||||
# Save the SSH command.
|
||||
global target_filename
|
||||
with open(target_filename, "w+") as target:
|
||||
target.write(f"{cmd.user if cmd.user else ''}\t{cmd.directory if cmd.directory else ''}\t{cmd.command}\n")
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
pd = pandoc.read(file=fn)
|
||||
snippets = extract_snippets(pd)
|
||||
for snip in snippets:
|
||||
cmds = parse_snippet(snip)
|
||||
cmds = user_action(cmds, context=cmds[0].context)
|
||||
for cmd in cmds:
|
||||
run_command(cmd)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,222 @@
|
||||
#!/bin/python3
|
||||
|
||||
import pandoc
|
||||
import pandoc.types as t
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import *
|
||||
|
||||
# There are three parts:
|
||||
# - Parse a given markdown and extract the commands
|
||||
# - Also: keep track of the groups / ...
|
||||
# - Let the user decide, what to run / edit / split / skip
|
||||
# - Run the commands remotely
|
||||
|
||||
# Usage: $0 remote.host.name script1 ...
|
||||
|
||||
host: str
|
||||
directory: str = None
|
||||
default_user = 'root'
|
||||
|
||||
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: Union[str, None]
|
||||
command: str
|
||||
context: str
|
||||
|
||||
def stringify(block) -> str:
|
||||
"""
|
||||
Take an object of some of pandoc.types and make the best of it.
|
||||
"""
|
||||
# The pandoc library has a fancy function for that...
|
||||
return pandoc.write(block)
|
||||
|
||||
def extract_snippets(pd: t.Pandoc) -> Sequence[Tuple[str, str]]:
|
||||
"""
|
||||
This extracts snippets from the code. The problem is, that we want context with it, so we return a list of tuples (previous_block_stringified, code_snippet).
|
||||
"""
|
||||
result = []
|
||||
last_block = ""
|
||||
for block in pd[1]:
|
||||
if isinstance(block, t.CodeBlock):
|
||||
result.append((last_block, block[1]))
|
||||
last_block = stringify(block)
|
||||
return result
|
||||
|
||||
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"
|
||||
|
||||
# The original context should be mentioned for all hunks
|
||||
context = s[0]
|
||||
|
||||
snippet = s[1]
|
||||
|
||||
# Fix line-breaks
|
||||
# FIXME: nonsystematic and ad hoc
|
||||
snippet = snippet.replace('\\\n', '')
|
||||
|
||||
# We now check the separate lines and possibly tweak them (e.g. by removing sudo-s)
|
||||
lines = snippet.split('\n')
|
||||
global directory
|
||||
result = []
|
||||
for line in lines:
|
||||
user = None # This allows (in conjunction with the right order of arguments to 'ssh') to specify the default user in the hostname (as 'user@host.name')
|
||||
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=4)
|
||||
assert parts[3] == '-H', "Insecure sudo?"
|
||||
user = parts[2]
|
||||
cmd = parts[4]
|
||||
elif line.startswith('sudo'):
|
||||
parts = line.split(maxsplit=1)
|
||||
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.
|
||||
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.
|
||||
|
||||
# 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'
|
||||
return result
|
||||
|
||||
|
||||
# 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, default), skip(n)?")
|
||||
if answer is None or answer == '':
|
||||
# Default is run, for convenience
|
||||
answer = 'Y'
|
||||
if answer.startswith(('e', 'E')):
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile(mode='r+') as tmpfile:
|
||||
# Fill the file with data
|
||||
for cmd in hunk:
|
||||
tmpfile.file.write(f"{cmd.user if cmd.user else ''}\t{cmd.directory if cmd.directory else ''}\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
|
||||
tmpfile.file.seek(0)
|
||||
new_hunk = []
|
||||
for line in tmpfile.file.readlines():
|
||||
if line == '':
|
||||
continue
|
||||
line = line.rstrip('\n')
|
||||
parts = line.split('\t', maxsplit=2)
|
||||
# Change user to None if it is not specified
|
||||
parts[0] = parts[0].strip() if parts[0].strip() != '' else None
|
||||
# Change directory to None if it is not specified
|
||||
parts[1] = parts[1].strip() if parts[1].strip() != '' else None
|
||||
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 []
|
||||
else:
|
||||
# Bad choice or error. Notify user and try again
|
||||
print("Bad choice, or maybe bug.")
|
||||
# No goto, use recursion
|
||||
return user_action(hunk, context)
|
||||
|
||||
|
||||
|
||||
def run_command(cmd: Command):
|
||||
# Generate and run the SSH command.
|
||||
global host
|
||||
local_command = ['ssh']
|
||||
remote_command = cmd.command
|
||||
if cmd.user is not None:
|
||||
local_command.extend(['-l', cmd.user])
|
||||
else:
|
||||
global default_user
|
||||
local_command.extend(['-l', default_user])
|
||||
local_command.append(host) # SSH seems to honor the first specified parameter. This allows to have username as a part of the hostname, yet still be able to honor the sudo-s in the sinppets.
|
||||
if cmd.directory is not None:
|
||||
remote_command = f'cd {cmd.directory}; {remote_command}'
|
||||
local_command.append(remote_command)
|
||||
|
||||
# Run
|
||||
import subprocess
|
||||
process = subprocess.run(local_command)
|
||||
if process.returncode != 0:
|
||||
# TODO: Print in color (red?)
|
||||
# FIXME: This kind-of implements 'set -e', but doesn't allow to skip commands in the same hunk
|
||||
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.
|
||||
"""
|
||||
with open(fn, "r+") as script:
|
||||
lines = script.readlines()
|
||||
for line in lines:
|
||||
if line == '':
|
||||
continue
|
||||
line = line.rstrip('\n')
|
||||
parts = line.split('\t', maxsplit=2)
|
||||
# Change user to None if it is not specified
|
||||
parts[0] = parts[0].strip() if parts[0].strip() != '' else None
|
||||
# Change directory to None if it is not specified
|
||||
parts[1] = parts[1].strip() if parts[1].strip() != '' else None
|
||||
cmd = Command(user=parts[0],directory=parts[1],command=parts[2],context=None)
|
||||
run_command(cmd)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Reference in New Issue