# explodeTK.py
# Copyright 2006, Eli Fulkerson
# www.elifulkerson.com

#
# This is a GUI utility for automatically generating repetitive config files, especially (as an example)
# the 'interface' section of Cisco Switches via a simple Macro Language.
#
# Due to the use of win32clipboard for cut/paste support, this version is Win32 only.
#

#This is a minimal language and interpreter for quickly generating config files (or fragments thereof) for Cisco networking devices. It implements the following rules:

#   1. Any line of the format:

#      #define something something_else

#      ... will set up a global value "something" that contains the data "something else"
#   2. Any string of the format {something} will replace itself with a previously #defined something_else
#   3. Any string of the format {a-b} where 'a' and 'b' are integer values will expand out that line and children (based on code blocks) for each value in the range from a..b inclusive.
#   4. Indentation is used to indicate 'blocks' of code. The amount of whitespace is irrelevant, as long as you remain consistent. For instance: if your first block is indented by 2 spaces, its children must be indented 4, and then 6, and so on. If you start with 5, its children must be indented 10, 15, etc. Irregular indentation will cause an error.
#   5. Only one macro substitution is permitted per line.
#   6. Currently, all '#define' values must not include spaces.
   
#
# Syntax and explanation at http://www.elifulkerson.com/projects/macro-language-for-cisco-configuration.php
#

# Today is Mar 18, 2006

import time, string, re
from Tkinter import *
import tkFileDialog
import win32clipboard, win32con


global defines
defines = {}

global interval
interval = 0

examplesyntax = """interface FastEthernet0/{1-5}
    switchport access vlan {visitor_vlan}
    no shutdown

interface FastEthernet0/{21-25}
    switchport access vlan {staff_vlan}
    no shutdown

#define visitor_vlan 200
#define staff_vlan 100

: Click "Expand Macro" to see this expand
"""

def send_to_clipboard(text):
    win32clipboard.OpenClipboard()
    win32clipboard.EmptyClipboard()

    # get the win32 endlines in...
    win32clipboard.SetClipboardText(string.replace(text, "\n", "\r\n"))
    
    win32clipboard.CloseClipboard()


def get_from_clipboard():
    win32clipboard.OpenClipboard()

    # get the win32 endlines in...
    tmp = win32clipboard.GetClipboardData(win32con.CF_TEXT)
    
    win32clipboard.CloseClipboard()

    return tmp


class Inputpad:
    def __init__(self, root):


        self.tk = root
                
        self.canvas = Canvas(bg='white')
    
        self.input = Text (self.canvas)
        self.input.pack(side=LEFT, fill=BOTH, expand=1)

        s = Scrollbar(root)
        self.input.focus_set()
        s.pack(side=RIGHT, fill=Y)
        s.config(command=self.input.yview)
        self.input.config(yscrollcommand=s.set)
        

        
    def show (self):
        """ use pack to make this instance appear in the gui """
        self.canvas.pack(fill=BOTH, expand=1)
        
    def hide (self):
        """ Use pack_forget to make this instance disappear from the GUI """
        self.canvas.pack_forget()
    
class Application:
    def __init__(self):
        

        self.root = Tk()
        
        root = self.root
        root.title("Explode-Macro for Cisco Configs")

        menubar = self.build_menu()

        # display the menu
        root.config(menu=menubar)
        
        top = Frame(height=25, bg="steelblue4")
                
        b = Button(top, padx=5, text="Expand Macro", command=self.process_macro)
        b.configure(fg="white", bg="navy")
        b.pack(side=LEFT)
    
                
        #l = Label(top, bg="steelblue4", fg="white", text="ctrl-x cut | ctrl-c copy | ctrl-v paste")
        l = Label(top, bg="steelblue4", fg="white", text="This utility lives at http://www.elifulkerson.com")
        
        l.pack(side=RIGHT)
        
        
        top.pack(side=BOTTOM, fill=X)

        self.inputpad = Inputpad(root)

        #self.show_inputpad()
        self.inputpad.show()

    def open(self):

        file = tkFileDialog.askopenfile(parent=self.root,mode='rb',title='Choose a file')
        if file != None:
            data = file.read()
            file.close()

            
            self.inputpad.input.delete('1.0', END)

            # have to do some wierdness to keep whitespace from creeping in.  Not sure why.
            tmp = data
            for each in tmp.split("\n"):
                each = each.rstrip()

                self.inputpad.input.insert(INSERT, each + "\n")
        
    def save(self):
        myFormats = [('Any','*.*')]

        filename = tkFileDialog.asksaveasfilename(parent=self.root,filetypes=myFormats ,title="Save the file as...")
        if len(filename ) > 0:
            f = open(filename, 'w')
            f.write(self.inputpad.input.get('1.0', END))
            f.close()
            
    def newfile(self):
        self.inputpad.input.delete("1.0", END)


    def copy(self):
        try:
            send_to_clipboard(self.inputpad.input.get(SEL_FIRST, SEL_LAST))
        except:
            pass

    def cut(self):
        self.copy()
        self.clear()

    def paste(self):
        try:
            self.inputpad.input.delete(SEL_FIRST, SEL_LAST)
        except:
            pass

        try:
            tmp = get_from_clipboard()
            self.inputpad.input.insert(INSERT, tmp)
        except:
            pass


    def clear(self):
        try:
            self.inputpad.input.delete(SEL_FIRST, SEL_LAST)
        except:
            pass

        

    def build_menu(self):
        root = self.root
        # create a toplevel menu
        menubar = Menu(root, bg="navy")

        # create a pulldown menu, and add it to the menu bar
        filemenu = Menu(menubar, tearoff=1, bg="navy", fg="white")
        filemenu.add_command(label="New", command=self.newfile)
        filemenu.add_command(label="Open", command=self.open)
        filemenu.add_command(label="Save", command=self.save)
        filemenu.add_separator()
        filemenu.add_command(label="Syntax Example", command=self.show_example_syntax)
        filemenu.add_separator()
        filemenu.add_command(label="Exit", command=root.quit)
        menubar.add_cascade(label="File", menu=filemenu)

        # create more pulldown menus
        editmenu = Menu(menubar, tearoff=1, bg="navy", fg="white")
        editmenu.add_command(label="Cut", command=self.cut)
        editmenu.add_command(label="Copy", command=self.copy)
        editmenu.add_command(label="Paste", command=self.paste)
        editmenu.add_command(label="Delete", command=self.clear)
        editmenu.add_separator()
        editmenu.add_command(label="Expand Macro", command=self.process_macro)
        menubar.add_cascade(label="Edit", menu=editmenu)
        
        return menubar

    def show_example_syntax(self):
        self.inputpad.input.delete('1.0', END)
        self.inputpad.input.insert("1.0", examplesyntax)


    def process_macro(self):
        tmp = self.inputpad.input.get('1.0', END)
        self.inputpad.input.delete('1.0', END)
        parse_text(tmp, self.inputpad.input)
        
        pass
        
    def run(self):
        self.root.mainloop()

class Block:
    "A Block is a set of lines that may be repeated, with their children, arbitrarily"
    def __init__(self, str, parent):
        self.str = string.strip(str)
        self.parent = parent
        self.children = []

    def addchild(self, child):
        self.children.append(child)

    def output(self,textbox,indent=-1):

        regex_variable = re.compile('\{\w+\}')
        regex_range = re.compile('\{\d+-\d+\}')

        match = regex_range.findall(self.str)
        start,stop = 1,2

        global interval

        # There is some redundent code here...
        
        if match:
            tmp = match[0]
            tmp = string.replace(tmp, "{", "")
            tmp = string.replace(tmp, "}", "")
            tmp = string.replace(tmp, "-", " ")
            start,stop = string.split(tmp)

            for each in range(int(start), int(stop)+1):
                textbox.insert(END, " " * interval * indent + string.replace(self.str, match[0], str(each)) + "\n")
                for each in self.children:
                    each.output(textbox,indent+1)
            return

        match = regex_variable.findall(self.str)
        if match:
            global defines
            tmp = match[0]
            tmp = string.replace(tmp, "{", "")
            tmp = string.replace(tmp, "}", "")

            textbox.insert(END, " " * interval * indent + string.replace(self.str, match[0], defines[tmp]) + "\n")
            for each in self.children:
                each.output(textbox,indent+1)
            return
            
        textbox.insert(END, " " * interval * indent + self.str  + "\n")
        for each in self.children:
            each.output(textbox,indent+1)

def parse_text(filestring, textbox):
    
    regex_define = re.compile('^#define\s\w+\s.+')
    regex_variable = re.compile('\{\w+\}')
    regex_range = re.compile('\{\d+-\d+\}')
    regex_tabs = re.compile('^\s+')

    global interval
    global defines

    numspaces = 0
    oldspaces = 0

    root = Block("", 0)
    current = root

    " first, build the structure and read in the defines... "
    for line in string.split(filestring, "\n"):

        " count the number of leading spaces "
        oldspaces = numspaces
        numspaces = 0
        match = regex_tabs.findall(line)
        if match:
            numspaces = len(match[0])

        if oldspaces != numspaces:
            if interval == 0:
                interval = numspaces - oldspaces
            else:
                if (oldspaces - numspaces) % interval != 0:
                    print "Error!  Unusual block spacing.  Once a spacing interval is set, all remaining lines must be a multiple of it."
                    #sys.exit(1)

        " we don't worry about queuing up #define statements... just save the values "
        match = regex_define.findall(line)
        if match:
            key,val=string.split(match[0])[1:]
            defines[key] = val

        else:
            line = string.strip(line)

            " this is a child element "
            if numspaces > oldspaces:
                tmp = Block(line, last)
                last.addchild(tmp)
                current = last

            " this is a peer element "
            if numspaces == oldspaces:
                tmp = Block(line, current)
                current.addchild(tmp)
                
            " we have to go up.  We may have to skip several layers.  Check the interval "
            if numspaces < oldspaces:
                x = 0
                while x < (oldspaces - numspaces) / interval:
                    current = current.parent
                    x = x + 1

                tmp = Block(line, current)
                current.addchild(tmp)
                
            last = tmp

    " The structure is built, let it output itself "
    root.output(textbox)

        
if __name__ == "__main__":

    App = Application()
    App.run()
    
    
