{"id":17760870,"url":"https://github.com/dbohdan/recur","last_synced_at":"2026-01-19T21:54:27.561Z","repository":{"id":258722629,"uuid":"875231787","full_name":"dbohdan/recur","owner":"dbohdan","description":"Retry a command with exponential backoff and jitter (+ Starlark expressions)","archived":false,"fork":false,"pushed_at":"2025-12-12T18:55:42.000Z","size":208,"stargazers_count":284,"open_issues_count":0,"forks_count":4,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-12-14T01:30:49.528Z","etag":null,"topics":["go","golang","retry","starlark","starlark-go"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dbohdan.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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-10-19T12:44:41.000Z","updated_at":"2025-11-17T10:58:19.000Z","dependencies_parsed_at":"2025-08-05T19:18:54.434Z","dependency_job_id":"5a8bf769-1548-4654-a462-65430bca5a61","html_url":"https://github.com/dbohdan/recur","commit_stats":null,"previous_names":["dbohdan/recur"],"tags_count":26,"template":false,"template_full_name":null,"purl":"pkg:github/dbohdan/recur","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dbohdan%2Frecur","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dbohdan%2Frecur/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dbohdan%2Frecur/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dbohdan%2Frecur/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dbohdan","download_url":"https://codeload.github.com/dbohdan/recur/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dbohdan%2Frecur/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28579265,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-19T17:42:58.221Z","status":"ssl_error","status_checked_at":"2026-01-19T17:40:54.158Z","response_time":67,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["go","golang","retry","starlark","starlark-go"],"created_at":"2024-10-26T19:13:56.029Z","updated_at":"2026-01-19T21:54:27.548Z","avatar_url":"https://github.com/dbohdan.png","language":"Go","funding_links":[],"categories":["Go","go","Users"],"sub_categories":[],"readme":"# recur\n\n**recur** is a command-line tool that runs a single command repeatedly until it succeeds or no more attempts are left.\nIt implements optional [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) with configurable [jitter](https://en.wikipedia.org/wiki/Thundering_herd_problem#Mitigation).\nIt lets you write the success condition in [Starlark](https://laurent.le-brun.eu/blog/an-overview-of-starlark).\n\n## Installation\n\n### Prebuilt binaries\n\nPrebuilt binaries for\nFreeBSD (amd64),\nLinux (aarch64, riscv64, x86_64),\nmacOS (arm64, x86_64),\nNetBSD (amd64),\nOpenBSD (amd64),\nand Windows (amd64, arm64, x86)\nare attached to [releases](https://github.com/dbohdan/recur/releases).\n\n### Homebrew\n\nYou can install recur [from Homebrew](https://formulae.brew.sh/formula/recur) on macOS and Linux:\n\n```shell\nbrew install recur\n```\n\n### Go\n\nInstall Go, then run:\n\n```shell\ngo install dbohdan.com/recur/v3@latest\n```\n\n## Build requirements\n\n- Go 1.22\n- [Task](https://taskfile.dev/) (go-task) 3.28\n\n## Usage\n\n### Command-line interface\n\n\u003c!-- BEGIN USAGE --\u003e\n```none\nUsage: recur [-h] [-V] [-a \u003cattempts\u003e] [-b \u003cbackoff\u003e] [-c \u003ccondition\u003e] [-d\n\u003cdelay\u003e] [-E] [-F] [-f] [-I] [-j \u003cjitter\u003e] [-m \u003cmax-delay\u003e] [-O] [-R \u003cpath\u003e] [-r\n\u003creset-time\u003e] [-s \u003cseed\u003e] [-t \u003ctimeout\u003e] [-v] [--] \u003ccommand\u003e [\u003carg\u003e ...]\n\nRetry a command with exponential backoff and jitter.\n\nArguments:\n  \u003ccommand\u003e\n          Command to run\n\n  [\u003carg\u003e ...]\n          Arguments to the command\n\nOptions:\n  -h, --help\n          Print this help message and exit\n\n  -V, --version\n          Print version number and exit\n\n  -a, --attempts 10\n          Maximum number of attempts (negative for infinite)\n\n  -b, --backoff 0\n          Base for exponential backoff (duration)\n\n  -c, --condition 'code == 0'\n          Success condition (Starlark expression)\n\n  -d, --delay 0\n          Constant delay (duration)\n\n  -E, --hold-stderr\n          Buffer standard error for each attempt and only print it on success\n\n  -F, --fib\n          Add Fibonacci backoff\n\n  -f, --forever\n          Infinite attempts\n\n  -I, --replay-stdin\n          Read standard input until EOF at the start and replay it on each\nattempt\n\n  -j, --jitter '0,0'\n          Additional random delay (maximum duration or 'min,max' duration)\n\n  -m, --max-delay 1h\n          Maximum allowed sum of constant delay, exponential backoff, and\nFibonacci backoff (duration)\n\n  -O, --hold-stdout\n          Buffer standard output for each attempt and only print it on success\n\n  -R, --report ''\n          Report output (file path or '-' for stderr; prefix with 'json:' or\n'text:' to override the format; empty to disable)\n\n  -r, --reset -1s\n          Minimum attempt time that resets exponential and Fibonacci backoff\n(duration; negative for no reset)\n\n  -s, --seed 0\n          Random seed for jitter (0 for automatic)\n\n  -t, --timeout -1s\n          Timeout for each attempt (duration; negative for no timeout)\n\n  -v, --verbose\n          Increase verbosity (up to 3 times)\n```\n\u003c!-- END USAGE --\u003e\n\nDuration arguments take [Go duration strings](https://pkg.go.dev/time#ParseDuration);\nfor example, `0`, `100ms`, `2.5s`, `0.5m`, or `1h`.\nThe value of `-j`/`--jitter` must be either a single duration or two durations joined with a comma, like `1s,2s` or `500ms, 0.5m`.\n\nIf the maximum delay (`-m`/`--max-delay`) is shorter than the constant delay (`-d`/`--delay`), the constant delay will automatically increase the maximum delay to match it.\nUse `-m`/`--max-delay` after `-d`/`--delay` if you want a shorter maximum delay.\n\nThe following recur options run the command `foo --config bar.cfg` indefinitely.\nEvery time `foo` exits, there is a delay that grows exponentially from two seconds to a minute.\nThe delay resets back to two seconds if the command runs for at least five minutes.\n\n```shell\nrecur --backoff 2s --condition False --forever --max-delay 1m --reset 5m foo --config bar.cfg\n```\n\nrecur exits with the last command's exit code unless the user overrides this in the condition.\nWhen the command is not found during the last attempt, recur exits with code 127.\nrecur exits with code 124 on timeout and 255 on internal error.\n\n### Standard input\n\nBy default, the command run by recur inherits its [standard input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)).\nThis means that if standard input is a terminal, every attempt can read interactively from the terminal.\nIf standard input is a pipe or redirected file, the data is consumed on the first attempt;\nlater attempts see an immediate [EOF](https://en.wikipedia.org/wiki/End-of-file).\n\nTo feed the command the same data on each attempt, use the `-I`/`--replay-stdin` option.\nWith this option, recur reads its entire stdin into memory and replays it on each attempt.\n\n```none\n$ echo hi | recur -a 3 -c False cat\nhi\nrecur [00:00:00.0]: maximum 3 attempts reached\n\n$ echo hi | recur -a 3 -c False -I cat\nhi\nhi\nhi\nrecur [00:00:00.0]: maximum 3 attempts reached\n```\n\nBecause the data is buffered in memory, `--replay-stdin` is not recommended for very large inputs.\n\n### Standard output and standard error\n\nThe command's [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)) and [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)) are passed through to recur's standard output and standard error by default.\nTo buffer standard output and only print it on success, use `-O`/`--hold-stdout`.\nWith this option, recur buffers the command's standard output and only prints it if the success condition is met or the condition expression calls `stdout.flush()`.\n\n```none\n$ recur -c 'attempt == 3' sh -c 'echo \"$RECUR_ATTEMPT\"'\n1\n2\n3\n\n$ recur -c 'attempt == 3' -O sh -c 'echo \"$RECUR_ATTEMPT\"'\n3\n\n$ recur -c 'stdout.flush() or attempt == 3' -O sh -c 'echo \"$RECUR_ATTEMPT\"'\n1\n2\n3\n```\n\nThe `-E`/`--hold-stderr` option and `stderr.flush()` method work similarly for standard error.\n\nBecause the data is buffered in memory, `--hold-stdout` and `--hold-stderr` are not recommended for commands that produce very large output.\n\n### Regular-expression matching\n\nYou can match regular expressions against recur's input and the command's output in your success condition using methods on the built-in objects `stdin`, `stdout`, and `stderr`:\n\n- `stdin.search()` — matches against standard input (requires `-I`/`--replay-stdin`)\n- `stdout.search()` — matches against standard output (requires `-O`/`--hold-stdout`)\n- `stderr.search()` — matches against standard error (requires `-E`/`--hold-stderr`)\n\nThese methods use [Go regular expressions](https://pkg.go.dev/regexp) with the [RE2 syntax](https://github.com/google/re2/wiki/Syntax).\n\nThe `stdin`, `stdout`, and `stderr` objects are `None` without their respective command-line option (`-I`/`--replay-stdin`, `-O`/`--hold-stdout`, or `-E`/`--hold-stderr`).\nCalling methods on `None` will result in an error.\n\nStandard input, standard output, and standard error are not available directly as Starlark strings to reduce memory usage.\nThe methods provide the only way to access them in conditions.\n\n#### Matching standard input\n\nThe following example waits for the input to contain `done` on a line after `status:`:\n\n```none\n$ printf 'Status:\\nDONE\\n' | recur \\\n    --condition 'stdin.search(r\"(?im)status:\\s*done$\")' \\\n    --replay-stdin \\\n    cat \\\n    ;\nStatus:\nDONE\n```\n\n`r\"...\"` disables the processing of backslash escapes in the string.\nIt is necessary because `\\s` is not a valid backslash escape.\nThe regular expression `(?im)status:\\s*done$` uses [RE2 inline flags](https://github.com/google/re2/wiki/Syntax#:~:text=case-insensitive):\n- `i` for case-insensitive matching\n- `m` for multiline mode (`$` matches the end of each line)\n\nThe condition evaluates to true when `stdin.search()` finds a match (returns a non-empty list) and false when no match is found (returns `None`).\n\nStandard input is perhaps of limited use for retrying a command because it is read once and never changes.\nHowever, it can be used to exit early.\n\n#### Matching standard output and standard error\n\nThis example extracts a status value from the command's output and validates it:\n\n```none\n$ recur \\\n    --condition 'stdout.search(r\"(?i)status:([^\\n]+)\", group=1, default=\"fail\").strip().lower() != \"fail\"' \\\n    --hold-stdout \\\n    echo 'Status: OK' \\\n    ;\nStatus: OK\n```\n\nIn this condition:\n\n- `stdout.search(r\"(?i)status:([^\\n]+)\", group=1, default=\"fail\")` searches for `\"status:\"` followed by text on the same line\n  - `r\"...\"` disables the processing of backslash escapes like `\\n` in the string\n  - `group=1` extracts just the captured text (for example, `\" OK\"` with a leading space)\n  - `default=\"fail\"` returns `\"fail\"` if no match is found\n- `.strip().lower()` normalizes the extracted value\n\nMatching against standard error with `stderr.search` works similarly.\n\n### Environment variables\n\nrecur sets the environment variable `RECUR_ATTEMPT` to the current attempt number so the command can access it.\nrecur also sets `RECUR_MAX_ATTEMPTS` to the value of `-a`/`--attempts`\nand `RECUR_ATTEMPT_SINCE_RESET` to the attempt number since exponential and Fibonacci backoff were reset.\n\nThe following command succeeds on the last attempt:\n\n```none\n$ recur sh -c 'echo \"Attempt $RECUR_ATTEMPT of $RECUR_MAX_ATTEMPTS\"; exit $((RECUR_MAX_ATTEMPTS - RECUR_ATTEMPT))'\nAttempt 1 of 10\nAttempt 2 of 10\nAttempt 3 of 10\nAttempt 4 of 10\nAttempt 5 of 10\nAttempt 6 of 10\nAttempt 7 of 10\nAttempt 8 of 10\nAttempt 9 of 10\nAttempt 10 of 10\n```\n\n## Conditions\n\nrecur supports a limited form of scripting.\nYou can define the success condition using an expression in [Starlark](https://laurent.le-brun.eu/blog/an-overview-of-starlark), a small scripting language derived from Python.\nThe default condition is `code == 0`.\nThis means recur stops retrying when the command exits with code zero.\n\nThe condition expression can evaluate to any value.\n`False`, `None`, numeric zero (`0`, `0.0`), and empty collections (`\"\"`, `()`, `[]`, `{}`) are considered false.\nAll other values are considered true.\n\nIf you know Python, you can quickly start writing recur conditions in Starlark.\nThe most significant differences between Starlark and Python for this purpose are:\n\n- Starlark has no `is`.\n  You must write `code == None`, not `code is None`.\n- Starlark has no sets.\n  Write `code in (1, 2, 3)` or `code in [1, 2, 3]` instead of `code in {1, 2, 3}`.\n\nYou can use the following variables in the condition expression:\n\n- `attempt`: `int` — the number of the current attempt, starting at one.\n  Combine with `--forever` to use the condition instead of the built-in attempt counter.\n- `attempt_since_reset`: `int` — the attempt number since exponential and Fibonacci backoff were reset, starting at one.\n- `code`: `int | None` — the exit code of the last command.\n  `code` is `None` when the command was not found and 124 when a timeout occurred.\n- `command_found`: `bool` — whether the last command was found.\n- `max_attempts`: `int` — the value of the `--attempts` option.\n  `--forever` sets it to -1.\n- `stderr`: `io_buffer | None` — an object representing standard error.\n  `None` without `-E`/`--hold-stderr`.\n- `stdin`: `io_buffer | None` — an object representing standard input.\n  `None` without `-I`/`--replay-stdin`.\n- `stdout`: `io_buffer | None` — an object representing standard output.\n  `None` without `-O`/`--hold-stdout`.\n- `time`: `float` — the time the most recent attempt took, in seconds.\n- `total_time`: `float` — the elapsed time from the start of the first attempt to the end of the most recent attempt, in seconds.\n\nrecur defines the following custom functions:\n\n- `exit(code: int | None) -\u003e None` — exit with the given exit code.\n  If `code` is `None`, exit with code 127 (command not found).\n- `inspect(value: Any, *, prefix: str = \"\") -\u003e Any` — log `value` prefixed by `prefix` and return `value`.\n  This is useful for debugging.\n\nThe `stdin`, `stdout`, and `stderr` objects have the following methods:\n\n- `stdout.flush() -\u003e None`, `stderr.flush() -\u003e None` — if recur is running with `-O`/`--hold-stdout` or `-E`/`--hold-stderr` respectively, recur will output the command's buffered standard output or standard error after evaluating the condition.\n  The output is printed whether the condition is true or false, and also if `exit` is called.\n  These methods cannot be called without their respective options because the objects are `None`.\n- `stdin.search`, `stdout.search`, and `stderr.search` with signature `search(pattern: str, *, group: int | None = None, default: Any = None) -\u003e Any` — match a [Go regular expression](https://pkg.go.dev/regexp) against standard input, standard output, or standard error.\n  `pattern` uses the [RE2 syntax](https://github.com/google/re2/wiki/Syntax).\n  If `group` is not specified, the function returns a list of submatches (with the full match as the first element) or `default` if no match is found.\n  If `group` is specified, it returns the given capture group or `default` if the group is not found.\n  These methods require `-I`/`--replay-stdin`, `-O`/`--hold-stdout`, and `-E`/`--hold-stderr` respectively.\n  Without the option, the corresponding object is `None`, and calling methods on it will result in an error.\n\nThe `exit` function allows you to override the default behavior of returning the last command's exit code.\nFor example, you can make recur exit with success when the command fails:\n\n```shell\nrecur --condition 'code != 0 and exit(0)' sh -c 'exit 1'\n# or\nrecur --condition 'False if code == 0 else exit(0)' sh -c 'exit 1'\n```\n\nIn the following example, recur stops early and does not retry when the command's exit code indicates incorrect usage or a problem with the installation:\n\n```shell\nrecur --condition 'code == 0 or (code in (1, 2, 3, 4) and exit(code))' curl \"$url\"\n```\n\n## Reports\n\nThe `-R`/`--report` option controls the output of statistics when recur exits.\n\nReporting is disabled by default.\nUse `-R -`/`--report -` to write a report to standard error or `-R foo/bar`/`--report foo/bar` to write it to the file `foo/bar`.\nThe report format, JSON or text, is determined by whether the file extension is `.json`.\nUse the prefix `json:` or `text:` to override the format.\n\n### Text report\n\nThe text report presents statistics in two columns:\n\n```none\n$ recur -a 3 -c False -R - sh -c 'exit 99'\nrecur [00:00:00.0]: maximum 3 attempts reached\n\n  Total attempts: 3\n       Successes: 0\n        Failures: 3\n\n      Total time: 0.003\n      Wait times: 0.000, 0.000, 0.000\n\n   Condition met: false, false, false\n   Command found: true, true, true\n      Exit codes: 99, 99, 99\n```\n\nSee the JSON section for an explanation of each value.\n\nThe text report format is not considered stable.\nDo not rely on parsing it.\n\n### JSON Report\n\nThe JSON report provides the same information in a machine-readable format:\n\n```none\n\u003e ./recur -a 3 -c False -R json:- sh -c 'exit 99'\nrecur [00:00:00.0]: maximum 3 attempts reached\n{\"attempts\":3,\"command_found\":[true,true,true],\"condition_met\":[false,false,false],\"exit_codes\":[99,99,99],\"failures\":3,\"successes\":0,\"total_time\":0.010311538,\"wait_times\":[0,0,0]}\n```\n\nThe JSON is indented when written to a file;\non standard error, it is a single line for easy filtering.\n\nThe JSON report contains:\n\n- `attempts`: the number of times the command was run\n- `command_found`: an array of booleans indicating whether the command was found for each attempt\n- `condition_met`: an array of booleans indicating whether the success condition was met for each attempt\n- `exit_codes`: an array of exit codes from each attempt\n- `failures`: the number of attempts when the condition was not met\n- `successes`: the number of attempts when the condition was met\n- `total_time`: the elapsed time from the start of the first attempt to the end of the last attempt, in seconds\n- `wait_times`: an array of delays _before_ each attempt, in seconds\n\n## License\n\nMIT.\n\n## Alternatives\n\nrecur was inspired by [retry-cli](https://github.com/tirsen/retry-cli).\nI wanted something like retry-cli but without the Node.js dependency.\n\nOther similar tools include:\n\n- [attempt](https://github.com/MaxBondABE/attempt).\n  Written in Rust.\n  `cargo install attempt-cli`.\n- [eb](https://github.com/rye/eb).\n  Written in Rust.\n  `cargo install eb`.\n- [retry (joshdk)](https://github.com/joshdk/retry).\n  Written in Go.\n  `go install github.com/joshdk/retry@master`.\n- [retry (kadwanev)](https://github.com/kadwanev/retry).\n  Written in Bash.\n- [retry (minfrin)](https://github.com/minfrin/retry).\n  Written in C.\n  Packaged for Debian and Ubuntu.\n  `sudo apt install retry`.\n- [retry (timofurrer)](https://github.com/timofurrer/retry-cmd).\n  Written in Rust.\n  `cargo install retry-cmd`.\n- [retry-cli](https://github.com/tirsen/retry-cli).\n  Written in JavaScript for Node.js.\n  `npx retry-cli`.\n- [SysBox](https://github.com/skx/sysbox) includes the command `splay`.\n  Written in Go.\n  `go install github.com/skx/sysbox@latest`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdbohdan%2Frecur","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdbohdan%2Frecur","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdbohdan%2Frecur/lists"}