Extract Strings from Text

In some languages, strings are started and ended with a quote mark ('). And quote itself is escaped by writing it twice sequentially. For example, empty string is written as '', and I'm is written as 'I''m'.

This question is about find out all non-overlapping strings from left to right in such format from the given input, while ignoring anything between or around these string literals.

Input / Output

You are given a string contains only printable ASCII. Quotes (') in inputs are always paired. And you need to find out all non-overlapping quoted strings in it. Output these strings in the order they appeared.

You are free to choose any acceptable I/O format you want. However, formats of strings for input and output MUST be the same. Which means that you cannot take raw string as input, and claim that your output string are formatted in quotes, with quotes escaped by writing twice. As this may trivialize the challenge.

Test Cases

All testcases here are written in JSON format.

Input           -> Output         # Comment (not a part of I/O)
"abc"           -> []             # text out side quotes are ignored
"''"            -> [""]           # '' is an empty string
"''''"          -> ["'"]          # two sequential quotes in string converted into a single one
"'abc'"         -> ["abc"]
"a'b'"          -> ["b"]
"'b'c"          -> ["b"]
"a'b'c"         -> ["b"]
"abc''def"      -> [""]
"'' ''"         -> ["", ""]       # there are 2 strings in this testcase
"'abc' 'def'"   -> ["abc", "def"]
"'abc'def'ghi'" -> ["abc", "ghi"] # separator between strings could be anything
"'abc''def'"    -> ["abc'def"]
"a'bc''de'f"    -> ["bc'de"]
"''''''"        -> ["''"]
"'''a'''"       -> ["'a'"]
"''''a''"       -> ["'", ""]
"''''''''"      -> ["'''"]
"'abc\"\"def'"  -> ["abc\"\"def"] # double quotes do not have special meanings
"'\\'\\''"      -> ["\\", ""]     # backslashes do not have special meanings
"'a'#48'b'"     -> ["a", "b"]     # hash signs do not have special meanings
"a,'a','a,''a''','a,''a'',''a,''''a'''''''" -> ["a", "a,'a'", "a,'a','a,''a'''"] # Testcase suggested by Command Master


And here are above testcases formatted with line breaks.

Rules

• Suggested test case: a,'a','a,''a''','a,''a'',''a,''''a''''''' Jul 13 at 19:00

Jelly, 19 17 bytes

=”'ŻḂ+¥\¬Ä×ḂƲḊƙŻḊ


Try it online!

Inspired by the prevalence of regex solutions, the core of the logic is a scan by the state transition table

$$\0\$$ $$\1\$$ $$\2\$$
a quote $$\1\$$ $$\2\$$ $$\1\$$
not a quote $$\0\$$ $$\1\$$ $$\0\$$

which fortunately simplifies pretty nicely to Ḃ+. $$\0\$$ is anything non-string, $$\1\$$ is an opening quote or anything inside a string, and $$\2\$$ is a closing quote or the first quote of an escape pair.

=”'                  For each character, is it a quote?
Ż                 Prepend a 0, then
Ḃ+¥\             scan by the table.
ƙ      Group the input
Ż     with a 0 prepended
¬Ä           by the cumulative number of zeroes in the scan result
×ḂƲ        multiplied by the result mod 2,
Ḋƙ      remove the opening quote from each group,
Ḋ    and remove the 0 group (everything outside a string).


Pip, 19 bytes

TMa@+'.*?'R"''"''


Try It Online!

-11 bytes thanks to DLosc.

  a@                # Match in input
+'.*?'        # A regex
TM                  # Remove the first and last
R"''"'' # Unescape '

$$$$

• Looks like the output is one ' short for inputs "''''", "''''''", "''''''''" etc. Jul 14 at 0:41
• @Dingus Ah, I had it in the wrong order. Jul 14 at 0:44
• Borrowing the regex from Neil's Retina answer gives 19 bytes! Jul 14 at 18:18

Retina 0.8.2, 25 bytes

M!('.*?')+
%^'|'$'' '  Try it online! Outputs each string on its own line but link wraps each test case in [ and ] to make it easier to tell where each output starts and ends. Explanation: M!('.*?')+  List all of the matched strings, still quoted. %^'|'$



Remove the leading and trailing quotes.

''
'


Deduplicate the interior quotes.

R, 83 bytes

\(x)gsub("''","'",substring(a,p<-el(gregexpr("'(''|[^'])*'",a))+1,p+attr(p,"m")-3))

Attempt This Online!

Same regex-based approach as emanresu A's answer.

• I had a nice strsplit based solution but it died on the quote escaping :(. Jul 13 at 12:30

J, 31 bytes

'''(''''|[^''])*'''".&.>@rxall]

Attempt This Online!

Mostly a port of emanresu A's answer.

The only difference is that after cutting out the quote pieces, we simply evaluate the resulting quoted string to handle the unescaping. This works because J is one of the languages described by this challenge, which also explains all the escaping in the regex.

• Will this fail if certain input trigger an invalid token or comment? I cannot speak J so I'm not sure what will happen here.
– tsh
Jul 13 at 8:21
• @ovs Great catch. Jul 13 at 8:48
• @ovs Fixed now with a more straightforward approach. Jul 13 at 9:13

05AB1E, 3529282116 15 bytes

''¡¦ιõÊ0šÅ¡''ý


Outputs an empty string if there are no strings. If it has to be an empty array, it's +1 for placing ε before ''ý

Explanation

''¡   split on quotes the implicit input
(e.g. ["a,", "a", ",", "a,", "", "a", "", "", ""], for a,'a','a,''a''')
¦     tail, remove the first part
(e.g. ["a", ",", "a,", "", "a", "", "", ""])
ι     uninterleave
(e.g. [["a", "a,", "a", ""], [",", "", "", ""]])
     dump to stack
õ     push an empty string
Ê     for each odd-indexed part, check if it's not equal to it
(e.g. [1, 0, 0, 0])
0š    prepend a zero
(e.g. [0, 1, 0, 0, 0])
Å¡    split the even-indexed parts on indices
the odd-indexed parts aren't empty
(e.g. [["a"], ["a,", "a", ""]])
''ý   join each on a quote
(e.g. ["a", "a,'a'"])

• Does 05AB1E not have regex? Jul 13 at 16:53
• @Steffan not on the wiki, at least. It might be possible to use the option to evaluate python, but it'll probably be longer Jul 13 at 16:56
• * in regards to my previous comment: I was wrong - you execute Elixir, not Python Jul 13 at 17:03
• If it's shorter though, you can use the legacy version of course to execute Python Jul 13 at 17:05
• тQ can be í for -1 Jul 14 at 10:59

C (gcc), 71 bytes

t,n;f(char*s){n=*s==39^t;t=n?t&&putchar(n%2?*s:10),n%2:t+t;*s&&f(s+1);}


Try it online!

State t Input *s Output Next n n%2 t+t
0 quote empty 1 1 1 -
0 others empty 0 0 - 0
1 quote empty 2 0 - 2
1 others as-is 1 1 1 -
2 quote as-is 1 3 1 -
2 others line-break 0 2 0 -

C (gcc), 112 95 bytes

• -5 thanks to ceilingcat
• -12 thanks to suggestions from tsh

Implements a simple state machine to determine whether the parser is inside a string.

#define P putchar(
t;f(char*s){for(t=0;*s;)t?*s-39?P*s++):*++s-39?t=0:P*s++):*s++-39||P t=10);}


Try it online!

Annotated:

t;f(char*s){
for(t=0;*s;) // initialize state to search mode
t? // is inside the string?
*s-'\''? // found quote?
putchar(*s++): // not quote, print it
*++s-'\''? // double quote?
t=0: // reset state to search
putchar(*s++): // print quote
*s++-'\''||  // found initial quote?
putchar(t=10); // print newline, set state
}

• You don't need to output the double quotes. As they are how I format the example output, but not a part of output. You can use line break as separators if you want. For example, the testcase input "'\\'\\''" is actually '\'\''. For more details, you may look into the link under testcases.
– tsh
Jul 14 at 2:23

BQN, 39 bytes

1⊸↓¨(-⟜1·2⊸|⊸×⟜(+·»⊸<×)·2⊸|⊸+=⟜''')⊸⊔


Try it at BQN online!

Explanation

The core logic is taken from Unrelated String's Jelly answer:

2⊸|⊸+=⟜'''
=⟜'''  Compare each character with ' character (1 if equal, 0 if not equal)
       Scan left-to-right on this function:
⊸+        Add the current value to
2⊸|          the accumulated value mod 2


This results in a list where:

• 0 represents a character outside any of the strings
• 1 represents an opening quote or a regular character inside a string
• 2 represents a close quote or the first of a pair of quotes

Next,

-⟜1·2⊸|⊸×⟜(+·»⊸<×)
×   Get the sign of each number (0 -> 0; 1,2 -> 1)
<    Compare each item in that list with
»⊸     the corresponding item in that list shifted to the right
This gives 1 at the beginning of each run of nonzeros, 0 elsewhere
+·        Cumulative sum
We now have a list of nondecreasing nonnegative numbers the same
length as the main list, such that the runs of nonzeros in the
main list are numbered 1, 2, 3, ...
⊸×⟜(      )   Multiply that list of numbers by
2⊸|               the main list mod 2
This puts zeros at every index where the main list has 0 or 2
(i.e. characters outside of strings, close quotes, and one quote
out of every escaped pair)
-⟜1·                  Subtract 1 from each (zero becomes -1, and group numbers start at 0)


For example (- represents -1):

Input:  a'b''c'd''e'''f'
Result: -00-00--1--2-22-


Then:

1⊸↓¨(...)⊸⊔
⊸⊔  Group the string into buckets
(...)     specified by the result of the above function
1⊸↓¨         Drop the first character (opening quote) from each bucket


Charcoal, 27 22 bytes

⪫Ｅ✂⪪Ｓ'¹±¹¦¹⎇﹪κ²⎇ι¶¦'ιω


Try it online! Link is to verbose version of code. Outputs a newline-separated list. Explanation: Now based on @CommandMaster's 05AB1E answer.

    Ｓ                   Input string
⪪ '                  Split on literal string '
✂   ¹±¹¦¹             Slice off first and last piece
Ｅ                      Map over remaining pieces
κ          Current index
﹪           Modulo
²         Literal integer 2
⎇            If current index is odd
⎇ι       If current piece is not empty
¶      Replace with newline
'    Else replace with literal string '
ι   Keep even pieces
⪫                    ω  Join together
Implicitly print


Python, 82 bytes

lambda s:[re.sub("''","'",x[1:-1])for x in re.findall("(?:'[^']*')+",s)]
import re

Attempt This Online!

Similar but not exactly the same regex as all the other answers.

Python, 80 bytes (using @Neil's non-greedy regex)

lambda s:[re.sub("''","'",x[1:-1])for x in re.findall("(?:'.*?')+",s)]
import re

Attempt This Online!

Vyxal, 21 bytes

(?:'€?')+ẎƛḢṪ‛''\'V


Try it Online!

Regex in golflangs go brr

Uses @Neil's regex, go upvote that.

Pip, 28 31 bytes

This non-regex solution ended up considerably longer than the regex solution, but I thought it was still interesting enough to post.

YUW:a^''y@1WV(#_\?n''M H S@y)|u


Outputs each string on a separate line. Try It Online!

Explanation

    a^''                         Split input string on single-quote character
UW:                             Unweave into two sublists of every other item
Y                                Yank into y
@y     Take the first sublist (substrings "outside" the quotes)
H S       Remove the first and last elements
M           Map this function to the remaining elements:
#_                   Length of substring
\?                 is nonzero?
n                If so, newline
''              If not, single-quote character
y@1WV(              )    Weave the other sublist with that result
|u  If list is empty, use nil instead to suppress printing
Join together and autoprint (implicit)


The idea here is to consider the quotes in pairs. When the end of one matched pair of quotes is immediately followed by the beginning of another pair, that represents an escaped quote (so we reintroduce a quote character); when there are some characters between the end of one pair and the beginning of the next, that represents two separate strings (so we introduce a newline).

• '' and abc` seem to result in the same output, which shouldn't be the case Jul 15 at 6:27
• @CommandMaster Aha. That's sort of an artifact of my output format: the first case outputs one empty string, while the second outputs zero empty strings. I changed it so the second case outputs nil, which means zero newlines (as opposed to the first case, which outputs one newline). You still can't see the difference on DSO, but if you run it on the command-line, you can. Jul 15 at 14:40