echo \\* - bash backslash escape behavior, is it evaluated backwards?

17,128

Solution 1

If you don't have a file in the current directory whose name starts with a backslash, this is expected behaviour. Bash expands * to match any existing file names, but:

If the pattern does not match any existing filenames or pathnames, the pattern string shall be left unchanged.

Because there was no filename starting with \, the pattern was left as-is and echo is given the argument \*. This behaviour is often confusing, and some other shells, such as zsh, do not have it. You can change it in Bash using shopt -o failglob, which will then give an error as zsh does and help you diagnose the problem instead of misbehaving.

The * and ? pattern characters can appear anywhere in the word, and characters before and after are matched literally. That is why echo \\* and echo \\ * give such different output: the first matches anything that starts with \ (and fails) and the second outputs a \, and then all filenames.


The most straightforward way of getting the output you want safely is probably to use printf:

printf '\\'; printf "%s " *

echo * is unsafe in the case of unusual filenames with - or \ in them in any case.

Solution 2

1: glob

When I do echo \* I get *

This seems right, as * is escaped and taken literally.

Yes, with an escape \ (quote), an otherwise special character becomes regular and therefore is printed back as it is, a *.

But I can't understand that, when I do echo \\* I get \*

Because you need to think about "an string" to expand (via globbing). The string is \\*, which, because it is un-quoted and it has an special character (the backslash) the first backslash quotes the second and becomes one backslash followed by an asterisk (\*). The string: backslash (\) followed by anything (*) should match literally that: a file name that start with a backslash and followed by more characters. If there are no files like \a, \one or \file the globbing of \* fails, that is, it can not expand to a "list of files". If some options in bash (failglob or nullglob) are not set, the string \* remains and is printed as it was. You get \* back.

2: concatenation

I was expecting:

    $ echo \\*
    \file1 file2 file3

Above we clarified why \\* ends up being the string \*, that is: one string. There is no concatenation of two strings as in:

$ a='\'
$ b='file1 file2 file3'
$ echo "$a$b"
/file1 file2 file3

That is:you need two strings to get them concatenated. One string could be a variable of the literal \, the other could be the list of files in the present directory as an string. One way to accomplish that is:

$ set -- *               # get the list of files as arguments.
$ echo '\'"$*"           # or "\\$*".  Convert arguments to an string $*
                         # and concatenate '\' to it.
\file1 file2 file3

Solution 3

I believe that all the other answers to the second half of the question (“What command should I use?”) are wrong, if only technically or in edge cases.  I believe that this addresses the problems in the other answers:

(set -- *; echo "\\$*")

and (FWIW) all the writing is being done in a single command
(unlike printf '\\'; printf "%s " *; echo).

  • Use a subshell to avoid setting / changing the positional parameters in an outer scope / context.
  • Naively, set * sets the positional parameters ($1, $2, $3, etc…) to the names of the files in the current directory.  (Filenames beginning with . (dot) are included, or not, based on whether the dotglob option is set.)
  • set * can fail if there is a file whose name begins with - (dash) (and it is the first file in the * list).  set -- * handles this; -- says “everything after this is an argument and not an option / flag.”
  • $* is the list of positional parameters ($1 $2 $3 …), with the parameters separated by space(s).  The \\ causes \ to be output at the beginning of the line, just as in the echo \\ * (example command in the question), but it doesn’t interfere with the expansion of the subsequent $*.
  • $* is the list of positional parameters ($1 $2 $3 …), with the parameters separated by the first character of $IFS.  If there’s a possibility that the first character of $IFS might not be space, do
(set -- *; IFS=" "; echo "\\$*")

Slightly more verbose options include

(set -- *; printf "%s\n" "\\$*")

and

(set -- *; printf "\\%s\n" "$*")

These are safer in an environment where echo might interpret backslashes in the argument strings even without an -e option.

Again, add IFS=" "; if there’s a possibility that the first character of $IFS might not be space.

Solution 4

Try this:

printf "\\%s" "$(echo *)"

Explanation:

  • printf takes a format argument and zero or more arguments that are substituted into the format string
  • \\ in the format has the same meaning as in echo \\
  • %s means take the next argument and substitute it into the result
  • "$(echo *)"means: execute echo *, and put the result into a single argument to printf; it has to be put into a single argument because of how printf works

Solution 5

No, bash escape character preserves the literal value of the next character that follows, from left to right, so \\* give you pattern \*.

This pattern is performed Filename Expansion, with the Pattern Matching rules.

So \* is interpret as all files starting with \ and follows by anything. In your case it matched nothing and bash left the pattern \* unchanged to echo.

Share:
17,128

Related videos on Youtube

Weishi Z
Author by

Weishi Z

Updated on September 18, 2022

Comments

  • Weishi Z
    Weishi Z over 1 year

    So in bash, When I do

    echo \*
    *
    

    This seems right, as * is escaped and taken literally.

    But I can't understand that, when I do

    echo \\*
    \*
    

    I thought the first backslash escaped the second one thus two backslash "\\" will give me one "\" in literal. and * followed carrying its special meaning. I was expecting:

    echo \\*
    \file1 file2 file3
    

    ANSWER SUMMARY: Since \ is taken literally, echo \* will behave just as echo a*, which will find any file that starts with literal "a".

    Follow up question, If I want to print out exactly like

    \file1 file2 file3
    

    What command should I use? e.g. like the following but I want no space

    echo \\ * 
    \ file1 file2 file3
    
  • cuonglm
    cuonglm about 9 years
    Is there any shells except zsh print the error message if matching fail by default?
  • Michael Homer
    Michael Homer about 9 years
    fish, at least. Maybe others, although I don't recall off-hand.
  • Scott - Слава Україні
    Scott - Слава Україні over 5 years
    I don’t quite understand what you’re doing (i.e., what you’re accomplishing; what problem you’re trying to solve).  And, maybe, if I understood that, I’d understand how this relates to the question — but I don’t.  Please do not respond in comments; edit your answer to make it clearer and more complete. P.S. Why do you say var=`echo value` instead of var=value?
  • Scott - Слава Україні
    Scott - Слава Україні over 5 years
    (1) The question says “like … echo \\ * … but I want no space”.  Your answer fails because it doesn’t include the newline at the end.  (Of course you can fix that by adding ; echo or ; printf '\n'.)  (2) Why do you use single quotes for one string and double quotes for the other?
  • Scott - Слава Україні
    Scott - Слава Україні over 5 years
    (1) The question says “like … echo \\ * … but I want no space”.  Your answer fails because it doesn’t include the newline at the end.  … … … … … …  (Of course you can fix that by saying printf "\\%s\n" "$(echo *)".) … … …  (2) You could change \\ to \ in your answer (printf "\%s" "$(echo *)"), and it would work as well.  … … … …  … … …  … …  (3) Also, as far as I can tell, echo "\\$(echo *)" would work as well.  … (Cont’d)
  • Scott - Слава Україні
    Scott - Слава Україні over 5 years
    (Cont’d) …  (4) The real problem with this answer is that it depends on the mercurial behavior of echo. If there is a file named -e or -n (and it is the first file in the * list), it will disappear.
  • Scott - Слава Україні
    Scott - Слава Україні over 5 years
    (1) The question says “like … echo \\ * … but I want no space”. Your answer fails because it doesn’t include the newline at the end. (It seems especially odd that you missed this, since you included a trailing newline in the last line of your answer, where you’re answering a different question.) (2) Since your answer uses set, it should be put into a subshell, to avoid setting / changing the positional parameters in an outer scope / context. … (Cont’d)
  • Scott - Слава Україні
    Scott - Слава Україні over 5 years
    (Cont’d) …  (3) The question talks about echo … *, and indicates that the current directory contains file1, file2, and file3. (Arguably, we should treat these as placeholders rather than examples.) Using file* in your answer seems inappropriate. (4) So, any correct answer should include a plain *. Making the appropriate change to your answer, we get set *, which can fail if there is a file whose name begins with - (dash). … (Cont’d)
  • Scott - Слава Україні
    Scott - Слава Україні over 5 years
    (Cont’d) …  (5) Why use $@ and a literal space in the format string when you could use $* and let the shell interpolate space(s) for you? (OK, yes, I know about IFS.) (6) People less clever and tricky than you might not realize that you are using a literal space in the format string because it isn’t quoted. A good answer would make a detail like that explicit, and explain it.
  • Wildcard
    Wildcard over 4 years
    The printf command given will print a trailing space, and will not print a trailing newline. It's sort of an academic question at that point anyway, but it's worth mentioning those caveats.