{"id":17748535,"url":"https://github.com/purarue/evry","last_synced_at":"2025-03-18T16:30:26.594Z","repository":{"id":57626559,"uuid":"276848162","full_name":"purarue/evry","owner":"purarue","description":"A shell-script-centric task scheduler; uses exit codes to determine control flow","archived":false,"fork":false,"pushed_at":"2024-11-21T06:48:43.000Z","size":138,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-11-21T22:41:46.812Z","etag":null,"topics":["automation","cli","cron","duration","pest-grammar","schedule-tasks","task-scheduler"],"latest_commit_sha":null,"homepage":"https://crates.io/crates/evry","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/purarue.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-07-03T08:26:37.000Z","updated_at":"2024-11-21T06:48:46.000Z","dependencies_parsed_at":"2023-02-08T21:02:36.792Z","dependency_job_id":"98eaa1e0-ffdc-47cf-8b25-5483aab11ba3","html_url":"https://github.com/purarue/evry","commit_stats":{"total_commits":77,"total_committers":1,"mean_commits":77.0,"dds":0.0,"last_synced_commit":"52133c8ac5fcfbfa45446a227374932452dd6b01"},"previous_names":["purarue/evry","seanbreckenridge/evry"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/purarue%2Fevry","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/purarue%2Fevry/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/purarue%2Fevry/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/purarue%2Fevry/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/purarue","download_url":"https://codeload.github.com/purarue/evry/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243940058,"owners_count":20372044,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","cli","cron","duration","pest-grammar","schedule-tasks","task-scheduler"],"created_at":"2024-10-26T10:02:12.117Z","updated_at":"2025-03-18T16:30:26.352Z","avatar_url":"https://github.com/purarue.png","language":"Rust","funding_links":[],"categories":["Rust"],"sub_categories":[],"readme":"# evry\n\nA shell-script-centric task scheduler; uses exit codes to determine control flow. Most of the time I call this behind [bgproc](https://github.com/purarue/bgproc).\n\n- [Install](#install)\n- [Rationale](#rationale)\n- [Duration Examples](#duration)\n- [Examples](#examples)\n- [Advanced Usage](#advanced-usage)\n\n### Install\n\nInstall `rust`/`cargo`, then:\n\n```\ncargo install evry\n```\n\n## Rationale\n\n```\nA tool to manually run commands -- periodically.\nUses shell exit codes to determine control flow in shell scripts\n\nUsage:\n  evry \u003cdescribe duration\u003e... \u003c-tagname\u003e\n  evry location \u003c-tagname\u003e\n  evry duration \u003cdescribe duration...\u003e\n  evry help\n```\n\nBest explained with an example:\n\n`evry 2 weeks -scrapesite \u0026\u0026 wget \"https://\" -o ....`\n\nIn other words, run the `wget` command every `2 weeks`.\n\n`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.\n\nWhen `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.\n\nThis 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`](https://github.com/purarue/bgproc) does)\n\nYou could have an infinite loop running in the background like:\n\n```bash\nwhile true; do\n  evry 1 month -runcommand \u0026\u0026 run command\n  sleep 60\ndone\n```\n\n... 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.\n\nThe `-runcommand` is just an arbitrary tag name so that `evry` can save metadata about a command to run/job. Its only use is to uniquely identify some task, and save a metadata file to your [local data directory](https://docs.rs/app_dirs/1.2.1/app_dirs/). If you want to overwrite the default location, you can set the `EVRY_DIR` variable. E.g., in your shell profile:\n\n```bash\nexport EVRY_DIR=\"$HOME/.local/share/tags\"\n```\n\nSince 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):\n\n```bash\nevry 2 months -selenium \u0026\u0026 {\n# evry succeeded, so the external command should be run\n    python selenium.py || {\n        # the python process exited with a non-zero exit code\n        # remove the tag file so we can re-try later\n        rm \"$(evry location -selenium)\"\n        # maybe notify you that this failed so you go and check on it\n        notify-send -u critical 'selenium failed!\"\n    }\n}\n```\n\n### Duration\n\nThe duration (e.g. `evry 2 months, 5 days`) is parsed with a [`PEG`](https://en.wikipedia.org/wiki/Parsing_expression_grammar), so its very flexible. All of these are valid duration input:\n\n- `2 months, 5 day`\n- `2weeks 5hrs` (commas are optional)\n- `60secs`\n- `5wk, 5d`\n- `5weeks, 2weeks` (is additive, so this would result in 7 weeks)\n- `60sec 2weeks` (order doesn't matter)\n\nSee [the grammar](https://github.com/purarue/evry/blob/5a98d5607654c90a43eb02ee3304d3bcae1a9a3a/src/time.pest#L5-L11) for all possible abbreviations.\n\nThis also includes a utility `duration` command to print a parsed duration in seconds:\n\n```\n$ evry duration 5m\n300\n$ evry duration 5 minutes\n300\n$ evry duration 10 days\n864000\n```\n\nCan run with `EVRY_JSON=1` to print JSON with more formats.\n\n### Examples\n\nThis could be used to do anything you might use anacron for. For example, to periodically sync files:\n\n```bash\nevry 1d -backup \u0026\u0026 rsync ...\n```\n\nOr, cache the output of a command, once a day (e.g. my [`jumplist`](https://github.com/purarue/dotfiles/blob/baf92d5fed00b87167b509f22d439c5e2075f63b/.local/scripts/generic/jumplist))\n\n```bash\n\nexpensive_command_cached() {\n\tevry 1d -expensive_command_cached \u0026\u0026 cmd \u003e~/.cache/cmd_output\n\tcat ~/.cache/cmd_output\n}\n\nexpensive_command_cached\n```\n\nI have certain jobs (e.g. scraping websites for metadata, using [`selenium`](https://www.selenium.dev/) to [login to some website and click a button](https://github.com/purarue/pythonanywhere-3-months), or [checking my music for metadata](https://github.com/purarue/plaintext_playlist_py) that I want to run periodically.\n\nPutting all my jobs I want to run periodically in one [`housekeeping`](https://github.com/purarue/dotfiles/blob/master/.local/scripts/linux/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.\n\nI often use this instead of cron when developing websites, e.g. [here](https://github.com/purarue/dbsentinel/blob/32b81d09b201a92f7308ceda0b4323eff52b7df5/update_data#L97-L115), where I use it to periodically run caching tasks for a webservice. Having them in a script like this means its the same interface/environment while I'm developing and deploying, so there's no issues with possibly missing environment variables/being in the wrong directory when deploying to production, and its easy to 'reset' a cron job while I'm developing\n\n### Advanced Usage\n\nThe `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.\n\n```\n$ EVRY_DEBUG=1 evry 2 months -pythonanywhere \u0026\u0026 pythonanywhere_3_months -Hc \"$(which chromedriver)\"\ntag_name:pythonanywhere\ndata_directory:/home/username/.local/share/evry/data\nlog:parsed '2 months' into 5184000000ms\nlog:60 days (5184000000ms) haven't elapsed since last run, exiting with code 1\nlog:Will next be able to run in '46 days, 16 hours, 46 minutes, 6 seconds' (4034766587ms)\n```\n\nThe `EVRY_PARSE_ERROR_LOG` environment variable can be set to save any duration parsing errors to a file, which can be useful for debugging, especially if you're dynamically generating the duration string. In your shell profile:\n\n```bash\nexport EVRY_PARSE_ERROR_LOG=\"$HOME/.cache/evry_parse_errors.log\"\n```\n\nIf you wanted to 'reset' a task, you could do: `rm ~/.local/share/evry/data/\u003ctag name\u003e`; 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 (see [`functions.sh`](./functions.sh)) to 'reset' tasks:\n\n```bash\njob-reset() {\n\tlocal EVRY_DATA_DIR\n\tEVRY_DATA_DIR=\"$(evry location - 2\u003e/dev/null)\"\n\tcd \"${EVRY_DATA_DIR}\"\n\tfzf -q \"$*\" -m | while read -r tag; do\n\t\trm -v \"${tag}\"\n\tdone\n\tcd -\n}\n```\n\nThe `EVRY_JSON` environment variable can be set to provide similar information in a more consumable format (e.g. with [`jq`](https://github.com/stedolan/jq))\n\nAs an example; `./schedule_task`:\n\n```bash\n#!/bin/bash\n\nif JSON_OUTPUT=\"$(EVRY_JSON=1 evry 2 hours -task)\"; then\n  echo \"Running task...\"\nelse\n  # extract the body for a particular log message\n  NEXT_RUN=\"$(echo \"$JSON_OUTPUT\" | jq -r '.[] | select(.type == \"till_next_pretty\") | .body')\"\n  printf 'task will next run in %s\\n' \"$NEXT_RUN\"\nfi\n```\n\n```\n$ ./schedule_task\nRunning task...\n$ ./schedule_task\ntask will next run in 1 hours, 59 minutes, 58 seconds\n```\n\nFor reference, typical JSON output when `evry` fails (command doesn't run):\n\n```json\n[\n  {\n    \"type\": \"tag_name\",\n    \"body\": \"task\"\n  },\n  {\n    \"type\": \"data_directory\",\n    \"body\": \"/home/username/.local/share/evry/data\"\n  },\n  {\n    \"type\": \"log\",\n    \"body\": \"parsed '2 hours' into 7200000ms\"\n  },\n  {\n    \"type\": \"duration\",\n    \"body\": \"7200000\"\n  },\n  {\n    \"type\": \"duration_pretty\",\n    \"body\": \"2 hours\"\n  },\n  {\n    \"type\": \"log\",\n    \"body\": \"2 hours (7200000ms) haven't elapsed since last run, exiting with code 1\"\n  },\n  {\n    \"type\": \"log\",\n    \"body\": \"Will next be able to run in '1 hours, 58 minutes, 17 seconds' (7097748ms)\"\n  },\n  {\n    \"type\": \"till_next\",\n    \"body\": \"7097748\"\n  },\n  {\n    \"type\": \"till_next_pretty\",\n    \"body\": \"1 hours, 58 minutes, 17 seconds\"\n  }\n]\n```\n\nIf you want to extract multiple values from the output which have distinct keys (e.g. `till_next_pretty`, `till_next` and `tag_name`), you might find this snippet useful:\n\n```bash\n$ EVRY_JSON=1 evry 12 hours -bleanser-zsh | jq '[.[] | {key: .type, value: .body}] | from_entries'\n{\n  \"tag_name\": \"bleanser-zsh\",\n  \"data_directory\": \"/home/username/.local/share/evry/data\",\n  \"log\": \"Tag file doesn't exist, creating and exiting with code 0\",\n  \"duration\": \"43200000\",\n  \"duration_pretty\": \"12 hours\"\n}\n```\n\nYou could then use `jq` to extract the `duration`/`duration_pretty`/`tag_name` values from that output, like:\n\n```bash\n$ OUT=\"$(EVRY_JSON=1 evry 12 hours -bleanser-zsh | jq '[.[] | {key: .type, value: .body}] | from_entries')\"\n$ echo \"$OUT\" | jq -r '.duration'\n43200000\n$ echo \"$OUT\" | jq -r '.duration_pretty'\n12 hours\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpurarue%2Fevry","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpurarue%2Fevry","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpurarue%2Fevry/lists"}