Bee*_*ope 15 bash error-handling files
用户使用文件路径调用我的脚本,该文件路径将在脚本中的某个点被创建或覆盖,例如foo.sh file.txt或foo.sh dir/file.txt。
创建或覆盖行为很像将文件放在>输出重定向运算符右侧的要求,或者将其作为参数传递给tee(实际上,将其作为参数传递给tee正是我正在做的)。
在深入了解脚本之前,我想对文件是否可以创建/覆盖进行合理的检查,但实际上不能创建它。这个检查不一定是完美的,是的,我意识到情况可以在检查和实际写入文件的点之间改变 - 但在这里我可以使用尽力而为类型的解决方案,所以我可以尽早退出在文件路径无效的情况下。
无法创建文件的原因示例:
dir/file.txt但该目录dir不存在是的,我意识到“预先”检查权限不是The UNIX Way™,而是我应该尝试操作并稍后请求原谅。然而,在我的特定脚本中,这会导致糟糕的用户体验,我无法更改负责的组件。
Dop*_*oti 12
显而易见的测试是:
if touch /path/to/file; then
: it can be created
fi
Run Code Online (Sandbox Code Playgroud)
但如果文件不存在,它实际上会创建文件。我们可以自己清理:
if touch /path/to/file; then
rm /path/to/file
fi
Run Code Online (Sandbox Code Playgroud)
但这会删除您可能不想要的已经存在的文件。
然而,我们确实有办法解决这个问题:
if mkdir /path/to/file; then
rmdir /path/to/file
fi
Run Code Online (Sandbox Code Playgroud)
您不能拥有与该目录中的另一个对象同名的目录。我想不出您可以创建目录但不能创建文件的情况。在这个测试之后,您的脚本将可以自由地创建一个常规的/path/to/file并随心所欲地使用它。
根据我收集的内容,您想在使用时检查
tee -- "$OUT_FILE"
Run Code Online (Sandbox Code Playgroud)
(注意,--否则它不适用于以 - 开头的文件名),tee将成功打开文件进行写入。
就这样:
我们现在将忽略像 vfat、ntfs 或 hfsplus 这样的文件系统,它们对文件名可能包含的字节值、磁盘配额、进程限制、selinux、apparmor 或其他安全机制有限制、完整的文件系统、没有剩余的 inode、设备由于某种原因无法以这种方式打开的文件,当前映射在某个进程地址空间中的可执行文件,所有这些都可能影响打开或创建文件的能力。
与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
}
Run Code Online (Sandbox Code Playgroud)
在bash或任何类似 Bourne 的 shell 中,只需替换
zmodload zsh/system
tee_would_likely_succeed() {
<zsh-code>
}
Run Code Online (Sandbox Code Playgroud)
和:
tee_would_likely_succeed() {
zsh -s -- "$@" << 'EOF'
zmodload zsh/system
<zsh-code>
EOF
}
Run Code Online (Sandbox Code Playgroud)
zsh此处的特定功能是$ERRNO(暴露上次系统调用的错误代码)和$errnos[]关联数组以转换为相应的标准 C 宏名称。以及$var:h(来自 csh)和$var:P(需要 zsh 5.3 或更高版本)。
bash 还没有等效的功能。
$file:h可以替换为dir=$(dirname -- "$file"; echo .); dir=${dir%??},或者替换为 GNU dirname: IFS= read -rd '' dir < <(dirname -z -- "$file")。
对于$errnos[ERRNO] == ENOENT,一种方法可能是ls -Ld在文件上运行并检查错误消息是否对应于 ENOENT 错误。但是,可靠且可移植地做到这一点很棘手。
一种方法可能是:
msg_for_ENOENT=$(LC_ALL=C ls -d -- '/no such file' 2>&1)
msg_for_ENOENT=${msg_for_ENOENT##*:}
Run Code Online (Sandbox Code Playgroud)
(假设错误消息以syserror()of的翻译结束,ENOENT并且该翻译不包含 a :)然后,而不是[ -e "$file" ],执行:
err=$(ls -Ld -- "$file" 2>&1)
Run Code Online (Sandbox Code Playgroud)
并检查 ENOENT 错误
case $err in
(*:"$msg_for_ENOENT") ...
esac
Run Code Online (Sandbox Code Playgroud)
这$file:P部分在 中是最难实现的bash,尤其是在 FreeBSD 上。
FreeBSD 确实有一个realpath命令和一个readlink接受-f选项的命令,但它们不能用于文件是无法解析的符号链接的情况。这与perl's相同Cwd::realpath()。
python的os.path.realpath()工作方式似乎与 类似zsh $file:P,因此假设至少python安装了一个版本并且有一个python命令引用其中之一(在 FreeBSD 上未给出),您可以执行以下操作:
dir=$(python -c '
import os, sys
print(os.path.realpath(sys.argv[1]) + ".")' "$dir") || return
dir=${dir%.}
Run Code Online (Sandbox Code Playgroud)
但是,您不妨在python.
或者您可以决定不处理所有这些极端情况。
您可能要考虑的一种选择是尽早创建文件,但稍后才在脚本中填充它。您可以使用该exec命令在文件描述符(例如 3、4 等)中打开文件,然后使用重定向到文件描述符(>&3等)将内容写入该文件。
就像是:
#!/bin/bash
# Open the file for read/write, so it doesn't get
# truncated just yet (to preserve the contents in
# case the initial checks fail.)
exec 3<>dir/file.txt || {
echo "Error creating dir/file.txt" >&2
exit 1
}
# Long checks here...
check_ok || {
echo "Failed checks" >&2
# cleanup file before bailing out
rm -f dir/file.txt
exit 1
}
# We're ready to write, first truncate the file.
# Use "truncate(1)" from coreutils, and pass it
# /dev/fd/3 so the file doesn't need to be reopened.
truncate -s 0 /dev/fd/3
# Now populate the file, use a redirection to write
# to the previously opened file descriptor.
populate_contents >&3
Run Code Online (Sandbox Code Playgroud)
您还可以使用 atrap来清理出错的文件,这是一种常见的做法。
通过这种方式,您可以获得真正的权限检查,您将能够创建文件,同时能够尽早执行它,如果失败,您就不必花时间等待长时间的检查。
更新:为了避免在检查失败的情况下破坏文件,请使用 bash 的fd<>file重定向,它不会立即截断文件。(我们不关心从文件中读取数据,这只是一种解决方法,所以我们不会截断它。附加 with>>也可能会起作用,但我倾向于发现这个更优雅一些,保留 O_APPEND 标志图片的。)
当我们准备好替换内容时,我们需要先截断文件(否则,如果我们写入的字节数少于文件中之前的字节数,则尾随字节将保留在那里。)我们可以使用truncate(1)为此目的,我们可以从 coreutils 发送命令,我们可以将我们拥有的打开文件描述符(使用/dev/fd/3伪文件)传递给它,因此它不需要重新打开文件。(同样,技术上更简单的: >dir/file.txt方法可能会起作用,但不必重新打开文件是一个更优雅的解决方案。)