@ -10,14 +10,25 @@ import sys
# - Let the user decide, what to run / edit / split / skip
# - Let the user decide, what to run / edit / split / skip
# - Run the commands remotely
# - Run the commands remotely
# Usage: $0 remote.host.name md1 md2 ...
# Usage: $0 remote.host.name md1 ...
# TODO: implement it
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
@dataclass
class Command :
class Command :
user : str
user : str
directory : str
directory : Any [ str , None ]
command : str
command : str
context : str
context : str
@ -40,54 +51,136 @@ def extract_snippets(pd: t.Pandoc) -> Sequence[Tuple[str, str]]:
last_block = stringify ( block )
last_block = stringify ( block )
return result
return result
def parse_snippet ( s : Tuple [ str , str ] ) - > Sequence [ Sequence [ Command ] ] :
def parse_snippet ( s : Tuple [ str , str ] ) - > Sequence [ Command ] :
# We need to preserve the groups of commands, so we return a list of groups (lists) of commands.
# Will return always a list, even if it has only one element.
# 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"
# 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 } \n CMD: { 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.
# 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
# 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.
# 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 ) :
def run_command ( cmd : Command ) :
# Generate and run the SSH 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 ) :
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 .
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
pd = pandoc . read ( open ( fn ) )
pass
snippets = extract_snippets ( pd )
for snip in snippets :
cmds = parse_snippet ( snip )
# FIXME: tohle má být parametr
cmds = user_action ( cmds )
TARGET = " nothing.nowhere.invalid "
for cmd in cmds :
run_command ( cmd )
def format_command ( command , user = None , directory = None , target = None ) : - > str
cmd = " ssh "
if __name__ == ' __main__ ' :
main ( )
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