handling of ANSI terminals using Expect

Stefan Finzel

There are a lot of network devices not supporting raw mode output but only ansi mode. To remove the ANSI sequences in output formatted line by line use:

 proc un_ansi {data} {
    # --------------------------------------------------------------------------    
    # remove ansi escape sequences
    # --------------------------------------------------------------------------
    set txt {}
    while {[string length ${data}]} {
      set match { }
      switch -regexp -- ${data} {
        {^\x1b(\[|\(|\))[;?0-9]*[0-9A-Za-z]} {
             regexp -- {^\x1b(\[|\(|\))[;?0-9]*[0-9A-Za-z]} ${data} match
             append txt "\n"
           }
        {^(.+?)\x1b} {
             regexp -- {^(.+?)\x1b} ${data} UNUSED match
             # handle special escape sequences
             regsub -all -- {\\([\\\[\]])} ${match} {\1} raw_match
             append txt ${raw_match}
           }
        {^\x1b} {
             # do nothing
           }
        default {
             set match ${data}
             append txt ${match}
           }
       }
       set data [string range ${data} [string length ${match}] end]
   }

   # remove white spaces not needed anymore
   regsub -all -- "\t+" ${txt} { } txt
   regsub -all -- " +" ${txt} { } txt
   regsub -all -nocase -- "\n+" [string trim ${txt}] "\n" txt
   return ${txt}
 }

There are devices building the screen not line by line. I even found devices that try to be intelligent and speed up the terminal output by just replacing the changing parts of the screen in any order. Therefore something like a virtual terminal is useful:

 proc un_ansi_vt {data {mode raw} {bounding {0 25 0 80 0 0 0 1}}} {
    # --------------------------------------------------------------------------
    # simulation of a virtual terminal to create line by line output 
    # --------------------------------------------------------------------------
    global terminal

    foreach {row_min row_max col_min col_max linewrap pagewrap status listmode} ${bounding} {
        break
    }
    set col_last 0
    set row_last 0
    set linewrap_last ${linewrap}
    if {${status} && [info exists terminal(status)]} {
        foreach {row_last col_last linewrap_last} $terminal(status) {
            break
        }
    }
    switch -glob -- ${mode} {
        delta -
        diff* -
        raw {
                catch {unset terminal}
            }
        reset* {
                catch {unset terminal}
                # FRINK: set row, col by for
                for {set row ${row_min}} {${row} < ${row_max}} {incr row} {
                    for {set col ${col_min}} {${col} < ${col_max}} {incr col} {
                        set terminal(${row},${col}) { }
                    }
                }
                set col_last 0
                set row_last 0
                set linewrap_last ${linewrap}
            }
        default {
                # an append/overwrite mode
                if {![array size terminal]} {
                    for {set row ${row_min}} {${row} < ${row_max}} {incr row} {
                        for {set col ${col_min}} {${col} < ${col_max}} {incr col} {
                            set terminal(${row},${col}) { }
                        }
                    }
                }
            }
    }
    set col ${row_last}
    set row ${col_last}
    set linewrap ${linewrap_last}

    while {[string length ${data}]} {
        set match { }
        switch -regexp -- ${data} {
            {^\x1b\[([0-9]*);([0-9]*)[Hf]} {
                    # position cursor
                    regexp -- {^\x1b\[0*([0-9]*);0*([0-9]*)[Hf]} ${data} match row col
                    if {![llength ${row}]} {
                        set row 0
                    }
                    if {![llength ${col}]} {
                        set col 0
                    }
                }
            {^\x1b\[[0-9]*[A-D]} {
                    # move cursor
                    regexp -- {^\x1b\[0*([0-9]*)([A-D])} ${data} match cnt chr
                    if {![llength ${cnt}]} {
                        set cnt 1
                    }
                    switch -exact -- ${chr} {
                        A {
                                # moves the cursor up by 'cnt' rows
                                incr row -${cnt}
                            }
                        B {
                                # moves the cursor down by 'cnt' rows
                                incr row ${cnt}
                            }
                        C {
                                # moves the cursor forward by 'cnt' columns
                                incr col ${cnt}
                            }
                        D {
                                # moves the cursor backward by 'cnt' columns
                                incr col -${cnt}
                            }
                    }
                    if {(${col} < ${col_min}) ||(${col_max} < ${col})} {
                        if {${linewrap}} {
                            set col ${col_min}
                            incr row
                        } else {
                            puts stderr "line position\t'${match}' at (${row},${col})"
                        }
                    }
                    if {(${row} < ${row_min}) ||(${row_max} < ${row})} {
                        if {${pagewrap}} {
                            set row ${row_min}
                        } else {
                            puts stderr "page bounding\t'${match}' at (${row},${col})"
                        }
                    }
                }
            {^\x1b\[7[hl]} {
                    regexp -- {^\x1b\[7([hl])} ${data} match chr
                    # h/l: enable/disable line wrap
                    set linewrap [string match {h} ${chr}]
                }
            {^\x1b\[[;0-9]*m} {
                    # reset attributes
                    regexp -- {^\x1b\[[;0-9]*m} ${data} match
                    set terminal(${row},${col}) { }
                }
            {^\x1b\[[12]J} {
                    # erase line
                    regexp -- {^\x1b\[([12])J} ${data} match chr
                    switch -exact -- ${chr} {
                        2 {
                                # erases the screen with the background colour and moves the\
                                  cursor to home.
                                catch {unset terminal}
                                if {[string compare {raw} ${mode}]} {
                                    for {set row ${row_min}} {${row} < ${row_max}} {incr row} {
                                        for {set col ${col_min}} {${col} < ${col_max}} {incr col} {
                                            set terminal(${row},${col}) { }
                                        }
                                    }
                                }
                                set row 0
                                set col 0
                            }
                        1 {
                                # erases the screen from the current line up to the top of the\
                                  screen. 
                                foreach pos [array names *,*] {
                                    if {[lindex [split ${pos} {,}] 0] <= ${row}} {
                                        unset terminal(${pos})
                                    }
                                }
                            }
                        default {
                                # erases the screen from the current line down to the bottom of\
                                  the screen.
                                foreach pos [array names *,*] {
                                    if {${row} <= [lindex [split ${pos} {,}] 0]} {
                                        unset terminal(${pos})
                                    }
                                }
                            }
                    }
                }
            {^\x1b\[[12]K} {
                    # erase column
                    regexp -- {^\x1b\[([12])K} ${data} match chr
                    switch -exact -- ${chr} {
                        2 {
                                # erases the entire current line.
                                foreach pos [array names "[set row],*"] {
                                    set terminal(${pos}) { }
                                }
                            }
                        1 {
                                # erases from the current cursor position to the start of the\
                                  current line. 
                                foreach pos [array names "[set row],*"] {
                                    if {[lindex [split ${pos} {,}] end] <= ${col}} {
                                        set terminal(${pos}) { }
                                    }
                                }
                            }
                        default {
                                # erases from the current cursor position to the end of the\
                                  current line.
                                foreach pos [array names "[set row],*"] {
                                    if {${col} <= [lindex [split ${pos} {,}] end]} {
                                        set terminal(${pos}) { }
                                    }
                                }
                            }
                    }
                }
            {^\x1b\[\?*[0-9]*[A-Za-z]} {
                    regexp -- {^\x1b\[\?*[0-9;]*[A-Za-z]} ${data} match
                    puts stderr "ignore sequence\t'${match}'"
                }
            {^\x1b[\(\)][0-9A-Za-z]?} {
                    # set fonts
                    regexp -- {^\x1b[\(\)][0-9A-Za-z]?} ${data} match
                }
            {^\x1b} {
                    # raw escape
                    regexp -- {^\x1b} ${data} match
                }
            {^(.+?)\x1b} {
                    regexp -- {^(.+?)\x1b} ${data} UNUSED match
                    # handle special escape sequences
                    regsub -all -- {\\([\\\[\]])} ${match} {\1} raw_match
                    foreach ch [split ${raw_match} {}] {
                        if {${col_max} < ${col}} {
                            if {${linewrap}} {
                                set col ${col_min}
                                incr row
                            } else {
                                puts stderr "line overrun\t'[string range ${data} 0 20]'"
                                break
                            }
                        }
                        if {${row_max} < ${row}} {
                            if {${pagewrap}} {
                                set row ${row_min}
                            } else {
                                puts stderr "page overrun\t'[string range ${data} 0 20]'"
                                break
                            }
                        }
                        set terminal(${row},${col}) ${ch}
                        incr col
                    }
                }
            default {
                    set match ${data}
                }
        }
        set data [string range ${data} [string length ${match}] end]
    }
    set terminal(status) [list ${row} ${col} ${linewrap}]
    set res {}
    set row_last [lindex [split [lsort -dictionary [array names terminal *,*]] {,}] 0]
    foreach pos [lsort -dictionary [array names terminal *,*]] {
        set row [lindex [split ${pos} {,}] 0]
        if {${listmode} &&(${row} != ${row_last})} {
            append res "\n"
            set row_last ${row}
        }
        append res $terminal(${pos})
    }
    if {${listmode}} {
        set res [split ${res} "\n"]
    }

    return ${res}
 }

By setting the bounding you are able to change to different sizes of terminals. Using mode will allow to switch between current changes (mode: raw, diff, delta) and new screen (mode: reset) or keeping previous screen (mode: default) making changes there. Of cause erasing screen in last mode will have the same effect as reset mode.


Category Example Category Expect