5

I tried doing ls [a-z][a-z], but it doesn't seem to be working.

  • Names of regular files or any filename (symbolic links, names of directories, or otherwise)? – Kusalananda Oct 5 '18 at 20:27
14

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
  • You may also want to use nullglob? – Kusalananda Oct 5 '18 at 17:24
  • erm, not quite -- failglob removes the error but also removes everything (thus listing everything) if there are no matches – Jeff Schaller Oct 5 '18 at 17:29
  • a file named cc is print 2 times – ctac_ Oct 5 '18 at 19:21
  • 2
    We'll need clarification on whether a file named cc consists of "one" c :) – Jeff Schaller Oct 5 '18 at 19:24
10

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 '??'
  • 2
    with the caveat about discovering subdirectories (-maxdepth 1 would help for GNU find) – Jeff Schaller Oct 5 '18 at 19:29
  • @ Jeff Schaller here too We'll need clarification on whether we search the file in the courrant directory or anywhere :) – ctac_ Oct 5 '18 at 19:43
  • gnu ls have a option --hide=PATTERN so I try ls *c* --hide='?' --hide='???*' but it's buggy or I don't know how to use it – ctac_ Oct 5 '18 at 19:59
  • 1
    It works just like -I ... it's not buggy, it only works if you don't specify a pattern for ls (that's what they mean by implied entries - whatever ls would print by default... if you run ls *c* ... that's no longer implicit, it's explicit...) – don_crissti Oct 5 '18 at 20:07
  • 1
    With hide, this works: ls --hide='?' --hide='???*' --hide='[^c][^c]'. – Isaac Oct 7 '18 at 2:45
3

In bash:

  • Either c followed by a character (?c) or a character (including . with dotglob) followed by c (?c).

    shopt -s extglob failglob dotglob
    printf '%s\n' @(?c|c?)
    
  • All files but those that either don't contain c (!(*c*)) or are not two characters (!(??)). Here using a double-negation to achieve conjunction.

    shopt -s extglob failglob dotglob
    printf '%s\n' !(!(*c*)|!(??))
    

In zsh:

  • printf '%s\n' (?c|c?)(D)

    (where D enables dotglob; failglob (called nomatch in zsh) is on by default there).

  • double negation using the ~ except operator and ^ negation extended operators:

    set -o extendedglob
    printf '%s\n' *c*~^??(D)
    

In ksh93:

  • there's no nomatch/failglob option in ksh93, but you can do it by hand there:

    FIGNORE='@(.|..)' # only ignore . and .. and not the other dotfiles
    files=(~(N)@(?c|c?))
    if ((${#files[@]})); then
      printf '%s\n' "${files[@]}"
    else
      echo >&2 No match
      exit 1
    fi
    

    (where ~(N) enables nullglob for that glob).

  • ksh93 does have a conjunction operator. So here, you can also change the files=(...) line to:

    files=(~(N)@(*c*&??))
    
  • Maybe add a note for Nullglob as this: zsh -c 'printf "%s\n" ?m(N) m?(N) yes' – Isaac Oct 6 '18 at 23:07
  • Nitpicking: What the original poster (OP) posted: ls * prints results in one line. Your `printf %s\n`` (in all your options) prints one line per result. – Isaac Oct 6 '18 at 23:12
  • @Isaac, ++Nitpicking, ls * prints one file per line unless stdout is a tty device in which case the format is "implementation-defined" per POSIX and in practice in most implementations, printed in columns. – Stéphane Chazelas Oct 6 '18 at 23:20
  • @isaac, zsh -c 'printf "%s\n" ?m(N) m?(N) yes' would print mm twice (and may also not give a sorted list). zsh -c 'printf "%s\n" (?c|c?)(N)' would print an empty line if there was no matching file. To print no output when there's no matching file: (){ (($#)) && printf "%s\n" $@; } (m?|?m)(N) or () { printf %s $^@$'\n'; } (m?|?m)(N). It's actually annoying that there's no simpler command just to print a list of lines given as argument (you'd expect print -rl to do that but it doesn't). – Stéphane Chazelas Oct 6 '18 at 23:27
  • It would be useful both for ls and print (and probably many other utilities) to have an option of -e (for empty) to be used in scripts that will avoid doing anything on an empty list of arguments. – Isaac Oct 6 '18 at 23:42
3

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
  • bash exits in bash -c 'shopt -s failglob; echo ?m m?; echo not reached'. Note that the nomatch zsh option name comes from csh/tcsh. The behaviour is slightly different though between csh and tcsh. See the cshnullglob option in zsh to emulate csh's (or pre-Bourne shells using /etc/glob) behaviour. fish now also behaves like zsh/bash -o failglob except that failing matches don't exit the shell. – Stéphane Chazelas Oct 6 '18 at 22:35
  • Exit? Only if both commands are in one line, try bash -c $'shopt -s failglob; echo ?m m?\n echo not reached'. Or (I don't recomend this) bash -c eval\ $'shopt -s failglob; echo ?m m?\n echo not reached'. I tested it inside an script, one command per line. @StéphaneChazelas – Isaac Oct 6 '18 at 23:02
  • Hmmm, the rest seems to be too much information for one answer IMhO. As the answer is tagged bash, I believe that I will pass adding that. @StéphaneChazelas – Isaac Oct 6 '18 at 23:04
  • Technically, failglob in an interactive shell will bring you back to the prompt (unless the failed glob happened in a subshell). And it's the same in scripts, it resumes execution at the next line the shell would read. So for instance, if the failing glob happens in a function and not in a subshell there, that aborts the function and whatever other commands that were connected. It's a bit messed up. (bash also doesn't have any exception catching like zsh's always {...}). – Stéphane Chazelas Oct 6 '18 at 23:16
  • Technically, don't you think that it is useful to other users to say that bash will not (in all cases) stop executing an script. zsh stops in all cases. @StéphaneChazelas – Isaac Oct 6 '18 at 23:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.