How can I generate a comma-delimited, case-insensitive list of the first characters of the names of the files and directories in a directory, excluding initial "." items, without duplicates? I don't need it to be recursive - just one level deep.

For example, from a directory where ls produces…

drwxr-xr-x+ 10 bryan  staff   340B Jun  6 15:32 .
drwxrwx---@ 27 bryan  staff   918B Jun  6 15:29 ..
-rw-r--r--@  1 bryan  staff   6.0K Jun  6 15:32 .secrets
drwxr-xr-x+  2 bryan  staff    68B Jun  6 15:30 Apoptosis
drwxr-xr-x+  2 bryan  staff    68B Jun  6 15:32 Fanciful Notions
-rwxr-x---@  1 bryan  staff   351B Jun  2 16:57 Pungent
-rwxr-x---@  1 bryan  staff   351B Jun  2 16:57 Zoophilia
-rwxr-x---@  1 bryan  staff   351B Jun  2 16:57 addled_symbionts
-rwxr-x---@  1 bryan  staff   351B Jun  2 16:57 putrid
drwxr-xr-x+  2 bryan  staff    68B Jun  6 15:30 zuegma mandegreen

…I'd like to return…

a, f, p, z

I'm using BSD, am a nube, and can't quite make this work.

  • For what purpose? Is this an assignment? – Ulric Eriksson Jun 6 '15 at 23:02
  • Nope, not any sort of assignment. (That would be cheating!) I'm arbitrarily parsing directory contents into progress-reportable units of work. – Bryan Jun 6 '15 at 23:43
  • Egaads… I'm overwhelmed by the plethora of answers. Thanks, everyone, for your generosity. – Bryan Jun 8 '15 at 1:45

You could do it with the shell/coreutils:

for f in *; do printf "%s\n"  "${f:0:1}" ; done | 
    tr '[A-Z]' '[a-z]' | sort | uniq | paste -d, -s

The ${var:X:Y} syntax prints a Y-character long substring of variable $var starting at position X

|improve this answer|||||
  • Thanks. I went with Mapio's approach, but learned something from reading yours that I'm sure will come in handy eventually. – Bryan Jun 6 '15 at 23:47
  • That should be echo "${f:0:1}", in case there is a file whose name begins with *. – G-Man Says 'Reinstate Monica' Jun 7 '15 at 1:06

You can go with

ls -1 |  cut -b1 | tr '[:upper:]' '[:lower:]' | sort -u | paste -d, -s


  • ls -1 gives you a list of filenames (one per line),
  • cut -b1 takes just the first char per line,
  • tr '[:upper:]' '[:lower:]' turns all to lower cap,
  • sort -u removes the duplicate, and
  • paste -d, -s puts the lines together using , as a separator.
|improve this answer|||||
  • Nice one but note that it will break on file names with newlines. Also, -u is a GNU flag, I don't think that BSD sort will have it. Use uniq instead. – terdon Jun 6 '15 at 23:13
  • @terdon; sort -u goes back until at least SystemV R4 in the early 1980's; it certainly is no GNUism. – Janis Jun 6 '15 at 23:21
  • @terdon: I've tested it on OSX and GNU/Linux (I don't have a BSD machine at hand), but if I create a file with a newline in the name ls returns the newline as a ? – so it seems my solution will not break. Am I missing something? – Mapio Jun 6 '15 at 23:24
  • @Janis and Mapio I stand corrected, I thought it was GNU. As for the newline, yes ls will show it as a ? but if you parse it it will be a newline. foo\nbar will return an f and a b. – terdon Jun 6 '15 at 23:31
  • Thank you - particularly for the annotations.. I went with 'ls -1ash | cut -c 59 | sort -fu | paste -s -d , -' to avoid some complications with odd ball ls environment variables on some of our hosts. – Bryan Jun 6 '15 at 23:43
printf %c,\\n * | 
sort -fu        |
dd cbs=8 conv=lcase,block


  1. print the first character of every argument matched in the * glob followed by a comma and a \newline to stdout
  2. sort that stream while ignoring case and squeezing duplicates
  3. fold that stream into a single line of 8 (space-padded) chars per record while simultaneously converting all upper-case chars to lower-case

I guess I have some weird filenames in this dir that need removing, but ...

0,      1,      =,      a,      b,      c,      d,      e,      f,      g,      h,      i,      k,      l,      m,      n,      o,      p,      q,      r,      s,      t,      w,      x,      y,      z,      _,      ~,      
0+1 records in
0+1 records out
224 bytes (224 B) copied, 7.2813e-05 s, 3.1 MB/s

The last bit is dd's processing report. You can see it's pretty fast. You can drop the report with 2>/dev/null though as you like.

Optionally add another dd or fold or something to the tail of the pipeline to reign in the line length. Like:

CMD | dd cbs=80 conv=unblock


CMD | fold -w80

...which differ only a little - dd will strip trailing spaces from the folded results at 80 byte boundaries - which should render at maximum 10 results per line at 74 bytes per line, while fold will interpret chars as chars (not bytes) but also interpret backspaces and tabs in column-sensitive ways without stripping trailing spaces.

dd's twice folded results as passed through sed -n l:

0,      1,      =,      a,      b,      c,      d,      e,      f,      g,$
h,      i,      k,      l,      m,      n,      o,      p,      q,      r,$
s,      t,      w,      x,      y,      z,      _,      ~,$
|improve this answer|||||

Just for the record; a shell only solution with ksh (ksh93 required here) or (newer versions of) bash if all the external processes shall be avoided:

typeset -A a
for file in *
do typeset -l f=${file:0:1} ; a+=( [$f]= )
list=$( printf ", %s" "${!a[@]}" )
printf "%s" "${list#, }"
|improve this answer|||||
  • Note to readers: this is ksh syntax. For bash, change "typeset" to "declare" – glenn jackman Jun 7 '15 at 1:11
  • Note: Newer versions of bash support typeset. – Janis Jun 7 '15 at 7:34
  • hmm, true, but: help typeset returns ... Set variable values and attributes. Obsolete. See help declare'` – glenn jackman Jun 7 '15 at 19:54
  • That's rather strange; declared obsolete shortly after it had been introduced. - I was under the impression the shell developers tried to converge the design of their matching features. – Janis Jun 8 '15 at 5:16
#!/usr/bin/awk -f
  OFS = ", "
  for (z in ARGV)
    y[tolower(substr(ARGV[z], 1, 1))]
  for (x in y)
    $(++w) = x

Call like this

hello.awk *
|improve this answer|||||

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.