134
Here is a snippet that includes my string.
'ls\r\n\x1b[00m\x1b[01;31mexamplefile.zip\x1b[00m\r\n\x1b[01;31m'

The string was returned from an SSH command that I executed. I can't use the string in its current state because it contains ANSI standardized escape sequences. How can I programmatically remove the escape sequences so that the only part of the string remaining is 'examplefile.zip'.

JΛYDΞV
  • 8,532
  • 3
  • 51
  • 77
SpartaSixZero
  • 2,183
  • 5
  • 27
  • 46

8 Answers8

241

Delete them with a regular expression:

import re

# 7-bit C1 ANSI sequences
ansi_escape = re.compile(r'''
    \x1B  # ESC
    (?:   # 7-bit C1 Fe (except CSI)
        [@-Z\\-_]
    |     # or [ for CSI, followed by a control sequence
        \[
        [0-?]*  # Parameter bytes
        [ -/]*  # Intermediate bytes
        [@-~]   # Final byte
    )
''', re.VERBOSE)
result = ansi_escape.sub('', sometext)

or, without the VERBOSE flag, in condensed form:

ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
result = ansi_escape.sub('', sometext)

Demo:

>>> import re
>>> ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
>>> sometext = 'ls\r\n\x1b[00m\x1b[01;31mexamplefile.zip\x1b[00m\r\n\x1b[01;31m'
>>> ansi_escape.sub('', sometext)
'ls\r\nexamplefile.zip\r\n'

The above regular expression covers all 7-bit ANSI C1 escape sequences, but not the 8-bit C1 escape sequence openers. The latter are never used in today's UTF-8 world where the same range of bytes have a different meaning.

If you do need to cover the 8-bit codes too (and are then, presumably, working with bytes values) then the regular expression becomes a bytes pattern like this:

# 7-bit and 8-bit C1 ANSI sequences
ansi_escape_8bit = re.compile(br'''
    (?: # either 7-bit C1, two bytes, ESC Fe (omitting CSI)
        \x1B
        [@-Z\\-_]
    |   # or a single 8-bit byte Fe (omitting CSI)
        [\x80-\x9A\x9C-\x9F]
    |   # or CSI + control codes
        (?: # 7-bit CSI, ESC [ 
            \x1B\[
        |   # 8-bit CSI, 9B
            \x9B
        )
        [0-?]*  # Parameter bytes
        [ -/]*  # Intermediate bytes
        [@-~]   # Final byte
    )
''', re.VERBOSE)
result = ansi_escape_8bit.sub(b'', somebytesvalue)

which can be condensed down to

# 7-bit and 8-bit C1 ANSI sequences
ansi_escape_8bit = re.compile(
    br'(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])'
)
result = ansi_escape_8bit.sub(b'', somebytesvalue)

For more information, see:

The example you gave contains 4 CSI (Control Sequence Introducer) codes, as marked by the \x1B[ or ESC [ opening bytes, and each contains a SGR (Select Graphic Rendition) code, because they each end in m. The parameters (separated by ; semicolons) in between those tell your terminal what graphic rendition attributes to use. So for each \x1B[....m sequence, the 3 codes that are used are:

  • 0 (or 00 in this example): reset, disable all attributes
  • 1 (or 01 in the example): bold
  • 31: red (foreground)

However, there is more to ANSI than just CSI SGR codes. With CSI alone you can also control the cursor, clear lines or the whole display, or scroll (provided the terminal supports this of course). And beyond CSI, there are codes to select alternative fonts (SS2 and SS3), to send 'private messages' (think passwords), to communicate with the terminal (DCS), the OS (OSC), or the application itself (APC, a way for applications to piggy-back custom control codes on to the communication stream), and further codes to help define strings (SOS, Start of String, ST String Terminator) or to reset everything back to a base state (RIS). The above regexes cover all of these.

Note that the above regex only removes the ANSI C1 codes, however, and not any additional data that those codes may be marking up (such as the strings sent between an OSC opener and the terminating ST code). Removing those would require additional work outside the scope of this answer.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Does your solution support all Fe Escape sequences? I am testing it with Java. CSI seems to work, but OSC seems not. This is my example: `var input = "some text \u001B]0;this is the window title BEL"; var output = input.replaceAll("\u001B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])", "ZZZ"); System.out.println(output);` Output is `some text ZZZ0;this is the window title BEL`. – Pavel_K Mar 15 '22 at 15:18
  • @Pavel_K see the last paragraph in my answer. – Martijn Pieters Mar 15 '22 at 22:11
53

The accepted answer only takes into account ANSI Standardized escape sequences that are formatted to alter foreground colors & text style. Many sequences do not end in 'm', such as: cursor positioning, erasing, and scroll regions. The pattern bellow attempts to cover all cases beyond setting foreground color and text-style.


Below is the regular expression for ANSI standardized control sequences:
/(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]/


Additional References:
JΛYDΞV
  • 8,532
  • 3
  • 51
  • 77
Jeff
  • 2,095
  • 25
  • 18
  • 1
    It misses OSC (both beginning and end). – Thomas Dickey Jul 29 '16 at 21:59
  • 1
    OSC is in ECMA-48 sec. 5.6 - what is the point of bring that up here? – Jeff Aug 04 '16 at 01:36
  • 4
    OSC is an "ANSI escape sequence", is frequently used, and would begin with a different pattern. Your answer is **incomplete**. – Thomas Dickey Aug 04 '16 at 07:57
  • This doesn't work for color codes produced by `bluetoothctl`, example: `\x1b[0;94m`. Making the expression case insensitive or replacing `1B` with `1b` in the pattern made no difference. I'm using Python and the line `re.compile(r'/(\x9b|\x1b\[)[0-?]*[ -\/]*[@-~]/', re.I)`. Then I'm doing `pattern.sub("", my_string)` which doesn't accomplish anything. Am I doing something wrong? – Hubro Dec 30 '16 at 08:11
  • (I was too slow to edit my previous comment). I assume your pattern is using features not available in Python's `re` module? – Hubro Dec 30 '16 at 08:18
  • NOPE, my bad, just turns out you gotta remove the `/` delimiters from the string before giving it to Python. – Hubro Dec 30 '16 at 08:19
  • `(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]` will work too: `[ -\/]` escaping is redundant. – Rodrigo Oliveira Oct 01 '17 at 19:10
  • 3
    I see three issues with this answer: 1) `/.../` is *not Python syntax*, but rather syntax you'd use in VI or Perl or awk. 2) the `\x9B` opener (for CSI codes) is incompatible with UTF-8 and so now rarely used, and ESC `[` is preferred and 3) your pattern only covers CSI codes, not the whole range of ANSI escapes (which not only includes OSC, which Thomas Dickly mentions, but SS2, SS3, DCS, ST, OSC, SOS, PM, APC and RIS as well)! – Martijn Pieters Jul 24 '19 at 16:58
  • Is this answer still relevent now that the main answer has been updated? – fuzzyTew Mar 25 '22 at 07:35
  • It consist of non-global RegExp argument – Abhay Jan 12 '23 at 14:22
43

Function

Based on Martijn Pieters♦'s answer with Jeff's regexp.

def escape_ansi(line):
    ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
    return ansi_escape.sub('', line)

Test

def test_remove_ansi_escape_sequence(self):
    line = '\t\u001b[0;35mBlabla\u001b[0m                                  \u001b[0;36m172.18.0.2\u001b[0m'

    escaped_line = escape_ansi(line)

    self.assertEqual(escaped_line, '\tBlabla                                  172.18.0.2')

Testing

If you want to run it by yourself, use python3 (better unicode support, blablabla). Here is how the test file should be:

import unittest
import re

def escape_ansi(line):
    …

class TestStringMethods(unittest.TestCase):
    def test_remove_ansi_escape_sequence(self):
    …

if __name__ == '__main__':
    unittest.main()
Édouard Lopez
  • 40,270
  • 28
  • 126
  • 178
10

The suggested regex didn't do the trick for me so I created one of my own. The following is a python regex that I created based on the spec found here

ansi_regex = r'\x1b(' \
             r'(\[\??\d+[hl])|' \
             r'([=<>a-kzNM78])|' \
             r'([\(\)][a-b0-2])|' \
             r'(\[\d{0,2}[ma-dgkjqi])|' \
             r'(\[\d+;\d+[hfy]?)|' \
             r'(\[;?[hf])|' \
             r'(#[3-68])|' \
             r'([01356]n)|' \
             r'(O[mlnp-z]?)|' \
             r'(/Z)|' \
             r'(\d+)|' \
             r'(\[\?\d;\d0c)|' \
             r'(\d;\dR))'
ansi_escape = re.compile(ansi_regex, flags=re.IGNORECASE)

I tested my regex on the following snippet (basically a copy paste from the ascii-table.com page)

\x1b[20h    Set
\x1b[?1h    Set
\x1b[?3h    Set
\x1b[?4h    Set
\x1b[?5h    Set
\x1b[?6h    Set
\x1b[?7h    Set
\x1b[?8h    Set
\x1b[?9h    Set
\x1b[20l    Set
\x1b[?1l    Set
\x1b[?2l    Set
\x1b[?3l    Set
\x1b[?4l    Set
\x1b[?5l    Set
\x1b[?6l    Set
\x1b[?7l    Reset
\x1b[?8l    Reset
\x1b[?9l    Reset
\x1b=   Set
\x1b>   Set
\x1b(A  Set
\x1b)A  Set
\x1b(B  Set
\x1b)B  Set
\x1b(0  Set
\x1b)0  Set
\x1b(1  Set
\x1b)1  Set
\x1b(2  Set
\x1b)2  Set
\x1bN   Set
\x1bO   Set
\x1b[m  Turn
\x1b[0m Turn
\x1b[1m Turn
\x1b[2m Turn
\x1b[4m Turn
\x1b[5m Turn
\x1b[7m Turn
\x1b[8m Turn
\x1b[1;2    Set
\x1b[1A Move
\x1b[2B Move
\x1b[3C Move
\x1b[4D Move
\x1b[H  Move
\x1b[;H Move
\x1b[4;3H   Move
\x1b[f  Move
\x1b[;f Move
\x1b[1;2    Move
\x1bD   Move/scroll
\x1bM   Move/scroll
\x1bE   Move
\x1b7   Save
\x1b8   Restore
\x1bH   Set
\x1b[g  Clear
\x1b[0g Clear
\x1b[3g Clear
\x1b#3  Double-height
\x1b#4  Double-height
\x1b#5  Single
\x1b#6  Double
\x1b[K  Clear
\x1b[0K Clear
\x1b[1K Clear
\x1b[2K Clear
\x1b[J  Clear
\x1b[0J Clear
\x1b[1J Clear
\x1b[2J Clear
\x1b5n  Device
\x1b0n  Response:
\x1b3n  Response:
\x1b6n  Get
\x1b[c  Identify
\x1b[0c Identify
\x1b[?1;20c Response:
\x1bc   Reset
\x1b#8  Screen
\x1b[2;1y   Confidence
\x1b[2;2y   Confidence
\x1b[2;9y   Repeat
\x1b[2;10y  Repeat
\x1b[0q Turn
\x1b[1q Turn
\x1b[2q Turn
\x1b[3q Turn
\x1b[4q Turn
\x1b<   Enter/exit
\x1b=   Enter
\x1b>   Exit
\x1bF   Use
\x1bG   Use
\x1bA   Move
\x1bB   Move
\x1bC   Move
\x1bD   Move
\x1bH   Move
\x1b12  Move
\x1bI  
\x1bK  
\x1bJ  
\x1bZ  
\x1b/Z 
\x1bOP 
\x1bOQ 
\x1bOR 
\x1bOS 
\x1bA  
\x1bB  
\x1bC  
\x1bD  
\x1bOp 
\x1bOq 
\x1bOr 
\x1bOs 
\x1bOt 
\x1bOu 
\x1bOv 
\x1bOw 
\x1bOx 
\x1bOy 
\x1bOm 
\x1bOl 
\x1bOn 
\x1bOM 
\x1b[i 
\x1b[1i
\x1b[4i
\x1b[5i

Hopefully this will help others :)

kfir
  • 331
  • 4
  • 12
  • That spec is also not complete, the standard allows for a lot of expansion that VT100 didn't use but other terminals do, and your regex is overly verbose for the purpose. – Martijn Pieters Jul 24 '19 at 17:00
  • Your pattern has several weird discrepancies as well; ESC-`O` (SS3) 'shifts' the terminal into an alternate font mode, and the next byte is interpreted in that specific mode. The possible values in that mode are not limited to `m`, `n`, `l`, or `p` through `z`. I'd not even strip the byte following SS3. SS2 is basically the same functionality (just a different font), but your regex doesn't pull in the next byte. – Martijn Pieters Jul 24 '19 at 19:20
  • 2
    Last but not least, your regex fails to actually *remove the full ANSI codes in the question example*, as it leaves behind the `m` final byte. – Martijn Pieters Jul 24 '19 at 19:22
2

none of the regex solutions worked in my case with OSC sequences (\x1b])

to actually render the visible output, you will need a terminal emulator like pyte

#! /usr/bin/env python3

import pyte # terminal emulator: render terminal output to visible characters

pyte_screen = pyte.Screen(80, 24)
pyte_stream = pyte.ByteStream(pyte_screen)

bytes_ = b''.join([
  b'$ cowsay hello\r\n', b'\x1b[?2004l', b'\r', b' _______\r\n',
  b'< hello >\r\n', b' -------\r\n', b'        \\   ^__^\r\n',
  b'         \\  (oo)\\_______\r\n', b'            (__)\\       )\\/\\\r\n',
  b'                ||----w |\r\n', b'                ||     ||\r\n',
  b'\x1b]0;user@laptop1:/tmp\x1b\\', b'\x1b]7;file://laptop1/tmp\x1b\\', b'\x1b[?2004h$ ',
])
pyte_stream.feed(bytes_)

# pyte_screen.display always has 80x24 characters, padded with whitespace
# -> use rstrip to remove trailing whitespace from all lines
text = ("".join([line.rstrip() + "\n" for line in pyte_screen.display])).strip() + "\n"
print("text", text)

print("cursor", pyte_screen.cursor.y, pyte_screen.cursor.x)
print("title", pyte_screen.title)
milahu
  • 2,447
  • 1
  • 18
  • 25
0

If it helps future Stack Overflowers, I was using the crayons library to give my Python output a bit more visual impact, which is advantageous as it works on both Windows and Linux platforms. However I was both displaying onscreen as well as appending to log files, and the escape sequences were impacting legibility of the log files, so wanted to strip them out. However the escape sequences inserted by crayons produced an error:

expected string or bytes-like object

The solution was to cast the parameter to a string, so only a tiny modification to the commonly accepted answer was needed:

def escape_ansi(line):
    ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]')
    return ansi_escape.sub('', str(line))
Rory
  • 167
  • 1
  • 7
  • That's not really the same problem though. There are *loads* of different libraries that might produce custom objects that wrap a string, we don't need answers here for every variant that needs conversion to string before a regex works on them. – Martijn Pieters Jul 24 '19 at 19:24
  • Thats exactly what I was searching for. If you do sub-process control you get bytes; `out.decode("utf-8")` will clash with ansi control codes raising: `UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf7 in position 13894: invalid start byte` and the regex won't work on the bytes object. – dothebart May 19 '21 at 08:31
-3

if you want to remove the \r\n bit, you can pass the string through this function (written by sarnold):

def stripEscape(string):
    """ Removes all escape sequences from the input string """
    delete = ""
    i=1
    while (i<0x20):
        delete += chr(i)
        i += 1
    t = string.translate(None, delete)
    return t

Careful though, this will lump together the text in front and behind the escape sequences. So, using Martijn's filtered string 'ls\r\nexamplefile.zip\r\n', you will get lsexamplefile.zip. Note the ls in front of the desired filename.

I would use the stripEscape function first to remove the escape sequences, then pass the output to Martijn's regular expression, which would avoid concatenating the unwanted bit.

Community
  • 1
  • 1
Neodied
  • 119
  • 1
  • 8
  • The question doesn't ask for whitespace to be removed, only **ANSI** escape codes. Your translation of sarnold's `string.translate()` option is not exactly idiomatic either (why use `while` when `for` over `xrange()` would do, e.g. `''.join([chr(i) for i in range(0x20)])`), and not applicable to Python 3 (where you could just use `dict.fromkeys(range(0x20)))` as the `string.translate()` map). – Martijn Pieters Jul 25 '19 at 11:41
-4

For 2020 with python 3.5 it as easy as string.encode().decode('ascii')

ascii_string = 'ls\r\n\x1b[00m\x1b[01;31mexamplefile.zip\x1b[00m\r\n\x1b[01;31m'
decoded_string = ascii_string.encode().decode('ascii')
print(decoded_string) 

>ls
>examplefile.zip
>
V.Ignatov
  • 23
  • 4
  • 3
    This code doesn't do anything: `repr(decoded_string)` yelds `"'ls\\r\\n\\x1b[00m\\x1b[01;31mexamplefile.zip\\x1b[00m\\r\\n\\x1b[01;31m'"`, while using the `\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])` regex yields `"'ls\\r\\nexamplefile.zip\\r\\n'"` – Leonardo Feb 22 '21 at 03:45
  • There were no requests for a change of string representation In original post. It is enough for printing or passing to some api methond – V.Ignatov Feb 23 '21 at 08:37