| 3 | 1 #!/bin/sh | 
|  | 2 | 
|  | 3 # Convert ANSI (terminal) colours and attributes to HTML | 
|  | 4 | 
|  | 5 # Author: | 
|  | 6 #    http://www.pixelbeat.org/docs/terminal_colours/ | 
|  | 7 # Examples: | 
|  | 8 #    ls -l --color=always | ansi2html.sh > ls.html | 
|  | 9 #    git show --color | ansi2html.sh > last_change.html | 
|  | 10 #    Generally one can use the `script` util to capture full terminal output. | 
|  | 11 # Changes: | 
|  | 12 #    V0.1, 24 Apr 2008, Initial release | 
|  | 13 #    V0.2, 01 Jan 2009, Phil Harnish <philharnish@gmail.com> | 
|  | 14 #                         Support `git diff --color` output by | 
|  | 15 #                         matching ANSI codes that specify only | 
|  | 16 #                         bold or background colour. | 
|  | 17 #                       P@draigBrady.com | 
|  | 18 #                         Support `ls --color` output by stripping | 
|  | 19 #                         redundant leading 0s from ANSI codes. | 
|  | 20 #                         Support `grep --color=always` by stripping | 
|  | 21 #                         unhandled ANSI codes (specifically ^[[K). | 
|  | 22 #    V0.3, 20 Mar 2009, http://eexpress.blog.ubuntu.org.cn/ | 
|  | 23 #                         Remove cat -v usage which mangled non ascii input. | 
|  | 24 #                         Cleanup regular expressions used. | 
|  | 25 #                         Support other attributes like reverse, ... | 
|  | 26 #                       P@draigBrady.com | 
|  | 27 #                         Correctly nest <span> tags (even across lines). | 
|  | 28 #                         Add a command line option to use a dark background. | 
|  | 29 #                         Strip more terminal control codes. | 
|  | 30 #    V0.4, 17 Sep 2009, P@draigBrady.com | 
|  | 31 #                         Handle codes with combined attributes and color. | 
|  | 32 #                         Handle isolated <bold> attributes with css. | 
|  | 33 #                         Strip more terminal control codes. | 
|  | 34 #    V0.12, 12 Jul 2011 | 
|  | 35 #      http://github.com/pixelb/scripts/commits/master/scripts/ansi2html.sh | 
|  | 36 | 
|  | 37 if [ "$1" = "--version" ]; then | 
|  | 38     echo "0.12" && exit | 
|  | 39 fi | 
|  | 40 | 
|  | 41 if [ "$1" = "--help" ]; then | 
|  | 42     echo "This utility converts ANSI codes in data passed to stdin" >&2 | 
|  | 43     echo "It has 2 optional parameters:" >&2 | 
|  | 44     echo "   --bg=dark --palette=linux|solarized|tango|xterm" >&2 | 
|  | 45     echo "E.g.: ls -l --color=always | ansi2html.sh --bg=dark > ls.html" >&2 | 
|  | 46     exit | 
|  | 47 fi | 
|  | 48 | 
|  | 49 [ "$1" = "--bg=dark" ] && { dark_bg=yes; shift; } | 
|  | 50 | 
|  | 51 if [ "$1" = "--palette=solarized" ]; then | 
|  | 52    # See http://ethanschoonover.com/solarized | 
|  | 53    P0=073642;  P1=D30102;  P2=859900;  P3=B58900; | 
|  | 54    P4=268BD2;  P5=D33682;  P6=2AA198;  P7=EEE8D5; | 
|  | 55    P8=002B36;  P9=CB4B16; P10=586E75; P11=657B83; | 
|  | 56   P12=839496; P13=6C71C4; P14=93A1A1; P15=FDF6E3; | 
|  | 57   shift; | 
|  | 58 elif [ "$1" = "--palette=solarized-xterm" ]; then | 
|  | 59    # Above mapped onto the xterm 256 color palette | 
|  | 60    P0=262626;  P1=AF0000;  P2=5F8700;  P3=AF8700; | 
|  | 61    P4=0087FF;  P5=AF005F;  P6=00AFAF;  P7=E4E4E4; | 
|  | 62    P8=1C1C1C;  P9=D75F00; P10=585858; P11=626262; | 
|  | 63   P12=808080; P13=5F5FAF; P14=8A8A8A; P15=FFFFD7; | 
|  | 64   shift; | 
|  | 65 elif [ "$1" = "--palette=tango" ]; then | 
|  | 66    # Gnome default | 
|  | 67    P0=000000;  P1=CC0000;  P2=4E9A06;  P3=C4A000; | 
|  | 68    P4=3465A4;  P5=75507B;  P6=06989A;  P7=D3D7CF; | 
|  | 69    P8=555753;  P9=EF2929; P10=8AE234; P11=FCE94F; | 
|  | 70   P12=729FCF; P13=AD7FA8; P14=34E2E2; P15=EEEEEC; | 
|  | 71   shift; | 
|  | 72 elif [ "$1" = "--palette=xterm" ]; then | 
|  | 73    P0=000000;  P1=CD0000;  P2=00CD00;  P3=CDCD00; | 
|  | 74    P4=0000EE;  P5=CD00CD;  P6=00CDCD;  P7=E5E5E5; | 
|  | 75    P8=7F7F7F;  P9=FF0000; P10=00FF00; P11=FFFF00; | 
|  | 76   P12=5C5CFF; P13=FF00FF; P14=00FFFF; P15=FFFFFF; | 
|  | 77   shift; | 
|  | 78 else # linux console | 
|  | 79    P0=000000;  P1=AA0000;  P2=00AA00;  P3=AA5500; | 
|  | 80    P4=0000AA;  P5=AA00AA;  P6=00AAAA;  P7=AAAAAA; | 
|  | 81    P8=555555;  P9=FF5555; P10=55FF55; P11=FFFF55; | 
|  | 82   P12=5555FF; P13=FF55FF; P14=55FFFF; P15=FFFFFF; | 
|  | 83   [ "$1" = "--palette=linux" ] && shift | 
|  | 84 fi | 
|  | 85 | 
|  | 86 [ "$1" = "--bg=dark" ] && { dark_bg=yes; shift; } | 
|  | 87 | 
|  | 88 echo -n "<html> | 
|  | 89 <head> | 
|  | 90 <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/> | 
|  | 91 <style type=\"text/css\"> | 
|  | 92 .ef0,.f0 { color: #$P0; } .eb0,.b0 { background-color: #$P0; } | 
|  | 93 .ef1,.f1 { color: #$P1; } .eb1,.b1 { background-color: #$P1; } | 
|  | 94 .ef2,.f2 { color: #$P2; } .eb2,.b2 { background-color: #$P2; } | 
|  | 95 .ef3,.f3 { color: #$P3; } .eb3,.b3 { background-color: #$P3; } | 
|  | 96 .ef4,.f4 { color: #$P4; } .eb4,.b4 { background-color: #$P4; } | 
|  | 97 .ef5,.f5 { color: #$P5; } .eb5,.b5 { background-color: #$P5; } | 
|  | 98 .ef6,.f6 { color: #$P6; } .eb6,.b6 { background-color: #$P6; } | 
|  | 99 .ef7,.f7 { color: #$P7; } .eb7,.b7 { background-color: #$P7; } | 
|  | 100 .ef8, .f0 > .bold,.bold > .f0 { color: #$P8; font-weight: normal; } | 
|  | 101 .ef9, .f1 > .bold,.bold > .f1 { color: #$P9; font-weight: normal; } | 
|  | 102 .ef10,.f2 > .bold,.bold > .f2 { color: #$P10; font-weight: normal; } | 
|  | 103 .ef11,.f3 > .bold,.bold > .f3 { color: #$P11; font-weight: normal; } | 
|  | 104 .ef12,.f4 > .bold,.bold > .f4 { color: #$P12; font-weight: normal; } | 
|  | 105 .ef13,.f5 > .bold,.bold > .f5 { color: #$P13; font-weight: normal; } | 
|  | 106 .ef14,.f6 > .bold,.bold > .f6 { color: #$P14; font-weight: normal; } | 
|  | 107 .ef15,.f7 > .bold,.bold > .f7 { color: #$P15; font-weight: normal; } | 
|  | 108 .eb8  { background-color: #$P8; } | 
|  | 109 .eb9  { background-color: #$P9; } | 
|  | 110 .eb10 { background-color: #$P10; } | 
|  | 111 .eb11 { background-color: #$P11; } | 
|  | 112 .eb12 { background-color: #$P12; } | 
|  | 113 .eb13 { background-color: #$P13; } | 
|  | 114 .eb14 { background-color: #$P14; } | 
|  | 115 .eb15 { background-color: #$P15; } | 
|  | 116 " | 
|  | 117 | 
|  | 118 # The default xterm 256 colour palette | 
|  | 119 for red in $(seq 0 5); do | 
|  | 120   for green in $(seq 0 5); do | 
|  | 121     for blue in $(seq 0 5); do | 
|  | 122         c=$((16 + ($red * 36) + ($green * 6) + $blue)) | 
|  | 123         r=$((($red * 40 + 55) * ($red > 0))) | 
|  | 124         g=$((($green * 40 + 55) * ($green > 0))) | 
|  | 125         b=$((($blue * 40 + 55) * ($blue > 0))) | 
|  | 126         printf ".ef%d { color: #%2.2x%2.2x%2.2x; } " $c $r $g $b | 
|  | 127         printf ".eb%d { background-color: #%2.2x%2.2x%2.2x; }\n" $c $r $g $b | 
|  | 128     done | 
|  | 129   done | 
|  | 130 done | 
|  | 131 for gray in $(seq 0 23); do | 
|  | 132   c=$(($gray+232)) | 
|  | 133   l=$(($gray*10 + 8)) | 
|  | 134   printf ".ef%d { color: #%2.2x%2.2x%2.2x; } " $c $l $l $l | 
|  | 135   printf ".eb%d { background-color: #%2.2x%2.2x%2.2x; }\n" $c $l $l $l | 
|  | 136 done | 
|  | 137 | 
|  | 138 echo -n ' | 
|  | 139 .f9 { color: '`[ "$dark_bg" ] && echo "#$P7;" || echo "#$P0;"`' } | 
|  | 140 .b9 { background-color: #'`[ "$dark_bg" ] && echo $P0 || echo $P15`'; } | 
|  | 141 .f9 > .bold,.bold > .f9, body.f9 > pre > .bold { | 
|  | 142   /* Bold is heavy black on white, or bright white | 
|  | 143      depending on the default background */ | 
|  | 144   color: '`[ "$dark_bg" ] && echo "#$P15;" || echo "#$P0;"`' | 
|  | 145   font-weight: '`[ "$dark_bg" ] && echo 'normal;' || echo 'bold;'`' | 
|  | 146 } | 
|  | 147 .reverse { | 
|  | 148   /* CSS doesnt support swapping fg and bg colours unfortunately, | 
|  | 149      so just hardcode something that will look OK on all backgrounds. */ | 
|  | 150   '"color: #$P0; background-color: #$P7;"' | 
|  | 151 } | 
|  | 152 .underline { text-decoration: underline; } | 
|  | 153 .line-through { text-decoration: line-through; } | 
|  | 154 .blink { text-decoration: blink; } | 
|  | 155 | 
|  | 156 </style> | 
|  | 157 </head> | 
|  | 158 | 
|  | 159 <body class="f9 b9"> | 
|  | 160 <pre> | 
|  | 161 ' | 
|  | 162 | 
|  | 163 p='\x1b\['        #shortcut to match escape codes | 
|  | 164 P="\(^[^°]*\)¡$p" #expression to match prepended codes below | 
|  | 165 | 
|  | 166 # Handle various xterm control sequences. | 
|  | 167 # See /usr/share/doc/xterm-*/ctlseqs.txt | 
|  | 168 sed " | 
|  | 169 s#\x1b[^\x1b]*\x1b\\\##g  # strip anything between \e and ST | 
|  | 170 s#\x1b][0-9]*;[^\a]*\a##g # strip any OSC (xterm title etc.) | 
|  | 171 | 
|  | 172 #handle carriage returns | 
|  | 173 s#^.*\r\{1,\}\([^$]\)#\1# | 
|  | 174 s#\r\$## # strip trailing \r | 
|  | 175 | 
|  | 176 # strip other non SGR escape sequences | 
|  | 177 s#[\x07]##g | 
|  | 178 s#\x1b[]>=\][0-9;]*##g | 
|  | 179 s#\x1bP+.\{5\}##g | 
|  | 180 s#${p}[0-9;?]*[^0-9;?m]##g | 
|  | 181 | 
|  | 182 #remove backspace chars and what they're backspacing over | 
|  | 183 :rm_bs | 
|  | 184 s#[^\x08]\x08##g; t rm_bs | 
|  | 185 " | | 
|  | 186 | 
|  | 187 # Normalize the input before transformation | 
|  | 188 sed " | 
|  | 189 # escape HTML | 
|  | 190 s#\&#\&#g; s#>#\>#g; s#<#\<#g; s#\"#\"#g | 
|  | 191 | 
|  | 192 # normalize SGR codes a little | 
|  | 193 | 
|  | 194 # split 256 colors out and mark so that they're not | 
|  | 195 # recognised by the following 'split combined' line | 
|  | 196 :e | 
|  | 197 s#${p}\([0-9;]\{1,\}\);\([34]8;5;[0-9]\{1,3\}\)m#${p}\1m${p}¬\2m#g; t e | 
|  | 198 s#${p}\([34]8;5;[0-9]\{1,3\}\)m#${p}¬\1m#g; | 
|  | 199 | 
|  | 200 :c | 
|  | 201 s#${p}\([0-9]\{1,\}\);\([0-9;]\{1,\}\)m#${p}\1m${p}\2m#g; t c   # split combined | 
|  | 202 s#${p}0\([0-7]\)#${p}\1#g                                 #strip leading 0 | 
|  | 203 s#${p}1m\(\(${p}[4579]m\)*\)#\1${p}1m#g                   #bold last (with clr) | 
|  | 204 s#${p}m#${p}0m#g                                          #add leading 0 to norm | 
|  | 205 | 
|  | 206 # undo any 256 color marking | 
|  | 207 s#${p}¬\([34]8;5;[0-9]\{1,3\}\)m#${p}\1m#g; | 
|  | 208 | 
|  | 209 # map 16 color codes to color + bold | 
|  | 210 s#${p}9\([0-7]\)m#${p}3\1m${p}1m#g; | 
|  | 211 s#${p}10\([0-7]\)m#${p}4\1m${p}1m#g; | 
|  | 212 | 
|  | 213 # change 'reset' code to a single char, and prepend a single char to | 
|  | 214 # other codes so that we can easily do negative matching, as sed | 
|  | 215 # does not support look behind expressions etc. | 
|  | 216 s#°#\°#g; s#${p}0m#°#g | 
|  | 217 s#¡#\¡#g; s#${p}[0-9;]*m#¡&#g | 
|  | 218 " | | 
|  | 219 | 
|  | 220 # Convert SGR sequences to HTML | 
|  | 221 sed " | 
|  | 222 :ansi_to_span # replace ANSI codes with CSS classes | 
|  | 223 t ansi_to_span # hack so t commands below only apply to preceeding s cmd | 
|  | 224 | 
|  | 225 /^[^¡]*°/ { b span_end } # replace 'reset code' if no preceeding code | 
|  | 226 | 
|  | 227 # common combinations to minimise html (optional) | 
|  | 228 s#${P}3\([0-7]\)m¡${p}4\([0-7]\)m#\1<span class=\"f\2 b\3\">#;t span_count | 
|  | 229 s#${P}4\([0-7]\)m¡${p}3\([0-7]\)m#\1<span class=\"f\3 b\2\">#;t span_count | 
|  | 230 | 
|  | 231 s#${P}1m#\1<span class=\"bold\">#;                            t span_count | 
|  | 232 s#${P}4m#\1<span class=\"underline\">#;                       t span_count | 
|  | 233 s#${P}5m#\1<span class=\"blink\">#;                           t span_count | 
|  | 234 s#${P}7m#\1<span class=\"reverse\">#;                         t span_count | 
|  | 235 s#${P}9m#\1<span class=\"line-through\">#;                    t span_count | 
|  | 236 s#${P}3\([0-9]\)m#\1<span class=\"f\2\">#;                    t span_count | 
|  | 237 s#${P}4\([0-9]\)m#\1<span class=\"b\2\">#;                    t span_count | 
|  | 238 | 
|  | 239 s#${P}38;5;\([0-9]\{1,3\}\)m#\1<span class=\"ef\2\">#;        t span_count | 
|  | 240 s#${P}48;5;\([0-9]\{1,3\}\)m#\1<span class=\"eb\2\">#;        t span_count | 
|  | 241 | 
|  | 242 s#${P}[0-9;]*m#\1#g; t ansi_to_span # strip unhandled codes | 
|  | 243 | 
|  | 244 b # next line of input | 
|  | 245 | 
|  | 246 # add a corresponding span end flag | 
|  | 247 :span_count | 
|  | 248 x; s/^/s/; x | 
|  | 249 b ansi_to_span | 
|  | 250 | 
|  | 251 # replace 'reset code' with correct number of </span> tags | 
|  | 252 :span_end | 
|  | 253 x | 
|  | 254 /^s/ { | 
|  | 255   s/^.// | 
|  | 256   x | 
|  | 257   s#°#</span>°# | 
|  | 258   b span_end | 
|  | 259 } | 
|  | 260 x | 
|  | 261 s#°## | 
|  | 262 b ansi_to_span | 
|  | 263 " | | 
|  | 264 | 
|  | 265 # Convert alternative character set | 
|  | 266 # Note we convert here, as if we do at start we have to worry about avoiding | 
|  | 267 # conversion of SGR codes etc., whereas doing here we only have to | 
|  | 268 # avoid conversions of stuff between &...; or <...> | 
|  | 269 # | 
|  | 270 # Note we could use sed to do this based around: | 
|  | 271 #   sed 'y/abcdefghijklmnopqrstuvwxyz{}`~/▒␉␌␍␊°±␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π£◆·/' | 
|  | 272 # However that would be very awkward as we need to only conv some input. | 
|  | 273 # The basic scheme that we do in the python script below is: | 
|  | 274 #  1. enable transliterate once ¡ char seen | 
|  | 275 #  2. disable once µ char seen (may be on diff line to ¡) | 
|  | 276 #  3. never transliterate between &; or <> chars | 
|  | 277 sed " | 
|  | 278 # change 'smacs' and 'rmacs' to a single char so that we can easily do | 
|  | 279 # negative matching, as sed does not support look behind expressions etc. | 
|  | 280 # Note we don't use ° like above as that's part of the alternate charset. | 
|  | 281 s#\x1b(0#¡#g; | 
|  | 282 s#µ#\µ#g; s#\x1b(B#µ#g | 
|  | 283 " | | 
|  | 284 ( | 
|  | 285 python -c " | 
|  | 286 # vim:fileencoding=utf8 | 
|  | 287 | 
|  | 288 import sys | 
|  | 289 import locale | 
|  | 290 encoding=locale.getpreferredencoding() | 
|  | 291 | 
|  | 292 old='abcdefghijklmnopqrstuvwxyz{}\`~' | 
|  | 293 new='▒␉␌␍␊°±␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π£◆·' | 
|  | 294 new=unicode(new, 'utf-8') | 
|  | 295 table=range(128) | 
|  | 296 for o,n in zip(old, new): table[ord(o)]=n | 
|  | 297 | 
|  | 298 (STANDARD, ALTERNATIVE, HTML_TAG, HTML_ENTITY) = (0, 1, 2, 3) | 
|  | 299 | 
|  | 300 state = STANDARD | 
|  | 301 last_mode = STANDARD | 
|  | 302 for c in unicode(sys.stdin.read(), encoding): | 
|  | 303   if state == HTML_TAG: | 
|  | 304     if c == '>': | 
|  | 305       state = last_mode | 
|  | 306   elif state == HTML_ENTITY: | 
|  | 307     if c == ';': | 
|  | 308       state = last_mode | 
|  | 309   else: | 
|  | 310     if c == '<': | 
|  | 311       state = HTML_TAG | 
|  | 312     elif c == '&': | 
|  | 313       state = HTML_ENTITY | 
|  | 314     elif c == u'¡' and state == STANDARD: | 
|  | 315       state = ALTERNATIVE | 
|  | 316       last_mode = ALTERNATIVE | 
|  | 317       continue | 
|  | 318     elif c == u'µ' and state == ALTERNATIVE: | 
|  | 319       state = STANDARD | 
|  | 320       last_mode = STANDARD | 
|  | 321       continue | 
|  | 322     elif state == ALTERNATIVE: | 
|  | 323       c = c.translate(table) | 
|  | 324   sys.stdout.write(c.encode(encoding)) | 
|  | 325 " 2>/dev/null || | 
|  | 326 sed 's/[¡µ]//g' # just strip aternative flag chars | 
|  | 327 ) | 
|  | 328 | 
|  | 329 echo "</pre> | 
|  | 330 </body> | 
|  | 331 </html>" |