【问题标题】:How can I make a bash command run periodically?如何使 bash 命令定期运行?
【发布时间】:2013-06-30 20:04:57
【问题描述】:

我想执行一个脚本并让它每 x 分钟运行一个命令。

此外,关于学习 bash 脚本的任何资源的任何一般性建议都非常酷。我将 Linux 用于我的个人开发工作,因此 bash 脚本对我来说并不完全陌生,我只是没有从头开始编写任何自己的脚本。

【问题讨论】:

  • 你看cron了吗?
  • 至于您的资源请求,bash-hackers wiki 有一个精心维护的list of resources,它被明确修剪以避免不小心避开经典pitfalls 的指南。

标签: bash


【解决方案1】:

如果要定期运行命令,有 3 种方式:

  • 使用crontab 命令例如。 * * * * * command(每分钟运行一次)
  • 使用像这样的循环:while true; do ./my_script.sh; sleep 60; done(不精确)
  • 使用systemd timer

cron

一些关于最佳 bash 脚本实践的建议:

http://mywiki.wooledge.org/BashFAQ
导游:http://mywiki.wooledge.org/BashGuide
参考:http://www.gnu.org/software/bash/manual/bash.html
http://wiki.bash-hackers.org/
使用更多报价!:http://www.grymoire.com/Unix/Quote.html
脚本等:http://www.shelldorado.com/

【讨论】:

  • Crontab 在这里是更好的选择,特别是如果你想“永远”(很长时间)运行它。
  • 我不知道更好更坏的选择。他们都有自己的优点和缺点。 cron 方法的一个缺点是您可以同时运行命令的多个实例。前台 while 循环 示例不会发生这种情况。
  • 真的取决于你想做什么。 while 循环 将退出 sync (不管怎样)。如果你必须在时间上准确,那么循环就行不通了(至少不是那么容易)。
  • Cron 当然是一开始对我来说应该很明显的东西。这是一个每十五或三十分钟运行一次的过程,只需几秒钟即可完成。
【解决方案2】:

这是我减少循环负载运行时间漂移的解决方案。

tpid=0
while true; do
  wait ${tpid}
  sleep 3 & tpid=$!
  { body...; }
done

定时器对象方法有一些近似,睡眠命令与所有其他命令并行执行,甚至包括条件检查中的 true。我认为它是最精确的变体,没有使用 date 命令的漂移计数。

可能有真正的计时器对象 bash 函数,仅通过 'echo' 调用实现计时器事件,然后使用 read cmd 管道循环,如下所示:

timer | { while read ev; do...; done; }

【讨论】:

    【解决方案3】:

    ma​​cOS 用户:这是 的 GNU watch 命令(截至版本 0.3.0)的部分实现主要用于视觉检查的交互式定期调用:

    它与 GNU 版本的语法兼容,如果使用了未实现的功能,则会失败并显示特定的错误消息。

    显着限制:

    • 输出不限于一屏。
    • 不支持显示输出差异。
    • 不支持使用精确计时。
    • 彩色输出总是通过(隐含--color)。

    还实现了一些非标准功能,例如等待 成功 (-E) 以补充等待错误 (-e) 并将最后一次调用的时间显示为以及到目前为止经过的总时间。

    运行watch -h了解详情。

    例子:

    watch -n 1 ls # list current dir every second
    watch -e 'ls *.lockfile' # list lock files and exit once none exist anymore.
    

    源代码(粘贴到名为watch 的脚本文件中,使其可执行,然后放在$PATH 的目录中;注意此处的语法高亮显示已损坏,但代码有效):

    #!/usr/bin/env bash
    
    THIS_NAME=$(basename "$BASH_SOURCE")
    
    VERSION='0.1'
    
    # Helper function for exiting with error message due to runtime error.
    #   die [errMsg [exitCode]]
    # Default error message states context and indicates that execution is aborted. Default exit code is 1.
    # Prefix for context is always prepended.
    # Note: An error message is *always* printed; if you just want to exit with a specific code silently, use `exit n` directly.
    die() {
      echo "$THIS_NAME: ERROR: ${1:-"ABORTING due to unexpected error."}" 1>&2
      exit ${2:-1} # Note: If the argument is non-numeric, the shell prints a warning and uses exit code 255.
    }
    
    # Helper function for exiting with error message due to invalid parameters.
    #   dieSyntax [errMsg]
    # Default error message is provided, as is prefix and suffix; exit code is always 2.
    dieSyntax() {
      echo "$THIS_NAME: PARAMETER ERROR: ${1:-"Invalid parameter(s) specified."} Use -h for help." 1>&2
      exit 2
    }
    
    # Get the elapsed time since the specified epoch time in format HH:MM:SS.
    # Granularity: whole seconds.
    # Example:
    #   tsStart=$(date +'%s')
    #   ...
    #   getElapsedTime $tsStart 
    getElapsedTime() {
      date -j -u -f '%s' $(( $(date +'%s') - $1 ))  +'%H:%M:%S' 
    }
    
    # Command-line help.
    if [[ "$1" == '--help' || "$1" == '-h' ]]; then
      cat <<EOF
    
    SYNOPSIS
      $THIS_NAME [-n seconds] [opts] cmd [arg ...]
    
    DESCRIPTION
      Executes a command periodically and displays its output for visual inspection.
    
      NOTE: This is a PARTIAL implementation of the GNU \`watch\` command, for OS X.
      Notably, the output is not limited to one screenful, and displaying
      output differences and using precise timing are not supported.
      Also, colored output is always passed through (--color is implied).
      Unimplemented features are marked as [NOT IMPLEMENTED] below.
      Conversely, features specific to this implementation are marked as [NONSTD].
      Reference version is GNU watch 0.3.0.
    
      CMD may be a simple command with separately specified
      arguments, if any, or a single string containing one or more
      ;-separated commands (including arguments) - in the former case the command
      is directly executed by bash, in the latter the string is passed to \`bash -c\`.
      Note that GNU watch uses sh, not bash.
      To use \`exec\` instead, specify -x (see below).
    
      By default, CMD is re-invoked indefinitely; terminate with ^-C or
      exit based on conditions:
      -e, --errexit
        exits once CMD indicates an error, i.e., returns a non-zero exit code.
      -E, --okexit [NONSTD] 
        is the inverse of -e: runs until CMD returns exit code 0.
    
      By default, all output is passed through; the following options modify this
      behavior; note that suppressing output only relates to CMD's output, not the
      messages output by this utility itself:
      -q, --quiet [NONSTD]
        suppresses stdout output from the command invoked;
      -Q, --quiet-both [NONSTD] 
        suppresses both stdout and stderr output.
    
      -l, --list [NONSTD]
        list-style display; i.e., suppresses clearing of the screen 
        before every invocation of CMD.
    
      -n secs, --interval secs
        interval in seconds between the end of the previous invocation of CMD
        and the next invocation - 2 seconds by default, fractional values permitted;
        thus, the interval between successive invocations is the specified interval
        *plus* the last CMD's invocation's execution duration.
    
      -x, --exec
        uses \`exec\` rather than bash to execute CMD; this requires
        arguments to be passed to CMD to be specified as separate arguments 
        to this utility and prevents any shell expansions of these arguments
        at invocation time.
    
      -t, --no-title
        suppresses the default title (header) that displays the interval, 
        and (NONSTD) a time stamp, the time elapsed so far, and the command executed.
    
      -b, --beep
        beeps on error (bell signal), i.e., when CMD reports a non-zero exit code.
    
      -c, --color
        IMPLIED AND ALWAYS ON: colored command output is invariably passed through.
    
      -p, --precise [NOT IMPLEMENTED]
    
      -d, --difference [NOT IMPLEMENTED]
    
    EXAMPLES
        # List files in home folder every second.
      $THIS_NAME -n 1 ls ~
        # Wait until all *.lockfile files disappear from the current dir, checking every 2 secs.
      $THIS_NAME -e 'ls *.lockfile'
    
    EOF
        exit 0
    fi
    
      # Make sure that we're running on OSX.
    [[ $(uname) == 'Darwin' ]] || die "This script is designed to run on OS X only."
    
    # Preprocess parameters: expand compressed options to individual options; e.g., '-ab' to '-a -b'
    params=() decompressed=0 argsReached=0
    for p in "$@"; do
      if [[ $argsReached -eq 0 && $p =~ ^-[a-zA-Z0-9]+$ ]]; then # compressed options?
        decompressed=1
        params+=(${p:0:2})
        for (( i = 2; i < ${#p}; i++ )); do
            params+=("-${p:$i:1}")
        done
      else
        (( argsReached && ! decompressed )) && break
        [[ $p == '--' || ${p:0:1} != '-' ]] && argsReached=1
        params+=("$p")
      fi
    done
    (( decompressed )) && set -- "${params[@]}"; unset params decompressed argsReached p # Replace "$@" with the expanded parameter set.
    
    # Option-parameters loop.
    interval=2  # default interval
    runUntilFailure=0
    runUntilSuccess=0
    quietStdOut=0
    quietStdOutAndStdErr=0
    dontClear=0
    noHeader=0
    beepOnErr=0
    useExec=0
    while (( $# )); do
      case "$1" in
        --) # Explicit end-of-options marker.
          shift   # Move to next param and proceed with data-parameter analysis below.
          break
          ;;
        -p|--precise|-d|--differences|--differences=*)
          dieSyntax "Sadly, option $1 is NOT IMPLEMENTED."
          ;;
        -v|--version)
          echo "$VERSION"; exit 0
          ;;
        -x|--exec)
          useExec=1
          ;;
        -c|--color)
          # a no-op: unlike the GNU version, we always - and invariably - pass color codes through.
          ;;
        -b|--beep)
          beepOnErr=1
          ;;
        -l|--list)
          dontClear=1
          ;;
        -e|--errexit)
          runUntilFailure=1
          ;;
        -E|--okexit)
          runUntilSuccess=1
          ;;
        -n|--interval)
          shift; interval=$1;
          errMsg="Please specify a positive number of seconds as the interval."
          interval=$(bc <<<"$1") || dieSyntax "$errMsg"
          (( 1 == $(bc <<<"$interval > 0") )) || dieSyntax "$errMsg"
          [[ $interval == *.* ]] || interval+='.0'
          ;;
        -t|--no-title)
          noHeader=1
          ;;
        -q|--quiet)
          quietStdOut=1
          ;;
        -Q|--quiet-both)
          quietStdOutAndStdErr=1
          ;;
        -?|--?*) # An unrecognized switch.
          dieSyntax "Unrecognized option: '$1'. To force interpretation as non-option, precede with '--'."
          ;;
        *)  # 1st data parameter reached; proceed with *argument* analysis below.
          break
          ;;
      esac
      shift
    done
    
    # Make sure we have at least a command name
    [[ -n "$1" ]] || dieSyntax "Too few parameters specified."
    
    # Suppress output streams, if requested.
    # Duplicate stdout and stderr first.
    # This allows us to produce output to stdout (>&3) and stderr (>&4) even when suppressed.
    exec 3<&1 4<&2  
    if (( quietStdOutAndStdErr )); then
      exec &> /dev/null
    elif (( quietStdOut )); then
      exec 1> /dev/null
    fi
    
    # Set an exit trap to ensure that the duplicated file descriptors are closed.
    trap 'exec 3>&- 4>&-' EXIT
    
    # Start loop with periodic invocation.
    # Note: We use `eval` so that compound commands - e.g. 'ls; bash --version' - can be passed.
    tsStart=$(date +'%s')
    while :; do
      (( dontClear )) || clear
      (( noHeader )) || echo "Every ${interval}s. [$(date +'%H:%M:%S') - elapsed: $(getElapsedTime $tsStart)]: $@"$'\n' >&3
      if (( useExec )); then
        (exec "$@")  # run in *subshell*, otherwise *this* script will be replaced by the process invoked
      else
        if [[ $* == *' '* ]]; then
          # A single argument with interior spaces was provided -> we must use `bash -c` to evaluate it properly.
          bash -c "$*"
        else
          # A command name only or a command name + arguments were specified as separate arguments -> let bash run it directly.
          "$@"
        fi
      fi
      ec=$?
      (( ec != 0 && beepOnErr )) && printf '\a'
      (( ec == 0 && runUntilSuccess )) && { echo $'\n'"[$(date +'%H:%M:%S') - elapsed: $(getElapsedTime $tsStart)] Exiting as requested: exit code 0 reported." >&3; exit 0; }
      (( ec != 0 && runUntilFailure )) && { echo $'\n'"[$(date +'%H:%M:%S') - elapsed: $(getElapsedTime $tsStart)] Exiting as requested: non-zero exit code ($ec) reported." >&3; exit 0; }
      sleep $interval
    done
    

    【讨论】:

    • 该死,现在这个是一个答案!
    【解决方案4】:

    除了@sputnick 的回答,还有watch。从手册页:

    Execute a program periodically, showing output full screen
    

    默认情况下,这是每 2 秒一次。例如,watch 对于 tailing 日志很有用。

    【讨论】:

    • 重要的是要指出watch 是非标准的,默认情况下不存在于大多数非 GNU 系统(包括 Mac)上。
    【解决方案5】:

    避免时间漂移

    这是我为消除命令运行所需的时间并仍按计划执行的操作:

    #One-liner to execute a command every 600 seconds avoiding time drift
    #Runs the command at each multiple of :10 minutes
    
    while sleep $(echo 600-`date "+%s"`%600 | bc); do ls; done
    

    这将漂移不超过一秒钟。然后它将与时钟同步回弹。如果您需要小于 1 秒的漂移并且您的 sleep 命令支持浮点数,请尝试在计算中添加纳秒,如下所示

    while sleep $(echo 6-`date "+%s.%N"`%6 | bc); do date '+%FT%T.%N'; done
    

    【讨论】:

    • 这很聪明!这实际上删除了命令的运行时。但是,它可以进行一些改进。
    • 我将实施以下更新。这消除了调用 echo datebc : t=$SECONDS; while true; do cmd; sleep $(((t+=6) - SECONDS)); done 导致的所有开销。变量SECONDS 是一个bash 内部变量。如果将cmd 替换为date '+%FT%T.%6N',您可以看到漂移最小
    • @kvantour 很棒的意见!您的解决方案仍然无限期地漂移。但你给了我更多考虑。我已经更新了我的解决方案!
    • 小dt的回弹可能有问题
    【解决方案6】:
    • 如果您需要直观地监控给出静态输出的命令,请使用watch [options] command。例如,要监控空闲内存,请运行:

      watch -n 1 free -m
      

      -n 1 选项将更新间隔设置为 1 秒(默认为 2 秒)。
      详情请查看man watchonline manual


    • 如果您需要直观地监控日志文件中的更改,tail 是您的首选命令,例如:

      tail -f /path/to/logs/file.log
      

      -f(用于“跟随”)选项告诉程序随着文件的增长输出附加的数据。
      详情请查看man tailonline manual

    【讨论】:

      【解决方案7】:

      我想执行脚本并让它每隔{时间间隔}运行一个命令

      cron (https://en.wikipedia.org/wiki/Cron) 就是为此目的而设计的。如果您运行man cronman crontab,您将找到有关如何使用它的说明。

      关于学习 bash 脚本的任何资源的任何一般性建议都可能非常酷。我将 Linux 用于我的个人开发工作,因此 bash 脚本对我来说并不完全陌生,我只是没有从头开始编写任何自己的脚本。

      如果您习惯使用 bash,我建议您先阅读 bash 手册页 (man bash) -- 有很多很酷的花絮。

      【讨论】:

        【解决方案8】:

        我最近面临这个挑战。我想要一种方法,在每 5 分钟在 crontab 上运行的 bash 脚本文件中每小时执行一段脚本,而不必使用睡眠或完全依赖 crontab。如果 TIME_INTERVAL 不满足,脚本将在第一个条件下失败。如果满足 TIME_INTERVAL,但未满足 TIME_CONDITION,则脚本将在第二个条件下失败。下面的脚本确实与 crontab 协同工作 - 相应地进行调整。

        注意:touch -m "$0" - 这将改变 bash 脚本文件的修改时间戳。如果您不想更改 bash 脚本文件的修改时间戳,则必须创建一个单独的文件来存储上次脚本运行时间。

        CURRENT_TIME=$(date "+%s")
        LAST_SCRIPT_RUN_TIME=$(date -r "$0" "+%s")
        TIME_INTERVAL='3600'
        START_TIME=$(date -d '07:00:00' "+%s")
        END_TIME=$(date -d '16:59:59' "+%s")
        TIME_DIFFERENCE=$((${CURRENT_TIME} - ${LAST_SCRIPT_RUN_TIME}))
        TIME_CONDITION=$((${START_TIME} <= ${CURRENT_TIME} && ${CURRENT_TIME} <= $END_TIME}))
        
        
        if [[ "$TIME_DIFFERENCE" -le "$TIME_INTERVAL" ]];
            then
                >&2 echo "[ERROR] FAILED - script failed to run because of time conditions"
        elif [[ "$TIME_CONDITION" = '0' ]];
            then
                >&2 echo "[ERROR] FAILED - script failed to run because of time conditions"
        elif [[ "$TIME_CONDITION" = '1' ]];
            then
                >&2 touch -m "$0"
                >&2 echo "[INFO] RESULT - script ran successfully"
        fi
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-12-28
          • 2016-05-13
          • 2015-10-27
          • 2017-06-13
          • 2020-10-02
          • 1970-01-01
          • 2013-11-30
          • 1970-01-01
          相关资源
          最近更新 更多