Simple bash recursion ambiguity - bash

I'm writing very simple bash script to change encoding of
.html files and want to handle directories recursively.
The function working properly for first level directory only.
Can you tell me where I'm wrong? Here is me code.
#!/bin/bash
handleFiles () {
local REGEXP='.+\.html$'
echo $1
for f in $1/*
do
if [[ -d $f ]]
then
handleFiles "$f"
elif [[ $f =~ $REGEXP ]]
then
echo "Converting $f"
enconv -L bg -x UTF-8 "$f"
fi
done
}
# The script show all .html files in test
# enter into subdirectory but not working ..
handleFiles "test"
Here is structure of test directory:
test$ tree
.
├── test.html
└── Untitled Folder
└── test1.html
1 directory, 2 files
When I run the script I get following output:
./converter.sh
test
Converting test/test.html
test/Untitled Folder
To be the whole story I post my final solution. I hope this will be useful to someone with a similar problem.
#!/bin/bash
########################################################
# This bash script assume directory as a argument
# and convert all .html,js and xml files from
# windows-1251 encoding into utf-8 encoding.
# #author Georgi Naumov
# #email gonaumov#gmail.com for contacts and
# suggestions.
########################################################
if [[ $# -ne 1 ]] ; then
echo "Usage $0 <<directory to change encoding reqursively>>"
exit 1
fi
handleFiles () {
local REGEXP='.+\.(html|js|xml)$'
for f in "$1"/*
do
if [[ -d "$f" ]]
then
handleFiles "$f"
elif [[ "$f" =~ $REGEXP ]]
then
echo "Converting $f"
enconv -L bg -x UTF-8 "$f"
fi
done
}
handleFiles "$1"

The whole approach is rather more complicated than necessary. I recommend using find:
find test -name '*.html' -exec enconv -L bg -x UTF-8 '{}' \;
If you want to do it manually, you have to put $f in double quotes everywhere (i.e., "$f"), or it will break if a directory contains spaces, as you have noticed, because the shell will expand it into two (or more, as the case may be) separate tokens.

Quote also $1 to handle whitespaces (e.g. Untitled Folder):
for f in "$1"/*

Related

How to pass files to a script that processes folders

So I have this bash script which will rename all the files from the current directory. I need help modifying it so I can instead specify only certain files which will be renamed, but also still have the ability to pass it a directory instead. I'm not super familiar with bash so it's fairly confusing to me.
#!/bin/bash
#
# Filename: rename.sh
# Description: Renames files and folders to lowercase recursively
# from the current directory
# Variables: Source = x
# Destination = y
#
# Rename all directories. This will need to be done first.
#
# Process each directory’s contents before the directory itself
for x in `find * -depth -type d`;
do
# Translate Caps to Small letters
y=$(echo $x | tr '[A-Z]' '[a-z]');
# check if directory exits
if [ ! -d $y ]; then
mkdir -p $y;
fi
# check if the source and destination is the same
if [ "$x" != "$y" ]; then
# check if there are files in the directory
# before moving it
if [ $(ls "$x") ]; then
mv $x/* $y;
fi
rmdir $x;
fi
done
#
# Rename all files
#
for x in `find * -type f`;
do
# Translate Caps to Small letters
y=$(echo $x | tr '[A-Z]' '[a-z]');
if [ "$x" != "$y" ]; then
mv $x $y;
fi
done
exit 0
Your script has a large number of beginner errors, but the actual question in the title has some merit.
For a task like this, I would go for a recursive solution.
tolower () {
local f g
for f; do
# If this is a directory, process its contents first
if [ -d "$f" ]; then
# Recurse -- run the same function over directory entries
tolower "$f"/*
fi
# Convert file name to lower case (Bash 4+)
g=${f,,}
# If lowercased version differs from original, move it
if [ "${f##*/}" != "${g##*/}" ]; then
mv "$f" "$g"
fi
done
}
Notice how variables which contain file names always need to be quoted (otherwise, your script will fail on file names which contain characters which are shell metacharacters) and how Bash has built-in functionality for lowercasing a variable's value (in recent versions).
Also, tangentially, don't use ls in scripts and try http://shellcheck.net/ before asking for human debugging help.

bash script - ignoring whitespaces in script parameters

I'm quite new to bash scripting and I've ran out of ideas in my homework script.
The script takes 4 arguments - pathToDirectory, [-c/-m/-r] == copy/move/remove, ext1 and ext2 (example of running a script: script.sh /home/user/somefolder -c a.txt b.sh ).
The script should find all files in /home/user/someFolder (and its all subfolders) that contain 'a.txt' in their names and (in -c and -m case) rename that 'a.txt' part to 'b.sh' and depending on -c/-m argument either create a new file or just rename an existing file (in -r case it just removes the file) and then write in stdout something like 'old name => new name'.
example output of a script mentioned above:
/home/user/someFolder/bbb.txt => /home/user/someFolder/bba.txt
Well, that was not a problem to implement, everything worked until I posted my code to our upload system (evaluates our script).
The very first Upload System's try to run my script looked like "script.sh /something/graph 1 -c .jpg .jpeg".
The problem now is, that the whole '/something/graph 1' is a path and that whitespace before '1' ruins it all.
expected output: ./projekty/graph 1.jpg => ./projekty/graph 1.jpeg
my script output: ./projekty/graph => ./projekty/graph.jpeg
1.jpg => 1.jpeg
What I have so far:
if [ "$2" = "-r" ]; then
for file in $(find $1 -name "*$3"); do
echo $file
rm -f $file
done
elif [ "$2" = "-c" ]; then
for file in $(find "$1" -name "*$3") ; do
cp "$file" "${file//$3/$4}"
echo $file "=>" ${file%$3}$4
done
elif [ "$2" = "-m" ]; then
for file in $(find $1 -name "*$3"); do
mv "$file" "${file//$3/$4}"
echo $file "=>" ${file%$3}$4
done
else
echo Unknown parameter >&2
fi
My tried&notworking&probablystupid idea: as the -r/-c/-m parameter should be at $2, I was able to detect that $2 is something else (assumpting something that still belongs to the path) and append that $2 thing to $1, so then I had a variable DIR which was the whole path. Using shift I moved all parameters to the left (because of the whitespace, the -r/-m/-c parameter was not on $2 but on $3, so I made it $2 again) and then the code looked like: (just the -c part)
DIR=$1
if [ "$2" != "-r" ] && [ "$2" != "-c" ] && [ "$2" != "-m" ]; then
DIR+=" $2"
shift
fi
if [ "$2" = "-c" ]; then
for file in $(find "$DIR" -name "*$3") ; do
cp "$file" "${file//$3/$4}"
echo $file "=>" ${file%$3}$4
done
fi
when i echoed "$DIR", it showed the whole path (correctly), but it still didn't work..
Is there any other/better/any way how to fix this please ? :/
Thanks in advance !
As the target string needs to be replaced only at the very end of a filename, "${file//$3/$4}" is a bad idea.
Example: ./projekty/graph.jpg.jpg.jpg.graph.jpg
Passing a string prone to unquoted expansion to a loop is a no better idea either.
The fact is that find works as expected and its output looks like:
./projekty/graph 1.jpg
But inside a loop it is expanded incorrectly:
./projekty/graph
1.jpg
To avoid this, you can save the output of find to a variable and then tokenize it until no text is left:
list="$(find $1 -name "*$3")"
while [ -n "$list" ]; do
file="${list%%$'\n'*}"
list="${list#$file}"
list="${list#$'\n'}"
# your commands here
# ...
done

How do I detect specific files and use ImageMagick to make thumbnail while in a loop

I need to convert a lot of PNG files in many folders and process crop files separately to make thumbnails 100x100 px for just the "crop" files.
File naming is:
????_thumb.png
????_snapshot.png
????_crop.png
where ???? is a number.
My script so far is working to do conversions just fine,
however I need to detect when a "crop" file is reached and
then call ImageMagick and create a 100x100px thumbnail from it named ????_crop_th.png
I can't seem to figure how to detect on a wildcard ????_crop.png.
My script so far:
#!/bin/bash
BASE64=/root/scripts/base64
logfile=/root/tester/convert_failed.txt
goodfile=/root/tester/goodfile.txt
proc_dir=/root/tester/testing
temp_file=/root/tester/temp.png
b64=/root/tester/b64.txt
cd $proc_dir
for i in *
do
if [ -d $i ]
then
for j in $i/*.png
do
if [ -f $j ]
then
#just get files name without extension
fname=`echo $j | cut -d'.' -f1`
#perform operations
cp $j ${fname}.b64
$BASE64/base64 -d $j $temp_file
if [ $ -eq 0 ]
then
cp $temp_file $j
echo $j >> $goodfile
rm -f ${fname}.b64
fi
fi
done
fi
done
`find $proc_dir -name *.b64 -print >$b64`
sort $logfile -o $logfile
sort $goodfile -o $goodfile
sort $b64 -o $b64
Any help is well appreciated.
There are a few things that your script does less-than-perfectly, as well as some redundancies.
I also don't see anything in your script that uses Imagemagick to generate thumbnails from *_crop.png files, which is theoretically what this question is about
I vote for a rewrite. I have no idea if the following will be directly applicable to your situation, but the techniques should at least let you write better shell scripts.
#!/bin/bash
base64=/root/scripts/base64/base64
logfile=/root/tester/convert_failed.log
goodfile=/root/tester/goodfile.txt
proc_dir=/root/tester/testing
# The `cd` command will fail, if it fails. (Really.)
if cd "$proc_dir"; then
# Find all the PNGs in all subdirectories one level under our WD
for file in */*.png; do
# Do stuff (I have no idea what this is for...)
if $base64 -d "$file" "${file%.png}".b64 && mv "${file%.png}".b64 "$file"; then
echo "$file" >> $goodfile
else
printf '[%s] FAILED: %s\n' "${date '+%Y-%m-%d %T')" "$file" >> $logfile
fi
# Only make thumbnails if we need them
if [[ $file =~ _crop.png$ ]] && [[ ! -f "${file%_crop.png}_thumb.png" ]]; then
convert "$file" -scale 100x100 "${file%_crop.png}_thumb.png"
fi
done
fi
You can use regex matching or trailing substring removal, such as:
if [[ "$j" =~ _crop.png$ ]]
or
if [[ "${j%_crop.png}" != "$j" ]]
Also note that chopping off the extension is similarly easy:
fname=${j%.*}
Another useful bash feature is recursive globbing, so you don't need the nested loops and the specialized directory handling:
shopt -s globstar
for j in **/*.png

How to test filename expansion result in bash?

I want to check whether a directory has files or not in bash.
My code is here.
for d in {,/usr/local}/etc/bash_completion.d ~/.bash/completion.d
do
[ -d "$d" ] && [ -n "${d}/*" ] &&
for f in $d/*; do
[ -f "$f" ] && echo "$f" && . "$f"
done
done
The problem is that "~/.bash/completion.d" has no file.
So, $d/* is regarded as simple string "~/.bash/completion.d/*", not empty string which is result of filename expansion.
As a result of that code, bash tries to run
. "~/.bash/completion.d/*"
and of course, it generates error message.
Can anybody help me?
If you set the nullglob bash option, through
shopt -s nullglob
then globbing will drop patterns that don't match any file.
# NOTE: using only bash builtins
# Assuming $d contains directory path
shopt -s nullglob
# Assign matching files to array
files=( "$d"/* )
if [ ${#files[#]} -eq 0 ]; then
echo 'No files found.'
else
# Whatever
fi
Assignment to an array has other benefits, including desirable (correct!) handling of filenames/paths containing white-space, and simple iteration without using a sub-shell, as the following code does:
find "$d" -type f |
while read; do
# Process $REPLY
done
Instead, you can use:
for file in "${files[#]}"; do
# Process $file
done
with the benefit that the loop is run by the main shell, meaning that side-effects (such as variable assignment, say) made within the loop are visible for the remainder of script. Of course, it's also way faster, if performance is an issue.
Finally, an array can also be inserted in command line arguments (without splitting arguments containing white-space):
$ md5sum fileA "${files[#]}" fileZ
You should always attempt to correctly handle files/paths containing white-space, because one day, they will happen!
You could use find directly in the following way:
for f in $(find {,/usr/local}/etc/bash_completion.d ~/.bash/completion.d -maxdepth 1 -type f);
do echo $f; . $f;
done
But find will print a warning if some of the directory isn't found, you can either put a 2> /dev/null or put the find call after testing if the directories exist (like in your code).
find() {
for files in "$1"/*;do
if [ -d "$files" ];then
numfile=$(ls $files|wc -l)
if [ "$numfile" -eq 0 ];then
echo "dir: $files has no files"
continue
fi
recurse "$files"
elif [ -f "$files" ];then
echo "file: $files";
:
fi
done
}
find /path
Another approach
# prelim stuff to set up d
files=`/bin/ls $d`
if [ ${#files} -eq 0 ]
then
echo "No files were found"
else
# do processing
fi

Checking from shell script if a directory contains files

From a shell script, how do I check if a directory contains files?
Something similar to this
if [ -e /some/dir/* ]; then echo "huzzah"; fi;
but which works if the directory contains one or several files (the above one only works with exactly 0 or 1 files).
Three best tricks
shopt -s nullglob dotglob; f=your/dir/*; ((${#f}))
This trick is 100% bash and invokes (spawns) a sub-shell. The idea is from Bruno De Fraine and improved by teambob's comment.
files=$(shopt -s nullglob dotglob; echo your/dir/*)
if (( ${#files} ))
then
echo "contains files"
else
echo "empty (or does not exist or is a file)"
fi
Note: no difference between an empty directory and a non-existing one (and even when the provided path is a file).
There is a similar alternative and more details (and more examples) on the 'official' FAQ for #bash IRC channel:
if (shopt -s nullglob dotglob; f=(*); ((${#f[#]})))
then
echo "contains files"
else
echo "empty (or does not exist, or is a file)"
fi
[ -n "$(ls -A your/dir)" ]
This trick is inspired from nixCraft's article posted in 2007. Add 2>/dev/null to suppress the output error "No such file or directory".
See also Andrew Taylor's answer (2008) and gr8can8dian's answer (2011).
if [ -n "$(ls -A your/dir 2>/dev/null)" ]
then
echo "contains files (or is a file)"
else
echo "empty (or does not exist)"
fi
or the one-line bashism version:
[[ $(ls -A your/dir) ]] && echo "contains files" || echo "empty"
Note: ls returns $?=2 when the directory does not exist. But no difference between a file and an empty directory.
[ -n "$(find your/dir -prune -empty)" ]
This last trick is inspired from gravstar's answer where -maxdepth 0 is replaced by -prune and improved by phils's comment.
if [ -n "$(find your/dir -prune -empty 2>/dev/null)" ]
then
echo "empty (directory or file)"
else
echo "contains files (or does not exist)"
fi
a variation using -type d:
if [ -n "$(find your/dir -prune -empty -type d 2>/dev/null)" ]
then
echo "empty directory"
else
echo "contains files (or does not exist or is not a directory)"
fi
Explanation:
find -prune is similar than find -maxdepth 0 using less characters
find -empty prints the empty directories and files
find -type d prints directories only
Note: You could also replace [ -n "$(find your/dir -prune -empty)" ] by just the shorten version below:
if [ `find your/dir -prune -empty 2>/dev/null` ]
then
echo "empty (directory or file)"
else
echo "contains files (or does not exist)"
fi
This last code works most of the cases but be aware that malicious paths could express a command...
The solutions so far use ls. Here's an all bash solution:
#!/bin/bash
shopt -s nullglob dotglob # To include hidden files
files=(/some/dir/*)
if [ ${#files[#]} -gt 0 ]; then echo "huzzah"; fi
How about the following:
if find /some/dir/ -maxdepth 0 -empty | read v; then echo "Empty dir"; fi
This way there is no need for generating a complete listing of the contents of the directory. The read is both to discard the output and make the expression evaluate to true only when something is read (i.e. /some/dir/ is found empty by find).
Try:
if [ ! -z `ls /some/dir/*` ]; then echo "huzzah"; fi
Take care with directories with a lot of files! It could take a some time to evaluate the ls command.
IMO the best solution is the one that uses
find /some/dir/ -maxdepth 0 -empty
# Works on hidden files, directories and regular files
### isEmpty()
# This function takes one parameter:
# $1 is the directory to check
# Echoes "huzzah" if the directory has files
function isEmpty(){
if [ "$(ls -A $1)" ]; then
echo "huzzah"
else
echo "has no files"
fi
}
DIR="/some/dir"
if [ "$(ls -A $DIR)" ]; then
echo 'There is something alive in here'
fi
Could you compare the output of this?
ls -A /some/dir | wc -l
This may be a really late response but here is a solution that works. This line only recognizes th existance of files! It will not give you a false positive if directories exist.
if find /path/to/check/* -maxdepth 0 -type f | read
then echo "Files Exist"
fi
# Checks whether a directory contains any nonhidden files.
#
# usage: if isempty "$HOME"; then echo "Welcome home"; fi
#
isempty() {
for _ief in $1/*; do
if [ -e "$_ief" ]; then
return 1
fi
done
return 0
}
Some implementation notes:
The for loop avoids a call to an external ls process. It still reads all the directory entries once. This can only be optimized away by writing a C program that uses readdir() explicitly.
The test -e inside the loop catches the case of an empty directory, in which case the variable _ief would be assigned the value "somedir/*". Only if that file exists will the function return "nonempty"
This function will work in all POSIX implementations. But be aware that the Solaris /bin/sh doesn't fall into that category. Its test implementation doesn't support the -e flag.
This tells me if the directory is empty or if it's not, the number of files it contains.
directory="/some/dir"
number_of_files=$(ls -A $directory | wc -l)
if [ "$number_of_files" == "0" ]; then
echo "directory $directory is empty"
else
echo "directory $directory contains $number_of_files files"
fi
ZSH
I know the question was marked for bash; but, just for reference, for zsh users:
Test for non-empty directory
To check if foo is non-empty:
$ for i in foo(NF) ; do ... ; done
where, if foo is non-empty, the code in the for block will be executed.
Test for empty directory
To check if foo is empty:
$ for i in foo(N/^F) ; do ... ; done
where, if foo is empty, the code in the for block will be executed.
Notes
We did not need to quote the directory foo above, but we can do so if we need to:
$ for i in 'some directory!'(NF) ; do ... ; done
We can also test more than one object, even if it is not a directory:
$ mkdir X # empty directory
$ touch f # regular file
$ for i in X(N/^F) f(N/^F) ; do echo $i ; done # echo empty directories
X
Anything that is not a directory will just be ignored.
Extras
Since we are globbing, we can use any glob (or brace expansion):
$ mkdir X X1 X2 Y Y1 Y2 Z
$ touch Xf # create regular file
$ touch X1/f # directory X1 is not empty
$ touch Y1/.f # directory Y1 is not empty
$ ls -F # list all objects
X/ X1/ X2/ Xf Y/ Y1/ Y2/ Z/
$ for i in {X,Y}*(N/^F); do printf "$i "; done; echo # print empty directories
X X2 Y Y2
We can also examine objects that are placed in an array. With the directories as above, for example:
$ ls -F # list all objects
X/ X1/ X2/ Xf Y/ Y1/ Y2/ Z/
$ arr=(*) # place objects into array "arr"
$ for i in ${^arr}(N/^F); do printf "$i "; done; echo
X X2 Y Y2 Z
Thus, we can test objects that may already be set in an array parameter.
Note that the code in the for block is, obviously, executed on every directory in turn. If this is not desirable then you can simply populate an array parameter and then operate on that parameter:
$ for i in *(NF) ; do full_directories+=($i) ; done
$ do_something $full_directories
Explanation
For zsh users there is the (F) glob qualifier (see man zshexpn), which matches "full" (non-empty) directories:
$ mkdir X Y
$ touch Y/.f # Y is now not empty
$ touch f # create a regular file
$ ls -dF * # list everything in the current directory
f X/ Y/
$ ls -dF *(F) # will list only "full" directories
Y/
The qualifier (F) lists objects that match: is a directory AND is not empty. So, (^F) matches: not a directory OR is empty. Thus, (^F) alone would also list regular files, for example. Thus, as explained on the zshexp man page, we also need the (/) glob qualifier, which lists only directories:
$ mkdir X Y Z
$ touch X/f Y/.f # directories X and Y now not empty
$ for i in *(/^F) ; do echo $i ; done
Z
Thus, to check if a given directory is empty, you can therefore run:
$ mkdir X
$ for i in X(/^F) ; do echo $i ; done ; echo "finished"
X
finished
and just to be sure that a non-empty directory would not be captured:
$ mkdir Y
$ touch Y/.f
$ for i in Y(/^F) ; do echo $i ; done ; echo "finished"
zsh: no matches found: Y(/^F)
finished
Oops! Since Y is not empty, zsh finds no matches for (/^F) ("directories that are empty") and thus spits out an error message saying that no matches for the glob were found. We therefore need to suppress these possible error messages with the (N) glob qualifier:
$ mkdir Y
$ touch Y/.f
$ for i in Y(N/^F) ; do echo $i ; done ; echo "finished"
finished
Thus, for empty directories we need the qualifier (N/^F), which you can read as: "don't warn me about failures, directories that are not full".
Similarly, for non-empty directories we need the qualifier (NF), which we can likewise read as: "don't warn me about failures, full directories".
dir_is_empty() {
[ "${1##*/}" = "*" ]
}
if dir_is_empty /some/dir/* ; then
echo "huzzah"
fi
Assume you don't have a file named * into /any/dir/you/check, it should work on bash dash posh busybox sh and zsh but (for zsh) require unsetopt nomatch.
Performances should be comparable to any ls which use *(glob), I guess will be slow on directories with many nodes (my /usr/bin with 3000+ files went not that slow), will use at least memory enough to allocate all dirs/filenames (and more) as they are all passed (resolved) to the function as arguments, some shell probably have limits on number of arguments and/or length of arguments.
A portable fast O(1) zero resources way to check if a directory is empty would be nice to have.
update
The version above doesn't account for hidden files/dirs, in case some more test is required, like the is_empty from Rich’s sh (POSIX shell) tricks:
is_empty () (
cd "$1"
set -- .[!.]* ; test -f "$1" && return 1
set -- ..?* ; test -f "$1" && return 1
set -- * ; test -f "$1" && return 1
return 0 )
But, instead, I'm thinking about something like this:
dir_is_empty() {
[ "$(find "$1" -name "?*" | dd bs=$((${#1}+3)) count=1 2>/dev/null)" = "$1" ]
}
Some concern about trailing slashes differences from the argument and the find output when the dir is empty, and trailing newlines (but this should be easy to handle), sadly on my busybox sh show what is probably a bug on the find -> dd pipe with the output truncated randomically (if I used cat the output is always the same, seems to be dd with the argument count).
Taking a hint (or several) from olibre's answer, I like a Bash function:
function isEmptyDir {
[ -d $1 -a -n "$( find $1 -prune -empty 2>/dev/null )" ]
}
Because while it creates one subshell, it's as close to an O(1) solution as I can imagine and giving it a name makes it readable. I can then write
if isEmptyDir somedir
then
echo somedir is an empty directory
else
echo somedir does not exist, is not a dir, is unreadable, or is not empty
fi
As for O(1) there are outlier cases: if a large directory has had all or all but the last entry deleted, "find" may have to read the whole thing to determine whether it's empty. I believe that expected performance is O(1) but worst-case is linear in the directory size. I have not measured this.
I am surprised the wooledge guide on empty directories hasn't been mentioned. This guide, and all of wooledge really, is a must read for shell type questions.
Of note from that page:
Never try to parse ls output. Even ls -A solutions can break (e.g. on HP-UX, if you are root, ls -A does
the exact opposite of what it does if you're not root -- and no, I can't make up something that
incredibly stupid).
In fact, one may wish to avoid the direct question altogether. Usually people want to know whether a
directory is empty because they want to do something involving the files therein, etc. Look to the larger
question. For example, one of these find-based examples may be an appropriate solution:
# Bourne
find "$somedir" -type f -exec echo Found unexpected file {} \;
find "$somedir" -maxdepth 0 -empty -exec echo {} is empty. \; # GNU/BSD
find "$somedir" -type d -empty -exec cp /my/configfile {} \; # GNU/BSD
Most commonly, all that's really needed is something like this:
# Bourne
for f in ./*.mpg; do
test -f "$f" || continue
mympgviewer "$f"
done
In other words, the person asking the question may have thought an explicit empty-directory test was
needed to avoid an error message like mympgviewer: ./*.mpg: No such file or directory when in fact no
such test is required.
Small variation of Bruno's answer:
files=$(ls -1 /some/dir| wc -l)
if [ $files -gt 0 ]
then
echo "Contains files"
else
echo "Empty"
fi
It works for me
With some workaround I could find a simple way to find out whether there are files in a directory. This can extend with more with grep commands to check specifically .xml or .txt files etc. Ex : ls /some/dir | grep xml | wc -l | grep -w "0"
#!/bin/bash
if ([ $(ls /some/dir | wc -l | grep -w "0") ])
then
echo 'No files'
else
echo 'Found files'
fi
if [[ -s somedir ]]; then
echo "Files present"
fi
In my testing with bash 5.0.17, [[ -s somedir ]] will return true if somedir has any children. The same is true of [ -s somedir ]. Note that this will also return true if there are hidden files or subdirectories. It may also be filesystem-dependent.
It really feels like there should be an option to test for an empty directory.
I'll leave that editorial comment as a suggestion to the maintainers of the test command, but the counterpart exists for empty files.
In the trivial use case that brought me here, I'm not worried about looping through a huge number of files, nor am I worried about .files. I was hoping to find the aforementioned "missing" operand to test. C'est la guerre.
In the example below directory empty is empty, and full has files.
$ for f in empty/*; do test -e $f; done
$ echo $?
1
$ for f in full/*; do test -e $f; done
$ echo $?
0
Or, shorter and uglier still, but again only for relatively trivial use cases:
$ echo empty/*| grep \*
$ echo $?
1
$ echo full/* | grep \*
$ echo $?
0
So far I haven't seen an answer that uses grep which I think would give a simpler answer (with not too many weird symbols!). Here is how I would
check if any files exist in the directory using bourne shell:
this returns the number of files in a directory:
ls -l <directory> | egrep -c "^-"
you can fill in the directory path in where directory is written. The first half of the pipe ensures that the first character of output is "-" for each file. egrep then counts the number of line that start with that
symbol using regular expressions. now all you have to do is store the number you obtain and compare it using backquotes like:
#!/bin/sh
fileNum=`ls -l <directory> | egrep -c "^-"`
if [ $fileNum == x ]
then
#do what you want to do
fi
x is a variable of your choice.
Mixing prune things and last answers, I got to
find "$some_dir" -prune -empty -type d | read && echo empty || echo "not empty"
that works for paths with spaces too
Simple answer with bash:
if [[ $(ls /some/dir/) ]]; then echo "huzzah"; fi;
I would go for find:
if [ -z "$(find $dir -maxdepth 1 -type f)" ]; then
echo "$dir has NO files"
else
echo "$dir has files"
This checks the output of looking for just files in the directory, without going through the subdirectories. Then it checks the output using the -z option taken from man test:
-z STRING
the length of STRING is zero
See some outcomes:
$ mkdir aaa
$ dir="aaa"
Empty dir:
$ [ -z "$(find aaa/ -maxdepth 1 -type f)" ] && echo "empty"
empty
Just dirs in it:
$ mkdir aaa/bbb
$ [ -z "$(find aaa/ -maxdepth 1 -type f)" ] && echo "empty"
empty
A file in the directory:
$ touch aaa/myfile
$ [ -z "$(find aaa/ -maxdepth 1 -type f)" ] && echo "empty"
$ rm aaa/myfile
A file in a subdirectory:
$ touch aaa/bbb/another_file
$ [ -z "$(find aaa/ -maxdepth 1 -type f)" ] && echo "empty"
empty
Without calling utils like ls, find, etc.:
POSIX safe, i.e. not dependent on your Bash / xyz shell / ls / etc. version:
dir="/some/dir"
[ "$(echo $dir/*)x" != "$dir/*x" ] || [ "$(echo $dir/.[^.]*)x" != "$dir/.[^.]*x" ] || echo "empty dir"
The idea:
echo * lists non-dot files
echo .[^.]* lists dot files except of "." and ".."
if echo finds no matches, it returns the search expression, i.e. here * or .[^.]* - which both are no real strings and have to be concatenated with e.g. a letter to coerce a string
|| alternates the possibilities in a short circuit: there is at least one non-dot file or dir OR at least one dot file or dir OR the directory is empty - on execution level: "if first possibility fails, try next one, if this fails, try next one"; here technically Bash "tries to execute" echo "empty dir", put your action for empty dirs here (eg. exit).
Checked with symlinks, yet to check with more exotic possible file types.
In another thread How to test if a directory is empty with find i proposed this
[ "$(cd $dir;echo *)" = "*" ] && echo empty || echo non-empty
With the rationale that, $dir do exist because the question is "Checking from shell script if a directory contains files", and that * even on big dir is not that big, on my system /usr/bin/* is just 12Kb.
Update: Thanx #hh skladby, the fixed one.
[ "$(cd $dir;echo .* *)" = ". .. *" ] && echo empty || echo non-empty
if ls /some/dir/* >/dev/null 2>&1 ; then echo "huzzah"; fi;
to test a specific target directory
if [ -d $target_dir ]; then
ls_contents=$(ls -1 $target_dir | xargs);
if [ ! -z "$ls_contents" -a "$ls_contents" != "" ]; then
echo "is not empty";
else
echo "is empty";
fi;
else
echo "directory does not exist";
fi;
Try with command find.
Specify the directory hardcoded or as argument.
Then initiate find to search all files inside the directory.
Check if return of find is null.
Echo the data of find
#!/bin/bash
_DIR="/home/user/test/"
#_DIR=$1
_FIND=$(find $_DIR -type f )
if [ -n "$_FIND" ]
then
echo -e "$_DIR contains files or subdirs with files \n\n "
echo "$_FIND"
else
echo "empty (or does not exist)"
fi
I dislike the ls - A solutions posted. Most likely you wish to test if the directory is empty because you don't wish to delete it. The following does that. If however you just wish to log an empty file, surely deleting and recreating it is quicker then listing possibly infinite files?
This should work...
if ! rmdir ${target}
then
echo "not empty"
else
echo "empty"
mkdir ${target}
fi
Works well for me this (when dir exist):
some_dir="/some/dir with whitespace & other characters/"
if find "`echo "$some_dir"`" -maxdepth 0 -empty | read v; then echo "Empty dir"; fi
With full check:
if [ -d "$some_dir" ]; then
if find "`echo "$some_dir"`" -maxdepth 0 -empty | read v; then echo "Empty dir"; else "Dir is NOT empty" fi
fi

Resources