12

I am looking for a way to make a simple loop in bash over everything my directory contains, i.e. files, directories and links including hidden ones.

I will prefer if it could be specifically in bash but it has to be the most general. Of course, file names (and directory names) can have white space, break line, symbols. Everything but "/" and ASCII NULL (0×0), even at the first character. Also, the result should exclude the '.' and '..' directories.

Here is a generator of files on which the loop has to deal with :

#!/bin/bash
mkdir -p test
cd test
touch A 1 ! "hello world" \$\"sym.dat .hidden " start with space" $'\n start with a newline' 
mkdir -p ". hidden with space" $'My Personal\nDirectory'

So my loop should look like (but has to deal with the tricky stuff above):

for i in * ;
  echo ">$i<"
done

My closest try was the use of ls and bash array, but it is not working with, is:

IFS=$(echo -en "\n\b")
l=( $(ls -A .) )
for i in ${l[@]} ; do
echo ">$i<"
done
unset IFS

Or using bash arrays but the ".." directory is not exclude:

IFS=$(echo -en "\n\b")
l=( [[:print:]]* .[[:print:]]* )
for i in ${l[@]} ; do
echo ">$i<"
done
unset IFS
iqstatic
  • 2,322
  • 3
  • 21
  • 39
Sigmun
  • 1,002
  • 2
  • 12
  • 23
  • What is the use case? When would you need to include `.` and `..` in the list? – l0b0 Oct 15 '14 at 11:58
  • 1
    `While read line; do; echo $line; done <<<$(ls -a) ` should work i think – Ashish Oct 15 '14 at 11:59
  • Duplicate ? : http://stackoverflow.com/questions/2135770/bash-for-loop-with-wildcards-and-hidden-files ? – P.P Oct 15 '14 at 12:03
  • If you have to modify IFS (you don't here if you follow Ashish' comment), then I advice you to open a subshell (parenthesis) to avoid any side-effect: ( IFS='stuff' ; do; do ) – mcoolive Oct 15 '14 at 14:56
  • @l0b0 On the contrary, I need to avoid them (because of recursive call) – Sigmun Oct 15 '14 at 15:03
  • @Ashish This doesn't work, alas ! – Sigmun Oct 15 '14 at 15:07
  • @BlueMoon This is not enougth, `shopt -s dotglob` but doesn't deal with newline character in filenames – Sigmun Oct 15 '14 at 15:08
  • Sorry i added extra ; there after do `while read line ; do echo $line ; done <<< $(ls -a | grep -v -w ".") ` this worked in my case when i tried on rhel machine – Ashish Oct 15 '14 at 15:22

4 Answers4

28

* doesn't match files beginning with ., so you just need to be explicit:

for i in * .[^.]*; do
    echo ">$i<"
done

.[^.]* will match all files and directories starting with ., followed by a non-. character, followed by zero or more characters. In other words, it's like the simpler .*, but excludes . and ... If you need to match something like ..foo, then you might add ..?* to the list of patterns.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • A loop is the wrong way to go through a directory structure though - you have to use recursion. – GodEater Oct 15 '14 at 14:43
  • 1
    `bash` 4 can use recursive patterns like `**/*` to iterate through the directory hierarchy. (`shopt -s globstar` enables `**`, which represents 0 or more directories in a matching path). – chepner Oct 15 '14 at 14:51
  • The file substitution doesn't work if the number of files is too big – mcoolive Oct 15 '14 at 14:54
  • `bash` iterates over the pattern internally; too many files is only an issue for building argument lists to pass to external commands (e.g., `ls *` can fail if `*` produces too many files to fit in the space allocated for the argument list to pass to `ls`). – chepner Oct 15 '14 at 14:54
  • 1
    I agree with your approach @chepner, your method is much better approach then my answer + 1Up for your answer – Ashish Oct 15 '14 at 15:35
  • @chepner - amazing - I hadn't come across that pattern in bash's documentation - thanks for pointing it out! – GodEater Oct 16 '14 at 13:19
1

As chepner noted in the comments below, this solution assumes you're running GNU bash along with GNU find GNU sort...

GNU find can be prevented from recursing into subdirectories with the -maxdepth option. Then use -print0 to end every filename with a 0x00 byte instead of the newline you'd usually get from -print.

The sort -z sorts the filenames between the 0x00 bytes.

Then, you can use sed to get rid of the dot and dot-dot directory entries (although GNU find seems to exclude the .. already).

I also used sed to get read of the ./ in front of every filename. basename could do that too, but older systems didn't have basename, and you might not trust it to handle the funky characters right.

(These sed commands each required two cases: one for a pattern at the start of the string, and one for the pattern between 0x00 bytes. These were so ugly I split them out into separate functions.)

The read command doesn't have a -z or -0 option like some commands, but you can fake it with -d "" and blanking the IFS environment variable.

The additional -r option prevents a backslash-newline combo from being interpreted as a line continuation. (A file called backslash\\nnewline would otherwise be mangled to backslashnewline.) It might be worth seeing if other backslash-combos get interpreted as escape sequences.

remove_dot_and_dotdot_dirs()
{
    sed \
      -e 's/^[.]\{1,2\}\x00//' \
      -e 's/\x00[.]\{1,2\}\x00/\x00/g'
}

remove_leading_dotslash()
{
    sed \
      -e 's/^[.]\///' \
      -e 's/\x00[.]\//\x00/g'
}

IFS=""
find . -maxdepth 1 -print0 |
  sort -z |
  remove_dot_and_dotdot_dirs |
  remove_leading_dotslash |
  while read -r -d "" filename
  do
      echo "Doing something with file '${filename}'..."
  done
Kevin J. Chase
  • 3,856
  • 4
  • 21
  • 43
  • This is the best answer at the moment. I was looking for something more "bash" based (using arrays for example) but it seems to be less portable than this one. Thanks – Sigmun Oct 16 '14 at 12:20
  • This isn't portable: it requires GNU `find` for `-print0` and GNU `sort` for `-z`. Also, `read -d` is a `bash` extension as well. – chepner Oct 16 '14 at 17:26
  • @chepner: I see that as OK, because the question itself was also non-portable. Escaping or otherwise handling shell metacharacters depends heavily on what shell you're running. (Also, this question was tagged [bash].) I did allude to GNU `find`, but not clearly... I'll make that more explicit. – Kevin J. Chase Oct 16 '14 at 21:55
0

Try the find command, something like:

find .

That will list all the files in all recursive directories.

To output only files excluding the leading . or .. try:

find . -type f -printf %P\\n
10k3y3
  • 1
  • 2
  • This include the `.` and `..` path but also doesn't deal with space nor newline character in filenames – Sigmun Oct 15 '14 at 15:10
  • I would amend that to `find . -type f`, since the OP is interested in files, not directories and other entities.... – twalberg Oct 15 '14 at 15:24
  • @twalberg Intersting but it gives additional "./" before every filename output – Ashish Oct 15 '14 at 15:33
  • @Ashish That can easily be fixed by using `sed`... e.g. `find . -type f | sed -e 's;^./;;'`. – twalberg Oct 15 '14 at 15:38
  • Yes Thanks, I noticed as soon as i commented back – Ashish Oct 15 '14 at 15:40
  • I know that find + sed can be the solution, but I found it not proper enough (in particular to deal with space and newline in filenames). I guess bash can provide me something easy to loop over a directory. – Sigmun Oct 16 '14 at 07:36
0

It may not be the most favorable way but I tried bellow thing

while read line ; do echo $line; done <<< $(ls -a | grep -v -w ".")

check the below trail which I did Check the output

Ashish
  • 1,856
  • 18
  • 30