How can I make a bash command run periodically?

Bash

Bash Problem Overview


I want to execute a script and have it run a command every x minutes.

Also any general advice on any resources for learning bash scripting could be really cool. I use Linux for my personal development work, so bash scripts are not totally foreign to me, I just haven't written any of my own from scratch.

Bash Solutions


Solution 1 - Bash

If you want to run a command periodically, there's 3 ways :

  • using the crontab command ex. * * * * * command (run every minutes)
  • using a loop like : while true; do ./my_script.sh; sleep 60; done (not precise)
  • using systemd timer

See cron

Some pointers for best bash scripting practices :

http://mywiki.wooledge.org/BashFAQ
Guide: http://mywiki.wooledge.org/BashGuide
ref: http://www.gnu.org/software/bash/manual/bash.html
http://wiki.bash-hackers.org/
USE MORE QUOTES!: http://www.grymoire.com/Unix/Quote.html
Scripts and more: http://www.shelldorado.com/

Solution 2 - Bash

In addition to @sputnick's answer, there is also watch. From the man page:

Execute a program periodically, showing output full screen

By default this is every 2 seconds. watch is useful for tailing logs, for example.

Solution 3 - Bash

macOS users: here's a partial implementation of the GNU watch command (as of version 0.3.0) for interactive periodic invocations for primarily visual inspection:

It is syntax-compatible with the GNU version and fails with a specific error message if an unimplemented feature is used.

Notable limitations:

  • The output is not limited to one screenful.
  • Displaying output differences is not supported.
  • Using precise timing is not supported.
  • Colored output is always passed through (--color is implied).

Also implements a few non-standard features, such as waiting for success (-E) to complement waiting for error (-e) and showing the time of day of the last invocation as well as the total time elapsed so far.

Run watch -h for details.

Examples:

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

Source code (paste into a script file named watch, make it executable, and place in a directory in your $PATH; note that syntax highlighting here is broken, but the code works):

#!/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

Solution 4 - Bash

  • If you need to visually monitor a command which gives a static output, use watch [options] command. For example, for monitoring free memory, run:

      watch -n 1 free -m
    

where the -n 1 option sets update interval to 1 second (default is 2 seconds).
Check man watch or the online manual for details.


  • If you need to visually monitor changes in a log file, tail is your command of choice, for example:

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

where the -f (for “follow”) option tells the program to output appended data as the file grows.
Check man tail or the online manual for details.

Solution 5 - Bash

> I want to execute the script and have it run a command every {time interval}

cron (https://en.wikipedia.org/wiki/Cron) was designed for this purpose. If you run man cron or man crontab you will find instructions for how to use it.

> any general advice on any resources for learning bash scripting could be really cool. I use Linux for my personal development work, so bash scripts are not totally foreign to me, I just haven't written any of my own from scratch.

If you are comfortable working with bash, I recommend reading through the bash manpage first (man bash) -- there are lots of cool tidbits.

Solution 6 - Bash

Avoiding Time Drift

Here's what I do to remove the time it takes for the command to run and still stay on schedule:

#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

This will drift off by no more than one second. Then it will snap back in sync with the clock. If you need something with less than 1 second drift and your sleep command supports floating point numbers, try adding including nanoseconds in the calculation like this

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

Solution 7 - Bash

Here is my solution to reduce drift from loop payload run time.

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

There is some approximation to timer object approach, with sleep command executed parallel with all other commands, including even true in condition check. I think it's most precise variant without drift cound using date command.

There could be true timer object bash function, implementing timer event by just 'echo' call, then piped to loop with read cmd, like this:

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

Solution 8 - Bash

I was faced with this challenge recently. I wanted a way to execute a piece of the script every hour within the bash script file that runs on crontab every 5 minutes without having to use sleep or rely fully on crontab. if the TIME_INTERVAL is not met, the script will fail at the first condition. If the TIME_INTERVAL is met, however the TIME_CONDITION is not met, the script will fail at second condition. The below script does work hand-in-hand with crontab - adjust according.

NOTE: touch -m "$0" - This will change modification timestamp of the bash script file. You will have to create a separate file for storing the last script run time, if you don't want to change the modification timestamp of the bash script file.

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionjeromeView Question on Stackoverflow
Solution 1 - BashGilles QuenotView Answer on Stackoverflow
Solution 2 - BashYossarianView Answer on Stackoverflow
Solution 3 - Bashmklement0View Answer on Stackoverflow
Solution 4 - BashKiriSakowView Answer on Stackoverflow
Solution 5 - BashSheetJSView Answer on Stackoverflow
Solution 6 - BashDavid H.View Answer on Stackoverflow
Solution 7 - BashNikita ZlobinView Answer on Stackoverflow
Solution 8 - BashZoloholicView Answer on Stackoverflow