6

I'm looking for a script that counts files in the current directory (excluding sub directories). It should iterate through each file and if not a directory increment a count. The output should just be an integer representing the number of files.

I've come up with

find . -type f | wc -l

But I don't really think it does the whole counting bit. This is for an assignment so if you only want point me in the right direction that would be great.

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
user182477
  • 61
  • 1
  • 1
  • 2

9 Answers9

10

If you want only regular files,

With GNU find:

find . -maxdepth 1 -type f -printf . | wc -c

Other finds:

find . ! -name . -prune -type f -print | grep -c /

(you don't want -print | wc -l as that wouldn't work if there are file names with newline characters).

With zsh:

files=(*(ND.)); echo $#files

With ls:

ls -Anq | grep -c '^-'

To include symlinks to regular files, change -type f to -xtype f with GNU find, or -exec test -f {} \; with other finds, or . with -. with zsh, or add the -L option to ls. Note however that you may get false negatives in cases where the type of the target of the symlink can't be determined (for instance because it lies in a directory you don't have access to).

If you want any type of file (symlink, directory, pipes, devices...), not only regular one:

find . ! -name . -prune -printf . | wc -c

(change to -print | grep -c / with non-GNU find, (ND.) to (ND) with zsh, grep -c '^-' with wc -l with ls).

That will however not count . or .. (generally, one doesn't really care about those as they are always there) unless you replace -A with -a with ls.

If you want all types of files except directories, Replace -type f with ! -type d (or ! -xtype d to also exclude symlinks to directories), and with zsh, replace . with ^/, and with ls, replace grep -c '^-' with grep -vc '^d'.

If you want to exclude hidden files, add a ! -name '.*' or with zsh, remove the D or with ls, remove the A.

  • You mentioned that grep -c / is not guaranteed to work — so wouldn’t it be better, in your POSIX example, to do -exec echo ";" or -exec dirname -- {} +, and then count lines (with wc -l)?   Granted, the dirname example will crash and burn if the name of the starting directory contains newline(s) (e.g., find $'foo\nbar' …), but ! name . assumes that you’re doing find . …, doesn’t it?   I would suggest -exec stat -c "" -- {} +, but stat isn’t in POSIX either (right?). – G-Man Says 'Reinstate Monica' Aug 01 '16 at 23:31
  • @G-Man, that's theoretical. POSIX leaves the behaviour of grep unspecified on non-text input. Here, it could be non-text if filenames contain non-characters (LC_ALL=C would work around that), or if the file paths are larger than LINE_MAX. Here for one level of directory, in practice, that won't happen. Even if that happened, in practice, what I see in implementations that have a LINE_MAX limit for grep is it only looking at the first LINE_MAX bytes of the line which is OK here as the / is in 2nd position. That / would also appear before the first invalid character if any. – Stéphane Chazelas Aug 02 '16 at 09:16
  • @G-Man, stat is the archetype of the non-portable command. And among the numerous incompatible implementations, the GNU one (which you seem to be refering to with that -c syntax) is one of the worst IMO (especially considering that GNU find had a much better interface (with the -printfsyntax different from GNU stat's (!?)) long before GNU stat was created. Here you could do find . -exec printf '%.1s' {} + | wc -c, but that potentially runs more commands than the grep -c / approach and is less efficient as it means holding the data and running potentially huge command lines. – Stéphane Chazelas Aug 02 '16 at 09:22
  • I think ls -A1 |wc -l is a bit more intuitive for the ls options and will probably be a bit faster. Bring the steak sauce. – flickerfly Mar 07 '17 at 15:01
  • @flickerfly, that counts all directory entries but . and .. not only the regular files or non-directory files. That also doesn't work properly for file names with newline characters. Note that -1 is superflous with POSIX compliant ls implementations. – Stéphane Chazelas Mar 07 '17 at 15:26
3

To exclude subdirectories

find . -maxdepth 1 -type f | wc -l

This assumes that none of the filenames contain newline characters, and that your implementation of find supports the -maxdepth option.

user4556274
  • 8,995
  • 2
  • 33
  • 37
  • 4
    That doesn't work if there are filenames with newline characters. Replace wc -l with grep -c /. You can't assume a file path to be a line of text, you can't even assume it to be text at all as generally on Unix-like systems, file paths are just arrays of non-0 bytes, with nothing enforcing those bytes to form valid characters in the current locale. In that regard grep -c / is not even guaranteed to work (though in practice it generally does) as grep is only required to work properly on valid text. – Stéphane Chazelas Aug 01 '16 at 10:30
  • @StéphaneChazelas given that the context is an assignment this Answer is almost certainly sufficient. – Chris Davies Aug 01 '16 at 11:47
  • 5
    @roaima, on the contrary, if that's about teaching, you want to teach the right thing and good practice. We don't want the next generation of coders making those incorrect assumption and write unreliable/vulnerable code. – Stéphane Chazelas Aug 01 '16 at 11:51
  • Not sure of stackexchange etiquette, but choosing not to edit my answer in response to @StéphaneChazelas comment, as his own answer covers this improvement and more. – user4556274 Aug 01 '16 at 11:58
  • You could start the answer with "If you can guarantee that none of the file names will contain newline characters, and assuming your find implementation supports the (non-standard) -maxdepth predicate". IOW, point out the limitations of the solution. My initial comment served that purpose, but if I've convinced you that it is indeed a limitation worth point out, then I'll be glad that you incorporate it in your answer to give it more visibility (no need to give credit (as far as I'm concerned)). – Stéphane Chazelas Aug 01 '16 at 12:12
  • @StéphaneChazelas when ones teaches, one starts with the basics and glosses over complexities. As an example in mathematics, young children are taught only integer arithmetic. As time progresses they are taught that only positive numbers have have a square root. Yet further on, one is introduced to i. By all means provide a professionally competent solution, but I urge that "sufficient" answers should also be offered, particularly when the asker explains that they are working on an (introductory) assignment. Unlike ServerFault we don't require a minimum standard of competence to participate. – Chris Davies Aug 01 '16 at 14:05
  • 1
    @roaima. I don't agree. What first grades learn about arithmetic is correct. They don't learn that 1+1=2 only to learn later that it's incorrect and that they should forget about it. Here, find . | wc -l is incorrect or at least is only correct if you make some assumptions (which in practice you can rarely enforce). So, at the very least, those assumptions should be stated here. But IMO, it's better to teach a correct solution instead so people don't have to unlearn what they've be taught before when they move to a more advanced grade. – Stéphane Chazelas Aug 01 '16 at 14:37
3

To iterate over everything in the current directory, use

for f in ./*; do

Then use either

[ -f "$f" ]

or

test -f "$f"

to test whether the thing you got is a regular file (or a symbolic link to one) or not.

Note that this will exclude files that are sockets or special in other ways.

To just exclude directories, use

[ ! -d "$f" ]

instead.

The quoting of the variable is important as it would otherwise miscount if there, for example, exists a directory called hello world and a file called hello.

To also match hidden files (files with a leading . in their filenames), either set the dotglob shell option with shopt -s dotglob (in bash), or use

for f in ./* ./.*; do

It may be wise also to set the nullglob shell option (in bash) if you want the shell to return nothing when these glob patterns match nothing (otherwise you'll get the literal strings ./* and/or ./.* in f).

Note: I avoid giving a complete script as this is an assignment.

Kusalananda
  • 333,661
  • @JeffSchaller Thanks. I think I fixed that now. – Kusalananda Aug 01 '16 at 10:01
  • 1
    Quoting only disables the split+glob operator, it does nothing to change the meaning of - (unless - is in $IFS). test -f -x or test -f "-x" are the same thing (and are generally not a problem except in very old shells, but then you'd solve the problem with for f in ./*, not with quoting). Also note that test -f hello world would give you an error regardless of whether hello is a regular file or not (except in AT&T ksh) – Stéphane Chazelas Aug 01 '16 at 12:22
  • @StéphaneChazelas I should write all of these things down... Thanks again! I'll remove the passage about - as I'm bypassing it with ./*. – Kusalananda Aug 01 '16 at 12:26
  • Why not use dotglob to match hidden files as well? – Konrad Rudolph Aug 01 '16 at 13:26
  • @KonradRudolph You know, that's a good suggestion. In my own scripts (and I rarely write bash scripts) I tend to not rely on shopts very much as they change the behaviour of basic things so radically. But in this case, just for doing this, it's a viable alternative. I'll mention it. – Kusalananda Aug 01 '16 at 13:31
2
#!/bin/bash

# initialize counter
count=0;

# go through the whole directory listing, including hidden files
for name in * .*
do
    if [[ ! -d $name ]]
    then
        # not a directory so count it
        count=$(($count+1))
    fi
done

echo $count
1

If you want something that will handle files containing embedded newlines and other unprintable characters, this solution will work. But it's almost certainly overkill for your assignment.

find . -maxdepth 1 -type f -print0 | tr -dc '\0' | wc -c

What this does is to discard everything apart from the trailing \0 at the end of each filename. It then counts those characters, which gives the number of files.

Chris Davies
  • 116,213
  • 16
  • 160
  • 287
1

Here's a bash script that will count up and then echo the number of (non-directory) files in the current directory:

#!/usr/bin/env bash
i=0
shopt -s nullglob dotglob
for file in *
do
  [[ ! -d "$file" ]] && i=$((i+1))
done
echo "$i"

Setting "nullglob" gets the count right when there are no files (hidden or otherwise) in the current directory; leaving nullglob unset would mean that the for loop would (incorrectly) see one item: *.

Setting "dotglob" tells bash to also include hidden files (those start with a .) when globbing with *. By default, bash will not include . or .. (see: GLOBIGNORE) when generating the list of hidden files. If your assignment does not want to count hidden files, then do not set dotglob.

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
-1

I believe the simplest way to count the files and directories in the current directory is:

$ ls | wc -w

-w: is used to count words (not lines as the -l does) because ls outputs all files and directories as an empty-space separated list of words.

And to count only the files :
You can playing a little-bit with the marvelous ls flags you can get the directories out by adding a forward slash to directories ls -p make ls print the files in lines with -l and then invert grep to filter out the forward slashes:

$ ls -pl | grep -v '/' | wc -l
-3

Your command will include sub-directories.

I'd go with:

count=0
for file in $(ls -a); do 
  if [ -f $file ]; then 
    count=$(expr $count + 1)
  fi
done
echo "There are $count files on $(pwd)"
  • 2
    See http://unix.stackexchange.com/questions/128985/why-not-parse-ls – Kusalananda Aug 01 '16 at 09:13
  • @Kusalananda, ls only does (may do) that when the output goes to a tty device. Here, it's going to a pipe or socketpair depending on the shell. The issues would be more about filenames containing IFS or glob characters (try after touch '*' 'a b' for instance. – Stéphane Chazelas Aug 01 '16 at 12:46
  • @StéphaneChazelas Thanks for putting me straight again. – Kusalananda Aug 01 '16 at 12:49
-3

I usually stick to the **ls** command. It gives me all kinds of possibilities.

A basic example :

ls | wc -l 

Will list out the count of all files and directories in a path

ls -R | wc -l 

Will list out the count of all files and directories in a path and its sub-directories.

ls -R *.log | wc -l

The count of only log files in the path and also all the sub-directories.

ls -la *.log | wc -l

Will give you only the log files count in the current directory and not sub-directories.

You limit here is your ls command

'ls' also gives us the ability to filter out files only by certain date, by file formats, and a lot more, using wc in par with 'ls', is possibly the best robust solution I would consider.

Have a look at 'man ls'

voidspacexyz
  • 135
  • 7
  • See: http://unix.stackexchange.com/questions/128985/why-not-parse-ls – Kusalananda Aug 01 '16 at 12:03
  • @Kusalananda, ls -aq | wc -l would be OK as then filenames are guaranteed to be on single lines (since a \n in the filename is rendered as ?). So the first example is almost right. The rest have more issues. – Stéphane Chazelas Aug 01 '16 at 12:41
  • @StéphaneChazelas My comment was in this case a comment on the recommendation to use ls for these sort of things. It's not the tool for the job really. – Kusalananda Aug 01 '16 at 12:47