Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/python-strip-ansi-port.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"e2b": patch
"@e2b/python-sdk": patch
---

Align ANSI stripping of template build log messages across both SDKs. The Python SDK's `strip_ansi_escape_codes` now ports the JS SDK's `stripAnsi` regex: OSC sequences (hyperlinks, window titles) are matched non-greedily up to the first string terminator — including sequences spanning newlines — and CSI sequences are stripped without requiring a terminator. Both implementations additionally strip the remaining ECMA-48 string controls (DCS/Sixel, SOS, PM, APC) through their string terminator so control payloads no longer leak into cleaned logs.
6 changes: 3 additions & 3 deletions packages/js-sdk/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,13 @@ export async function dynamicImport<T>(module: string): Promise<T> {
function ansiRegex({ onlyFirst = false } = {}) {
// Valid string terminator sequences are BEL, ESC\, and 0x9c
const ST = '(?:\\u0007|\\u001B\\u005C|\\u009C)'
// OSC sequences only: ESC ] ... ST (non-greedy until the first ST)
const osc = `(?:\\u001B\\][\\s\\S]*?${ST})`
// String controls (OSC, DCS, SOS, PM, APC): ESC ]/P/X/^/_ ... ST (non-greedy until the first ST)
const strings = `(?:\\u001B[\\]PX^_][\\s\\S]*?${ST})`
// CSI and related: ESC/C1, optional intermediates, optional params (supports ; and :) then final byte
const csi =
'[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]'

const pattern = `${osc}|${csi}`
const pattern = `${strings}|${csi}`

return new RegExp(pattern, onlyFirst ? undefined : 'g')
}
Expand Down
88 changes: 88 additions & 0 deletions packages/js-sdk/tests/stripAnsi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { expect, test } from 'vitest'
import { stripAnsi } from '../src/utils'

// Mirrors packages/python-sdk/tests/shared/template/utils/test_strip_ansi_escape_codes.py

test('strips basic SGR color', () => {
expect(stripAnsi('\x1b[31mred\x1b[0m')).toBe('red')
})

test('strips semicolon-separated params', () => {
expect(stripAnsi('\x1b[1;31;42mhi\x1b[0m')).toBe('hi')
})

test('strips semicolon 256-color', () => {
expect(stripAnsi('\x1b[38;5;82mX\x1b[0m')).toBe('X')
})

test('strips colon 256-color', () => {
expect(stripAnsi('\x1b[38:5:82mX\x1b[0m')).toBe('X')
})

test('strips colon truecolor', () => {
expect(stripAnsi('\x1b[38:2::255:0:0mRED\x1b[0m')).toBe('RED')
})

test('strips colon curly underline', () => {
expect(stripAnsi('\x1b[4:3mX\x1b[0m')).toBe('X')
})

test('leaves plain text unchanged', () => {
expect(stripAnsi('no escape codes here')).toBe('no escape codes here')
})

test('strips OSC hyperlink', () => {
expect(stripAnsi('\x1b]8;;https://e2b.dev\x07E2B\x1b]8;;\x07')).toBe('E2B')
})

test('strips OSC window title (BEL terminated)', () => {
expect(stripAnsi('\x1b]0;my title\x07text')).toBe('text')
})

test('strips OSC window title (ESC backslash terminated)', () => {
expect(stripAnsi('\x1b]0;my title\x1b\\text')).toBe('text')
})

test('strips OSC spanning newlines', () => {
expect(stripAnsi('\x1b]0;line1\nline2\x07after')).toBe('after')
})

test('strips only to the first OSC terminator', () => {
expect(stripAnsi('\x1b]0;title\x07middle\x1b]0;other\x07end')).toBe(
'middleend'
)
})

test('strips cursor movement', () => {
expect(stripAnsi('\x1b[2Ax\x1b[1000Dy')).toBe('xy')
})

test('strips erase line', () => {
expect(stripAnsi('\x1b[2Kdone')).toBe('done')
})
test('strips DCS (ESC backslash terminated)', () => {
expect(stripAnsi('\x1bPabc\x1b\\done')).toBe('done')
})

test('strips DCS sixel payload', () => {
expect(stripAnsi('\x1bPq#0;2;0;0;0~~@@\x1b\\image')).toBe('image')
})

test('strips SOS', () => {
expect(stripAnsi('\x1bXhidden\x1b\\ok')).toBe('ok')
})

test('strips PM', () => {
expect(stripAnsi('\x1b^private msg\x1b\\ok')).toBe('ok')
})

test('strips APC', () => {
expect(stripAnsi('\x1b_app command\x07ok')).toBe('ok')
})

test('strips only the intro of an unterminated DCS', () => {
expect(stripAnsi('\x1bPno terminator here')).toBe('no terminator here')
})
test('leaves CSI with non-ASCII digits intact', () => {
expect(stripAnsi('\x1b[٣١mred\x1b[0m')).toBe('\x1b[٣١mred')
})
15 changes: 10 additions & 5 deletions packages/python-sdk/e2b/template/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,11 +316,16 @@ def strip_ansi_escape_codes(text: str) -> str:
"""
# Valid string terminator sequences are BEL, ESC\, and 0x9c
st = r"(?:\u0007|\u001B\u005C|\u009C)"
pattern = [
rf"[\u001B\u009B][\[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?{st})",
r"(?:(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))",
]
ansi_escape = re.compile("|".join(pattern), re.UNICODE)
# String controls (OSC, DCS, SOS, PM, APC): ESC ]/P/X/^/_ ... ST
# (non-greedy until the first ST)
strings = rf"(?:\u001B[\]PX^_][\s\S]*?{st})"
# CSI and related: ESC/C1, optional intermediates, optional params
# (supports ; and :) then final byte
csi = (
r"[\u001B\u009B][\[\]()#;?]*(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]"
)
# re.ASCII keeps \d to 0-9 like JS; [\s\S] still matches any char
ansi_escape = re.compile(f"{strings}|{csi}", re.ASCII)
return ansi_escape.sub("", text)


Comment thread
mishushakov marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,65 @@ def test_strips_colon_curly_underline():

def test_leaves_plain_text_unchanged():
assert strip_ansi_escape_codes("no escape codes here") == "no escape codes here"


def test_strips_osc_hyperlink():
assert (
strip_ansi_escape_codes("\x1b]8;;https://e2b.dev\x07E2B\x1b]8;;\x07") == "E2B"
)


def test_strips_osc_window_title_bel_terminated():
assert strip_ansi_escape_codes("\x1b]0;my title\x07text") == "text"


def test_strips_osc_esc_backslash_terminated():
assert strip_ansi_escape_codes("\x1b]0;my title\x1b\\text") == "text"


def test_strips_osc_spanning_newlines():
assert strip_ansi_escape_codes("\x1b]0;line1\nline2\x07after") == "after"


def test_strips_only_first_osc_terminator():
assert (
strip_ansi_escape_codes("\x1b]0;title\x07middle\x1b]0;other\x07end")
== "middleend"
)


def test_strips_cursor_movement():
assert strip_ansi_escape_codes("\x1b[2Ax\x1b[1000Dy") == "xy"


def test_strips_erase_line():
assert strip_ansi_escape_codes("\x1b[2Kdone") == "done"


def test_strips_dcs_esc_backslash_terminated():
assert strip_ansi_escape_codes("\x1bPabc\x1b\\done") == "done"


def test_strips_dcs_sixel_payload():
assert strip_ansi_escape_codes("\x1bPq#0;2;0;0;0~~@@\x1b\\image") == "image"


def test_strips_sos():
assert strip_ansi_escape_codes("\x1bXhidden\x1b\\ok") == "ok"


def test_strips_pm():
assert strip_ansi_escape_codes("\x1b^private msg\x1b\\ok") == "ok"


def test_strips_apc():
assert strip_ansi_escape_codes("\x1b_app command\x07ok") == "ok"


def test_strips_unterminated_dcs_intro_only():
assert strip_ansi_escape_codes("\x1bPno terminator here") == "no terminator here"


def test_leaves_csi_with_non_ascii_digits_intact():
# Python's \d would match Unicode digits without re.ASCII; JS \d never does
assert strip_ansi_escape_codes("\x1b[٣١mred\x1b[0m") == "\x1b[٣١mred"
Loading