rusht/cached/
args.rs

1use ::std::time::Duration;
2
3use ::clap::Parser;
4use ::parse_duration0::parse as parse_dur;
5
6use crate::common::CommandArgs;
7
8#[derive(Parser, Debug, PartialEq)]
9#[command(
10    name = "cached",
11    about = "Cache the output of a command for a given duration, running it only if there is no cache or it has expired. Stderr is only shown on first run."
12)]
13pub struct CachedArgs {
14    /// Duration after which the cache should be invalidated, e.g. "30 min" or "1 day -1 hour".
15    #[arg(value_parser = parse_dur, short = 'd', long = "duration", default_value = "15 min")]
16    pub duration: Duration,
17    #[clap(flatten)]
18    pub key: CachedKeyArgs,
19    /// When loading from cache, do not show the previous output.
20    #[arg(short = 's', long)]
21    pub no_cached_output: bool,
22    /// Use exit code 0 if the command is cached, and exit code 255 if it ran successfully.
23    #[arg(short = 'x', long)]
24    pub exit_code: bool,
25    /// Print extra information, e.g. whether the command was run or not.
26    #[arg(short = 'v', long)]
27    pub verbose: bool,
28    #[command(subcommand)]
29    pub cmd: CommandArgs,
30}
31
32#[derive(Parser, Debug, PartialEq, Default)]
33#[command()]
34pub struct CachedKeyArgs {
35    /// Invalidates cache if the current git HEAD commit is different. For only code changes, see --git-head-diff
36    #[arg(short = 'g', long)]
37    pub git_head: bool,
38    /// Invalidates cache if the current git branch merge-base commit is different.
39    #[arg(short = 'b', long, conflicts_with = "git_head")]
40    pub git_base: bool,
41    /// Invalidates cache if the diff of HEAD has changed. This is more lenient than --git-head as it ignores commit message and trivial rebases.
42    #[arg(short = 'G', long, conflicts_with = "git_head")]
43    pub git_head_diff: bool,
44    /// Invalidates cache if we're in a different git repo. This doesn't make much sense without --no-dir.
45    #[arg(long)]
46    pub git_repo_dir: bool,
47    /// Invalidates cache if the uncommitted git files change.
48    #[arg(short = 'p', long)]
49    pub git_pending: bool,
50    /// Name of an environment variable. Invalidates cache if the value changes.
51    #[arg(short = 'e', long)]
52    pub env: Vec<String>,
53    /// Just a text. Invalidates cache if a different text is passed.
54    #[arg(short = 't', long)]
55    pub text: Vec<String>,
56    /// Does NOT invalidate cache if the command is run from a different directory.
57    #[arg(short = 'D', long)]
58    pub no_dir: bool,
59    /// Does NOT cache if a different command is run. Should perhaps be used with e.g. --text
60    #[arg(short = 'C', long)]
61    pub no_command: bool,
62    /// Does NOT cache if different env vars are passed to the command (does not include inherited env)
63    #[arg(short = 'E', long)]
64    pub no_direct_env: bool,
65}
66
67
68impl CachedArgs {
69    pub fn any_explicit_key(&self) -> bool {
70        self.key.git_head || self.key.git_head_diff || self.key.git_base || self.key.git_repo_dir ||
71            self.key.git_pending || !self.key.env.is_empty() || !self.key.text.is_empty()
72    }
73}
74
75impl Default for CachedArgs {
76    fn default() -> Self {
77        CachedArgs {
78            duration: Duration::from_secs(15 * 60),
79            key: CachedKeyArgs {
80                git_head: false,
81                git_base: false,
82                git_head_diff: false,
83                git_repo_dir: false,
84                git_pending: false,
85                env: vec![],
86                text: vec![],
87                no_dir: false,
88                no_command: false,
89                no_direct_env: false,
90            },
91            no_cached_output: false,
92            exit_code: false,
93            verbose: false,
94            cmd: CommandArgs::Cmd(Vec::new()),
95        }
96    }
97}
98
99#[test]
100fn test_cli_args() {
101    let mut args = CachedArgs::try_parse_from(&["cmd", "ls"]).unwrap();
102    assert!(!args.any_explicit_key());
103    assert_eq!(args, CachedArgs {
104        cmd: CommandArgs::Cmd(vec!["ls".to_owned()]),
105        ..CachedArgs::default()
106    });
107    args = CachedArgs::try_parse_from(&["cmd", "--duration", "1 year", "ls"]).unwrap();
108    assert!(!args.any_explicit_key());
109    args = CachedArgs::try_parse_from(&["cmd", "-d1y", "--git-head", "--git-pending", "ls", "-a", "-l", "-s", "-h"]).unwrap();
110    assert!(args.any_explicit_key());
111    args = CachedArgs::try_parse_from(&["cmd", "-d1y", "--text", "string", "ls", "-alsh"]).unwrap();
112    assert!(args.any_explicit_key());
113    args = CachedArgs::try_parse_from(&["cmd", "-d1y", "-gpe", "ENV_VAR", "-CDEt", "string", "-t", "another string", "--", "ls"]).unwrap();
114    assert!(args.any_explicit_key());
115}