diff --git a/.golangci.yaml b/.golangci.yaml index c340052..440af97 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,7 +1,6 @@ --- linters: disable: - - err113 - nlreturn - typecheck - wsl diff --git a/README.md b/README.md index 6f94a90..e8345a4 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,13 @@ $ is known cli version --major zsh ### Has gofumpt been modified in the last week? ```text -$ is cli age gofumpt lt 7 d +is cli age gofumpt lt 7 d +``` + +### Has a file been modified in the last hour? + +```text +is fso age ./stats.txt lt 1 h ``` ### echo the OS name @@ -183,7 +189,9 @@ is os version-codename unlike ventura 🚨 Leaky abstraction alert! -Regex patterns are passed directly to Golang's `regexp.MatchString`. We can take advantage of this when crafting regexes. For instance, for a case insensitive search: +Regex patterns are passed directly to Golang's `regexp.MatchString`. We can +take advantage of this when crafting regexes. For instance, for a case +insensitive search: ```text is cli output stdout date like "(?i)wed" @@ -392,14 +400,14 @@ Optional argument to command. Can be used more than once. Let's match on the results of `uname -m -n`. -``` +```bash is cli output stdout uname --arg="-m" --arg="-n" eq "olafs-mbp-2.lan x86_64" ``` If our args don't contain special characters or spaces, we may not need to quote them. Let's match on the results of `cat README.md`. -``` +```bash is cli output stdout cat --arg README.md like "an inspector for your environment" ``` @@ -463,7 +471,7 @@ is cli output stdout "bash -c" -a "date|wc -l" eq 1 Passing negative integers as expected values is a bit tricky, since we don't want them to be interpreted as flags. -``` +```bash $ is cli output stdout 'bash -c' -a 'date|wc -l' gt -1 ``` @@ -471,7 +479,7 @@ $ is cli output stdout 'bash -c' -a 'date|wc -l' gt -1 We can use `--` before the expected value to get around this. 😅 -``` +```bash $ is cli output stdout 'bash -c' -a 'date|wc -l' gt -- -1 ``` @@ -502,6 +510,53 @@ in some cases try to do an optimistic comparison. That is, it will try a string comparison first and then a numeric comparison. Hopefully this will "do the right thing" for you. If not, please open an issue. +### fso + +`fso` is short for filesystem object (file, directory, link, etc). This command +is very similar to `cli age`. The difference between `cli age` and `fso age` is +that `fso` will not search your `$PATH`. You may provide either a relative or +an absolute path. + +#### age + +Compare against the last modified date of a file. + +```bash +is cli age /tmp/programs.csv lt 18 hours +``` + +Compare against the last modified date of a directory. + +```bash +is cli age ~./local/cache gt 1 d +``` + +Supported comparisons are: + +* `lt` +* `gt` + +Supported units are: + +* `s` +* `second` +* `seconds` +* `m` +* `minute` +* `minutes` +* `h` +* `hour` +* `hours` +* `d` +* `day` +* `days` + +Note that `d|day|days` is shorthand for 24 hours. DST offsets are not taken +into account here. + +The `--debug` flag can give us some helpful information when troubleshooting +date math. + ### os Information specific to the current operating system @@ -835,6 +890,7 @@ $ is known cli version --minor tmux $ is known cli version --patch tmux 0 ``` + Please see the docs on `os version` for more information on `--major`, `--minor` and `--patch`. diff --git a/api.go b/api.go index c336c47..710ef2c 100644 --- a/api.go +++ b/api.go @@ -42,6 +42,13 @@ type CLICmd struct { Output OutputCmp `cmd:"" help:"Check output of a command. e.g. \"is cli output stdout \"uname -a\" like \"Kernel Version 22.5\""` } +// FSOCmd type is configuration for FSO checks. +// +//nolint:lll +type FSOCmd struct { + Age AgeCmp `cmd:"" help:"Check age (last modified time) of an fso (2h, 4d). e.g. \"is fso age /tmp/log.txt gt 1 d\""` +} + // OSCmd type is configuration for OS level checks. // //nolint:lll diff --git a/cli.go b/cli.go index a60e3fa..70209c2 100644 --- a/cli.go +++ b/cli.go @@ -32,7 +32,7 @@ func execCommand(ctx *types.Context, stream, cmd string, args []string) (string, // Run "is cli ...". func (r *CLICmd) Run(ctx *types.Context) error { if r.Age.Name != "" { - return runAge(ctx, r.Age.Name, r.Age.Op, r.Age.Val, r.Age.Unit) + return runCliAge(ctx, r.Age.Name, r.Age.Op, r.Age.Val, r.Age.Unit) } if r.Version.Name != "" { output, err := parser.CLIOutput(ctx, r.Version.Name) @@ -96,12 +96,15 @@ func compareAge(ctx *types.Context, modTime, targetTime time.Time, operator, pat } } -func runAge(ctx *types.Context, name, ageOperator, ageValue, ageUnit string) error { +func runCliAge(ctx *types.Context, name, ageOperator, ageValue, ageUnit string) error { path, err := exec.LookPath(name) if err != nil { return errors.Join(errors.New("could not find command"), err) } + return runAge(ctx, path, ageOperator, ageValue, ageUnit) +} +func runAge(ctx *types.Context, path, ageOperator, ageValue, ageUnit string) error { info, err := os.Stat(path) if err != nil { return errors.Join(errors.New("could not stat command"), err) diff --git a/fso.go b/fso.go new file mode 100644 index 0000000..4922f0a --- /dev/null +++ b/fso.go @@ -0,0 +1,15 @@ +// Package main contains the logic for the "fso" command +package main + +import ( + "errors" + + "github.com/oalders/is/types" +) + +func (r *FSOCmd) Run(ctx *types.Context) error { + if r.Age.Name != "" { + return runAge(ctx, r.Age.Name, r.Age.Op, r.Age.Val, r.Age.Unit) + } + return errors.New("unimplemented command") +} diff --git a/fso_test.go b/fso_test.go new file mode 100644 index 0000000..f286929 --- /dev/null +++ b/fso_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "testing" + + "github.com/oalders/is/ops" + "github.com/oalders/is/types" + "github.com/stretchr/testify/assert" +) + +func TestFSOLastModifiedTime(t *testing.T) { + t.Parallel() + const tmux = "testdata/bin/tmux" + { + ctx := types.Context{Debug: true} + cmd := FSOCmd{Age: AgeCmp{tmux, ops.Gt, "1", "s"}} + err := cmd.Run(&ctx) + assert.NoError(t, err) + assert.True(t, ctx.Success) + } + { + ctx := types.Context{Debug: true} + cmd := FSOCmd{Age: AgeCmp{tmux, ops.Lt, "100000", "days"}} + err := cmd.Run(&ctx) + assert.NoError(t, err) + assert.True(t, ctx.Success) + } + { + ctx := types.Context{Debug: true} + cmd := FSOCmd{Age: AgeCmp{tmux, ops.Lt, "1.1", "d"}} + err := cmd.Run(&ctx) + assert.Error(t, err) + assert.False(t, ctx.Success) + } + { + ctx := types.Context{Debug: true} + cmd := FSOCmd{Age: AgeCmp{"tmuxxx", ops.Lt, "1", "d"}} + err := cmd.Run(&ctx) + assert.Error(t, err) + assert.False(t, ctx.Success) + } +} diff --git a/main.go b/main.go index 785cb13..61fcb34 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ func main() { Arch ArchCmd `cmd:"" help:"Check arch e.g. \"is arch like x64\""` CLI CLICmd `cmd:"" help:"Check cli version. e.g. \"is cli version tmux gte 3\""` Debug bool `help:"turn on debugging statements"` + FSO FSOCmd `cmd:"" help:"Check fso (file system object). e.g. \"is fso age gte 3 days\""` //nolint:lll Known KnownCmd `cmd:""` OS OSCmd `cmd:"" help:"Check OS attributes. e.g. \"is os name eq darwin\""` There ThereCmd `cmd:"" help:"Check if command exists. e.g. \"is there git\""`