Regular Expressions – A Quick Guide

Read it out loud: “Regular Expressions.” It chimes like the name of a horror movie Alfred Hitchcock would be proud to tag his name to. But it shouldn’t. Don’t be frightened by the name. It is, after all, only a name. Familiarize it. Shorten it to Regex or Regexp and give it a little hug as you tell it “We’re going to be lifelong buddies…”

What follows in this guide is an introduction to regular expressions and a reference table of regular expression metacharacters with examples for their usage. Unless you really need an introduction, jump straight ahead to the table because that is where you will learn most.

Getting to Know Regex

Regex is a Language

Like English, Regex makes it possible to convey instructions. In English, we can ask people to look left, to look right, to move objects around or to only carry out our instructions when the circumstances are right. We can do the same with computers using Regular Expressions.

Whereas English has hundreds of thousands of words, Regex has about 30. And they are all very easy to learn.

Like any language, Regex has ways to indicate where something is and where something isn’t.

When searching for a word such as “look” we can tell the regex engine to look behind the word and check that it has another word such as “forward” in front of it before it decides it has found a match for the specific word it is being asked to find.

Regex also has ways to specify whether a search is for a number, a letter, a whitespace character like a space or carriage return, a word or any grouping or combination of the aforementioned. It even has pronouns – it lets us specify groups that we can refer to in both the search and the replacement string.

Using regular expressions, we can search for the string “look” in the phrase “I am a forward looking guy who’s always on the lookout for fun.” and replace “look” with “fac” only when it is preceded by the string “forward ” to make the phrase read “I am a forward facing guy who’s always on the lookout for fun.” Try doing the same find & replace in a text editor without regular expressions.

The regex for performing our hypothetical exchange in would search for #(?<=forward )look(ing)# and replace it with “fac\1”. You can check it here.

The Regex Alphabet and Words

Regex is a written language that uses its own alphabet of special characters to form its verbs and nouns. These verbs and nouns are properly called metacharacters or operators and are written with the following characters:

{ }?:
( ).\
[ ]+!
< >*|
^$=

Western numerals (0-9) and letters of the Latin alphabet (a-z) are also used in conjunction with those characters.

A typical regex consists of a search pattern and a replacement pattern expressed with a combination of metacharacters and non metacharacters.

Metacharacters tell a regex engine what to look for, where to look, when to turn a blind eye to what it is looking at and what to do once it’s matched its search expression to something.

Non metacharacters are the symbols, digits and words being searched for. They have no special meaning to the regex engine and represent exactly what they are – regular text strings. Non metacharacters are usually called literal characters or literals.

We can tell a regex engine to treat a metacharacter as a literal character by placing a backslash (\) in front of it. This act is known as “commenting out”.

Compare the following two regex search patterns:

looking (?=good)
looking \(\?\=good\)

There is no need to understand them. They only serve to illustrate how metacharacters are switched off.

The first regex contains the literal strings “looking ” and “good”. The characters “(, ?, = and )” all have special significance to a regex engine. The parentheses “()” create a grouping and the “?=” immediately after the opening parenthesis instructs the regex engine to look ahead of the string “looking ” and check it is followed by the string “good” before the regex engine determines it has discovered a match.

The second regex tells the regex engine to look for the literal string “looking (?=good)”. The backslashes comment out the special metacharacters “(?=)” to reduce them to literals. To search for a literal backslash, you would need to comment it out too e.g \\.

I mentioned earlier that different regex engines operate slightly differently. The Regular Expression examples and metacharacters shown in the table below are for Perl Compatible Regular Expressions (PCRE’s) as used by PHP’s preg_match () function.

I recommend you use a text editor or an online testing tool for testing your regular expressions.

I recommend the following Regex capable text editors:

These three online regex testing tools are also suitable for most needs:

I prefer to use Kate because it handles most of what I need a regex engine to do within a text editor. You can read more about Kate’s regular expression features here.

PowerGrep (free download) and EditPadPro (free download) are two Windows programs that handle lookbehinds and lookaheads very nicely. EditPadPro is ideal for simple RegEx tasks. PowerGrep is a little overwhelming to begin with but it comes with a very good tutorial to regular expressions.

Tips for Writing Regular Expressions

There are four basic steps to writing a regular expression:

  1. know what you want to change
  2. know how you want to change it
  3. write the regex that finds it
  4. write the regex that changes it

Let’s look at those steps in more detail.

Know what you want to change

Regexes don’t just replace strings. They let us play with them. They allow us to be (almost) as precise or as general as circumstances permit and warrant about what we are looking for. We can find a letter, number, symbol, word, sentence, paragraph or any other type of string. In many cases, we only need to know the characteristics of what we’re looking for.

Questions to look at when deciding what to replace or change include:

  • Are you looking for something specific like a word or phrase?
  • Are you looking for something that changes like a URL with variable anchor texts?
  • Will changing your target string suit all contexts surrounding its every occurrence?
  • Are there common markers surrounding the string you want to change?
  • Can any common markers be used to ensure the string to be changed is only changed when those markers are present?
  • Are you looking for more than one string?

Look for anything common to either the string being changed or its surroundings where it needs to be changed. Unintended consequences are easy to create but not always easy to fix so, when possible, try to be precise about what you want the regex engine find.

Know what your replacement is

Regular expressions are not just about finding and replacing text. They can also be used to re-organize text as-well-as to do things to text surrounding the search string.

Questions to consider include:

  • Do you want to overwrite the search string with the replacement?
  • If the search pattern looks for more than one string, do you want to swap them round?
  • If the search pattern looks for more than one string, do you want to edit what lies between them?

The key consideration is whether the search string, replacement string or both strings are start and end markers for what you want to achieve or whether one is to replace the other.

Write the regular expression

A typical regular expression consists of a search pattern and a replacement pattern.

The search pattern can be a literal string or a mixture of metacharacters and literal characters. The replacement pattern usually consists of literals and/or references to groupings specified within the search string.

In writing either the search or replacement pattern it helps to know that they are processed from left to right by regex engines.

When you intend to search for one string and replace it with another string, simply state the search string as the search pattern and the replacement string as the replacement pattern. Take care to comment out (i.e backslash) metacharacters to convert them to literals.

There are two types of parentheses: capturing and non-capturing.

Capturing parentheses create a grouping that stores the matched pattern in the regex engine’s memory. They consist of a string encased in parentheses. For example (one string) (two strings).

When you intend to place a string contained within a “replacement” pattern next to the string contained within a search pattern, put the search pattern in Capturing Parentheses.

Each capturing parentheses pair is given a name. That name is a number. That number is the count of opening parentheses from the search pattern’s left side to its right side. To refer to that grouping (to recall its content), place a backslash in front of its number. For example If the search pattern is #(one string)# then the text “one string” may be recalled by writing “\1”.

Non-capturing parentheses do not create backreferences – the strings mentioned within them are not recorded unless capturing parentheses are used around the strings to be recorded. For example, #(?=one)string# does not create a backreference i.e “\1” does not recall the value “one”; #(?=(one))string# does create a backreference i.e “\1” does recall the value “one”.

Non-capturing parentheses have a question mark (?) immediately after the opening parentheses. That is to say that capturing parentheses never begin with a question mark.

Here’s a longer example

If a search pattern was written “(one)(two) then “\1” would refer to a first set of parentheses; “\2” would refer to a second set of parentheses; and a replacement pattern of “\2 times \1” would return “Two times One”.

That example also demonstrates how to swap strings around: create groupings in the search pattern and change their name order within the replacement pattern.

More complicated example

Do not worry about it looking complex. It is complex. The reference table below here uses less complex examples for each of the metacharacters used in this example. Glide over this example if you need to and return to it once you have reviewed the reference table. It will then make more sense.

Let’s look at the characters in the following regex which matches variations of “This is my [number] apple pie [time]. It is yummy!”.

Here’s the regex:

#[Tt]his is my (\d*(st|nd|rd|th)?) apple (pie)? (today|this ((mor|eve)ning|afternoon))\. It is ([Yy]ummy.?.?)+#

Here’s what those characters do:

The opening and closing delimiter is an hash sign (#). It could easily be a forward slash or any other symbol that does not occur within the regex.

[Tt] is a character class that specifies that either “T” or “t” are acceptable matches at the point where the character class occurs.

All the characters “Tt his is my st nd rd th apple pie today this mor eve ning afternoon It is Yyummy” are literals

The metacharacter “\d” represents a digit from 0 to 9. It is also the only token, or argument, that defines the value of the asterisk to its right

The asterisk (*) is a metacharacter that says the digit represented by “\d” is optional and can be any length from zero digits to an infinite number of digits.

The grouping “(st|nd|rd|th)” says that any of “st” or “nd” or “rd” or “th” are valid matches. The curly brackets denote the literals are to be considered a group token and any metacharacter that follows it applies to the complete group. The pipe symbol, |, is an “or” operator; it says that whatever to the left of it and whatever is to its right are both valid matches at the point where it occurs.

The question mark (?) to the right of the grouping “(st|nd|rd|th)?” tells the regex engine to match any of the grouped literals either never or only once i.e they are optional.

Jumping ahead to the backslash (\) and the dot (.). The dot has a special meaning to regex engines: it represents any single character except a newline. It may be a a full-stop, a letter of the alphabet, a digit of a number series or a non-alphanumeric symbol. The backslash switches off the dot’s metacharacter status and instructs the regex engine to treat it as a literal dot. “\.” tells the regex engine to match a full-stop.

The “.?.?” in the grouping “([Yy]ummy.?.?)” instructs that the token “[Yy]ummy” may be followed by any two singular characters or by nothing at all for the regex to match.

The plus in “([Yy]ummy.?.?)+” specifies that the token “[Yy]ummy.?.?” must be in the statement at least once and it may even be repeated several times for the regex to find a match.

Any of the following statements will be matched by the regex:

  • This is my 1st apple pie today. It is yummy!
  • This is my 1st apple pie today. It is yummy! Yummy! Yummy!
  • This is my 21st apple this evening. It is yummy.
  • This is my 2nd apple pie this morning. It is yummy..

The regex engine stores any matched grouping encased in “capturing” parentheses. The match may be referred to in the replacement string by a backslash and a number e.g “\1” or “\3”. To find the back reference’s group number, count the number of opening parentheses within the expression from left to right. Ignore non capturing parentheses. See the table below to learn which parentheses are capturing and non capturing.

The first grouping in our example regex is “(\d*(st|nd|rd|th)?)”; The second grouping is (st|nd|rd|th); and the third grouping is (pie). Each of these groupings may be referred to in either the search or replacement pattern by the metacharacters “\1”, “\2” and “\3”.

Example replacement patterns for the statement “This is my 1st apple pie this evening. It is yummy.” are

“Mmmmmm!!!” would replace the matched string with “Mmmmmm!!!”

“Mmmmmm!!! My \1 \3.” would replace it with “Mmmmmm!!! My 1st pie”

“Mmmmmm!!! My \1 \3 \4.” would replace it with “Mmmmmm!!! My 1st pie this evening”

Notice that the dot at the end of the replacement pattern is treated as a literal. Fewer characters are treated as metacharacters in the replacement patterns than in search patterns.

Also notice that the reference to grouping 4, “\4”, treats the nested fifth group within group 4 as though it were not a separate group. If the nested group was referred to in the replacement pattern with “\5” then it would return the value stored when group 5 is matched, in this case, it is the string “evening”.

If you need a more complete guide to regular expressions, one that is easy to understand and easy to read, then I recommend the tutorial that comes with PowerGrep.

Please post suggestions, questions and supportive corrections at the bottom of this post.

Quick Reference: Metacharacters and Examples

The left hand column of the table lists metacharacters. Related metacharacters are listed in successive rows. The examples shown in the right hand column use an hash symbol (#) as the delimiter for the search pattern.

The Language of Regular Expressions

Metacharacter

Information

()

Name

Capturing Parentheses

Purpose

These group their content into a single token.When a repetition metacharacter such as the asterisk (*), plus sign (+), question mark and braces ({}), the repetition metacharacter repeats the complete (token) grouping.

Being capturing parentheses, the content may be referred to by using a backslash and a number e.g \1 or \2. That number is deduced by counting the opening Capturing Parentheses from left to right in the search pattern. These are known as back-references.

Example

In the string “cooking apple” a search for #(cooking)(apple )# will search for both “cooking” and “apple”. “(cooking)” is group one, “(apple )” is group two. “(cooking)” can be referred to as “\1” and “(apple )” as “\2”. Using the replacement pattern “\2 \1” will return “apple cooking” instead of “cooking apple”.

(?:)

Name

Non Capturing Parentheses

Purpose
A question mark and colon, “?:”, positioned immediately after an opening parenthesis prevents the creation of a back-reference for the contents i.e their contents may not be back-referenced by a backslash and a number “\1” or “\2” etc.

Example

In the string “cooking apple “, a search for “(?:cooking)(apple )” and replacement “\1\2” would return “cooking2”. Notice the backslash in “\2” is stripped out from the returned replacement string.

(?>)

Name

Atomic Grouping

Purpose

A question mark and greater than sign positioned immediately after an opening parenthesis instructs the regex engine to test through a list of strings stated within the parentheses. The strings are tested one at a time, from left to right until a first first match is found. The regex engine quits testing for further matches from the list once one match is found.These are non-capturing parentheses.

Example

#a(?>bc|c)c# will find “abcc” but not “abc“. “abc” will not match because once a regex engine finds a match for “abcc” it gives up looking beyond the “bc” stated in the atomic grouping.

(?=)

Name

Lookahead (Zero-Width Assertion)

Purpose

Tells the regex engine to match the token before the opening parenthesis only when that token is followed by whatever follows the equality sign within the parentheses.Only tokens outside of the parentheses are replaced.These are non-capturing parentheses.

Example

#a(?=bc)# will only find “a” when it is followed by “bc”. It will find “abc” but neither “a” nor “ab” nor “bc”. Only “a” will be affected by a replacement pattern.

(?!)

Name

Negative Lookahead (Zero-Width Assertion)

Purpose

Tells the regex engine to match the token before the opening parenthesis only when that token is NOT followed by whatever follows the exclamation mark within the parentheses.Only tokens outside of the parentheses are replaced.These are non-capturing parentheses.

Example

#a(?!bc)# will only match “a” when it is not followed by “bc”. It will find “axy” and “acb” but never “abc”.

(?<=)

Name

Lookbehind (Zero-Width Assertion)

Purpose

Tells the regex engine to match the token after the closing parenthesis only when it follows whatever is after the equality sign within the parentheses.Only tokens outside of the parentheses are replaced.

These are non-capturing parentheses.

Example

#(?<=a)bc# will match “bc” only when there is an “a” in front of “bc”. It would match “abc” but not “bbc”.

#(?<=tasty )rhubarb(?= crumble)# will match “rhubarb” only when it is sandwiched between “tasty ” and ” crumble”as in “tasty rhubarb crumble”.

(?<!)

Name

Negative Lookbehind (Zero-Width Assertion)

Purpose

Tells the regex engine to match the token after the closing parenthesis only when it is NOT preceded by whatever is after the exclamation mark within the parentheses.Only tokens outside of the parentheses are replaced.

These are non-capturing parentheses.

Example

#(?<!a)bc# will match “bc” only when there is not an “a” in front of “bc”. It will match “bbc” but not “abc”.

(?())

Name

I really don’t know what this one is called. I call it a “Conditional Statement”.

Purpose

Creates a conditional (?(if)then|else) expression. The question mark always comes immediately after the opening parenthesis and the “if” argument should be encased in parentheses. The “then” argument is tested when the “if” argument is true. The “else” statement is tested when the “if” argument is false.A back-reference may be recalled into the “if” argument by stating its number without a backslash e.g #(?(1)then|else)# instead of #(?(\1)then|else)#

Example

A search for #(?(?<=(apple\s))pie|crumble)# will match “pie” in “apple pie”, it will match “crumble” in “rhubarb crumble”. The search pattern says “find the string ‘pie’ preceded by ‘apple’ or find the string crumble when it is not preceded by ‘apple ‘.

\[number]

Name

Back-Reference

Purpose

Tells the regex engine to re-use/recall the group with the name indicated by the number. Strings contained within parentheses create the group that is to be recalled.

Example

Putting #(cooking)(apple)# in the search pattern and “\2 \1” in the replacement pattern would make the words “cooking apple” become “apple cooking”. Using “\1 \2” in the replacement pattern will return “cooking apple”

Fun Example

Jumble word pairs by searching for #(\w+)(.)(\w+)# and replacing it with #\3\2\1#

Jumble letter pairs by searching for #(\w)(\w)# and replacing it with #\2\1#

[]

Name

Character Class

Purpose

Brackets specify a character class (also known as a character set). A character class tells a regex engine to look for one character that matches any of the characters within the class. Think of it as searching for a range of characters at the point where one character could be.

Example

Searching for #[aeiou]# will find either an “a” or an “e” or an “i” or an “o” or a “u” but not a grouping of any of those characters.

Searching for #[aeiou]+# will find either an “a” or an “e” or an “i” or an “o” or a “u” or a grouping of any of those characters such as “aeiou” or “aaaaa” or “ooooooo”.

Searching for #c[aou]t# will find the letter “a” the letter “o” or the letter “u” between the letters “c” and “t” so it will find “cat”, “cut” and “cot”.

[a-zA-Z] will find an instance of any letter from “a” to “z” whether lowercase or uppercase.

[0-9] will match an instance of any digit from 0 to 9.

“w[he][ea]ther” will match both “whether” and “weather”.

[a-zA-Z0-9_] is the same as using the shorthand metacharacter \w.

Searching for #[ ]{2,}# will locate any spaces that occur twice or more times consecutively. This can be used to replace accidental double spaces with a single space.

[^]

Name

Negated Character Class

Purpose

A caret, “^”, placed immediately after the opening bracket removes the characters within the brackets from the search. It says, “don’t look for this here but do look for anything else here.”

Example

#c[^a-zA-Z]t# would match any group of three characters that start with “c” and end in “t” but do not contain letters a to z or A-Z. It would match “c1t”, c$t” but not “cat” or “cut”.

|

Name

Alternation

Purpose

A pipe separates alternatives. It means “or” as in “this or that” and “1 or 2”. It is treated as a literal character when placed within brackets “[]”.

Example

#(cat|dog)# will match “cat” or “dog”.

#[|]# will match “|”

\b

Name

Word Boundary Metacharacter

Purpose

Represents a word boundary. It ensures a search only matches the isolated string and does not match when the string is found within another set of characters.When an uppercase “B” is used (\B), a regex engine will test for non word boundaries.

Example

#\band\b# will match “and” in the sentence “big and beautiful” but would not match “and” in “Land of the Giants”.

#and\b# will match “and” in “Hand” but not “and” in “Handle”.

#\bend# will match “end” in “endocrine” but not in “send”.

#\Bend# will find “end” in “send” but not in “endocrine”.

\s

Name

Whitespace Metacharacter

Purpose

Represents any whitespace such as a tab, a space or a line break.Additional whitespace metacharacters include \t for a tab, \r for a carriage return, \n for feed-break, \v for a vertical tab. Windows text documents use \r\n to signal the end of a line (the EOL).

Example

Typing “\s” is the same as looking for [ \t\r\n] i.e a space, a tab or a line break.

\d

Name

Digit Metacharacter

Purpose

Represents any single digit. It is a shorthand for the character class [0-9].The inverse of this metacharacter is “\D” which represents any single character that is not a digit. It is the same as the character class [^0-9].

Example

#\d# would match any single digit number such as “1” or “2” or “3”.

#\d\d# would match any double digit number such as “12” or “21” or “13”.

#\d+# would match any set of digits whether there is only 1 digit or as many digits as the regex engine can manage.

#\d*# is similar to #\d+# except it will match whether a digit is present or not.

#\b\d+\b# is like #\d+# except it will only match when the digits are standalone. The previous examples will match digits among other characters; this example will not because of the word boundary metacharacter (\b).

\w

Name

Word Character Metacharacter

Purpose

Represents a solo character of the type that would normally be found within a word. Different regex engines define the word character class differently. Typically “\w” represents letters of the alphabet, any digit from 0 to 9 and the underscore. It is usually equal to the character class [a-zA-Z0-9_].Its inverse is “\W” which represents any character that is not defined as a word character by the regex engine that parses it.

Example

Using #[a-zA-Z0-9_]# is the same as using #\w#.

Searching for #\w+# will match any grouping of the characters “a” to “z”, “A” to “Z”, “0” to “9” and “_”.

.

Name

Dot (or Period) Metacharacter

Purpose

Represents any single character except newlines. It is the same as the character class [^\n] and (for Windows) [^\r\n].

Example

#.# will match one character.

#..# will match 2 characters.

#…# will match 3 characters.

#.+# will match one or more characters.

#.*# will match none or all characters.

#.?# will match none or one character.

^

Name

Beginning of Line Anchor

Purpose

Represents the beginning of a line.

Example

#^dig# will match the string “dig” only when it is placed at the beginning of a line.

#^dig.*# will match any complete line that begins with the string “dig”.

$

Name

End of Line Anchor

Purpose

Specifies the end of a line.

Example

Using the search pattern #$# and the replacement pattern # End of Line# will append ” End of Line” to the end of every line.

The search pattern #(?<=end)$# will match any line that ends with the string “end” whether it is suffixed to other characters or not.

The search pattern #(?<=\bend)$# will match any line that ends with the string “end” when it is preceded by a whitespace only.

^^

Name

Start of File Anchor

Purpose

Specifies the start of a file.

Example

A search pattern for #^^# and replacement pattern #Top of the File\r\n# will place the string “Top of the File” followed by a carriage return at the top of any file.

$$

Name

End of File Anchor

Purpose

Specifies the end of a file.

Example

A search pattern for #$$# and replacement pattern #\r\nEnd of the File# will place a carriage return followed by the string “End of the File” at the bottom of any file.

\a & \z

Note

Some regex engines use “\a” to specify the top of a file and “\z” to specify the end of a file.

\A & \Z

Note

Some regex engines use “\A” to specify the beginning of a line, and “\Z” to specify the end of a line. Perl uses this notation.

\

Name

Backslash

Purpose

Tells a regex engine to treat any metacharacter that it precedes as a literal character.Inside a character class, the backslash character is treated as a literal character e.g “[\]” is the same as “\\”.

When it precedes a number, it creates a back-reference to a grouping (see the Capturing Parentheses Metacharacter for more details about back-references).

Example

The anchor characters, “^” or “$” can be used as literals when prefixed by “\” to form “\^” or “\$”.

#\^abc# will match “^abc” wherever it occurs in a line.

#abc\$# will NOT look for “abc” at the end of a line but will look for “abc$” within a line.

The backslash character will be treated as a literal character when preceded with another backslash character such as “\\”

\t

Name

Tab Metacharacter

Purpose

Matches a tab character.

\r

Name

Carriage Return Metacharacter

Purpose

Matches a carriage return.

\n, \r\n

Name

Line Feed Metacharacter

Purpose

Matches a line feed – end of a line. \n for Unix and \r\n for Windows.

?

Name

Option Metacharacter (Question Mark Quantifier)

Purpose

Makes the token prior to it optional. It tells the regex engine to match its precedent token zero times or once only.

Example

#cat(s)?# will match both “cat” and “cats” whereas “cats” will match “cats” but not “cat”.

#cat(hode)?# will match “cat” or “cathode”.

+

Name

Plus Quantifier

Purpose

Instructs the regex engine to repeat the token before the plus sign at least once. Its precedent token is not optional but its total number of consecutive occurrences is.

Example

Searching for “[0-9]” to replace it with “A” will replace “111” with “AAA”.

Searching for “[0-9]+” will replace “111” with “At”. The whole group is treated as a whole.

Searching for “cat.+” will match any characters after the occurrence of “cat” e.g “cat”, “cathode” and/or “cation”.

#^.+$# will match a complete line from start to finish provided its length is at least one character. It will not match empty lines.

*

Name

Asterisk Quantifier

Purpose

Instructs the regex engine to match the token before it zero or more times. The token it proceeds does not need to occur in a string in order for the string to match.

Example

In the search pattern “.*”, the “.” represents any character. The asterisk ensures the “any character” token, i.e the dot, is repeated. A search for “.*” will match everything or nothing.

#^.*$# will match a complete line from start to finish regardless of its length. It will match empty lines too.

{min,max}

Name

Quantifier

Purpose

Allows the repetition level to be specifically stated. “min” represents the minimum number of repetitions and “max” represents the maximum number of repetitions.

Example

“(once){0,1}” is the same as writing “(once)?”.

“(repeat){1,} is the same as “(repeat)+”.

“(repeat){0,} is the same as “(repeat)*”.

“(repeat){3,3} is the same as “(repeat){3}”. Removing the comma instructs the regex engine to find the token repeated consecutively exactly the stated number of times.

 

Sharing is caring!

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

10 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
10
0
Would love your thoughts, please comment.x
()
x