1#!/bin/sh 2# shellcheck disable=SC2154,SC3043 3# zed-functions.sh 4# 5# ZED helper functions for use in ZEDLETs 6 7 8# Variable Defaults 9# 10: "${ZED_LOCKDIR:="/var/lock"}" 11: "${ZED_NOTIFY_INTERVAL_SECS:=3600}" 12: "${ZED_NOTIFY_VERBOSE:=0}" 13: "${ZED_RUNDIR:="/var/run"}" 14: "${ZED_SYSLOG_PRIORITY:="daemon.notice"}" 15: "${ZED_SYSLOG_TAG:="zed"}" 16 17ZED_FLOCK_FD=8 18 19 20# zed_check_cmd (cmd, ...) 21# 22# For each argument given, search PATH for the executable command [cmd]. 23# Log a message if [cmd] is not found. 24# 25# Arguments 26# cmd: name of executable command for which to search 27# 28# Return 29# 0 if all commands are found in PATH and are executable 30# n for a count of the command executables that are not found 31# 32zed_check_cmd() 33{ 34 local cmd 35 local rv=0 36 37 for cmd; do 38 if ! command -v "${cmd}" >/dev/null 2>&1; then 39 zed_log_err "\"${cmd}\" not installed" 40 rv=$((rv + 1)) 41 fi 42 done 43 return "${rv}" 44} 45 46 47# zed_log_msg (msg, ...) 48# 49# Write all argument strings to the system log. 50# 51# Globals 52# ZED_SYSLOG_PRIORITY 53# ZED_SYSLOG_TAG 54# 55# Return 56# nothing 57# 58zed_log_msg() 59{ 60 logger -p "${ZED_SYSLOG_PRIORITY}" -t "${ZED_SYSLOG_TAG}" -- "$@" 61} 62 63 64# zed_log_err (msg, ...) 65# 66# Write an error message to the system log. This message will contain the 67# script name, EID, and all argument strings. 68# 69# Globals 70# ZED_SYSLOG_PRIORITY 71# ZED_SYSLOG_TAG 72# ZEVENT_EID 73# 74# Return 75# nothing 76# 77zed_log_err() 78{ 79 zed_log_msg "error: ${0##*/}:""${ZEVENT_EID:+" eid=${ZEVENT_EID}:"}" "$@" 80} 81 82 83# zed_lock (lockfile, [fd]) 84# 85# Obtain an exclusive (write) lock on [lockfile]. If the lock cannot be 86# immediately acquired, wait until it becomes available. 87# 88# Every zed_lock() must be paired with a corresponding zed_unlock(). 89# 90# By default, flock-style locks associate the lockfile with file descriptor 8. 91# The bash manpage warns that file descriptors >9 should be used with care as 92# they may conflict with file descriptors used internally by the shell. File 93# descriptor 9 is reserved for zed_rate_limit(). If concurrent locks are held 94# within the same process, they must use different file descriptors (preferably 95# decrementing from 8); otherwise, obtaining a new lock with a given file 96# descriptor will release the previous lock associated with that descriptor. 97# 98# Arguments 99# lockfile: pathname of the lock file; the lock will be stored in 100# ZED_LOCKDIR unless the pathname contains a "/". 101# fd: integer for the file descriptor used by flock (OPTIONAL unless holding 102# concurrent locks) 103# 104# Globals 105# ZED_FLOCK_FD 106# ZED_LOCKDIR 107# 108# Return 109# nothing 110# 111zed_lock() 112{ 113 local lockfile="$1" 114 local fd="${2:-${ZED_FLOCK_FD}}" 115 local umask_bak 116 local err 117 118 [ -n "${lockfile}" ] || return 119 if ! expr "${lockfile}" : '.*/' >/dev/null 2>&1; then 120 lockfile="${ZED_LOCKDIR}/${lockfile}" 121 fi 122 123 umask_bak="$(umask)" 124 umask 077 125 126 # Obtain a lock on the file bound to the given file descriptor. 127 # 128 eval "exec ${fd}>> '${lockfile}'" 129 if ! err="$(flock --exclusive "${fd}" 2>&1)"; then 130 zed_log_err "failed to lock \"${lockfile}\": ${err}" 131 fi 132 133 umask "${umask_bak}" 134} 135 136 137# zed_unlock (lockfile, [fd]) 138# 139# Release the lock on [lockfile]. 140# 141# Arguments 142# lockfile: pathname of the lock file 143# fd: integer for the file descriptor used by flock (must match the file 144# descriptor passed to the zed_lock function call) 145# 146# Globals 147# ZED_FLOCK_FD 148# ZED_LOCKDIR 149# 150# Return 151# nothing 152# 153zed_unlock() 154{ 155 local lockfile="$1" 156 local fd="${2:-${ZED_FLOCK_FD}}" 157 local err 158 159 [ -n "${lockfile}" ] || return 160 if ! expr "${lockfile}" : '.*/' >/dev/null 2>&1; then 161 lockfile="${ZED_LOCKDIR}/${lockfile}" 162 fi 163 164 # Release the lock and close the file descriptor. 165 if ! err="$(flock --unlock "${fd}" 2>&1)"; then 166 zed_log_err "failed to unlock \"${lockfile}\": ${err}" 167 fi 168 eval "exec ${fd}>&-" 169} 170 171 172# zed_notify (subject, pathname) 173# 174# Send a notification via all available methods. 175# 176# Arguments 177# subject: notification subject 178# pathname: pathname containing the notification message (OPTIONAL) 179# 180# Return 181# 0: notification succeeded via at least one method 182# 1: notification failed 183# 2: no notification methods configured 184# 185zed_notify() 186{ 187 local subject="$1" 188 local pathname="$2" 189 local num_success=0 190 local num_failure=0 191 192 zed_notify_email "${subject}" "${pathname}"; rv=$? 193 [ "${rv}" -eq 0 ] && num_success=$((num_success + 1)) 194 [ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1)) 195 196 zed_notify_pushbullet "${subject}" "${pathname}"; rv=$? 197 [ "${rv}" -eq 0 ] && num_success=$((num_success + 1)) 198 [ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1)) 199 200 zed_notify_slack_webhook "${subject}" "${pathname}"; rv=$? 201 [ "${rv}" -eq 0 ] && num_success=$((num_success + 1)) 202 [ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1)) 203 204 zed_notify_pushover "${subject}" "${pathname}"; rv=$? 205 [ "${rv}" -eq 0 ] && num_success=$((num_success + 1)) 206 [ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1)) 207 208 zed_notify_ntfy "${subject}" "${pathname}"; rv=$? 209 [ "${rv}" -eq 0 ] && num_success=$((num_success + 1)) 210 [ "${rv}" -eq 1 ] && num_failure=$((num_failure + 1)) 211 212 [ "${num_success}" -gt 0 ] && return 0 213 [ "${num_failure}" -gt 0 ] && return 1 214 return 2 215} 216 217 218# zed_notify_email (subject, pathname) 219# 220# Send a notification via email to the address specified by ZED_EMAIL_ADDR. 221# 222# Requires the mail executable to be installed in the standard PATH, or 223# ZED_EMAIL_PROG to be defined with the pathname of an executable capable of 224# reading a message body from stdin. 225# 226# Command-line options to the mail executable can be specified in 227# ZED_EMAIL_OPTS. This undergoes the following keyword substitutions: 228# - @ADDRESS@ is replaced with the space-delimited recipient email address(es) 229# - @SUBJECT@ is replaced with the notification subject 230# If @SUBJECT@ was omited here, a "Subject: ..." header will be added to notification 231# 232# 233# Arguments 234# subject: notification subject 235# pathname: pathname containing the notification message (OPTIONAL) 236# 237# Globals 238# ZED_EMAIL_PROG 239# ZED_EMAIL_OPTS 240# ZED_EMAIL_ADDR 241# 242# Return 243# 0: notification sent 244# 1: notification failed 245# 2: not configured 246# 247zed_notify_email() 248{ 249 local subject="${1:-"ZED notification"}" 250 local pathname="${2:-"/dev/null"}" 251 252 : "${ZED_EMAIL_PROG:="mail"}" 253 : "${ZED_EMAIL_OPTS:="-s '@SUBJECT@' @ADDRESS@"}" 254 255 # For backward compatibility with ZED_EMAIL. 256 if [ -n "${ZED_EMAIL}" ] && [ -z "${ZED_EMAIL_ADDR}" ]; then 257 ZED_EMAIL_ADDR="${ZED_EMAIL}" 258 fi 259 [ -n "${ZED_EMAIL_ADDR}" ] || return 2 260 261 zed_check_cmd "${ZED_EMAIL_PROG}" || return 1 262 263 [ -n "${subject}" ] || return 1 264 if [ ! -r "${pathname}" ]; then 265 zed_log_err \ 266 "${ZED_EMAIL_PROG##*/} cannot read \"${pathname}\"" 267 return 1 268 fi 269 270 # construct cmdline options 271 ZED_EMAIL_OPTS_PARSED="$(echo "${ZED_EMAIL_OPTS}" \ 272 | sed -e "s/@ADDRESS@/${ZED_EMAIL_ADDR}/g" \ 273 -e "s/@SUBJECT@/${subject}/g")" 274 275 # pipe message to email prog 276 # shellcheck disable=SC2086,SC2248 277 { 278 # no subject passed as option? 279 if [ "${ZED_EMAIL_OPTS%@SUBJECT@*}" = "${ZED_EMAIL_OPTS}" ] ; then 280 # inject subject header 281 printf "Subject: %s\n" "${subject}" 282 fi 283 # output message 284 cat "${pathname}" 285 } | 286 eval ${ZED_EMAIL_PROG} ${ZED_EMAIL_OPTS_PARSED} >/dev/null 2>&1 287 rv=$? 288 if [ "${rv}" -ne 0 ]; then 289 zed_log_err "${ZED_EMAIL_PROG##*/} exit=${rv}" 290 return 1 291 fi 292 return 0 293} 294 295 296# zed_notify_pushbullet (subject, pathname) 297# 298# Send a notification via Pushbullet <https://www.pushbullet.com/>. 299# The access token (ZED_PUSHBULLET_ACCESS_TOKEN) identifies this client to the 300# Pushbullet server. The optional channel tag (ZED_PUSHBULLET_CHANNEL_TAG) is 301# for pushing to notification feeds that can be subscribed to; if a channel is 302# not defined, push notifications will instead be sent to all devices 303# associated with the account specified by the access token. 304# 305# Requires awk, curl, and sed executables to be installed in the standard PATH. 306# 307# References 308# https://docs.pushbullet.com/ 309# https://www.pushbullet.com/security 310# 311# Arguments 312# subject: notification subject 313# pathname: pathname containing the notification message (OPTIONAL) 314# 315# Globals 316# ZED_PUSHBULLET_ACCESS_TOKEN 317# ZED_PUSHBULLET_CHANNEL_TAG 318# 319# Return 320# 0: notification sent 321# 1: notification failed 322# 2: not configured 323# 324zed_notify_pushbullet() 325{ 326 local subject="$1" 327 local pathname="${2:-"/dev/null"}" 328 local msg_body 329 local msg_tag 330 local msg_json 331 local msg_out 332 local msg_err 333 local url="https://api.pushbullet.com/v2/pushes" 334 335 [ -n "${ZED_PUSHBULLET_ACCESS_TOKEN}" ] || return 2 336 337 [ -n "${subject}" ] || return 1 338 if [ ! -r "${pathname}" ]; then 339 zed_log_err "pushbullet cannot read \"${pathname}\"" 340 return 1 341 fi 342 343 zed_check_cmd "awk" "curl" "sed" || return 1 344 345 # Escape the following characters in the message body for JSON: 346 # newline, backslash, double quote, horizontal tab, vertical tab, 347 # and carriage return. 348 # 349 msg_body="$(awk '{ ORS="\\n" } { gsub(/\\/, "\\\\"); gsub(/"/, "\\\""); 350 gsub(/\t/, "\\t"); gsub(/\f/, "\\f"); gsub(/\r/, "\\r"); print }' \ 351 "${pathname}")" 352 353 # Push to a channel if one is configured. 354 # 355 [ -n "${ZED_PUSHBULLET_CHANNEL_TAG}" ] && msg_tag="$(printf \ 356 '"channel_tag": "%s", ' "${ZED_PUSHBULLET_CHANNEL_TAG}")" 357 358 # Construct the JSON message for pushing a note. 359 # 360 msg_json="$(printf '{%s"type": "note", "title": "%s", "body": "%s"}' \ 361 "${msg_tag}" "${subject}" "${msg_body}")" 362 363 # Send the POST request and check for errors. 364 # 365 msg_out="$(curl -u "${ZED_PUSHBULLET_ACCESS_TOKEN}:" -X POST "${url}" \ 366 --header "Content-Type: application/json" --data-binary "${msg_json}" \ 367 2>/dev/null)"; rv=$? 368 if [ "${rv}" -ne 0 ]; then 369 zed_log_err "curl exit=${rv}" 370 return 1 371 fi 372 msg_err="$(echo "${msg_out}" \ 373 | sed -n -e 's/.*"error" *:.*"message" *: *"\([^"]*\)".*/\1/p')" 374 if [ -n "${msg_err}" ]; then 375 zed_log_err "pushbullet \"${msg_err}"\" 376 return 1 377 fi 378 return 0 379} 380 381 382# zed_notify_slack_webhook (subject, pathname) 383# 384# Notification via Slack Webhook <https://api.slack.com/incoming-webhooks>. 385# The Webhook URL (ZED_SLACK_WEBHOOK_URL) identifies this client to the 386# Slack channel. 387# 388# Requires awk, curl, and sed executables to be installed in the standard PATH. 389# 390# References 391# https://api.slack.com/incoming-webhooks 392# 393# Arguments 394# subject: notification subject 395# pathname: pathname containing the notification message (OPTIONAL) 396# 397# Globals 398# ZED_SLACK_WEBHOOK_URL 399# 400# Return 401# 0: notification sent 402# 1: notification failed 403# 2: not configured 404# 405zed_notify_slack_webhook() 406{ 407 [ -n "${ZED_SLACK_WEBHOOK_URL}" ] || return 2 408 409 local subject="$1" 410 local pathname="${2:-"/dev/null"}" 411 local msg_body 412 local msg_tag 413 local msg_json 414 local msg_out 415 local msg_err 416 local url="${ZED_SLACK_WEBHOOK_URL}" 417 418 [ -n "${subject}" ] || return 1 419 if [ ! -r "${pathname}" ]; then 420 zed_log_err "slack webhook cannot read \"${pathname}\"" 421 return 1 422 fi 423 424 zed_check_cmd "awk" "curl" "sed" || return 1 425 426 # Escape the following characters in the message body for JSON: 427 # newline, backslash, double quote, horizontal tab, vertical tab, 428 # and carriage return. 429 # 430 msg_body="$(awk '{ ORS="\\n" } { gsub(/\\/, "\\\\"); gsub(/"/, "\\\""); 431 gsub(/\t/, "\\t"); gsub(/\f/, "\\f"); gsub(/\r/, "\\r"); print }' \ 432 "${pathname}")" 433 434 # Construct the JSON message for posting. 435 # 436 msg_json="$(printf '{"text": "*%s*\\n%s"}' "${subject}" "${msg_body}" )" 437 438 # Send the POST request and check for errors. 439 # 440 msg_out="$(curl -X POST "${url}" \ 441 --header "Content-Type: application/json" --data-binary "${msg_json}" \ 442 2>/dev/null)"; rv=$? 443 if [ "${rv}" -ne 0 ]; then 444 zed_log_err "curl exit=${rv}" 445 return 1 446 fi 447 msg_err="$(echo "${msg_out}" \ 448 | sed -n -e 's/.*"error" *:.*"message" *: *"\([^"]*\)".*/\1/p')" 449 if [ -n "${msg_err}" ]; then 450 zed_log_err "slack webhook \"${msg_err}"\" 451 return 1 452 fi 453 return 0 454} 455 456# zed_notify_pushover (subject, pathname) 457# 458# Send a notification via Pushover <https://pushover.net/>. 459# The access token (ZED_PUSHOVER_TOKEN) identifies this client to the 460# Pushover server. The user token (ZED_PUSHOVER_USER) defines the user or 461# group to which the notification will be sent. 462# 463# Requires curl and sed executables to be installed in the standard PATH. 464# 465# References 466# https://pushover.net/api 467# 468# Arguments 469# subject: notification subject 470# pathname: pathname containing the notification message (OPTIONAL) 471# 472# Globals 473# ZED_PUSHOVER_TOKEN 474# ZED_PUSHOVER_USER 475# 476# Return 477# 0: notification sent 478# 1: notification failed 479# 2: not configured 480# 481zed_notify_pushover() 482{ 483 local subject="$1" 484 local pathname="${2:-"/dev/null"}" 485 local msg_body 486 local msg_out 487 local msg_err 488 local url="https://api.pushover.net/1/messages.json" 489 490 [ -n "${ZED_PUSHOVER_TOKEN}" ] && [ -n "${ZED_PUSHOVER_USER}" ] || return 2 491 492 if [ ! -r "${pathname}" ]; then 493 zed_log_err "pushover cannot read \"${pathname}\"" 494 return 1 495 fi 496 497 zed_check_cmd "curl" "sed" || return 1 498 499 # Read the message body in. 500 # 501 msg_body="$(cat "${pathname}")" 502 503 if [ -z "${msg_body}" ] 504 then 505 msg_body=$subject 506 subject="" 507 fi 508 509 # Send the POST request and check for errors. 510 # 511 msg_out="$( \ 512 curl \ 513 --form-string "token=${ZED_PUSHOVER_TOKEN}" \ 514 --form-string "user=${ZED_PUSHOVER_USER}" \ 515 --form-string "message=${msg_body}" \ 516 --form-string "title=${subject}" \ 517 "${url}" \ 518 2>/dev/null \ 519 )"; rv=$? 520 if [ "${rv}" -ne 0 ]; then 521 zed_log_err "curl exit=${rv}" 522 return 1 523 fi 524 msg_err="$(echo "${msg_out}" \ 525 | sed -n -e 's/.*"errors" *:.*\[\(.*\)\].*/\1/p')" 526 if [ -n "${msg_err}" ]; then 527 zed_log_err "pushover \"${msg_err}"\" 528 return 1 529 fi 530 return 0 531} 532 533 534# zed_notify_ntfy (subject, pathname) 535# 536# Send a notification via Ntfy.sh <https://ntfy.sh/>. 537# The ntfy topic (ZED_NTFY_TOPIC) identifies the topic that the notification 538# will be sent to Ntfy.sh server. The ntfy url (ZED_NTFY_URL) defines the 539# self-hosted or provided hosted ntfy service location. The ntfy access token 540# <https://docs.ntfy.sh/publish/#access-tokens> (ZED_NTFY_ACCESS_TOKEN) reprsents an 541# access token that could be used if a topic is read/write protected. If a 542# topic can be written to publicaly, a ZED_NTFY_ACCESS_TOKEN is not required. 543# 544# Requires curl and sed executables to be installed in the standard PATH. 545# 546# References 547# https://docs.ntfy.sh 548# 549# Arguments 550# subject: notification subject 551# pathname: pathname containing the notification message (OPTIONAL) 552# 553# Globals 554# ZED_NTFY_TOPIC 555# ZED_NTFY_ACCESS_TOKEN (OPTIONAL) 556# ZED_NTFY_URL 557# 558# Return 559# 0: notification sent 560# 1: notification failed 561# 2: not configured 562# 563zed_notify_ntfy() 564{ 565 local subject="$1" 566 local pathname="${2:-"/dev/null"}" 567 local msg_body 568 local msg_out 569 local msg_err 570 571 [ -n "${ZED_NTFY_TOPIC}" ] || return 2 572 local url="${ZED_NTFY_URL:-"https://ntfy.sh"}/${ZED_NTFY_TOPIC}" 573 574 if [ ! -r "${pathname}" ]; then 575 zed_log_err "ntfy cannot read \"${pathname}\"" 576 return 1 577 fi 578 579 zed_check_cmd "curl" "sed" || return 1 580 581 # Read the message body in. 582 # 583 msg_body="$(cat "${pathname}")" 584 585 if [ -z "${msg_body}" ] 586 then 587 msg_body=$subject 588 subject="" 589 fi 590 591 # Send the POST request and check for errors. 592 # 593 if [ -n "${ZED_NTFY_ACCESS_TOKEN}" ]; then 594 msg_out="$( \ 595 curl \ 596 -u ":${ZED_NTFY_ACCESS_TOKEN}" \ 597 -H "Title: ${subject}" \ 598 -d "${msg_body}" \ 599 -H "Priority: high" \ 600 "${url}" \ 601 2>/dev/null \ 602 )"; rv=$? 603 else 604 msg_out="$( \ 605 curl \ 606 -H "Title: ${subject}" \ 607 -d "${msg_body}" \ 608 -H "Priority: high" \ 609 "${url}" \ 610 2>/dev/null \ 611 )"; rv=$? 612 fi 613 if [ "${rv}" -ne 0 ]; then 614 zed_log_err "curl exit=${rv}" 615 return 1 616 fi 617 msg_err="$(echo "${msg_out}" \ 618 | sed -n -e 's/.*"errors" *:.*\[\(.*\)\].*/\1/p')" 619 if [ -n "${msg_err}" ]; then 620 zed_log_err "ntfy \"${msg_err}"\" 621 return 1 622 fi 623 return 0 624} 625 626 627 628# zed_rate_limit (tag, [interval]) 629# 630# Check whether an event of a given type [tag] has already occurred within the 631# last [interval] seconds. 632# 633# This function obtains a lock on the statefile using file descriptor 9. 634# 635# Arguments 636# tag: arbitrary string for grouping related events to rate-limit 637# interval: time interval in seconds (OPTIONAL) 638# 639# Globals 640# ZED_NOTIFY_INTERVAL_SECS 641# ZED_RUNDIR 642# 643# Return 644# 0 if the event should be processed 645# 1 if the event should be dropped 646# 647# State File Format 648# time;tag 649# 650zed_rate_limit() 651{ 652 local tag="$1" 653 local interval="${2:-${ZED_NOTIFY_INTERVAL_SECS}}" 654 local lockfile="zed.zedlet.state.lock" 655 local lockfile_fd=9 656 local statefile="${ZED_RUNDIR}/zed.zedlet.state" 657 local time_now 658 local time_prev 659 local umask_bak 660 local rv=0 661 662 [ -n "${tag}" ] || return 0 663 664 zed_lock "${lockfile}" "${lockfile_fd}" 665 time_now="$(date +%s)" 666 time_prev="$(grep -E "^[0-9]+;${tag}\$" "${statefile}" 2>/dev/null \ 667 | tail -1 | cut -d\; -f1)" 668 669 if [ -n "${time_prev}" ] \ 670 && [ "$((time_now - time_prev))" -lt "${interval}" ]; then 671 rv=1 672 else 673 umask_bak="$(umask)" 674 umask 077 675 grep -E -v "^[0-9]+;${tag}\$" "${statefile}" 2>/dev/null \ 676 > "${statefile}.$$" 677 echo "${time_now};${tag}" >> "${statefile}.$$" 678 mv -f "${statefile}.$$" "${statefile}" 679 umask "${umask_bak}" 680 fi 681 682 zed_unlock "${lockfile}" "${lockfile_fd}" 683 return "${rv}" 684} 685 686 687# zed_guid_to_pool (guid) 688# 689# Convert a pool GUID into its pool name (like "tank") 690# Arguments 691# guid: pool GUID (decimal or hex) 692# 693# Return 694# Pool name 695# 696zed_guid_to_pool() 697{ 698 if [ -z "$1" ] ; then 699 return 700 fi 701 702 guid="$(printf "%u" "$1")" 703 $ZPOOL get -H -ovalue,name guid | awk '$1 == '"$guid"' {print $2; exit}' 704} 705 706# zed_exit_if_ignoring_this_event 707# 708# Exit the script if we should ignore this event, as determined by 709# $ZED_SYSLOG_SUBCLASS_INCLUDE and $ZED_SYSLOG_SUBCLASS_EXCLUDE in zed.rc. 710# This function assumes you've imported the normal zed variables. 711zed_exit_if_ignoring_this_event() 712{ 713 if [ -n "${ZED_SYSLOG_SUBCLASS_INCLUDE}" ]; then 714 eval "case ${ZEVENT_SUBCLASS} in 715 ${ZED_SYSLOG_SUBCLASS_INCLUDE});; 716 *) exit 0;; 717 esac" 718 elif [ -n "${ZED_SYSLOG_SUBCLASS_EXCLUDE}" ]; then 719 eval "case ${ZEVENT_SUBCLASS} in 720 ${ZED_SYSLOG_SUBCLASS_EXCLUDE}) exit 0;; 721 *);; 722 esac" 723 fi 724} 725