evry

A shell-script-centric task scheduler; uses exit codes to determine control flow. Most of the time I call this behind bgproc.

Install

Install rust/cargo, then:

cargo install evry

Rationale

``` A tool to manually run commands -- periodically. Uses shell exit codes to determine control flow in shell scripts

Usage: evry [describe duration]... <-tagname> evry location <-tagname> evry help ```

Best explained with an example:

evry 2 weeks -scrapesite && wget "https://" -o ....

In other words, run the wget command every 2 weeks.

evry exits with an unsuccessful exit code if the command has been run in the last 2 weeks (see below for more duration examples), which means the wget command wouldn't run.

When evry exits with a successful exit code, it saves the current time to a metadata file for that tag (-scrapesite). That way, when evry is run again with that tag, it can compare the current time against that file.

This can sort of be thought of as cron alternative, but operations don't run in the background. It requires you to call the command yourself, but it won't run if its already run in the time frame you describe. (However, its not difficult to wrap tasks that run behind evry in an infinite loop that runs in the background, which is what bgproc does)

You could have an infinite loop running in the background like:

bash while true; do evry 1 month -runcommand && run command sleep 60 done

... and even though that tries to run the command every 60 seconds, evry exits with an unsuccessful exit code, so run command would only get run once per month.

The -runcommand is just an arbitrary tag name so that evry can save metadata about a command to run/job. It can be chosen arbitrarily, its only use is to uniquely identify some task, and save a metadata file to your local data directory.

Since this doesn't run in a larger context and evry can't know if a command failed to run - if a command fails, you can remove the tag file, to reset it to run again later (since if the file doesn't exist, evry assumes its a new task):

```bash evry 2 months -selenium && {

evry succeeded, so the external command should be run

python selenium.py || {
    # the python process exited with a non-zero exit code
    # remove the tag file so we can re-try later
    rm "$(evry location -selenium)"
    # maybe notify you that this failed so you go and check on it
    notify-send -u critical 'selenium failed!"
}

} ```

Duration

The duration (e.g. evry 2 months, 5 days) is parsed with a PEG, so its very flexible. All of these are valid duration input:

See the grammar for all possible abbreviations.

Examples

This could be used to do anything you might use anacron for. For example, to periodically sync files:

bash evry 1d -backup && rsync ...

Or, cache the output of a command, once a day (e.g. my jumplist)

```bash

expensivecommandcached() { evry 1d -expensivecommandcached && cmd >~/.cache/cmdoutput cat ~/.cache/cmdoutput }

expensivecommandcached ```

I have certain jobs (e.g. scraping websites for metadata, using selenium to login to some website and click a button, or checking my music for metadata that I want to run periodically.

Putting all my jobs I want to run periodically in one housekeeping script I run daily/weekly gives me the ability to monitor the output easily, but also allows me the flexibility of being able to schedule tasks to run at different rates. It also means that those scripts/commands can prompt me for input/confirmation, since this is run manually from a terminal, not in the background like cron.

Advanced Usage

The EVRY_DEBUG environment variable can be set to provide information on what was parsed from user input, and how long till the next run succeeds.

$ EVRY_DEBUG=1 evry 2 months -pythonanywhere && pythonanywhere_3_months -Hc "$(which chromedriver)" tag_name:pythonanywhere data_directory:/home/sean/.local/share/evry/data log:parsed '2 months' into 5184000000ms log:60 days (5184000000ms) haven't elapsed since last run, exiting with code 1 log:Will next be able to run in '46 days, 16 hours, 46 minutes, 6 seconds' (4034766587ms)

If you wanted to 'reset' a task, you could do: rm ~/.local/share/evry/data/<tag name>; removing the tag file. The next time that evry runs, it'll assume its a new task, and exit successfully. I use the following shell function to 'reset' tasks:

bash job-reset() { local EVRY_DATA_DIR EVRY_DATA_DIR="$(evry location - 2>/dev/null)" cd "${EVRY_DATA_DIR}" fzf -q "$*" -m | while read -r tag; do rm -v "${tag}" done cd - }

The EVRY_JSON environment variable can be set to provide similar information in a more consumable format (e.g. with jq)

As an example; ./schedule_task:

```bash

!/bin/bash

if JSONOUTPUT="$(EVRYJSON=1 evry 2 hours -task)"; then echo "Running task..." else # extract the body for a particular log message NEXTRUN="$(echo "$JSONOUTPUT" | jq -r '.[] | select(.type == "tillnextpretty") | .body')" printf 'task will next run in %s\n' "$NEXT_RUN" fi ```

$ ./schedule_task Running task... $ ./schedule_task task will next run in 1 hours, 59 minutes, 58 seconds

For reference, typical JSON output when evry fails (command doesn't run):

json [ { "type": "tag_name", "body": "task" }, { "type": "data_directory", "body": "/home/sean/.local/share/evry/data" }, { "type": "log", "body": "parsed '2 hours' into 7200000ms" }, { "type": "duration", "body": "7200000" }, { "type": "duration_pretty", "body": "2 hours" }, { "type": "log", "body": "2 hours (7200000ms) haven't elapsed since last run, exiting with code 1" }, { "type": "log", "body": "Will next be able to run in '1 hours, 58 minutes, 17 seconds' (7097748ms)" }, { "type": "till_next", "body": "7097748" }, { "type": "till_next_pretty", "body": "1 hours, 58 minutes, 17 seconds" } ]