1

I want to be able to return only the escaped literals in a string currently I'm doing this:

foo="\'\"\(foobar\)'another'[program]\[\$var\]()"
echo "${foo//[^\\\']/}"

but it outputs this:

\'\\''\\

desired output should be like this:

\'\"\(\)\[\$\]

I'm still just on a stage where I'm trying to get the literal single quotes, but somehow it's not working or is it really doable in bash expansion?

EDIT

string is from bash $READLINE_LINE so there will be no extra escaping like the tripple backslash for double quotes.

5
  • So, do I get it right that you want to find the parts of the string where there's a backslash followed by some other character? Or, in other words, remove everything but sequences of backslash+some other character. Note that because of the shell's quoting rules, the string you're assigning to foo is \'"\(foobar\)'another'[program]\[$var\](), there's no \" sequence in it. You'd need "...\\\"..." for that.
    – ilkkachu
    Commented Feb 2, 2022 at 14:24
  • @ilkkachu yes, exactly I want to extract only the parts/symbol with backslash in it.
    – cevhyruz
    Commented Feb 2, 2022 at 14:28
  • Does it have to be parameter expansion? Commented Feb 2, 2022 at 14:32
  • "there's no \" sequence in it" Yeah, I figured that might be the case since I'm not getting a backslash using the parameter expansion given above.
    – cevhyruz
    Commented Feb 2, 2022 at 14:32
  • @schrodigerscatcuriosity, I doesn't actually need to be a parameter expansion. I'm just trying to do it using expansion but I will resort to anything if that's impossible
    – cevhyruz
    Commented Feb 2, 2022 at 14:38

3 Answers 3

3

To set a variable to the literal value

\'\"\(foobar\)'another'[program]\[\$var\]()

using a double-quoted string, you would need to escape each literal backslash and each double quote or dollar sign that would otherwise trigger an expansion.

string="\\'\\\"\\(foobar\\)'another'[program]\\[\\\$var\\]()"

If using a single-quoted string, you only have to care about inserting the single quotes specially:

string='\'"'"'\"\(foobar\)'"'"'another'"'"'[program]\[\$var\]()'

Here, I've opted for breaking out of the single quoted string to add a double quoted single quote for each single quote, i.e. '"'"'. You could also have used an escaped single quote outside of the single quoted string, i.e. '\''.

You may also choose to use a quoted here-document if the quoteng becomes too cumbersome:

string=$( cat <<'END'
\'\"\(foobar\)'another'[program]\[\$var\]()
END
)

Note that this would trim off the trailing newline if a newline character was the last character in the string.

Your code then tries to delete all backslashes and single quotes, which does not seem right. Instead, use some tool to extract all instances of \ and the following character:

grep -o '\\.' <<<"$string"

This produces

\'
\"
\(
\)
\[
\$
\]

Or,

grep -o '\\.' <<<"$string" | paste -s -d '\0' -

to reproduce exactly the output in the question.

You could also do this in a loop in bash directly:

while [[ $string =~ \\. ]]; do
    printf '%s\n' "${BASH_REMATCH[0]}"
    string=${string#*\\?}
done

or,

while [[ $string =~ '\'. ]]; do
    printf '%s\n' "${BASH_REMATCH[0]}"
    string=${string#*'\'?}
done

This modifies the value of string by trimming off the bit up to the next match of a backslash and some other character, for as long as such a character sequence exists in the string. The bit matching the given regular expression is printed in each iteration.

5
  • Sorry I forgot to mention I'm parsing input from bash $READLINE_LINE so the input will be given by the user and there will be no extra escaping, the snippet above is just an example
    – cevhyruz
    Commented Feb 2, 2022 at 14:53
  • @cevhyruz Well, your example in the question has the issues outlined in my answer.
    – Kusalananda
    Commented Feb 2, 2022 at 14:54
  • @cevhyruz Just ignore the first parts of the answer if you don't think they apply to your added edit.
    – Kusalananda
    Commented Feb 2, 2022 at 15:35
  • Note that the grep -o approach doesn't work for \<newline>. Commented Feb 2, 2022 at 19:57
  • Within double quotes, the escaping of a backslash that doesn't precede a backslash, backtick, newline, $, or " is optional (discussion). The first example can therefore be shortened by 5 characters to string="\'\\\"\(foobar\)'another'[program]\[\\\$var\]()" Commented Dec 12, 2022 at 22:37
2

With zsh instead of bash, you could do:

set -o extendedglob
print -r -- ${foo//(#b)((\\?)|?)/$match[2]}

Or with ksh93:

print -r -- "${foo//@(@(\\?)|@(?))/\2}"

(you'd think print -r -- "${foo//@(@(\\?)|?)/\2}" should also work but it doesn't, which looks like a bug)

With fish:

string join '' (string match -ar '\\\\.' $foo)
1

I don't think you can use the pattern replacement operation to find such strings, as you need to look at the previous character to know if to remove the current one. You'd need something like Perl regexes' negative look-behind to do that. It's even harder if you consider strings like \\x, where the first backslash should escape the second, but the second should not escape the x, at least far as backslash-escapes usually work.

It'd be easier to just find the matching parts in a loop, not that Bash really makes that very simple either. (There's the regex match operator [[ text =~ re ]] and the matches can be found in ${BASH_REMATCH[@]}, but I don't think there's a way to loop for multiple hits other than manually.)

But you can do it with e.g. grep. E.g. this would output the matching sequences, one per line:

foo="\'\\\"\(foobar\)'another'[program]\[\$var\]()"
echo "$foo" | grep -oe '\\.'

And you then pipe the output to tr -d '\n' to remove the newlines. Or use while IFS= read -r line; do... if you need to handle them in the shell, but if you're doing that, you probably should use some other tool; the shell doesn't bend to text processing too well.

4
  • what if the string comes as an input, then the input is given as exactly my snippet above (without the escaped backslash on the single quotes)?
    – cevhyruz
    Commented Feb 2, 2022 at 14:44
  • @cevhyruz, mm, an input from where? If you're reading it from a file, you could do read -r foo < filename and then echo "$foo" | .... Or just grep -oe ... < filename. (-r is kinda relevant here, since without it, read itself processes backslashes using its own weird rules)
    – ilkkachu
    Commented Feb 2, 2022 at 14:46
  • sorry I forgot to mention I'm parsing input from $READLINE_LINE so somehow you don't expect the user to tripple backslash a double quotes.
    – cevhyruz
    Commented Feb 2, 2022 at 14:48
  • 1
    @cevhyruz, yeah, the thing there is just about the shell syntax with double quotes. foo="\\\"", foo='\"', and foo=\\\" all set foo to the string \". All the other stuff is just about the shell quoting. If you read it from a file or have it in a variable already, then that doesn't apply, since it's not in the script code. (The same way how having a variable str with the string $xyz does not expand the value of the variable xyz)
    – ilkkachu
    Commented Feb 2, 2022 at 14:52

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .