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

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

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

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

在深入了解脚本之前,我想对文件是否进行合理的检查被创建/覆盖,但实际上并没有创建它。这个检查不必是完美的,是的,我意识到情况可以在检查和实际写入文件的点之间发生变化 - 但在这里我可以接受最大努力输入解决方案,这样我就可以在文件路径无效的情况下尽早退出。

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

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

是的,我意识到“预先”检查权限并不是UNIX 方式™,相反,我应该先尝试一下操作,然后再请求原谅。然而,在我的特定脚本中,这会导致糟糕的用户体验,并且我无法更改负责的组件。

答案1

明显的测试是:

if touch /path/to/file; then
    : it can be created
fi

但如果该文件尚不存在,它实际上会创建该文件。我们可以自己清理:

if touch /path/to/file; then
    rm /path/to/file
fi

但这会删除您可能不想要的已存在的文件。

不过,我们确实有办法解决这个问题:

if mkdir /path/to/file; then
    rmdir /path/to/file
fi

目录不能与该目录中的另一个对象同名。我无法想象您可以创建目录但不能创建文件的情况。在此测试之后,您的脚本将可以自由地创建常规/path/to/file并执行任何它喜欢的操作。

答案2

根据我收集的信息,您想在使用时检查

tee -- "$OUT_FILE"

(请注意,--否则它不适用于以 - 开头的文件名),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
}

bash或任何类似 Bourne 的 shell 中,只需替换

zmodload zsh/system
tee_would_likely_succeed() {
  <zsh-code>
}

和:

tee_would_likely_succeed() {
  zsh -s -- "$@" << 'EOF'
  zmodload zsh/system
  <zsh-code>
EOF
}

这里的特定功能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##*:}

(假设错误消息结束syserror()的翻译和ENOENT该翻译不包括:) 然后,而不是[ -e "$file" ],做:

err=$(ls -Ld -- "$file" 2>&1)

并检查 ENOENT 错误

case $err in
  (*:"$msg_for_ENOENT") ...
esac

$file:P部分是在 .NET 中实现的最棘手的部分bash,尤其是在 FreeBSD 上。

FreeBSD 确实有一个realpath命令和一个readlink接受-f选项的命令,但它们不能在文件是无法解析的符号链接的情况下使用。这与perls相同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%.}

但是,您不妨在 中完成整个事情python

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

答案3

您可能需要考虑的一种选择是创造早期的文件,但只是稍后在脚本中填充它。您可以使用该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

您还可以使用 atrap来清理错误的文件,这是常见的做法。

这样,你就得到了一个真实的检查您将能够创建该文件的权限,同时能够尽早执行它,以便在失败时您不必花时间等待长时间的检查。


更新:为了避免在检查失败时破坏文件,请使用 bashfd<>file重定向它不会立即截断文件。 (我们不关心从文件中读取,这只是一种解决方法,所以我们不会截断它。附加 with>>可能也可以,但我倾向于发现这个更优雅,保留 O_APPEND 标志图片的。)

当我们准备好替换内容时,我们需要首先截断文件(否则,如果我们写入的字节数少于文件之前的字节数,则尾随字节将保留在那里。)我们可以使用截断(1)为此,我们可以向 coreutils 命令传递我们拥有的打开文件描述符(使用/dev/fd/3伪文件),这样它就不需要重新打开文件。 (同样,从技术上讲,类似的更简单的方法: >dir/file.txt可能会起作用,但不必重新打开文件是一个更优雅的解决方案。)

答案4

test使用下面概述的普通命令怎么样?

FILE=$1

DIR=$(dirname $FILE) # $DIR now contains '.' for file names only, 'foo' for 'foo/bar'

if [ -d $DIR ] ; then
  echo "base directory $DIR for file exists"
  if [ -e $FILE ] ; then
    if [ -w $FILE ] ; then
      echo "file exists, is writeable"
    else
      echo "file exists, NOT writeable"
    fi
  elif [ -w $DIR ] ; then
    echo "directory is writeable"
  else
    echo "directory is NOT writeable"
  fi
else
  echo "can NOT create file in non-existent directory $DIR "
fi

相关内容