如何检查是否可以在 bash 中创建或截断/覆盖文件?

Bee*_*ope 15 bash error-handling files

用户使用文件路径调用我的脚本,该文件路径将在脚本中的某个点被创建或覆盖,例如foo.sh file.txtfoo.sh dir/file.txt

创建或覆盖行为很像将文件放在>输出重定向运算符右侧的要求,或者将其作为参数传递给tee(实际上,将其作为参数传递给tee正是我正在做的)。

在深入了解脚本之前,我想对文件是否可以创建/覆盖进行合理的检查,但实际上不能创建它。这个检查不一定是完美的,是的,我意识到情况可以在检查和实际写入文件的点之间改变 - 但在这里我可以使用尽力而为类型的解决方案,所以我可以尽早退出在文件路径无效的情况下。

无法创建文件的原因示例:

  • 该文件包含一个目录组件,例如dir/file.txt但该目录dir不存在
  • 用户在指定目录中没有写权限(如果没有指定目录,则为 CWD

是的,我意识到“预先”检查权限不是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并随心所欲地使用它。

  • 这非常巧妙地处理了这么多问题!解释和证明不寻常解决方案的代码注释可能会超过您的答案的长度。:-) (2认同)

Sté*_*las 8

根据我收集的内容,您想在使用时检查

tee -- "$OUT_FILE"
Run Code Online (Sandbox Code Playgroud)

(注意,--否则它不适用于以 - 开头的文件名),tee将成功打开文件进行写入。

就这样:

  • 文件路径的长度不超过 PATH_MAX 限制
  • 该文件存在(在符号链接解析后)并且不是目录类型并且您对其具有写权限。
  • 如果文件不存在,则文件的目录名(在符号链接解析后)作为目录存在,并且您对其具有写入和搜索权限,并且文件名长度不超过该目录所在文件系统的 NAME_MAX 限制。
  • 或者该文件是一个符号链接,指向一个不存在的文件,它不是一个符号链接循环,但符合上面的标准

我们现在将忽略像 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()

pythonos.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.

或者您可以决定不处理所有这些极端情况。


fil*_*den 6

您可能要考虑的一种选择是尽早创建文件,但稍后才在脚本中填充它。您可以使用该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方法可能会起作用,但不必重新打开文件是一个更优雅的解决方案。)