Skip to content

Commit 32340be

Browse files
committed
Added escape() and unescape()
1 parent ca04118 commit 32340be

2 files changed

Lines changed: 106 additions & 2 deletions

File tree

src/ffmpegio/utils/__init__.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,81 @@
44
from .._utils import *
55

66

7+
def escape(txt):
8+
"""apply FFmpeg single quote escaping
9+
10+
:param txt: Unescaped string
11+
:type txt: any stringifiable object
12+
:return: Escaped string
13+
:rtype: str
14+
15+
See https://ffmpeg.org/ffmpeg-utils.html#Quoting-and-escaping
16+
"""
17+
18+
txt = str(txt)
19+
20+
if re.search(r"\s", txt, re.MULTILINE):
21+
# quote if txt has any white space
22+
txt = txt.replace("'", r"'\''")
23+
return f"'{txt}'"
24+
else:
25+
# if not quoted, escape quotes and backslashes
26+
return re.sub(r"(['\\])", r"\\\1", txt)
27+
28+
29+
def unescape(txt):
30+
"""undo FFmpeg single quote escaping
31+
32+
:param txt: Escaped string
33+
:type txt: str
34+
:return: Original string
35+
:rtype: str
36+
37+
See https://ffmpeg.org/ffmpeg-utils.html#Quoting-and-escaping
38+
"""
39+
40+
n = len(txt)
41+
if not n:
42+
return txt
43+
44+
re_start = re.compile(r"[^\\](?:\\\\)*'")
45+
re_sub = re.compile(r"\\([\\'])")
46+
47+
blks = []
48+
49+
# look for a first quoted text block
50+
m = re.search(r"(?:^|[^\\])(?:\\\\)*'", txt)
51+
if m:
52+
i0 = m.end()
53+
if i0 > 1:
54+
# unescape the initial unquoted block
55+
blks.append(re_sub.sub(r"\1", txt[0 : i0 - 1]))
56+
else:
57+
# no quoted text block, unescape the whole string
58+
return re_sub.sub(r"\1", txt)
59+
60+
# always starts with quoted block
61+
in_quote = True
62+
63+
while i0 < n:
64+
65+
if in_quote:
66+
# find the end quote
67+
i1 = txt.find("'", i0)
68+
if i1 < 0:
69+
raise ValueError("incorrectly escaped text: missing a closing quote.")
70+
blks.append(txt[i0:i1])
71+
else:
72+
# find the next starting quote
73+
m = re_start.search(txt, i0 - 1)
74+
i1 = m.end() - 1 if m else n
75+
blks.append(re_sub.sub(r"\1", txt[i0:i1]))
76+
i0 = i1 + 1
77+
in_quote = not in_quote
78+
79+
return "".join(blks)
80+
81+
782
def parse_spec_stream(spec, file_index=False):
883
if isinstance(spec, str):
984
out = {}

tests/test_utils.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,37 @@
22
from ffmpegio import utils
33
import pytest
44

5+
def test_string_escaping():
6+
raw = "Crime d'Amour"
7+
esc = utils.escape(raw)
8+
assert esc == r"'Crime d'\''Amour'"
9+
assert utils.unescape(esc) == raw
10+
11+
raw = " this string starts and ends with whitespaces "
12+
esc = utils.escape(raw)
13+
assert esc == "' this string starts and ends with whitespaces '"
14+
assert utils.unescape(esc) == raw
15+
16+
esc = r"' The string '\'string\'' is a string '"
17+
raw = r" The string 'string' is a string "
18+
assert raw == utils.unescape(utils.escape(raw))
19+
assert raw == utils.unescape(esc)
20+
21+
esc = r"'c:\foo' can be written as c:\\foo"
22+
raw = r"c:\foo can be written as c:\foo"
23+
assert raw == utils.unescape(esc)
24+
assert raw == utils.unescape(utils.escape(raw))
25+
26+
raw = "d'Amour"
27+
esc = utils.escape(raw)
28+
assert esc == r"d\'Amour"
29+
assert utils.unescape(esc) == raw
30+
31+
raw = r"c:\foo"
32+
esc = utils.escape(raw)
33+
assert esc == r"c:\\foo"
34+
assert utils.unescape(esc) == raw
35+
536

637
def test_parse_spec_stream():
738
assert utils.parse_spec_stream(1) == {"index": 1}
@@ -81,5 +112,3 @@ def test_get_audio_format():
81112
if __name__ == "__main__":
82113
import re
83114

84-
spec = "p:4"
85-
print(re.split(r"(?<![pi]\:|m\:.+?\:)\:", spec))

0 commit comments

Comments
 (0)