How do I display file names that contain two characters and one of them is c?

With bash, set the glob settings so that missing matches don't trigger an error:

shopt -u failglob  # avoid failure report (and discarding the whole line).
shopt -s nullglob  # remove (erase) non-matching globs.
ls ?c c?

Question-mark is a glob character representing a single character. Since you want two-character filenames, one of them has to be a c, and so it's either the first character or the last character.

With shopt -s dotglob this would also surface a file named .c.

If there are no matching files, setting these shell options causes all of the arguments to be removed, resulting in a bare ls -- listing anything/everything by default.

Use this, instead:

shopt -s nullglob  ## drop any missing globs
set -- ?c c?       ## populate the $@ array with (any) matches
if [ $# -gt 0 ]    ## if there are some, list them
  ls -d "$@"
fi

I think the better way is to use find :

find . -type f -name '*c*' -name '??'

That will search recursively. To list only files in the current directory:

find . ! -name . -prune  -type f -name '*c*' -name '??'

If the files exist (and have no escaped characters like \c), you can use a glob:

echo ?c c?

that will match files that have one character (?) followed by a c or that have a c followed by one character (?).

But will fail with file names that start with a dash like -n or -e or with backslash characters like \c or \n with echo as both -e and -n are special to (some shells) echo and \c or \n would be interpreted as an escape sequence (\c ends output and \n prints a new line, not the verbatim characters \n in some shell implementation of echo and with bash when the option shopt -s xpg_echo is set). Other applications or utilities (like ls) will have some other options and may fail with many other dash-started or interpret other escape characters in file names.

Will also list a file named cc twice.

  • If a file may start with a dash (like -n), use:

    $ ls -d -- ?n 
    -n 
    

    Or, better:

    $ ls -d ./?n
    ./-n
    

Caveats

  • Glob match no file.

    1. If the files do not exist, the glob will not be expanded and the glob will be printed in their original form.

      $ echo ?m m?
      ?m m?
      
    2. Except in zsh.

      $ zsh -c 'echo ?m m?'
      zsh:1: no matches found: ./m?
      

      The shell will exit with an error.
      This behavior is controlled by Zsh's nomatch option.

      $ zsh -c 'setopt +o nomatch; echo ?m m?'
      ?m m?
      

      Or:

      $ zsh -c 'echo ?m(N) m?(N)'
      ?m m?
      

      But that will print mm twice.

    3. In bash you could get a similar result to zsh if the shell option failglob is set:

      $ bash -c 'shopt -s failglob; echo ?m m?'
      bash: no match: ?m
      

      But the script will not stop, the shell will not exit. Well, technically, the line where the glob is used is not executed further but execution resumes on next script line.

    4. In bash you could set the option nullglob to remove globs that don't match:

      $ bash -c 'shopt -s nullglob; echo ?m m? done'
      done
      
  • Using ls (similar with other programs)

    1. With matching files there will be no problem, but will also match (and list) directories:

      $ ls ?c c?
      bc  cz  sc
      
      ac:
      hjk
      

      Beter use -d with ls (directories will be included but not expanded).

      $ ls -d ?c c?
      ac bc cz sc
      
    2. Globs failing to match a file (or a directory)

      Then ls will report a failure

      $ ls ?m m?
      ls: cannot access '?m': No such file or directory
      

      Other programs might (probably) do not do similar checks.

    3. Using nullglob with bash will give an empty list to ls, so, it will list the contents or the pwd as if only ls was executed:

      $ ls ?m m?
      ac a.out cz 3 b sc ab bc
      

      That could be avoided using ./

      $ ls -d ./?m ./m?
      ls: cannot access './?m': No such file or directory
      ls: cannot access './m?': No such file or directory
      

Or you can use a find regex (but it will traverse subdirectories with prune missing) (Note that this will not repeat a file called cc):

    $ find . ! -name . -prune -regex '.*/\(.c\|c.\)'
    ./ac
    ./cc
    ./sc
    ./bc
    ./cz