I don't understand TWO things.
(1) It is your code, under your control. You want to add escape sequences to your data and then strip them out again so that you can calculate the length of your data?? It seems much simpler to calculate the padding before adding the escape sequences. What am I missing?
Let's presume that none of the escape sequences change the cursor position. If they do, the currently accepted answer won't work anyway.
Let's assume that you have the string data for each column (before adding escape sequences) in a list named string_data
and the pre-determined column widths are in a list named width
. Try something like this:
temp = []
for colx, text in enumerate(string_data):
npad = width[colx] - len(text) # calculate padding size
assert npad >= 0
enhanced = fancy_text(text, colx, etc, whatever) # add escape sequences
temp.append(enhanced + " " * npad)
sys.stdout.write("".join(temp))
Update after OP's comment """The reason I want to strip them out and calculate the length after the string contains the color codes is because all the data is built up programmatically. I have a bunch of colorize methods and I'm building up the data something like this: str = "%s/%s/%s" % (GREEN(data1), BLUE(data2), RED(data3)) It would be pretty difficult to color the text after the fact."""
If the data is built up of pieces each with its own formatting, you can still compute the displayed length and pad as appropriate. Here's a function which does that for one cell's contents:
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(40, 48)
BOLD = 1
def render_and_pad(reqd_width, components, sep="/"):
temp = []
actual_width = 0
for fmt_code, text in components:
actual_width += len(text)
strg = "\x1b[%dm%s\x1b[m" % (fmt_code, text)
temp.append(strg)
if temp:
actual_width += len(temp) - 1
npad = reqd_width - actual_width
assert npad >= 0
return sep.join(temp) + " " * npad
print repr(
render_and_pad(20, zip([BOLD, GREEN, YELLOW], ["foo", "bar", "zot"]))
)
If you think that the call is overburdened by punctuation, you could do something like:
BOLD = lambda s: (1, s)
BLACK = lambda s: (40, s)
# etc
def render_and_pad(reqd_width, sep, *components):
# etc
x = render_and_pad(20, '/', BOLD(data1), GREEN(data2), YELLOW(data3))
(2) I don't understand why you don't want to use the supplied-with-Python regular expression kit. No "hackery" (for any possible meaning of "hackery" that I'm aware of) is involved:
>>> import re
>>> test = "1\x1b[a2\x1b[42b3\x1b[98;99c4\x1b[77;66;55d5"
>>> expected = "12345"
>>> # regex = re.compile(r"\x1b\[[;\d]*[A-Za-z]")
... regex = re.compile(r"""
... \x1b # literal ESC
... \[ # literal [
... [;\d]* # zero or more digits or semicolons
... [A-Za-z] # a letter
... """, re.VERBOSE)
>>> print regex.findall(test)
['\x1b[a', '\x1b[42b', '\x1b[98;99c', '\x1b[77;66;55d']
>>> actual = regex.sub("", test)
>>> print repr(actual)
'12345'
>>> assert actual == expected
>>>
Update after OP's comment """I still prefer Paul's answer since it's more concise"""
More concise that what? Isn't the regex solution concise enough for you:
# === setup ===
import re
strip_ANSI_escape_sequences_regex = re.compile(r"""
\x1b # literal ESC
\[ # literal [
[;\d]* # zero or more digits or semicolons
[A-Za-z] # a letter
""", re.VERBOSE)
def strip_ANSI_escape_sequences(s):
return strip_ANSI_escape_sequences_regex("", s)
# === usage ===
raw_data = strip_ANSI_escape_sequences(formatted_data)
??