awk(和 gawk):当 N 个输入文件中有 1 个不可读时如何避免致命错误

awk(和 gawk):当 N 个输入文件中有 1 个不可读时如何避免致命错误

测试用例:(使用非 root 用户,因为 root 会忽略 000 权限...)

#in a clean directory:
[ -f file_1 ] && chmod 600 file_? # for repeat tests...
for i in file_1 file_2 file_3; do
    printf 'A\nB\n' > "$i" 
    # we need at least 1 char : awk/gawk silently skips empty files...
done
chmod 000 file_2
awk '(FNR==1) { print FILENAME }' file_? 
  # tried with : regular (old unixes) awk on AIX. and gawk on Linux.
  # the fatal "permission denied" on file_2 stops [g]awk.

有没有办法捕获该致命错误并继续处理下一个文件?

(否则我觉得这很令人担忧:对一堆文件使用 awk 并不能保证处理所有文件,因为如果其中任何一个文件不可读,它将致命地退出)

如果可以的话请:回答

  • 对于常规 awk,
  • 和傻瓜
  • 还有其他相关的 awk 版本吗? (诺克?等)

答案1

与 GAWK 一起:

gawk 'BEGINFILE { if (ERRNO) nextfile } (FNR==1) { print FILENAME }' file_?

BEGINFILE块中,ERRNO如果文件打开成功,则为空,nextfile可用于跳到下一个并避免因错误而退出。

我认为 AWK 的其他实现不支持这一点。

可移植地,你可以迭代所有参数,检查它们是否指向不可读的文件,如果是,则在 AWK 开始处理它们之前将它们从参数中删除;GAWK 手册有一个示例实现。然而,这是很危险的,因为使用此循环检查的文件可能在 AWK 开始处理它之前变得不可读(反之亦然)。

答案2

正如 @StephenKitt 和 @ilkkachu 已经指出的 gawk 手册包含一些代码这将从ARGV[]BEGIN部分中删除不可读的文件,但在测试文件和 awk 实际尝试读取其内容之间存在竞争条件,如果前面的文件很大,则可能要晚得多。

如果您有 gawk 或 gawk 手册中的脚本,我会使用 @StephenKitt 的答案中的脚本,否则除非您确实认为可能存在竞争条件问题,因为 gawk 手册脚本更清晰、更简短、更简单、更高效等。比下面的要好,不需要临时文件和全局变量,但对于那些担心竞争条件的人来说 - 这是一个更复杂的脚本,可以在任何 awk 中工作,并依赖于创建一个临时文件以在尝试之前立即打开打开任何真实文件,然后测试即将到来的真实文件是否可读。

$ cat skip.awk
function addTmp(        cmd, oArgv, i, j) {
    cmd = "mktemp"
    cmd | getline TmpChkFile
    close(cmd)

    if ( TmpChkFile != "" ) {
        print "" > TmpChkFile
        close(TmpChkFile)

        for (i in ARGV) {
            oArgv[i] = ARGV[i]
        }
        oArgc = ARGC

        ARGC = 1
        for (i = 1; i < oArgc; i++) {
            if ( ! (oArgv[i] ~ /^[a-zA-Z_][a-zA-Z0-9_]*=.*/ \
                    || oArgv[i] == "-" || oArgv[i] == "/dev/stdin") ) {
                # not assignment or standard input so a file name
                ARGV[ARGC] = TmpChkFile
                ArgFileNames[++j] = oArgv[i]
                ArgFileIndices[j] = ++ARGC
            }
            ARGV[ARGC++] = oArgv[i]
        }
    }
}

function rmvTmp() {
    system("rm -f \047" TmpChkFile "\047")
}

function chkTmp(        stderr, line) {
    if ( (FNR == 1) && (FILENAME == TmpChkFile) ) {
        ++TmpFileNr
        if ( (getline line < ArgFileNames[TmpFileNr]) < 0 ) {
            stderr = "cat>&2"
            printf "Warning: skipping unreadable file \"%s\"\n", ArgFileNames[TmpFileNr] | stderr
            close(stderr)
            delete ARGV[ArgFileIndices[TmpFileNr]]
        }
        close(ArgFileNames[TmpFileNr])
        next
    }
}

BEGIN { addTmp() }
END { rmvTmp() }
{ chkTmp() }

如果您的 awk 支持多个-f参数(例如,按照POSIX)或同时执行多个脚本的任何其他方式(例如 GNU awk 有@include),那么您可以使用该方法将上述内容与您的实际脚本一起包含(否则将上述内容复制/粘贴到同一个文件中),例如假设您有脚本如下:

$ cat tst.awk
FNR == 1 { print FILENAME, $0 }

和文件如:

$ ls file_{1..3}
ls: cannot access 'file_2': No such file or directory
file_1  file_3

然后使用任何 POSIX awk(以及大多数(如果不是全部)其他),您可以执行以下操作:

$ awk -f skip.awk -f tst.awk file_{1..3}
file_1 A
Warning: skipping unreadable file "file_2"
file_3 C

上面的大部分工作都是在BEGIN第一个输入文件打开之前调用一次,以确保ARGV[]每个实际输入文件之前都存在一个可读的临时文件,然后chkTmp()为每一行输入调用,但仅在它是第一个输入时才执行某些操作(也是唯一)临时文件的行,并且尝试打开ARGV[].然后END只需删除临时文件。因此,真正的额外开销是对每个输入行的调用chkTmp()和测试。FNR==1

我正在创建一个临时文件而不是使用现有文件,因为没有任何文件可以保证在所有 Unix 机器上都存在,即使有,它也必须正好 1 行长,以避免增加额外的开销chkTmp()必须读取该文件的每一行,因为并非所有 awks 都支持nextfile(或者我们可以调用它而不是nextinside chkTmp())。

相关内容