From what I'm gathering, you want to check that when using
tee -- "$OUT_FILE"
(note the --
or it wouldn't work for file names that start with -), tee
would succeed to open the file for writing.
That is that:
- the length of the file path doesn't exceed the PATH_MAX limit
- the file exists (after symlink resolution) and is not of type directory and you have write permission to it.
- if the file doesn't exist, the dirname of the file exists (after symlink resolution) as a directory and you have write and search permission to it and the filename length doesn't exceed the NAME_MAX limit of the filesystem that directory resides in.
- or the file is a symlink that points to a file that doesn't exist and is not a symlink loop but meets the criteria just above
We'll ignore for now filesystems like vfat, ntfs or hfsplus that have limitations on what byte values file names may contain, disk quota, process limit, selinux, apparmor or other security mechanism in the way, full filesystem, no inode left, device files that can't be opened that way for a reason or another, files that are executables currently mapped in some process address space all of which could also affect the ability to open or create the file.
With zsh
:
zmodload zsh/system
tee_would_likely_succeed() {
local file=$1 ERRNO=0 LC_ALL=C
if [ -d "$file" ]; then
return 1 # directory
elif [ -w "$file" ]; then
return 0 # writable non-directory
elif [ -e "$file" ]; then
return 1 # exists, non-writable
elif [ "$errnos[ERRNO]" != ENOENT ]; then
return 1 # only ENOENT error can be recovered
else
local dir=$file:P:h base=$file:t
[ -d "$dir" ] && # directory
[ -w "$dir" ] && # writable
[ -x "$dir" ] && # and searchable
(($#base <= $(getconf -- NAME_MAX "$dir")))
return
fi
}
In bash
or any Bourne-like shell, just replace the
zmodload zsh/system
tee_would_likely_succeed() {
<zsh-code>
}
with:
tee_would_likely_succeed() {
zsh -s -- "$@" << 'EOF'
zmodload zsh/system
<zsh-code>
EOF
}
The zsh
-specific features here are $ERRNO
(which exposes the error code of the last system call) and $errnos[]
associative array to translate to the corresponding standard C macro names. And the $var:h
(from csh) and $var:P
(needs zsh 5.3 or above).
bash doesn't have equivalent features yet.
$file:h
can be replaced with dir=$(dirname -- "$file"; echo .); dir=${dir%??}
, or with GNU dirname
: IFS= read -rd '' dir < <(dirname -z -- "$file")
.
For $errnos[ERRNO] == ENOENT
, an approach could be to run ls -Ld
on the file and check whether the error message corresponds to the ENOENT error. Doing that reliably and portably is tricky though.
One approach could be:
msg_for_ENOENT=$(LC_ALL=C ls -d -- '/no such file' 2>&1)
msg_for_ENOENT=${msg_for_ENOENT##*:}
(assuming that the error message ends with the syserror()
translation of ENOENT
and that that translation doesn't include a :
) and then, instead of [ -e "$file" ]
, do:
err=$(ls -Ld -- "$file" 2>&1)
And check for a ENOENT error with
case $err in
(*:"$msg_for_ENOENT") ...
esac
The $file:P
part is the trickiest to achieve in bash
, especially on FreeBSD.
FreeBSD does have a realpath
command and a readlink
command that accepts a -f
option, but they can't be used in the cases where the file is a symlink that doesn't resolve. That's the same with perl
's Cwd::realpath()
.
python
's os.path.realpath()
does appear to work similarly to zsh
$file:P
, so assuming that at least one version of python
is installed and that there is a python
command that refers to one of them (which is not a given on FreeBSD), you could do:
dir=$(python -c '
import os, sys
print(os.path.realpath(sys.argv[1]) + ".")' "$dir") || return
dir=${dir%.}
But then, you might as well do the whole thing in python
.
Or you could decide not to handle all those corner cases.
/foo/bar/file.txt
. Basically I pass the path totee
liketee $OUT_FILE
whereOUT_FILE
is passed on the command line. That should "just work" with both absolute and relative paths, right?tee -- "$OUT_FILE"
at least. What if the file already exists or exists but is not a regular file (directory, symlink, fifo)?tee "${OUT_FILE}.tmp"
. If the file already exists,tee
overwrites, which is the desired behavior in this case. If it's a directory,tee
will fail (I think). symlink I'm not 100% sure?