[ -e "$1/file.txt" ]
In shells/[ implementations where it is supported¹ will return true if one can do a stat() system call on the file whose path is the expansion of $1/file.txt.
It's equivalent to:
if ls -Ld -- "$1/file.txt" > /dev/null 2>&1
For the equivalent of:
if ls -d -- "$1/file.txt" > /dev/null 2>&1
that is whether a lstat() can be done on the file, it would be:
if [ -e "$1/file.txt" ] || [ -L "$1/file.txt" ]
to cover the "broken" symlink case (for which -e would return false as it tests for existence after symlink resolution).
Now, stat(), lstat() failing doesn't necessarily mean that the file doesn't exist, stat()/lstat() could fail with other error conditions than ENOENT (no entry) ENOTDIR (a path component is not a directory). It could be that you don't have search access to any of the directory component for instance.
With the ls approach, you can remove the 2>&1 for ls to tell you the error. With [, you won't know unless you use zsh where you can use $ERRNO.
If you don't have search access to the directory, you may still have read access, so you could still read the directories content to see if there's a file by that name.
With zsh, that can be done with ()(($#)) $1/file.txt(N), an anonymous function that returns true if it receives at least one argument (()(($#))) to which we pass the list of files matching the $1/file.txt(N) glob, (N) being to turn on NULLGLOB for that one glob.
A separate problem with $1/file.txt is the case where $1 is /, in which case that becomes //file.txt which on some systems is not the same as /file.txt.
So with zsh, to check whether file.txt exists as an entry in the $1 directory:
exists() {
local file="${1%/}/file.txt"
zmodload zsh/system
local ERRNO=0
[ -e "$file" ] || [ -L "$file" ] && return 0
case $errnos[ERRNO] in
(ENOENT|ENOTDIR) return 1;; # does not exist for sure
(*) ()(($#)) $file && return 0;;
esac
return 2 # meaning: don't know
}
With other shells, you could try:
enoent_error=$(
LC_ALL=C ls -d /surely-does-not-exist 2>&1)
)
enoent_error=${enoent_error##*:}
enotdir_error=$(
LC_ALL=C ls -d /bin/sh/foo)
)
enotdir_error=${enotdir_error##*:}
exists() (
file=${1%/}/file.txt
if error=$(LC_ALL=C ls -d -- "$file" 2>&1 > /dev/null); then
return 0 # exists for sure
else
case $error in
(:"$enoent_error"|:"$enotdir_error")
return 1;; # does not exist for sure
esac
fi
return 2 # don't know
}
That assumes ls error messages in the C locale follow the *: fixed error message pattern where the fixed error message is different for each error condition but always the same for a given error and does not contain : (on my system only ETOOMANYREFS (Too many references: cannot splice) has a : in the corresponding standard libc error message, but that's not an errno returned by lstat(), and anyway cannot splice is not found in other error messages).
¹ [ aka test didn't support -e initially. ksh introduced -a first (which then doubled as both a unary and binary operator), for accessible I believe (which in a way is more correct), which was later renamed to -e (though most implementations still support -a as well) and that's -e that POSIX specifies for the standard test/[ utility. /bin/sh on Solaris 10 and older is still a non-POSIX Bourne shell and its [ builtin still doesn't support [ -e nor [ -a, so you'd need to resort to ls or call /bin/test if you were to use that shell (though you'd rather use a POSIX shell like /usr/xpg4/bin/sh there).
-eis the correct switch.-ftests whetherfile.txtexists and is a regular file.-ejust tests if the file exists. – Gilles 'SO- stop being evil' Nov 25 '16 at 00:37