Bash 函数产生错误输出,为什么?

Bash 函数产生错误输出,为什么?

我有一个使用 cron 定期运行的脚本。当这些脚本失败时,我希望收到电子邮件通知。我不希望每次运行并产生任何输出时都收到通知。

因此,我正在使用脚本克罗尼克在 cron 内部运行我的作业,这意味着只发送错误输出,而不仅仅是任何输出。

但是,在一个脚本中,我有一个 bash 函数来检测目录是否为空:

# function to check if directory is empty outputs 0 if empty, or
# some positive integer if it does not
is_dir_empty () 
{
  ls -A "$1" | wc -c
}

该函数在 if 语句中使用,如下所示:

if [ "$(is_dir_empty "${local_backup_location}/${folder_to_backup_basename}")" -eq 0 ]; then
    # save space by removing diffs older than 1 month
    rdiff-backup --remove-older-than 1M --force ${local_backup_location}/${folder_to_backup_basename} || logger -t ${logger_name}  "No existing data backup"
fi

有时我会收到来自 cronic 的这样的电子邮件

Cronic detected failure or error output for the command:
/usr/local/sbin/backup_uploads

RESULT CODE: 0

ERROR OUTPUT:
ls -A /mnt/storage-2/data_upload_backup/upload

STANDARD OUTPUT:
<snip>

TRACE-ERROR OUTPUT:
+ folder_to_backup=/mnt/storage-1/sftp_data/sftpuser1/upload
+ backup_server=ed-mh-x86001.mydomain.tld
+ backup_folder=data_upload_backup
+ remote_backup_storage_root=/mnt/storage-2
+ local_backup_storage_root=/mnt/storage-2
+ logger_name=backup_uploads
+ error_report_dir=/root/rdiff-backup-errors
+ [email protected]
+ problems=0
+ lockfilename=/root/.backup_uploads_lockfile.pid
+ test -e /root/.backup_uploads_lockfile.pid
+ echo
+ basename /mnt/storage-1/sftp_data/sftpuser1/upload
+ folder_to_backup_basename=upload
+ hostname
+ remote_server_backup_folder=/mnt/storage-2/data_upload_backup_ed-mh-pi01
+ mkdir -p /root/rdiff-backup-errors
+ logger -t backup_uploads Starting backup for folder /mnt/storage-1/sftp_data/sftpuser1/upload
+ test -d /mnt/storage-2
+ local_backup_location=/mnt/storage-2/data_upload_backup
+ mkdir -p /mnt/storage-2/data_upload_backup
+ logger -t backup_uploads Backing up to /mnt/storage-2/data_upload_backup
+ mkdir -p /root/rdiff-backup-errors
+ test -d /mnt/storage-2/data_upload_backup/upload
+ is_dir_empty /mnt/storage-2/data_upload_backup/upload
+ + wc -c
ls -A /mnt/storage-2/data_upload_backup/upload
+ [ 26 -eq 0 ]
+ logger -t backup_uploads Starting backup for /mnt/storage-1/sftp_data/sftpuser1/upload
+ hostname
+ rdiff-backup --ssh-no-compression /mnt/storage-1/sftp_data/sftpuser1/upload /mnt/storage-2/data_upload_backup/upload/
+ testssh [email protected]
+ remote_backup_machine_accessible=0
+ [ 0 -eq 0 ]
+ [email protected]:/mnt/storage-2/data_upload_backup_ed-mh-pi01
+ logger -t backup_uploads copying local backup up to [email protected]:/mnt/storage-2/data_upload_backup_ed-mh-pi01
+ rsync -aP --delete-after /mnt/storage-2/data_upload_backup/ [email protected]:/mnt/storage-2/data_upload_backup_ed-mh-pi01
+ rm -f /root/.backup_uploads_lockfile.pid
+ logger -t backup_uploads Lock file /root/.backup_uploads_lockfile.pid deleted, end of script

我并不是每次运行脚本时都会得到这个,只是偶尔,所以也许当目录实际上是空的或类似的东西时。

所以我的问题是为什么我会从此命令中收到错误输出,以及如何防止它(至少在我不希望出现的明显上下文中)?

这个问题与我问自己的另一个问题非常相似这里,但我向你保证它是不同的!

答案1

这取决于如何cronic捕获命令的错误输出以及如何尝试xtrace从中分离出输出。

它会查找以一个或多个+开头的行。

这是一个粗略的启发式。如果某些错误消息以其开头+,则将被归类为跟踪输出。如果 xtrace 行与错误消息行或并行启动的另一个命令的另一个 xtrace 行交错(如您的情况),则 可能+不在该行的开头,并且它将被分类为 xtrace 输出。如果脚本使用不同的PS4,则将无法正常工作,等等。

对于bash脚本,要将错误输出与 xtrace 输出分开,您应该使用BASH_XTRACEFD

BASH_XTRACEFD=7 bash -x /path/to/your-script > out 2> err 7> trace

答案2

与链接问题中的问题类似。在输出中查看这部分:

+ + wc -c
ls -A /mnt/storage-2/data_upload_backup/upload

当 cronic 解析输出以获取行时,它会看到前面strace没有 a 的行,并认为这是常规错误输出的一部分。+

那里的输出很混乱,它应该只显示这两个命令的正常 xtrace 输出,即:

+ ls -A /mnt/storage-2/data_upload_backup/upload
+ wc -c

这里必须发生的是,Dash 在调用将其发送到 stderr 之前不会费心缓冲整个输出行write(),而是逐段进行。由于有两个 shell 进程执行此操作,管道的每个部分各有一个,并且它们同时运行,因此输出可能会混淆。我可以在已加载的系统上重复这一点,不会有太多麻烦。

对于这个结果,它足以编写第一个+,然后是命令,但它似乎更糟糕,我也得到了这样的输出:

+ is_dir_empty .
+ + lswc -A -c .

也就是说,即使是单个单词也会交错。

我们可以看到个人用strace写入:

$ strace -etrace=write -f dash -xc 'echo aa bb cc'
write(2, "+ ", 2+ )                       = 2
write(2, "echo", 4echo)                     = 4
write(2, " aa", 3 aa)                      = 3
write(2, " bb", 3 bb)                      = 3
write(2, " cc", 3 cc)                      = 3
write(2, "\n", 1
)
...

除了将其修复在外壳中之外,我想不出解决该问题的方法。

Bash 似乎没有这个问题,所以也许改用它。

答案3

这可能相关也可能不相关,但是从不解析ls输出;甚至这里也不。

一个简单的

number_of_items () { echo $#; }
is_empty() {
 shopt -s dotglob
 shopt -s nullglob
 value=$(number_of_items $1/*)
 [[ "${value}" -eq 0 ]]
}

会做,这样你就可以

if is_empty "${local_backup_location}/${folder_to_backup_basename}" ; then

你真正想要的是contains_accessible_things

number_of_items () { echo $#; }
contains_accessible_things() {
 shopt -s dotglob
 shopt -s nullglob
 # check for directory and readability 
 [[ -d "$1" && -r "$1" && ( "$(number_of_items $1/*)" -gt 0 ) ]]
}
if contains_accessible_things "${local_backup_location}/${folder_to_backup_basename}" ; then
  do_stuff
fi

相关内容