├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── eng └── build.sh └── src └── main.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "cargo" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | merge_group: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - run: rustup update 16 | shell: bash 17 | - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 18 | - uses: baptiste0928/cargo-install@91c5da15570085bcde6f4d7aed98cb82d6769fd3 19 | with: 20 | crate: typos-cli 21 | - run: eng/build.sh 22 | shell: bash -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstyle" 7 | version = "1.0.8" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 10 | 11 | [[package]] 12 | name = "clap" 13 | version = "4.5.40" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" 16 | dependencies = [ 17 | "clap_builder", 18 | "clap_derive", 19 | ] 20 | 21 | [[package]] 22 | name = "clap_builder" 23 | version = "4.5.40" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" 26 | dependencies = [ 27 | "anstyle", 28 | "clap_lex", 29 | ] 30 | 31 | [[package]] 32 | name = "clap_derive" 33 | version = "4.5.40" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" 36 | dependencies = [ 37 | "heck", 38 | "proc-macro2", 39 | "quote", 40 | "syn", 41 | ] 42 | 43 | [[package]] 44 | name = "clap_lex" 45 | version = "0.7.4" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 48 | 49 | [[package]] 50 | name = "duration-string" 51 | version = "0.5.2" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "04782251e09dc67c90d694d89e9a3e5fc6cfe883df1b203202de672d812fb299" 54 | 55 | [[package]] 56 | name = "exponential-backoff" 57 | version = "2.1.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "020662aa57307d8884be79fca464cce073745cfe6ac70805770972113ca6ee95" 60 | dependencies = [ 61 | "fastrand", 62 | ] 63 | 64 | [[package]] 65 | name = "fastrand" 66 | version = "2.1.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" 69 | 70 | [[package]] 71 | name = "heck" 72 | version = "0.5.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 75 | 76 | [[package]] 77 | name = "proc-macro2" 78 | version = "1.0.86" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 81 | dependencies = [ 82 | "unicode-ident", 83 | ] 84 | 85 | [[package]] 86 | name = "quote" 87 | version = "1.0.37" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 90 | dependencies = [ 91 | "proc-macro2", 92 | ] 93 | 94 | [[package]] 95 | name = "retry-cli" 96 | version = "0.0.5" 97 | dependencies = [ 98 | "clap", 99 | "duration-string", 100 | "exponential-backoff", 101 | ] 102 | 103 | [[package]] 104 | name = "syn" 105 | version = "2.0.75" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" 108 | dependencies = [ 109 | "proc-macro2", 110 | "quote", 111 | "unicode-ident", 112 | ] 113 | 114 | [[package]] 115 | name = "unicode-ident" 116 | version = "1.0.12" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 119 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Brian Caswell "] 3 | categories = ["command-line-utilities"] 4 | description = "retry commands with automatic backoff" 5 | edition = "2021" 6 | license = "MIT" 7 | name = "retry-cli" 8 | repository = "https://github.com/demoray/retry-cli" 9 | version = "0.0.5" 10 | 11 | [[bin]] 12 | name = "retry" 13 | path = "src/main.rs" 14 | 15 | [dependencies] 16 | clap = { version = "4.5", default-features = false, features = ["derive", "help", "std", "usage"] } 17 | duration-string = "0.5" 18 | exponential-backoff = "2.1" 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brian Caswell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # retry (cli) 2 | 3 | ## Summary 4 | 5 | *A small command line application that assists in retrying failed commands.* 6 | 7 | `retry` is a command line tool written in [Rust](https://www.rust-lang.org/) intended to automatically re-run failed commands with a user configurable delay between tries. 8 | 9 | ## Usage 10 | 11 | ``` 12 | Usage: retry [OPTIONS] ... 13 | 14 | Arguments: 15 | ... 16 | 17 | 18 | Options: 19 | --attempts 20 | [default: 3] 21 | 22 | --min-duration 23 | minimum duration 24 | 25 | Examples: `10ms`, `2s`, `5m 30s`, or `1h10m` 26 | 27 | [default: 10ms] 28 | 29 | --max-duration 30 | maximum duration 31 | 32 | Examples: `10ms`, `2s`, `5m 30s`, or `1h10m` 33 | 34 | --jitter 35 | amount of randomization to add to the backoff 36 | 37 | [default: 0.3] 38 | 39 | --factor 40 | backoff factor 41 | 42 | [default: 2] 43 | 44 | -h, --help 45 | Print help (see a summary with '-h') 46 | 47 | -V, --version 48 | Print version 49 | ``` 50 | 51 | ## Installation 52 | 53 | ```console 54 | $ cargo install retry-cli 55 | ``` 56 | 57 | ## Examples 58 | 59 | Working successfully: 60 | ```console 61 | $ retry echo hi 62 | hi 63 | $ 64 | ``` 65 | 66 | The command fails to execute 67 | ```console 68 | $ retry cmd-does-not-exist 69 | Error: "unable to execute: Os { code: 2, kind: NotFound, message: \"No such file or directory\" }" 70 | $ 71 | ``` 72 | 73 | The command executes, but fails 74 | ```console 75 | $ retry false 76 | failed, retrying... 77 | failed, retrying... 78 | Error: "continued to fail after 3 attempts" 79 | $ 80 | ``` 81 | -------------------------------------------------------------------------------- /eng/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -uvex -o pipefail 4 | 5 | cd $(dirname ${BASH_SOURCE[0]})/../ 6 | 7 | which typos || cargo install typos-cli 8 | 9 | BUILD_COMMON="--locked --release" 10 | 11 | typos 12 | cargo clippy ${BUILD_COMMON} --all-targets --all-features -- -D warnings -D clippy::pedantic -A clippy::missing_errors_doc 13 | cargo clippy ${BUILD_COMMON} --tests --all-targets --all-features -- -D warnings 14 | cargo fmt --check 15 | cargo build ${BUILD_COMMON} 16 | cargo test ${BUILD_COMMON} 17 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use duration_string::DurationString; 3 | use exponential_backoff::Backoff; 4 | use std::{process::Command, thread::sleep}; 5 | 6 | #[derive(Parser)] 7 | #[command( 8 | author, 9 | version, 10 | propagate_version = true, 11 | disable_help_subcommand = true 12 | )] 13 | struct Args { 14 | #[clap(long, default_value = "3")] 15 | attempts: u32, 16 | 17 | /// minimum duration 18 | /// 19 | /// Examples: `10ms`, `2s`, `5m 30s`, or `1h10m` 20 | #[clap(long, default_value = "10ms")] 21 | min_duration: DurationString, 22 | 23 | /// maximum duration 24 | /// 25 | /// Examples: `10ms`, `2s`, `5m 30s`, or `1h10m` 26 | #[clap(long)] 27 | max_duration: Option, 28 | 29 | /// amount of randomization to add to the backoff 30 | #[clap(long, default_value = "0.3")] 31 | jitter: f32, 32 | 33 | /// backoff factor 34 | #[clap(long, default_value = "2")] 35 | factor: u32, 36 | 37 | #[clap(required = true)] 38 | command: Vec, 39 | } 40 | 41 | fn main() -> Result<(), String> { 42 | let Args { 43 | attempts, 44 | min_duration, 45 | max_duration, 46 | jitter, 47 | factor, 48 | mut command, 49 | } = Args::parse(); 50 | 51 | let mut backoff = Backoff::new(attempts, min_duration.into(), max_duration.map(Into::into)); 52 | backoff.set_factor(factor); 53 | backoff.set_jitter(jitter); 54 | 55 | let mut cmd = Command::new(command.remove(0)); 56 | if !command.is_empty() { 57 | cmd.args(command); 58 | } 59 | 60 | for duration in backoff { 61 | match cmd.status() { 62 | Ok(status) => { 63 | if status.success() { 64 | return Ok(()); 65 | } 66 | if let Some(duration) = duration { 67 | eprintln!("failed, retrying..."); 68 | sleep(duration); 69 | } 70 | } 71 | Err(fatal) => return Err(format!("unable to execute: {fatal:?}")), 72 | } 73 | } 74 | 75 | Err(format!("continued to fail after {attempts} attempts")) 76 | } 77 | --------------------------------------------------------------------------------