From e089bb02318d42e2b0b083459bfb5ab73b81cf1f Mon Sep 17 00:00:00 2001 From: "Eduardo A. Garcia" Date: Sun, 20 Apr 2025 12:07:32 -0500 Subject: [PATCH] feature(feature-core-recipe-components): core recipe components (#31) * feature(feature-core-recipe-components): initial crypto, data, generate, and string components * refactor(feature-core-recipe-components): handle escaping echo properly * feature(feature-core-recipe-components): file and data component updates * refactor(feature-core-recipe-components): noop, unset, op workdir * chore(feature-core-recipe-components): recipe updates * chore(feature-core-recipe-components): rename unset to cleanup * feature(feature-core-recipe-components): crypto and file updates * feature(feature-core-recipe-components): run components directly * chore(feature-core-recipe-components): version bump * refactor(feature-core-recipe-components): component cleanup and standards * chore(feature-core-recipe-components): file component updates * chore(feature-core-recipe-components): filter updates * feature(feature-core-recipe-components): add conversion components * chore(feature-core-recipe-components): cleanup crypto, data, and os * chore(feature-core-recipe-components): os updates * chore(feature-core-recipe-components): add todo components * chore(feature-core-recipe-components): remove testing * chore(feature-core-recipe-components): shell bug fix * chore(feature-core-recipe-components): bug fixes * feature(feature-core-recipe-components): add mime components * chore(feature-core-recipe-components): move files to general dir * feature(feature-core-recipe-components): more string utilities * feature(feature-core-recipe-components): add more mime functionality * chore(feature-core-recipe-components): cleanup * chore(feature-core-recipe-components): cleanup * feature(feature-core-recipe-components): list components * refactor(feature-core-recipe-components): use list component * feature(feature-core-recipe-components): json components and recipe, user path componet, file user path updates * chore(feature-core-recipe-components): list updates * feature(feature-core-recipe-components): math components * fix(feature-core-recipe-components): fix concurrent map read and write * feature(feature-core-recipe-components): json components and recipe * feature(feature-core-recipe-components): temp files * feature(feature-core-recipe-components): add lint components * refactor(feature-core-recipe-components): move lint * feature(feature-core-recipe-components): component output format * feature(feature-core-recipe-components): python component * feature(feature-core-recipe-components): password check components * chore(feature-core-recipe-components): move password * feature(feature-core-recipe-components): list shift, pop, not contains * feature(feature-core-recipe-components): add git components * refactor(feature-core-recipe-components): linst and list updates * refactor(feature-core-recipe-components): lint recipe update * refactor(feature-core-recipe-components): lint update * refactor(feature-core-recipe-components): encode escapes * refactor(feature-core-recipe-components): raw support * chore(feature-core-recipe-components): fix linting * chore(feature-core-recipe-components): early exit * feature(feature-core-recipe-components): add git staged and unstaged component * feature(feature-core-recipe-components): git staged+unstaged count * refactor(feature-core-recipe-components): no files to lint messaging * feature(feature-core-recipe-components): update user history components * refactor(feature-core-recipe-components): user component refactor * chore(feature-core-recipe-components): table updates * feature(feature-core-recipe-components): credential exposure recipes * chore(feature-core-recipe-components): language updates * chore(feature-core-recipe-components): remove todo files * chore(feature-core-recipe-components): simplify help text * chore(feature-core-recipe-components): version bump --- internal/app.go | 21 + internal/command.go | 6 + internal/component.go | 51 +- internal/condition.go | 7 +- internal/consts.go | 2 +- internal/dispatch.go | 58 + internal/prompts.go | 2 + internal/recipe.go | 111 +- internal/templates.go | 19 +- internal/types.go | 3 + internal/utils.go | 51 + recipes/1password/app.yaml | 19 - recipes/1password/components/items.yaml | 31 +- recipes/1password/components/utils.yaml | 2 +- recipes/1password/op.yaml | 29 + recipes/1password/password.yaml | 74 +- recipes/demo/arguments.yaml | 21 +- recipes/demo/background-tasks.yaml | 21 +- recipes/demo/color.yaml | 19 +- recipes/demo/components.yaml | 20 +- recipes/demo/conditional.yaml | 15 +- recipes/demo/flexible-tables.yaml | 16 +- recipes/demo/for.yaml | 14 +- recipes/demo/foreach.yaml | 13 +- recipes/demo/handlers.yaml | 19 +- recipes/demo/hello-world.yaml | 15 +- recipes/demo/monitor.yaml | 17 +- recipes/demo/operation-order.yaml | 18 +- recipes/demo/progress-mode.yaml | 19 +- recipes/demo/progress.yaml | 23 +- recipes/demo/prompts.yaml | 32 +- recipes/demo/tables.yaml | 20 +- recipes/demo/transform.yaml | 17 +- recipes/demo/while.yaml | 18 +- recipes/docker/components/container.yaml | 64 +- recipes/docker/logs.yaml | 38 +- recipes/docker/prune.yaml | 24 +- recipes/docker/shell.yaml | 29 +- recipes/gcp/components/project.yaml | 36 +- recipes/gcp/project.yaml | 21 +- recipes/gcp/secret.yaml | 30 +- recipes/git/lint.yaml | 35 + recipes/git/stash.yaml | 25 +- recipes/git/update.yaml | 21 +- recipes/git/version.yaml | 27 +- recipes/utils/components/clipboard.yaml | 15 +- recipes/utils/components/convert.yaml | 1600 ++++++++++++++++++++++ recipes/utils/components/crypto.yaml | 87 ++ recipes/utils/components/data.yaml | 60 + recipes/utils/components/dir.yaml | 81 ++ recipes/utils/components/file.yaml | 510 +++++++ recipes/utils/components/generate.yaml | 147 ++ recipes/utils/components/git.yaml | 114 ++ recipes/utils/components/history.yaml | 32 - recipes/utils/components/json.yaml | 153 +++ recipes/utils/components/lint.yaml | 160 +++ recipes/utils/components/list.yaml | 573 ++++++++ recipes/utils/components/math.yaml | 272 ++++ recipes/utils/components/mime.yaml | 158 +++ recipes/utils/components/os.yaml | 114 +- recipes/utils/components/password.yaml | 100 ++ recipes/utils/components/python.yaml | 43 + recipes/utils/components/string.yaml | 294 ++++ recipes/utils/components/user.yaml | 138 ++ recipes/utils/json.yaml | 62 + recipes/utils/os.yaml | 12 +- recipes/utils/terminal.yaml | 66 +- recipes/utils/time.yaml | 9 +- 68 files changed, 5312 insertions(+), 661 deletions(-) delete mode 100644 recipes/1password/app.yaml create mode 100644 recipes/1password/op.yaml create mode 100644 recipes/git/lint.yaml create mode 100644 recipes/utils/components/convert.yaml create mode 100644 recipes/utils/components/crypto.yaml create mode 100644 recipes/utils/components/data.yaml create mode 100644 recipes/utils/components/dir.yaml create mode 100644 recipes/utils/components/file.yaml create mode 100644 recipes/utils/components/generate.yaml create mode 100644 recipes/utils/components/git.yaml delete mode 100644 recipes/utils/components/history.yaml create mode 100644 recipes/utils/components/json.yaml create mode 100644 recipes/utils/components/lint.yaml create mode 100644 recipes/utils/components/list.yaml create mode 100644 recipes/utils/components/math.yaml create mode 100644 recipes/utils/components/mime.yaml create mode 100644 recipes/utils/components/password.yaml create mode 100644 recipes/utils/components/python.yaml create mode 100644 recipes/utils/components/string.yaml create mode 100644 recipes/utils/components/user.yaml create mode 100644 recipes/utils/json.yaml diff --git a/internal/app.go b/internal/app.go index d2fb989..8feaaee 100644 --- a/internal/app.go +++ b/internal/app.go @@ -38,6 +38,7 @@ func buildApp() *cli.App { listCommand(), syncCommand(), whichCommand(), + componentCommand(), }, } } @@ -213,3 +214,23 @@ func whichCommand() *cli.Command { }, } } + +// componentCommand defines the 'run-component' command +func componentCommand() *cli.Command { + return &cli.Command{ + Name: "component", + Aliases: []string{"comp", "c"}, + Usage: "Run a component directly with parameters as component inputs", + Description: "Run components directly by providing the component id and input parameters", + ArgsUsage: "component_id [--param1=value1 --param2=value2 ...]", + Action: func(c *cli.Context) error { + if c.Args().Len() == 0 { + fmt.Println("Error: component id is required") + return nil + } + + componentID := c.Args().First() + return dispatchComponent(componentID, c.Args().Tail()) + }, + } +} diff --git a/internal/command.go b/internal/command.go index 46206b4..f5b3eb4 100644 --- a/internal/command.go +++ b/internal/command.go @@ -215,7 +215,9 @@ func initializeBackgroundTask(taskID, cmd string, ctx *ExecutionContext) { } ctx.BackgroundTasks[taskID] = task + ctx.OperationMutex.Lock() ctx.OperationOutputs[taskID] = string(TaskPending) + ctx.OperationMutex.Unlock() ctx.OperationResults[taskID] = false } @@ -244,7 +246,9 @@ func handleBackgroundTaskFailure(op Operation, task *BackgroundTask, ctx *Execut task.Error = err.Error() ctx.OperationResults[op.ID] = false ctx.Vars["error"] = err.Error() + ctx.OperationMutex.Lock() ctx.OperationOutputs[op.ID] = fmt.Sprintf("Error: %s", err.Error()) + ctx.OperationMutex.Unlock() if op.ComponentInstanceID != "" { ctx.ExecutedOperationsByComponent[op.ComponentInstanceID] = @@ -271,7 +275,9 @@ func handleBackgroundTaskSuccess(op Operation, task *BackgroundTask, ctx *Execut task.Status = TaskComplete task.Output = output + ctx.OperationMutex.Lock() ctx.OperationOutputs[op.ID] = strings.TrimSpace(output) + ctx.OperationMutex.Unlock() ctx.OperationResults[op.ID] = true if op.ComponentInstanceID != "" { diff --git a/internal/component.go b/internal/component.go index f2258b4..d79a7d5 100644 --- a/internal/component.go +++ b/internal/component.go @@ -86,27 +86,43 @@ func ExpandComponentReferences(operations []Operation, opMap map[string]Operatio instanceNum := componentInstances[op.Uses] var instanceID string + parentPrefix := "" + if op.ComponentInstanceID != "" { + parentPrefix = op.ComponentInstanceID + "_" + } + if op.ID != "" { - instanceID = fmt.Sprintf("%s_%s_%d", op.Uses, op.ID, instanceNum) + instanceID = fmt.Sprintf("%s%s_%s_%d", parentPrefix, op.Uses, op.ID, instanceNum) } else { - instanceID = fmt.Sprintf("%s_%d", op.Uses, instanceNum) + instanceID = fmt.Sprintf("%s%s_%d", parentPrefix, op.Uses, instanceNum) } Log(CategoryComponent, fmt.Sprintf("Expanding component reference: %s (instance: %s)", op.Uses, instanceID)) var inputOps []Operation - if op.With != nil && len(op.With) > 0 { + if len(component.Inputs) > 0 { Log(CategoryComponent, fmt.Sprintf("Processing component inputs for: %s (instance: %s)", op.Uses, instanceID)) + Log(CategoryComponent, fmt.Sprintf("Component input count: %d", len(component.Inputs))) - if len(component.Inputs) > 0 { - for _, input := range component.Inputs { - if input.Required { - if _, exists := op.With[input.ID]; !exists { - return nil, fmt.Errorf("required input '%s' missing for component: %s", input.ID, op.Uses) - } + for _, input := range component.Inputs { + Log(CategoryComponent, fmt.Sprintf("Input: %s", input.ID)) + Log(CategoryComponent, fmt.Sprintf("Input required: %v", input.Required)) + + if input.Required { + if op.With == nil { + return nil, fmt.Errorf("required input '%s' missing for component: %s", input.ID, op.Uses) } + + if _, exists := op.With[input.ID]; !exists { + return nil, fmt.Errorf("required input '%s' missing for component: %s", input.ID, op.Uses) + } + + Log(CategoryComponent, fmt.Sprintf("With: %v", op.With[input.ID])) } } + } + + if op.With != nil && len(op.With) > 0 { for name, value := range op.With { var inputVar string = name @@ -120,7 +136,7 @@ func ExpandComponentReferences(operations []Operation, opMap map[string]Operatio inputOp := Operation{ Name: fmt.Sprintf("Set component input: %s", name), ID: inputVar, - Command: fmt.Sprintf("echo '%v'", value), + Command: fmt.Sprintf("echo '%s'", encodeEscapes(value)), Silent: true, } inputOps = append(inputOps, inputOp) @@ -141,7 +157,7 @@ func ExpandComponentReferences(operations []Operation, opMap map[string]Operatio defaultOp := Operation{ Name: fmt.Sprintf("Set default input: %s", input.Name), ID: input.ID, - Command: fmt.Sprintf("echo '%v'", input.Default), + Command: fmt.Sprintf("echo '%s'", encodeEscapes(input.Default)), Silent: true, } inputOps = append(inputOps, defaultOp) @@ -155,6 +171,7 @@ func ExpandComponentReferences(operations []Operation, opMap map[string]Operatio for i, compOp := range component.Operations { clonedOps[i] = compOp applyOperationProperties(&clonedOps[i], op) + clonedOps[i].ComponentInstanceID = instanceID if clonedOps[i].ID != "" { opMap[clonedOps[i].ID] = clonedOps[i] @@ -212,6 +229,18 @@ func applyOperationProperties(target *Operation, source Operation) { } } + if source.OnFailure == ":" && target.OnFailure == "" { + target.OnFailure = ":" + } + + if source.Workdir != "" && target.Workdir == "" { + target.Workdir = source.Workdir + } + + if source.OutputFormat != "" && target.OutputFormat == "" { + target.OutputFormat = source.OutputFormat + } + target.Silent = target.Silent || source.Silent target.Break = target.Break || source.Break target.Exit = target.Exit || source.Exit diff --git a/internal/condition.go b/internal/condition.go index 6e42c2e..1495b25 100644 --- a/internal/condition.go +++ b/internal/condition.go @@ -288,7 +288,12 @@ func resolveVariableValue(varRef string, ctx *ExecutionContext) string { if value, ok := ctx.Vars[varName]; ok { return fmt.Sprintf("%v", value) } - if value, ok := ctx.OperationOutputs[varName]; ok { + + ctx.OperationMutex.RLock() + value, ok := ctx.OperationOutputs[varName] + ctx.OperationMutex.RUnlock() + + if ok { return value } return "false" diff --git a/internal/consts.go b/internal/consts.go index ed88d73..4deca2d 100644 --- a/internal/consts.go +++ b/internal/consts.go @@ -6,5 +6,5 @@ const ( GithubRepo = "https://github.com/eduardoagarcia/shef" PublicRecipesFilename = "recipes.tar.gz" PublicRecipesFolder = "recipes" - Version = "v0.2.9" + Version = "v0.3.0" ) diff --git a/internal/dispatch.go b/internal/dispatch.go index 2e891b1..69ea800 100644 --- a/internal/dispatch.go +++ b/internal/dispatch.go @@ -408,6 +408,64 @@ func processShortFlag(arg string, vars map[string]interface{}) { } } +// dispatchComponent handles execution of a component directly +func dispatchComponent(componentID string, args []string) error { + loadComponents([]string{"local", "user", "public"}) + component, exists := globalComponentRegistry.Get(componentID) + if !exists { + return fmt.Errorf("component not found: %s", componentID) + } + Log(CategoryComponent, fmt.Sprintf("Found component: %s - %s", component.Name, component.Description)) + + _, inputVars := processRemainingArgs(args) + Log(CategoryComponent, "Component inputs", map[string]interface{}{ + "inputs": inputVars, + }) + missingInputs := []string{} + for _, input := range component.Inputs { + if input.Required { + _, exists := inputVars[input.ID] + if !exists { + missingInputs = append(missingInputs, input.ID) + } + } + } + if len(missingInputs) > 0 { + errorMsg := "missing required inputs: " + strings.Join(missingInputs, ", ") + return fmt.Errorf(errorMsg) + } + + recipe := Recipe{ + Name: fmt.Sprintf("Component: %s", component.Name), + Description: component.Description, + Operations: []Operation{ + { + Name: fmt.Sprintf("Run Component: %s", component.Name), + Uses: componentID, + With: make(map[string]interface{}), + }, + }, + } + + for _, input := range component.Inputs { + if value, exists := inputVars[input.ID]; exists { + recipe.Operations[0].With[input.ID] = value + } else if input.Default != nil { + recipe.Operations[0].With[input.ID] = input.Default + } + } + + Log(CategoryComponent, "Running component with inputs", map[string]interface{}{ + "with": recipe.Operations[0].With, + }) + + if err := evaluateRecipe(recipe, "", make(map[string]interface{})); err != nil { + return err + } + + return nil +} + // handleCategorySelection prompts the user to select a recipe from a category func handleCategorySelection(categoryName string, sourcePriority []string) (*Recipe, error) { recipes := collectRecipesInCategory(categoryName, sourcePriority) diff --git a/internal/prompts.go b/internal/prompts.go index 4c88002..6403c3d 100644 --- a/internal/prompts.go +++ b/internal/prompts.go @@ -297,7 +297,9 @@ func getPromptOptions(p Prompt, ctx *ExecutionContext) ([]string, map[string]str // getOptionsFromSourceOp extracts options from a source operation's output func getOptionsFromSourceOp(p Prompt, ctx *ExecutionContext) ([]string, map[string]string, error) { + ctx.OperationMutex.RLock() output, exists := ctx.OperationOutputs[p.SourceOp] + ctx.OperationMutex.RUnlock() if !exists { return nil, nil, fmt.Errorf("source operation %s not found or has no output", p.SourceOp) } diff --git a/internal/recipe.go b/internal/recipe.go index 03d6e17..967cd46 100644 --- a/internal/recipe.go +++ b/internal/recipe.go @@ -89,6 +89,12 @@ func evaluateRecipe(recipe Recipe, input string, vars map[string]interface{}) er IncreaseIndent() defer DecreaseIndent() + if op.Cleanup != "" { + defer func(operation Operation, context *ExecutionContext) { + handleCleanup(operation, context) + }(op, ctx) + } + renderedID := op.ID if op.ID != "" { var err error @@ -143,7 +149,19 @@ func evaluateRecipe(recipe Recipe, input string, vars map[string]interface{}) er LogCommand(cmd, nil) ctx.Vars["error"] = "" workdir := "" - if workdirVal, exists := ctx.Vars["workdir"]; exists { + if op.Workdir != "" { + renderedWorkdir, err := renderTemplate(op.Workdir, ctx.templateVars()) + if err != nil { + LogError("Failed to render workdir template", err, map[string]interface{}{"workdir": op.Workdir}) + return false, fmt.Errorf("failed to render workdir template: %w", err) + } + workdir = renderedWorkdir + Log(CategoryFileSystem, fmt.Sprintf("Using operation-level working directory: %s", workdir)) + if err := ensureWorkingDirectory(workdir); err != nil { + LogError("Failed to create operation working directory", err, map[string]interface{}{"workdir": workdir}) + return false, err + } + } else if workdirVal, exists := ctx.Vars["workdir"]; exists { workdir = fmt.Sprintf("%v", workdirVal) } @@ -246,7 +264,9 @@ func processPrompts(op Operation, ctx *ExecutionContext) error { varName = prompt.ID } ctx.Vars[varName] = value + ctx.OperationMutex.Lock() ctx.OperationOutputs[varName] = fmt.Sprintf("%v", value) + ctx.OperationMutex.Unlock() } return nil @@ -301,6 +321,11 @@ func handleCommandError(op Operation, ctx *ExecutionContext, opMap map[string]Op }) if op.OnFailure != "" { + if op.OnFailure == ":" { + Log(CategoryOperation, "No-op failure handler - ignoring error and continuing") + return op.Exit, nil + } + Log(CategoryOperation, fmt.Sprintf("Executing on_failure handler: %s", op.OnFailure)) nextOp, exists := opMap[op.OnFailure] @@ -339,21 +364,97 @@ func handleComponentOutputCollector(op Operation, ctx *ExecutionContext) (bool, if len(executedOps) > 0 { lastOpID := executedOps[len(executedOps)-1] - if lastOutput, exists := ctx.OperationOutputs[lastOpID]; exists { + ctx.OperationMutex.RLock() + lastOutput, exists := ctx.OperationOutputs[lastOpID] + ctx.OperationMutex.RUnlock() + + ctx.OperationMutex.Lock() + if exists { ctx.OperationOutputs[op.ID] = lastOutput Log(CategoryComponent, fmt.Sprintf("Set component output from %s to %s", lastOpID, op.ID)) } else { ctx.OperationOutputs[op.ID] = "" Log(CategoryComponent, fmt.Sprintf("Operation %s executed but didn't produce output", lastOpID)) } + ctx.OperationMutex.Unlock() } else { - ctx.OperationOutputs[op.ID] = "" - Log(CategoryComponent, fmt.Sprintf("No operations executed in component %s", op.ComponentInstanceID)) + ctx.OperationMutex.RLock() + inputVal, exists := ctx.OperationOutputs[op.ID] + ctx.OperationMutex.RUnlock() + if exists && inputVal != "" { + Log(CategoryComponent, fmt.Sprintf("No operations executed in component %s, preserving existing variable %s", + op.ComponentInstanceID, op.ID)) + } else { + ctx.OperationMutex.Lock() + ctx.OperationOutputs[op.ID] = "" + ctx.OperationMutex.Unlock() + Log(CategoryComponent, fmt.Sprintf("No operations executed in component %s", op.ComponentInstanceID)) + } } return op.Exit, nil } +// handleCleanup processes the cleanup operation which removes variables from the context +func handleCleanup(op Operation, ctx *ExecutionContext) { + if op.Cleanup == nil { + return + } + + var varsToCleanup []string + + switch cleanup := op.Cleanup.(type) { + case string: + if cleanup == "" { + return + } + varsToCleanup = []string{cleanup} + case []interface{}: + for _, v := range cleanup { + if strVal, ok := v.(string); ok && strVal != "" { + varsToCleanup = append(varsToCleanup, strVal) + } + } + case []string: + for _, v := range cleanup { + if v != "" { + varsToCleanup = append(varsToCleanup, v) + } + } + default: + if strVal, ok := op.Cleanup.(string); ok && strVal != "" { + varsToCleanup = []string{strVal} + } else { + Log(CategoryOperation, "Invalid cleanup value type") + return + } + } + + if len(varsToCleanup) == 0 { + return + } + + for _, varName := range varsToCleanup { + cleanupVarName := varName + if strings.Contains(cleanupVarName, "{") { + processedName, err := renderTemplate(cleanupVarName, ctx.templateVars()) + if err == nil { + cleanupVarName = processedName + } else { + LogError("Failed to render cleanup template", err, map[string]interface{}{"template": cleanupVarName}) + continue + } + } + + Log(CategoryOperation, fmt.Sprintf("Cleaning variable: %s", cleanupVarName)) + delete(ctx.Vars, cleanupVarName) + ctx.OperationMutex.Lock() + delete(ctx.OperationOutputs, cleanupVarName) + ctx.OperationMutex.Unlock() + delete(ctx.OperationResults, cleanupVarName) + } +} + // processCommandOutput handles successful command output func processCommandOutput(op Operation, output string, ctx *ExecutionContext, opMap map[string]Operation, executeOp func(Operation, int) (bool, error), depth int) (bool, error) { LogOutput(output, map[string]interface{}{ @@ -375,7 +476,9 @@ func processCommandOutput(op Operation, output string, ctx *ExecutionContext, op ctx.Data = output if op.ID != "" { + ctx.OperationMutex.Lock() ctx.OperationOutputs[op.ID] = strings.TrimSpace(output) + ctx.OperationMutex.Unlock() if op.ComponentInstanceID != "" { ctx.ExecutedOperationsByComponent[op.ComponentInstanceID] = append(ctx.ExecutedOperationsByComponent[op.ComponentInstanceID], op.ID) diff --git a/internal/templates.go b/internal/templates.go index 58f134a..92c227a 100644 --- a/internal/templates.go +++ b/internal/templates.go @@ -20,12 +20,18 @@ func (ctx *ExecutionContext) templateVars() map[string]interface{} { vars[k] = v } + ctx.OperationMutex.RLock() for opID, output := range ctx.OperationOutputs { vars[opID] = output } + operationOutputsCopy := make(map[string]string) + for k, v := range ctx.OperationOutputs { + operationOutputsCopy[k] = v + } + ctx.OperationMutex.RUnlock() vars["context"] = ctx - vars["operationOutputs"] = ctx.OperationOutputs + vars["operationOutputs"] = operationOutputsCopy vars["operationResults"] = ctx.OperationResults vars["allTasksComplete"] = ctx.allTasksComplete() @@ -80,6 +86,7 @@ func stringFunctions(funcs template.FuncMap) { funcs["exec"] = execCommand funcs["count"] = count funcs["list"] = createList + funcs["raw"] = encodeEscapes } // mathFunctions adds mathematical functions to the template function map @@ -443,6 +450,8 @@ func renderTemplate(tmplStr string, vars map[string]interface{}) (string, error) result := buf.String() result = handleDefaultEmpty(result) + result = escapeEchoInput(result) + result = decodeEscapes(result) return result, nil } @@ -455,3 +464,11 @@ func transformOutput(output, transform string, ctx *ExecutionContext) (string, e return renderTemplate(transform, vars) } + +// escapeEchoInput handles special characters within an echo string +func escapeEchoInput(input string) string { + if len(input) > 6 && strings.HasPrefix(input, "echo '") && strings.HasSuffix(input, "'") { + return fmt.Sprintf("echo '%s'", strings.ReplaceAll(input[6:len(input)-1], "'", "'\\''")) + } + return input +} diff --git a/internal/types.go b/internal/types.go index deee561..bc9005f 100644 --- a/internal/types.go +++ b/internal/types.go @@ -45,6 +45,8 @@ type Operation struct { Prompts []Prompt `yaml:"prompts,omitempty"` Break bool `yaml:"break,omitempty"` Exit bool `yaml:"exit,omitempty"` + Cleanup interface{} `yaml:"cleanup,omitempty"` + Workdir string `yaml:"workdir,omitempty"` ComponentInstanceID string `yaml:"-"` IsComponentOutputCollector bool `yaml:"-"` } @@ -110,6 +112,7 @@ type ExecutionContext struct { BackgroundTasks map[string]*BackgroundTask BackgroundMutex sync.RWMutex BackgroundWg sync.WaitGroup + OperationMutex sync.RWMutex LoopStack []*LoopContext CurrentLoopIdx int ExecutedOperationsByComponent map[string][]string diff --git a/internal/utils.go b/internal/utils.go index 598740f..67db90b 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "regexp" "strconv" "strings" "time" @@ -349,3 +350,53 @@ func formatResult(result []string, originalInput interface{}) interface{} { return strings.Join(result, "\n") } } + +// encodeEscapes prevents literal interpretation +func encodeEscapes(input interface{}) string { + text := fmt.Sprintf("%v", input) + + escapeMap := map[string]string{ + `\\n`: "", + `\\t`: "", + `\\r`: "", + `\\a`: "", + `\\b`: "", + `\\f`: "", + `\\v`: "", + `\\\\`: "", + `\\'`: "", + `\\"`: "", + } + + result := text + for escape, replacement := range escapeMap { + re := regexp.MustCompile(escape) + result = re.ReplaceAllString(result, replacement) + } + + return result +} + +// decodeEscapes converts back to original backslash+character sequences +func decodeEscapes(input string) string { + escapeMap := map[string]string{ + "": `\\n`, + "": `\\t`, + "": `\\r`, + "": `\\a`, + "": `\\b`, + "": `\\f`, + "": `\\v`, + "": `\\\\`, + "": `\\'`, + "": `\\"`, + } + + result := input + for replacement, escape := range escapeMap { + re := regexp.MustCompile(replacement) + result = re.ReplaceAllString(result, escape) + } + + return result +} diff --git a/recipes/1password/app.yaml b/recipes/1password/app.yaml deleted file mode 100644 index 28a0f60..0000000 --- a/recipes/1password/app.yaml +++ /dev/null @@ -1,19 +0,0 @@ -recipes: - - name: "open" - description: "Open 1Password" - category: "op" - operations: - - uses: "get_filtered_apps" - with: - app_filter: "1Password" - id: "op_app" - - - uses: "open_app" - with: - app: "{{ .op_app }}" - - - name: "lock" - description: "Lock 1Password" - category: "op" - operations: - - uses: "op_lock" diff --git a/recipes/1password/components/items.yaml b/recipes/1password/components/items.yaml index 4d86995..5f53705 100644 --- a/recipes/1password/components/items.yaml +++ b/recipes/1password/components/items.yaml @@ -1,29 +1,34 @@ components: - - id: "op_get_all_item_names" + - id: "op.get.item.names" inputs: - - id: "item_filter" + - id: "filter" operations: - command: op item list --format=json | jq -r '.[] | "\(.title)"' | sort -f silent: true - condition: .item_filter == "false" + condition: .filter == "false" - - command: op item list --format=json | jq -r '.[] | "\(.title)"' | sort -f | grep -i "{{ .item_filter }}" + - command: op item list --format=json | jq -r '.[] | "\(.title)"' | sort -f | grep -i "{{ .filter }}" silent: true - condition: .item_filter != "false" + condition: .filter != "false" + cleanup: + - "filter" - - id: "op_item_select" + - id: "op.item.select" inputs: - - id: "select_item_filter" + - id: "filter" operations: - - id: "op_item_names" - uses: "op_get_all_item_names" + - uses: "op.get.item.names" with: - item_filter: "{{ .select_item_filter }}" + filter: "{{ .filter }}" + id: "items" - - command: echo "{{ .op_item_name }}" + - unset: + - "filter" + + - command: echo "{{ .item }}" silent: true prompts: - - id: "op_item_name" + - id: "item" type: "select" message: "Select a 1Password Item" - source_operation: "op_item_names" + source_operation: "items" diff --git a/recipes/1password/components/utils.yaml b/recipes/1password/components/utils.yaml index 956a69c..f6c72d0 100644 --- a/recipes/1password/components/utils.yaml +++ b/recipes/1password/components/utils.yaml @@ -1,5 +1,5 @@ components: - - id: "op_lock" + - id: "op.lock" operations: - command: op signout --all silent: true diff --git a/recipes/1password/op.yaml b/recipes/1password/op.yaml new file mode 100644 index 0000000..3c759b7 --- /dev/null +++ b/recipes/1password/op.yaml @@ -0,0 +1,29 @@ +recipes: + - name: "open" + description: "Open 1Password" + category: "op" + help: | + Opens the 1Password application. + + Usage: + shef op open # Launch 1Password + operations: + - uses: "os.apps.filtered" + with: + filter: "1Password" + id: "op_app" + + - uses: "os.app.open" + with: + app: "{{ .op_app }}" + + - name: "lock" + description: "Lock 1Password" + category: "op" + help: | + Locks the 1Password application. + + Usage: + shef op lock # Lock 1Password + operations: + - uses: "op.lock" diff --git a/recipes/1password/password.yaml b/recipes/1password/password.yaml index 8cab7f4..9561564 100644 --- a/recipes/1password/password.yaml +++ b/recipes/1password/password.yaml @@ -2,51 +2,81 @@ recipes: - name: "password" description: "Get the password from an item in 1Password" category: "op" + help: | + Retrieves a password from a 1Password item and copies it to clipboard. + + Usage: + shef op password # Select from all items + shef op password [FILTER] # Filter items by name operations: - - id: "op_item" - uses: "op_item_select" + - id: "item" + uses: "op.item.select" with: - select_item_filter: "{{ .input }}" + filter: "{{ .input }}" - - id: "op_password" - command: op item get "{{ .op_item }}" --fields label=password --reveal + - id: "password" + command: op item get "{{ .item }}" --fields label=password --reveal silent: true + cleanup: + - "item" - - uses: "op_lock" + - uses: "op.lock" - - uses: "copy_to_clipboard" + - uses: "clipboard.copy" with: - value_to_copy: "{{ .op_password }}" + value: "{{ .password }}" + + - cleanup: + - "password" - name: "otp" description: "Get the one time password from an item in 1Password" category: "op" + help: | + Retrieves a one-time password (TOTP) from a 1Password item and copies it to clipboard. + + Usage: + shef op otp # Select from all items with OTP + shef op otp [FILTER] # Filter items by name operations: - - id: "op_item" - uses: "op_item_select" + - id: "item" + uses: "op.item.select" with: - select_item_filter: "{{ .input }}" + filter: "{{ .input }}" - - id: "op_password" - command: op item get "{{ .op_item }}" --format json | jq '.fields[] | select(.type == "OTP") | .totp' + - id: "password" + command: op item get "{{ .item }}" --format json | jq '.fields[] | select(.type == "OTP") | .totp' silent: true + cleanup: + - "item" - - uses: "op_lock" + - uses: "op.lock" - - uses: "copy_to_clipboard" + - uses: "clipboard.copy" with: - value_to_copy: "{{ .op_password }}" + value: "{{ .password }}" + + - cleanup: + - "password" - name: "item" description: "Get an item in 1Password" category: "op" + help: | + Displays detailed information for a selected 1Password item. + + Usage: + shef op item # Select from all items + shef op item [FILTER] # Filter items by name operations: - - id: "op_item" - uses: "op_item_select" + - id: "item" + uses: "op.item.select" with: - select_item_filter: "{{ .input }}" + filter: "{{ .input }}" - - id: "op_item" - command: op item get "{{ .op_item }}" + - id: "item" + command: op item get "{{ .item }}" + cleanup: + - "item" - - uses: "op_lock" + - uses: "op.lock" diff --git a/recipes/demo/arguments.yaml b/recipes/demo/arguments.yaml index d487b62..580f927 100644 --- a/recipes/demo/arguments.yaml +++ b/recipes/demo/arguments.yaml @@ -3,22 +3,13 @@ recipes: description: "A simple demo using arguments and flags" category: "demo" help: | - This recipe demonstrates how to use command-line arguments and flags with Shef. + Demonstrates how to use command-line arguments and flags in recipes. - Arguments can be passed to the recipe in two ways: - - Positional argument: The first argument after the recipe name becomes the "input" variable - - Named flags: Arguments starting with - or -- become variables - - Example flags: - -f Sets the variable "f" to true - --name=value Sets the variable "name" to "value" - - Examples: - shef arguments hello # Sets input to "hello" - shef arguments -f # Sets f flag to true - shef arguments --name=John # Sets name to "John" - shef arguments hello -f # Sets input to "hello" and f flag to true - shef arguments hello --name=John # Sets input to "hello" and name to "John" + Usage: + shef demo arguments [INPUT] # Pass a positional argument + shef demo arguments -f # Use a flag (sets f to true) + shef demo arguments --name=VALUE # Set a named variable + shef demo arguments [INPUT] -f # Combine positional and flag arguments operations: - name: "Display Arguments" command: | diff --git a/recipes/demo/background-tasks.yaml b/recipes/demo/background-tasks.yaml index 9c78656..7ce8ec0 100644 --- a/recipes/demo/background-tasks.yaml +++ b/recipes/demo/background-tasks.yaml @@ -3,25 +3,12 @@ recipes: description: "A simple demo showing the background execution of concurrent tasks" category: "demo" help: | - This recipe demonstrates how to run multiple tasks concurrently and monitor their status. + Demonstrates running multiple tasks in parallel with background execution mode. - The recipe: - 1. Starts three background tasks with different execution durations - 2. Monitors all tasks simultaneously with a status loop - 3. Displays completion status and final output when all tasks are finished + Usage: + shef demo background-tasks # Select fruit tasks to run concurrently - Background task features demonstrated: - - execution_mode: "background" # Runs tasks asynchronously - - bgTaskComplete function # Checks if a task has completed - - bgTaskStatus function # Returns the current status of a task - - Accessing task output # Access output after completion - - This demo showcases: - - How to launch multiple operations concurrently without blocking - - How to monitor the status of background tasks - - How to create a waiting loop that checks for task completion - - How to access output from completed background tasks - - Real-time inline status updates using progress_mode: true + Shows real-time status monitoring and accessing results from background operations. operations: - name: "Choose Fruit" id: "fruit_choice" diff --git a/recipes/demo/color.yaml b/recipes/demo/color.yaml index c9dad17..2e5cb36 100644 --- a/recipes/demo/color.yaml +++ b/recipes/demo/color.yaml @@ -3,23 +3,12 @@ recipes: description: "A simple demo of colors and styles" category: "demo" help: | - This recipe demonstrates how to use colors and text styles in Shef recipes. + Demonstrates using colors and text styles in recipe outputs. - Color Functions: - {{ color "colorname" "text" }} # Colorizes text - {{ style "stylename" "text" }} # Applies style to text + Usage: + shef demo color # See examples of colors and styles - Available Colors: - - black, red, green, yellow, blue, magenta, cyan, white - - bg-black, bg-red, bg-green, bg-yellow, bg-blue, bg-magenta, bg-cyan, bg-white - - Available Styles: - - bold, dim, italic, underline - - Colors and styles can be combined: - {{ style "bold" (color "blue" "text") }} - - The NO_COLOR environment variable disables all colors. + Shows text formatting with green, red, blue, bold, italic, underline, and more. operations: - name: "Basic color example" command: echo "{{ color "green" "This text is green" }} and {{ color "red" "this is red" }}" diff --git a/recipes/demo/components.yaml b/recipes/demo/components.yaml index 238e060..0d43ac0 100644 --- a/recipes/demo/components.yaml +++ b/recipes/demo/components.yaml @@ -3,24 +3,12 @@ recipes: description: "A simple demo of reusable components" category: "demo" help: | - This recipe demonstrates how to use reusable components in Shef. + Demonstrates reusable components for modular recipe design. - The recipe will: - 1. Use the "health-check" component to gather system information - 2. Display the collected information in a formatted report - 3. Show how components can be nested (health-check uses system-info) + Usage: + shef demo components # Run a system health check using components - Components are reusable blocks of operations that can be shared across recipes: - - Components are defined with an "id" that you reference with "uses" - - Component operations with IDs become available as variables - - Components can use other components (nesting) - - Operations inside components work just like regular operations - - Key concepts: - - Creating modular, reusable operation sets - - Breaking complex workflows into logical components - - Referencing component outputs by their operation IDs - - Building higher-level components from simpler ones + Shows nested components and how to reference component outputs. operations: - name: "Run System Checkup" uses: "health-check" diff --git a/recipes/demo/conditional.yaml b/recipes/demo/conditional.yaml index cd5cfdb..ee6eedb 100644 --- a/recipes/demo/conditional.yaml +++ b/recipes/demo/conditional.yaml @@ -3,19 +3,10 @@ recipes: description: "A simple demo of conditional operations using direct prompt values" category: "demo" help: | - This recipe demonstrates conditional operations based on user input. + Demonstrates conditional operations that run based on user input selections. - The recipe will: - 1. Prompt you to select a fruit (Apples or Oranges) - 2. Execute a specific operation based on your selection - - This shows how to use the "condition" property to run operations only when - specific conditions are met. - - Key concepts: - - Prompts for user input - - Conditional execution based on variables - - Using variable values in conditions + Usage: + shef demo conditional # Select a fruit to see conditional execution operations: - name: "Choose Fruit" id: "choose" diff --git a/recipes/demo/flexible-tables.yaml b/recipes/demo/flexible-tables.yaml index d5ba9eb..8f05994 100644 --- a/recipes/demo/flexible-tables.yaml +++ b/recipes/demo/flexible-tables.yaml @@ -3,20 +3,12 @@ recipes: description: "A demo of flexible table input formats" category: "demo" help: | - This recipe demonstrates the flexible table input formats in Shef. + Demonstrates flexible ways to format input for tables. - It shows how to: - - Use newline-separated headers - - Create tables with comma-separated row strings - - Build tables with array-style input - - Mix different input formats - - Generate and use dynamic data in tables + Usage: + shef demo tables-flexible # View examples of different table input formats - The table function accepts different input formats: - - Headers as a newline or comma-separated string - - Rows as comma-separated strings - - Array syntax for both headers and rows - - Dynamic data from command outputs + Shows newline-separated headers, comma-separated rows, array input, and dynamic data. operations: - name: "Newline-Separated Headers" command: | diff --git a/recipes/demo/for.yaml b/recipes/demo/for.yaml index 7dfe18f..b2c3c3c 100644 --- a/recipes/demo/for.yaml +++ b/recipes/demo/for.yaml @@ -3,18 +3,10 @@ recipes: description: "A simple demo of the for loop control flow" category: "demo" help: | - This recipe demonstrates the 'for' loop control flow in Shef. + Demonstrates the 'for' loop control flow with a fixed number of iterations. - The recipe runs a loop for a fixed number of iterations (5), showing: - - How to define a for loop with a set count - - How to access the current iteration count - - How to use color formatting within the loop - - How to access the output of operations in the loop after completion - - For loop parameters: - - type: "for" # Specifies the for loop control flow - - count: n # Number of iterations to run - - variable: "name" # Variable name to store the current iteration number + Usage: + shef demo for # Run a loop with 5 iterations operations: - name: "Run a For Loop" control_flow: diff --git a/recipes/demo/foreach.yaml b/recipes/demo/foreach.yaml index 1b89f92..d5ca19f 100644 --- a/recipes/demo/foreach.yaml +++ b/recipes/demo/foreach.yaml @@ -3,17 +3,10 @@ recipes: description: "A simple demo of the foreach control flow" category: "demo" help: | - This recipe demonstrates the 'foreach' loop control flow in Shef. + Demonstrates the 'foreach' loop that iterates through a collection of items. - The recipe: - 1. Defines a collection of fruit items - 2. Processes each item in the collection one by one - 3. Shows completion after all items are processed - - Foreach loop parameters: - - type: "foreach" # Specifies the foreach loop control flow - - collection: items # The collection to iterate over (string with items separated by newlines) - - as: "variable" # The variable name to store each item in + Usage: + shef demo foreach # Process a collection of fruit items operations: - name: "Process Each Fruit" control_flow: diff --git a/recipes/demo/handlers.yaml b/recipes/demo/handlers.yaml index 35ee9a1..0a58aeb 100644 --- a/recipes/demo/handlers.yaml +++ b/recipes/demo/handlers.yaml @@ -3,21 +3,12 @@ recipes: description: "A simple demo of error and success handlers" category: "demo" help: | - This recipe demonstrates how to use error and success handlers in Shef. + Demonstrates success and error handlers for operations. - The recipe will: - 1. Check if a directory exists (provide the directory path as input) - 2. Execute the success handler if the directory exists - 3. Execute the error handler if the directory doesn't exist - - Key concepts: - - on_success: Specifies an operation to run when a command succeeds - - on_failure: Specifies an operation to run when a command fails - - Conditional execution using the input variable - - Examples: - shef handlers /tmp # Tests if /tmp directory exists - shef handlers /nonexistent # Tests a directory that doesn't exist + Usage: + shef demo handlers [PATH] # Test if a directory exists + shef demo handlers /tmp # Example with existing directory + shef demo handlers /nonexistent # Example with non-existing directory operations: - name: "Help" command: echo {{ color "magenta" "Please provide a valid or invalid directory argument to test" }} diff --git a/recipes/demo/hello-world.yaml b/recipes/demo/hello-world.yaml index e0b0be7..44e24b3 100644 --- a/recipes/demo/hello-world.yaml +++ b/recipes/demo/hello-world.yaml @@ -3,19 +3,12 @@ recipes: description: "A simple hello world recipe" category: "demo" help: | - This is a simple introductory recipe to demonstrate the basics of Shef. + A simple introductory recipe demonstrating the basics of Shef. - The recipe will: - 1. Prompt you for your name (or use "World" as default) - 2. Greet you with a personalized message - 3. Show the current time - 4. Welcome you to Shef + Usage: + shef demo hello-world # Get a personalized greeting - This demonstrates: - - Basic prompts for user input - - Using variables in commands - - Colorizing output - - Multi-line commands + Shows basic prompts, variable usage, and colorized output. operations: - name: "Greet User" command: | diff --git a/recipes/demo/monitor.yaml b/recipes/demo/monitor.yaml index 55018b7..f967edf 100644 --- a/recipes/demo/monitor.yaml +++ b/recipes/demo/monitor.yaml @@ -3,21 +3,12 @@ recipes: description: "Monitor a service until it returns a success status code" category: "demo" help: | - This recipe demonstrates how to monitor a service until it returns a success status code. + Demonstrates service monitoring with a while loop until success status is received. - The recipe simulates: - 1. Checking a service's health status - 2. Displaying error messages while the service is unavailable - 3. Retrying until the service returns a success status code (200) - 4. Displaying a success message when the service is available + Usage: + shef demo monitor # Monitor simulated service health - This demonstrates: - - While loop control flow for continuous monitoring - - Conditional operations based on status - - Throttling requests with sleep - - Formatting output with colors and styles - - Note: For demonstration purposes, the service automatically succeeds after 3 polling attempts. + The simulated service returns success (200) after 3 polling attempts. operations: - name: "Initialize Empty Status Code" id: "status_code" diff --git a/recipes/demo/operation-order.yaml b/recipes/demo/operation-order.yaml index a304c79..1b26671 100644 --- a/recipes/demo/operation-order.yaml +++ b/recipes/demo/operation-order.yaml @@ -3,22 +3,12 @@ recipes: description: "A simple demo to show the order of operations in a recipe" category: "demo" help: | - This recipe demonstrates the order in which operations execute in a Shef recipe. + Demonstrates the execution order of operations in recipes. - Operation execution follows this order: - 1. Check condition (skip if not met) - 2. Run prompts to collect user input - 3. Execute control flow if it exists - 4. Run the command - 5. Apply transformations to the output - 6. Execute on_success/on_failure handlers + Usage: + shef demo operation-order -f # Run with flag to see full operation flow - This recipe shows: - - Complex operation structure - - How prompts collect user input - - How for loops work with a break condition - - How transformations modify output - - How success handlers chain operations + Shows conditions, prompts, control flow, commands, transformations, and handlers. operations: - name: "Hello" command: echo "Executing command [old text] {{ .item }}" diff --git a/recipes/demo/progress-mode.yaml b/recipes/demo/progress-mode.yaml index eebe2f0..ec50ca0 100644 --- a/recipes/demo/progress-mode.yaml +++ b/recipes/demo/progress-mode.yaml @@ -3,23 +3,12 @@ recipes: description: "A simple demo to show progress mode in control flows" category: "demo" help: | - This recipe demonstrates the 'progress_mode' feature in different control flows. + Demonstrates progress_mode for in-place updates during loop execution. - Progress mode allows operations within loops to update in-place on a single line, - making it perfect for displaying progress indicators, counters, or status updates - without cluttering your terminal with multiple lines of output. + Usage: + shef demo progress-mode # See different loop types with inline progress updates - The recipe shows progress_mode in three different control flow types: - - for loop: Counts through iterations with a numeric counter - - foreach loop: Processes items from a collection with updated status - - while loop: Shows a running timer that updates in real-time - - Usage in control_flow: - progress_mode: true # Enables inline updates for all operations in the loop - - Each loop's output replaces the previous output on the same line rather than - printing on new lines, creating a cleaner, more dynamic display of progress - information. + Shows for, foreach, and while loops with dynamic in-place status updates. operations: - name: "For Progress Mode" control_flow: diff --git a/recipes/demo/progress.yaml b/recipes/demo/progress.yaml index 9c55618..f9f26e1 100644 --- a/recipes/demo/progress.yaml +++ b/recipes/demo/progress.yaml @@ -3,27 +3,12 @@ recipes: description: "A simple demo to show progress bars, variables, and workdir" category: "demo" help: | - This recipe demonstrates how to use progress bars, pre-defined variables, and working directories. + Demonstrates progress bars, variables, and working directories. - The recipe will: - 1. Prompt for confirmation before creating temporary files - 2. Create a set number of temporary files with a progress bar - 3. Count and display the number of files created - 4. Delete each file with a customized progress bar - 5. Clean up the working directory + Usage: + shef demo progress # See progress bars during file operations - Key features demonstrated: - - Pre-defined variables using the `vars` section - - Setting a working directory with `workdir` - - Using the `count` function to count items - - Progress bars with different configurations and themes - - For loops with progress tracking - - ForEach loops with progress tracking - - Color-coded output for better readability - - This recipe is useful for: - - Understanding how to use the vars, workdir, and count features - - Seeing how progress bars can be customized + Shows customizable progress bars, different themes, and using workdir. vars: "tmp_dir": "/tmp" "shef_dir": "shef_progress_demo" diff --git a/recipes/demo/prompts.yaml b/recipes/demo/prompts.yaml index 86653be..fb86804 100644 --- a/recipes/demo/prompts.yaml +++ b/recipes/demo/prompts.yaml @@ -3,27 +3,13 @@ recipes: description: "A demo of all available prompt types and their options" category: "demo" help: | - This recipe demonstrates all available prompt types and options in Shef. + Demonstrates all available prompt types and their options. - Prompt types demonstrated: - - input: Basic text input - - select: Choose from a list of options - - confirm: Yes/No confirmation - - password: Masked password input - - multiselect: Select multiple options - - autocomplete: Filtered selection - - number: Numeric input with validation - - path: File path with validation - - editor: Multi-line text in your editor - - Dynamic options from operation output + Usage: + shef demo prompts # Try all prompt types - Each prompt demonstrates: - - Setting default values - - Help text - - Validation where applicable - - Formatting and displaying collected values - - This is a comprehensive reference for all available prompt types in Shef. + Includes text input, select, confirm, password, multiselect, autocomplete, number, + path, editor, and dynamic options from operation output. operations: - name: "Basic Input Types Demo" id: "basic_inputs" @@ -106,7 +92,7 @@ recipes: id: "number_input" command: | echo "Number input: {{ .number_value }}" - + # Calculate simple arithmetic on the number RESULT=$(( {{ .number_value }} * 2 )) echo "Doubled value: $RESULT" @@ -124,10 +110,10 @@ recipes: id: "file_path" command: | echo "Path input: {{ .file_path }}" - + if [ -f "{{ .file_path }}" ]; then echo "File exists, displaying first 3 lines:" - head -n 3 "{{ .file_path }}" + head -n 3 "{{ .file_path }}" else echo "Path doesn't exist or is not a file" fi @@ -149,7 +135,7 @@ recipes: echo "-----------------" echo "{{ .editor_content }}" echo "-----------------" - + LINE_COUNT=$(echo "{{ .editor_content }}" | wc -l) echo "Line count: $LINE_COUNT" prompts: diff --git a/recipes/demo/tables.yaml b/recipes/demo/tables.yaml index 0b13ece..f96556d 100644 --- a/recipes/demo/tables.yaml +++ b/recipes/demo/tables.yaml @@ -3,24 +3,12 @@ recipes: description: "A demo of rendering tables" category: "demo" help: | - This recipe demonstrates the table rendering feature in Shef. + Demonstrates table rendering with various styles and formatting options. - It shows how to: - - Create and render tables with clear syntax - - Apply different styles (rounded, light, double, ASCII) - - Format data in various ways - - Use JSON configuration for complex tables + Usage: + shef demo tables # View examples of different table styles - The table function accepts: - - headers: Array of column headers (use list function) - - rows: 2D array of row data (use list function) - - style: Table style (rounded, light, double, ascii) - - The tableJSON function accepts a JSON string with: - - headers: Array of column headers - - rows: 2D array of row data - - style: Table style (rounded, light, double, ascii) - - footers: Optional array of footer values + Shows rounded, double, light, and ASCII styles, column alignment, and JSON configuration. operations: - name: "Basic Table" command: | diff --git a/recipes/demo/transform.yaml b/recipes/demo/transform.yaml index 4a87463..ec28a22 100644 --- a/recipes/demo/transform.yaml +++ b/recipes/demo/transform.yaml @@ -3,21 +3,10 @@ recipes: description: "A simple demo of data transformation and pipeline flow" category: "demo" help: | - This recipe demonstrates data transformation and pipeline flow in Shef. + Demonstrates data transformation and pipeline flow between operations. - The recipe: - 1. Generates a list of items - 2. Filters the list for items containing "a" - 3. Displays filtered results in green - 4. Filters the list for items containing "her" - 5. Displays filtered results in yellow - - This showcases: - - Passing data between operations - - Using the filter template function - - Accessing output from previous operations - - Formatting output with colors - - Silent operations for intermediate processing + Usage: + shef demo transform # See data filtering and formatting in action operations: - name: "Generate a Simple List" id: "generate" diff --git a/recipes/demo/while.yaml b/recipes/demo/while.yaml index 44316df..d31defb 100644 --- a/recipes/demo/while.yaml +++ b/recipes/demo/while.yaml @@ -3,22 +3,10 @@ recipes: description: "A simple demo of the while loop control flow" category: "demo" help: | - This recipe demonstrates the 'while' loop control flow in Shef. + Demonstrates the 'while' loop control flow with a time-based condition. - The recipe simulates: - 1. A service performing work - 2. Checking the status on each iteration - 3. Completing when the status changes (after 3 seconds) - - While loop parameters: - - type: "while" # Specifies the while loop control flow - - condition: expr # Expression that must be true for the loop to continue - - progres_mode: true # Update output inline to show progress - - This shows: - - How to define a while loop with a condition - - How to access the current duration (seconds and millisecond format) - - How to change the condition to exit the loop + Usage: + shef demo while # Run a loop that continues for 3 seconds operations: - name: "Loop While Status is Running" control_flow: diff --git a/recipes/docker/components/container.yaml b/recipes/docker/components/container.yaml index c18c341..c2a2272 100644 --- a/recipes/docker/components/container.yaml +++ b/recipes/docker/components/container.yaml @@ -1,30 +1,35 @@ components: - - id: "docker_container_list" - description: "Gets all Docker containers and filter results, if desired" + - id: "docker.container.list" inputs: - - id: "list_filter" - description: "Filter the container results. Must be a string list: '[container-1 container-2 container-3]'" + - id: "filter" + description: "Filter the container results. Must be a string list: 'container-1,container-2,container-3'" operations: - id: "docker_list_containers" command: docker ps --format "{{ `{{ .Names }}` }}" silent: true - - id: "docker_list_containers" - transform: '{{ overlap .docker_list_containers .list_filter }}' - condition: .list_filter != "false" && .list_filter != "[]" + - uses: "list.overlap" + id: "docker_list_containers" + with: + "list_a": "{{ .docker_list_containers }}" + "list_b": "{{ .filter }}" + condition: .filter != "false" && .filter != "[]" && .filter != "" silent: true + cleanup: + - "filter" - - id: "docker_container_select" - description: "Gets all Docker containers and prompts the user to select one" + - id: "docker.container.select" inputs: - - id: "select_filter" + - id: "filter" operations: - - uses: "docker_container_list" + - uses: "docker.container.list" id: "containers" with: - list_filter: "{{ .select_filter }}" + filter: "{{ .filter }}" silent: true + - command: 'echo "containers: {{ .containers }}"' + - id: "docker_selected_container" command: echo "{{ .docker_container }}" silent: true @@ -34,15 +39,14 @@ components: message: "Select a container" source_operation: "containers" - - id: "docker_container_multiselect" - description: "Gets all Docker containers and prompts the user to select one or more containers" + - id: "docker.container.multiselect" inputs: - - id: "multiselect_filter" + - id: "filter" operations: - - uses: "docker_container_list" + - uses: "docker.container.list" id: "containers" with: - list_filter: "{{ .multiselect_filter }}" + filter: "{{ .filter }}" silent: true - id: "docker_multiselect_containers" @@ -54,33 +58,33 @@ components: message: "Select one or more containers" source_operation: "containers" - - id: "which_docker_container_shell" - description: "Determines which shell a container uses" + - id: "docker.which.shell" inputs: - - id: "docker_shell_container" + - id: "container" operations: - - id: "docker_shell" - command: | - if docker exec {{ .docker_shell_container }} which bash >/dev/null 2>&1; then + - command: | + if docker exec {{ .container }} which bash >/dev/null 2>&1; then echo "bash" - elif docker exec {{ .docker_shell_container }} which zsh >/dev/null 2>&1; then + elif docker exec {{ .container }} which zsh >/dev/null 2>&1; then echo "zsh" - elif docker exec {{ .docker_shell_container }} which dash >/dev/null 2>&1; then + elif docker exec {{ .container }} which dash >/dev/null 2>&1; then echo "dash" - elif docker exec {{ .docker_shell_container }} which ksh >/dev/null 2>&1; then + elif docker exec {{ .container }} which ksh >/dev/null 2>&1; then echo "ksh" - elif docker exec {{ .docker_shell_container }} which tcsh >/dev/null 2>&1; then + elif docker exec {{ .container }} which tcsh >/dev/null 2>&1; then echo "tcsh" - elif docker exec {{ .docker_shell_container }} which fish >/dev/null 2>&1; then + elif docker exec {{ .container }} which fish >/dev/null 2>&1; then echo "fish" - elif docker exec {{ .docker_shell_container }} which ash >/dev/null 2>&1; then + elif docker exec {{ .container }} which ash >/dev/null 2>&1; then echo "ash" - elif docker exec {{ .docker_shell_container }} which sh >/dev/null 2>&1; then + elif docker exec {{ .container }} which sh >/dev/null 2>&1; then echo "sh" else exit 1 fi on_failure: "handle_shell_error" + cleanup: + - "container" - id: "handle_shell_error" command: echo {{ color "red" "No common shell found in container." }} diff --git a/recipes/docker/logs.yaml b/recipes/docker/logs.yaml index d958a44..6affb27 100644 --- a/recipes/docker/logs.yaml +++ b/recipes/docker/logs.yaml @@ -3,44 +3,22 @@ recipes: description: "Stream logs from a Docker container with advanced filtering" category: "docker" help: | - This recipe streams logs from a Docker container. + Streams logs from a selected Docker container with filtering options. - The recipe will: - 1. List all running Docker containers - 2. Prompt you to select a container - 3. Stream logs from the selected container + Usage: + shef docker logs [TEXT] # Stream logs with optional text highlighting + shef docker logs -f [TEXT] # Filter to only show matching entries + shef docker logs --lines=N # Set context lines (default: 5) + shef docker logs --since=TIME # Show logs since time (e.g., "1h", "72h", "2025-01-01") - Requirements: - - Docker must be installed and running - - You must have permissions to access Docker - - Options: - -f Filter and highlight log entries - --lines=NUMBER Number of context lines before and after matches (default: 5) - --since=STRING Show logs from the specified time forward - Examples: "1h" (last hour), "72h" (last 3 days), "2025-01-01" (since date) - - Positional Arguments: - STRING Text to search for in logs (uses first argument) - - Examples: - shef docker logs # Stream all logs from a selected container - shef docker logs 'error' # Stream logs highlighting "error" entries - shef docker logs -f 'error' # Filter and highlight "error" entries - shef docker logs -f 'error' --lines=10 # Filter with 10 lines of context - shef docker logs --since=3h # Stream all logs from the last 3 hours - - Features: - - Uses the stream execution mode to display logs in real-time - - Automatically retrieves container names from docker ps - - Press Ctrl+C to stop log streaming + Press Ctrl+C to stop log streaming. vars: "filter": "" "filter_base": "2>&1 | grep --color=always -i" "filter_lines": 5 "since": "" operations: - - uses: "docker_container_select" + - uses: "docker.container.select" id: "container" silent: true diff --git a/recipes/docker/prune.yaml b/recipes/docker/prune.yaml index 53b1973..dc010e7 100644 --- a/recipes/docker/prune.yaml +++ b/recipes/docker/prune.yaml @@ -3,27 +3,13 @@ recipes: description: "Prune unused Docker resources" category: "docker" help: | - This recipe helps prune unused Docker resources to free up disk space. + Cleans up unused Docker resources to free disk space. - The recipe will: - 1. Show the initial disk usage - 2. Prompt you to select which Docker resources to prune - 3. Prune the selected resources - 4. Show the final disk usage and space saved + Usage: + shef docker prune # Select resources to prune - Resources that can be pruned: - - Dangling images (untagged images) - - All unused images (not used by containers) - - Stopped containers - - Unused volumes (not used by containers) - - Unused networks (not connected to containers) - - Build cache - - Requirements: - - Docker must be installed and running - - You must have permissions to access Docker - - Note: Use caution when pruning volumes as this will permanently delete data. + Options include dangling/unused images, stopped containers, volumes, networks, and build cache. + Use caution when pruning volumes as this permanently deletes data. vars: "initial_usage": "" "final_usage": "" diff --git a/recipes/docker/shell.yaml b/recipes/docker/shell.yaml index 99dcaf0..0d2b431 100644 --- a/recipes/docker/shell.yaml +++ b/recipes/docker/shell.yaml @@ -3,32 +3,23 @@ recipes: description: "Shell into a Docker container" category: "docker" help: | - This recipe opens an interactive shell into a Docker container. + Opens an interactive shell session into a running Docker container. - The recipe will: - 1. List all running Docker containers - 2. Prompt you to select a container - 3. Open an interactive shell session in the selected container + Usage: + shef docker shell # Select container and open interactive shell - Requirements: - - Docker must be installed and running - - You must have permissions to access Docker - - The container must have bash installed - - Special features: - - Uses the interactive execution mode for shell access - - Automatically retrieves container names from docker ps - - Exit the shell (usually with 'exit' command) + Exit the shell session with the 'exit' command. operations: - - uses: "docker_container_select" - id: "container" + - uses: "docker.container.select" + id: "docker_container" silent: true - - uses: "which_docker_container_shell" + - uses: "docker.which.shell" + id: "docker_shell" with: - docker_shell_container: "{{ .container }}" + container: "{{ .docker_container }}" silent: true - name: "Execute Shell" - command: docker exec -it {{ .container }} {{ .docker_shell }} + command: docker exec -it {{ .docker_container }} {{ .docker_shell }} execution_mode: "interactive" diff --git a/recipes/gcp/components/project.yaml b/recipes/gcp/components/project.yaml index ba0a8ab..22b41ea 100644 --- a/recipes/gcp/components/project.yaml +++ b/recipes/gcp/components/project.yaml @@ -1,41 +1,29 @@ components: - - id: "gcp_current_project" - name: "GCP Current Project" - description: "Gets the current GCP project" + - id: "gcp.project.current" operations: - - name: "Get Current GCP Project" - id: "gcp_current_project" - command: gcloud config get-value project + - command: gcloud config get-value project - - id: "gcp_project_select" - name: "GCP Project Select" - description: "Displays GCP project select prompt with the default set to the current GCP project" + - id: "gcp.project.select" operations: - - name: "GCP Current Project" - id: "gcp_current_project" - uses: "gcp_current_project" + - uses: "gcp.project.current" + id: "current_project" silent: true - - name: "List GCP Projects" - id: "gcp_projects_list" + - id: "projects_list" command: gcloud projects list --filter="NOT project_id:sys-*" --format="value(project_id,name)" | awk '{print $1 "=" $2}' silent: true - - name: "GCP Select Project" - id: "gcp_selected_project" + - id: "selected_project" command: echo "{{ .gcp_project_prompt }}" silent: true prompts: - - name: "GCP Project Select" - id: "gcp_project_prompt" + - id: "gcp_project_prompt" type: "select" message: "Select a GCP project" - source_operation: "gcp_projects_list" - default: "{{ .gcp_current_project }}" + source_operation: "projects_list" + default: "{{ .current_project }}" - - name: "Set GCP Project" - command: gcloud config set project {{ .gcp_selected_project }} + - command: gcloud config set project {{ .selected_project }} silent: true - - name: "Return Selected GCP Project" - command: echo "{{ .gcp_selected_project }}" + - command: echo "{{ .selected_project }}" diff --git a/recipes/gcp/project.yaml b/recipes/gcp/project.yaml index 4e81c41..d8959f1 100644 --- a/recipes/gcp/project.yaml +++ b/recipes/gcp/project.yaml @@ -3,31 +3,22 @@ recipes: description: "Select a GCP project" category: "gcp" help: | - This recipe helps you select a Google Cloud Platform (GCP) project. + Selects or displays the current Google Cloud Platform project. - The recipe will: - 1. List all available GCP projects (excluding system projects) - 2. Prompt you to select a project - 3. Set the selected project as active in your gcloud config - 4. Display the selected project - - Requirements: - - Google Cloud SDK (gcloud) must be installed - - You must be authenticated with gcloud - - Options: - -w, --which Show the currently selected project without changing it + Usage: + shef gcp project # Select a GCP project + shef gcp project -w # Show current project without changing operations: - name: "GCP Project Select" id: "project" - uses: "gcp_project_select" + uses: "gcp.project.select" condition: .w != "true" && .which != "true" silent: true on_success: "exit" - name: "GCP Current Project" id: "project" - uses: "gcp_current_project" + uses: "gcp.project.current" condition: .w == "true" || .which == "true" silent: true on_success: "exit" diff --git a/recipes/gcp/secret.yaml b/recipes/gcp/secret.yaml index cad9f5f..5bea739 100644 --- a/recipes/gcp/secret.yaml +++ b/recipes/gcp/secret.yaml @@ -3,29 +3,15 @@ recipes: description: "Fetch a GCP secret and copy its value to the clipboard" category: "gcp" help: | - This recipe fetches a Google Cloud Platform (GCP) secret and copies its value to the clipboard. + Fetches a Google Cloud Platform secret and copies its value to the clipboard. - The recipe will: - 1. List all available secrets in your GCP project - 2. Prompt you to select a secret - 3. Fetch the latest version of the selected secret - 4. Copy the secret value to your clipboard - - Requirements: - - Google Cloud SDK (gcloud) must be installed - - You must be authenticated with gcloud - - You must have permissions to access secrets - - A clipboard utility must be available (pbcopy, xclip, xsel, or clip) - - Options: - -f, --force Skip the prompt to select the GCP project - - Note: The recipe will detect your operating system and use the appropriate - clipboard command. If no clipboard utility is available, it will notify you. + Usage: + shef gcp secret # Select project and secret to copy + shef gcp secret -f # Skip project selection prompt operations: - name: "GCP Project Select" - id: "gcp_current_project" - uses: "gcp_project_select" + id: "gcp.project.current" + uses: "gcp.project.select" condition: .f != "true" && .force != "true" silent: true @@ -51,6 +37,6 @@ recipes: transform: "{{ .output | trim }}" silent: true - - uses: "copy_to_clipboard" + - uses: "clipboard.copy" with: - value_to_copy: "{{ .secret_value }}" + value: "{{ .secret_value }}" diff --git a/recipes/git/lint.yaml b/recipes/git/lint.yaml new file mode 100644 index 0000000..8930648 --- /dev/null +++ b/recipes/git/lint.yaml @@ -0,0 +1,35 @@ +recipes: + - name: "lint" + description: "Run basic lint for all staged and unstaged files" + category: "git" + help: | + Lints git staged and unstaged files for common issues. + + Usage: + shef git lint # Check files for trailing whitespace and missing EOF newlines + operations: + - uses: "git.staged+unstaged" + id: "files" + silent: true + + - command: echo "{{ color "yellow" "No files to lint" }}" + condition: .files == "" + exit: true + + - control_flow: + type: "foreach" + collection: "{{ .files }}" + as: "file_to_lint" + progress_mode: true + operations: + - command: 'echo "Linting: {{ style "dim" .file_to_lint }}"' + + - uses: "lint.trailing" + with: + file: "{{ .file_to_lint }}" + + - uses: "lint.eof" + with: + file: "{{ .file_to_lint }}" + + - command: echo "{{ color "green" "Complete!" }}" diff --git a/recipes/git/stash.yaml b/recipes/git/stash.yaml index 55a7c4a..1347292 100644 --- a/recipes/git/stash.yaml +++ b/recipes/git/stash.yaml @@ -3,29 +3,12 @@ recipes: description: "Easily list, save, apply, and drop git stashes" category: "git" help: | - This recipe helps manage Git stashes with an interactive interface. + Manages Git stashes with an interactive interface for listing, saving, applying, and dropping. - Available actions: - - List: View all existing stashes - - Save: Create a new stash with the current changes - - Apply: Apply a selected stash to your working directory - - Drop: Remove a selected stash + Usage: + shef git stash # Select from available actions (List, Save, Apply, Drop) - Save action features: - - Checks if there are changes to stash - - Shows git status before stashing - - Names stashes with the current branch name for easy identification - - Apply/Drop action features: - - Lists all existing stashes - - Shows formatted stash messages for selection - - Confirmation prompt before dropping a stash - - Requirements: - - Git must be installed - - You must be in a git repository - - Note: Stash names are prefixed with the branch name for better organization. + Stash names are prefixed with the current branch name for better organization. operations: - name: "Stash Action" id: "action" diff --git a/recipes/git/update.yaml b/recipes/git/update.yaml index 3a399f8..d381bed 100644 --- a/recipes/git/update.yaml +++ b/recipes/git/update.yaml @@ -3,25 +3,12 @@ recipes: description: "Update all git repositories within the current directory" category: "git" help: | - This recipe updates all Git repositories within the current directory. + Updates all Git repositories in the current directory with 'git pull'. - The recipe will: - 1. Find all subdirectories in the current directory - 2. Check if each subdirectory is a Git repository - 3. Update each Git repository with 'git pull' - 4. Skip directories that are not Git repositories - 5. Display a summary when complete + Usage: + shef git update # Pull updates for all repositories in current directory - Requirements: - - Git must be installed - - You must have permission to access the repositories - - Note: This recipe only looks for Git repositories in the immediate subdirectories - of the current directory (depth=1). It doesn't recurse into nested directories. - - Usage examples: - - Run from a directory containing multiple repositories - - Use in a projects directory to update all projects at once + Only processes immediate subdirectories (depth=1), skipping non-git directories. operations: - name: "Find Subdirectories" id: "find_dirs" diff --git a/recipes/git/version.yaml b/recipes/git/version.yaml index 9679cde..d30616f 100644 --- a/recipes/git/version.yaml +++ b/recipes/git/version.yaml @@ -3,25 +3,12 @@ recipes: description: "Create and push version tags to Git" category: "git" help: | - This recipe helps create and push version tags to Git. - - The recipe will: - 1. Check the current Git branch and fetch all tags - 2. Find the latest version tag (follows v0.0.0 format) - 3. Show the current version if it exists - 4. Let you choose the type of version increment: - - major: Increment first number (v1.0.0 → v2.0.0) - - minor: Increment second number (v1.0.0 → v1.1.0) - - patch: Increment third number (v1.0.0 → v1.0.1) - 5. Create a new version tag with an optional message - 6. Optionally push the tag to the remote repository - - Requirements: - - Git must be installed - - You must be in a git repository - - You must have permission to create and push tags - - This recipe follows semantic versioning principles (MAJOR.MINOR.PATCH). + Creates and pushes version tags to Git using semantic versioning (MAJOR.MINOR.PATCH). + + Usage: + shef git version # Create and optionally push a new version tag + + Allows major (1.0.0 → 2.0.0), minor (1.0.0 → 1.1.0), or patch (1.0.0 → 1.0.1) increments. operations: - name: "Check Current Branch" id: "branch" @@ -87,7 +74,7 @@ recipes: VERSION="${TAG#v}" # Split into parts IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" - + if [[ "{{ .version }}" == "custom" ]]; then printf "{{ .custom_version }}" elif [[ "{{ .version }}" == "major" ]]; then diff --git a/recipes/utils/components/clipboard.yaml b/recipes/utils/components/clipboard.yaml index 949b5e9..ee81529 100644 --- a/recipes/utils/components/clipboard.yaml +++ b/recipes/utils/components/clipboard.yaml @@ -1,7 +1,7 @@ components: - - id: "copy_to_clipboard" + - id: "clipboard.copy" inputs: - - id: "value_to_copy" + - id: "value" operations: - id: "clipboard_cmd" command: | @@ -18,14 +18,13 @@ components: fi silent: true - - name: "Copy to Clipboard" - command: printf "%s" "{{ .value_to_copy }}" | {{ .clipboard_cmd }} + - command: printf "%s" "{{ .value }}" | {{ .clipboard_cmd }} condition: .clipboard_cmd != "none" + cleanup: + - "value" - - name: "Clipboard Success Message" - command: echo {{ color "green" "Copied to the clipboard!" }} + - command: echo {{ color "green" "Copied to the clipboard!" }} condition: .clipboard_cmd != "none" - - name: "No Clipboard Available" - command: echo {{ color "red" "Unable to copy to the clipboard, no clipboard utility available." }} + - command: echo {{ color "red" "Unable to copy to the clipboard, no clipboard utility available." }} condition: .clipboard_cmd == "none" diff --git a/recipes/utils/components/convert.yaml b/recipes/utils/components/convert.yaml new file mode 100644 index 0000000..711763b --- /dev/null +++ b/recipes/utils/components/convert.yaml @@ -0,0 +1,1600 @@ +components: + - id: "convert._internal" + inputs: + - id: "equation" + operations: + - command: | + echo "{{ .equation }}" | bc -l | awk '{ + if ($1 == int($1)) { + printf "%.0f\n", $1 + } else { + printf "%.2f\n", $1 + } + }' + output_format: "trim" + cleanup: + - "equation" + + - id: "convert.feet->meters" + inputs: + - id: "feet" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .feet }} * 0.3048" + cleanup: + - "feet" + + - id: "convert.meters->feet" + inputs: + - id: "meters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .meters }} / 0.3048" + cleanup: + - "meters" + + - id: "convert.inches->centimeters" + inputs: + - id: "inches" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .inches }} * 2.54" + cleanup: + - "inches" + + - id: "convert.centimeters->inches" + inputs: + - id: "centimeters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .centimeters }} / 2.54" + cleanup: + - "centimeters" + + - id: "convert.inches->millimeters" + inputs: + - id: "inches" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .inches }} * 25.4" + cleanup: + - "inches" + + - id: "convert.millimeters->inches" + inputs: + - id: "millimeters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .millimeters }} / 25.4" + cleanup: + - "millimeters" + + - id: "convert.yards->meters" + inputs: + - id: "yards" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .yards }} * 0.9144" + cleanup: + - "yards" + + - id: "convert.meters->yards" + inputs: + - id: "meters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .meters }} / 0.9144" + cleanup: + - "meters" + + - id: "convert.millimeters->centimeters" + inputs: + - id: "millimeters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .millimeters }} / 10" + cleanup: + - "millimeters" + + - id: "convert.centimeters->millimeters" + inputs: + - id: "centimeters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .centimeters }} * 10" + cleanup: + - "centimeters" + + - id: "convert.centimeters->meters" + inputs: + - id: "centimeters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .centimeters }} / 100" + cleanup: + - "centimeters" + + - id: "convert.meters->centimeters" + inputs: + - id: "meters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .meters }} * 100" + cleanup: + - "meters" + + - id: "convert.millimeters->meters" + inputs: + - id: "millimeters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .millimeters }} / 1000" + cleanup: + - "millimeters" + + - id: "convert.meters->millimeters" + inputs: + - id: "meters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .meters }} * 1000" + cleanup: + - "meters" + + - id: "convert.kilometers->miles" + inputs: + - id: "kilometers" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kilometers }} / 1.60934" + cleanup: + - "kilometers" + + - id: "convert.miles->kilometers" + inputs: + - id: "miles" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .miles }} * 1.60934" + cleanup: + - "miles" + + - id: "convert.kilometers->meters" + inputs: + - id: "kilometers" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kilometers }} * 1000" + cleanup: + - "kilometers" + + - id: "convert.meters->kilometers" + inputs: + - id: "meters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .meters }} / 1000" + cleanup: + - "meters" + + - id: "convert.miles->feet" + inputs: + - id: "miles" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .miles }} * 5280" + cleanup: + - "miles" + + - id: "convert.feet->miles" + inputs: + - id: "feet" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .feet }} / 5280" + cleanup: + - "feet" + + - id: "convert.nautical_miles->miles" + inputs: + - id: "nautical_miles" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .nautical_miles }} * 1.15078" + cleanup: + - "nautical_miles" + + - id: "convert.miles->nautical_miles" + inputs: + - id: "miles" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .miles }} / 1.15078" + cleanup: + - "miles" + + - id: "convert.nautical_miles->kilometers" + inputs: + - id: "nautical_miles" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .nautical_miles }} * 1.852" + cleanup: + - "nautical_miles" + + - id: "convert.kilometers->nautical_miles" + inputs: + - id: "kilometers" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kilometers }} / 1.852" + cleanup: + - "kilometers" + + - id: "convert.square_feet->square_meters" + inputs: + - id: "square_feet" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .square_feet }} * 0.092903" + cleanup: + - "square_feet" + + - id: "convert.square_meters->square_feet" + inputs: + - id: "square_meters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .square_meters }} / 0.092903" + cleanup: + - "square_meters" + + - id: "convert.square_inches->square_centimeters" + inputs: + - id: "square_inches" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .square_inches }} * 6.4516" + cleanup: + - "square_inches" + + - id: "convert.square_centimeters->square_inches" + inputs: + - id: "square_centimeters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .square_centimeters }} / 6.4516" + cleanup: + - "square_centimeters" + + - id: "convert.acres->hectares" + inputs: + - id: "acres" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .acres }} * 0.404686" + cleanup: + - "acres" + + - id: "convert.hectares->acres" + inputs: + - id: "hectares" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .hectares }} / 0.404686" + cleanup: + - "hectares" + + - id: "convert.square_kilometers->square_miles" + inputs: + - id: "square_kilometers" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .square_kilometers }} * 0.386102" + cleanup: + - "square_kilometers" + + - id: "convert.square_miles->square_kilometers" + inputs: + - id: "square_miles" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .square_miles }} / 0.386102" + cleanup: + - "square_miles" + + - id: "convert.pounds->kilograms" + inputs: + - id: "pounds" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .pounds }} * 0.4536" + cleanup: + - "pounds" + + - id: "convert.kilograms->pounds" + inputs: + - id: "kilograms" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kilograms }} / 0.4536" + cleanup: + - "kilograms" + + - id: "convert.ounces->grams" + inputs: + - id: "ounces" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .ounces }} * 28.3495" + cleanup: + - "ounces" + + - id: "convert.grams->ounces" + inputs: + - id: "grams" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .grams }} / 28.3495" + cleanup: + - "grams" + + - id: "convert.grams->kilograms" + inputs: + - id: "grams" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .grams }} / 1000" + cleanup: + - "grams" + + - id: "convert.kilograms->grams" + inputs: + - id: "kilograms" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kilograms }} * 1000" + cleanup: + - "kilograms" + + - id: "convert.tons->kilograms" + inputs: + - id: "tons" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .tons }} * 907.185" + cleanup: + - "tons" + + - id: "convert.kilograms->tons" + inputs: + - id: "kilograms" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kilograms }} / 907.185" + cleanup: + - "kilograms" + + - id: "convert.metric_tons->kilograms" + inputs: + - id: "metric_tons" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .metric_tons }} * 1000" + cleanup: + - "metric_tons" + + - id: "convert.kilograms->metric_tons" + inputs: + - id: "kilograms" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kilograms }} / 1000" + cleanup: + - "kilograms" + + - id: "convert.fahrenheit->celsius" + inputs: + - id: "fahrenheit" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "({{ .fahrenheit }}-32)*(5/9)" + cleanup: + - "fahrenheit" + + - id: "convert.celsius->fahrenheit" + inputs: + - id: "celsius" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .celsius }}*(9/5)+32" + cleanup: + - "celsius" + + - id: "convert.celsius->kelvin" + inputs: + - id: "celsius" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .celsius }}+273.15" + cleanup: + - "celsius" + + - id: "convert.kelvin->celsius" + inputs: + - id: "kelvin" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kelvin }}-273.15" + cleanup: + - "kelvin" + + - id: "convert.fahrenheit->kelvin" + inputs: + - id: "fahrenheit" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "({{ .fahrenheit }}-32)*(5/9)+273.15" + cleanup: + - "fahrenheit" + + - id: "convert.kelvin->fahrenheit" + inputs: + - id: "kelvin" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "({{ .kelvin }}-273.15)*(9/5)+32" + cleanup: + - "kelvin" + + - id: "convert.gallons->liters" + inputs: + - id: "gallons" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .gallons }} * 3.78541" + cleanup: + - "gallons" + + - id: "convert.liters->gallons" + inputs: + - id: "liters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .liters }} / 3.78541" + cleanup: + - "liters" + + - id: "convert.fluidounces->milliliters" + inputs: + - id: "fluidounces" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .fluidounces }} * 29.5735" + cleanup: + - "fluidounces" + + - id: "convert.milliliters->fluidounces" + inputs: + - id: "milliliters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .milliliters }} / 29.5735" + cleanup: + - "milliliters" + + - id: "convert.cups->milliliters" + inputs: + - id: "cups" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .cups }} * 236.588" + cleanup: + - "cups" + + - id: "convert.milliliters->cups" + inputs: + - id: "milliliters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .milliliters }} / 236.588" + cleanup: + - "milliliters" + + - id: "convert.pints->liters" + inputs: + - id: "pints" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .pints }} * 0.473176" + cleanup: + - "pints" + + - id: "convert.liters->pints" + inputs: + - id: "liters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .liters }} / 0.473176" + cleanup: + - "liters" + + - id: "convert.quarts->liters" + inputs: + - id: "quarts" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .quarts }} * 0.946353" + cleanup: + - "quarts" + + - id: "convert.liters->quarts" + inputs: + - id: "liters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .liters }} / 0.946353" + cleanup: + - "liters" + + - id: "convert.milliliters->liters" + inputs: + - id: "milliliters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .milliliters }} / 1000" + cleanup: + - "milliliters" + + - id: "convert.liters->milliliters" + inputs: + - id: "liters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .liters }} * 1000" + cleanup: + - "liters" + + - id: "convert.cubicfeet->cubicmeters" + inputs: + - id: "cubicfeet" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .cubicfeet }} * 0.0283168" + cleanup: + - "cubicfeet" + + - id: "convert.cubicmeters->cubicfeet" + inputs: + - id: "cubicmeters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .cubicmeters }} / 0.0283168" + cleanup: + - "cubicmeters" + + - id: "convert.cubicinches->cubiccentimeters" + inputs: + - id: "cubicinches" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .cubicinches }} * 16.3871" + cleanup: + - "cubicinches" + + - id: "convert.cubiccentimeters->cubicinches" + inputs: + - id: "cubiccentimeters" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .cubiccentimeters }} / 16.3871" + cleanup: + - "cubiccentimeters" + + - id: "convert.kb->mb" + inputs: + - id: "kb" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kb }} / 1024" + cleanup: + - "kb" + + - id: "convert.kb->gb" + inputs: + - id: "kb" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kb }} / 1024 / 1024" + cleanup: + - "kb" + + - id: "convert.mb->kb" + inputs: + - id: "mb" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .mb }} * 1024" + cleanup: + - "mb" + + - id: "convert.mb->gb" + inputs: + - id: "mb" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .mb }} / 1024" + cleanup: + - "mb" + + - id: "convert.gb->kb" + inputs: + - id: "gb" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .gb }} * 1024 * 1024" + cleanup: + - "gb" + + - id: "convert.gb->mb" + inputs: + - id: "gb" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .gb }} * 1024" + cleanup: + - "gb" + + - id: "convert.gb->tb" + inputs: + - id: "gb" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .gb }} / 1024" + cleanup: + - "gb" + + - id: "convert.tb->gb" + inputs: + - id: "tb" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .tb }} * 1024" + cleanup: + - "tb" + + - id: "convert.tb->pb" + inputs: + - id: "tb" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .tb }} / 1024" + cleanup: + - "tb" + + - id: "convert.pb->tb" + inputs: + - id: "pb" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .pb }} * 1024" + cleanup: + - "pb" + + - id: "convert.bytes->kb" + inputs: + - id: "bytes" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .bytes }} / 1024" + cleanup: + - "bytes" + + - id: "convert.kb->bytes" + inputs: + - id: "kb" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kb }} * 1024" + cleanup: + - "kb" + + - id: "convert.kbps->mbps" + inputs: + - id: "kbps" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kbps }} / 1000" + cleanup: + - "kbps" + + - id: "convert.mbps->kbps" + inputs: + - id: "mbps" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .mbps }} * 1000" + cleanup: + - "mbps" + + - id: "convert.mbps->gbps" + inputs: + - id: "mbps" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .mbps }} / 1000" + cleanup: + - "mbps" + + - id: "convert.gbps->mbps" + inputs: + - id: "gbps" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .gbps }} * 1000" + cleanup: + - "gbps" + + - id: "convert.calories->joules" + inputs: + - id: "calories" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .calories }} * 4.184" + cleanup: + - "calories" + + - id: "convert.joules->calories" + inputs: + - id: "joules" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .joules }} / 4.184" + cleanup: + - "joules" + + - id: "convert.kilocalories->kilojoules" + inputs: + - id: "kilocalories" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kilocalories }} * 4.184" + cleanup: + - "kilocalories" + + - id: "convert.kilojoules->kilocalories" + inputs: + - id: "kilojoules" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kilojoules }} / 4.184" + cleanup: + - "kilojoules" + + - id: "convert.joules->kilojoules" + inputs: + - id: "joules" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .joules }} / 1000" + cleanup: + - "joules" + + - id: "convert.kilojoules->joules" + inputs: + - id: "kilojoules" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kilojoules }} * 1000" + cleanup: + - "kilojoules" + + - id: "convert.kilowatt_hours->joules" + inputs: + - id: "kilowatt_hours" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kilowatt_hours }} * 3600000" + cleanup: + - "kilowatt_hours" + + - id: "convert.joules->kilowatt_hours" + inputs: + - id: "joules" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .joules }} / 3600000" + cleanup: + - "joules" + + - id: "convert.electron_volts->joules" + inputs: + - id: "electron_volts" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .electron_volts }} * 1.602e-19" + cleanup: + - "electron_volts" + + - id: "convert.joules->electron_volts" + inputs: + - id: "joules" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .joules }} / 1.602e-19" + cleanup: + - "joules" + + - id: "convert.hertz->kilohertz" + inputs: + - id: "hertz" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .hertz }} / 1000" + cleanup: + - "hertz" + + - id: "convert.kilohertz->hertz" + inputs: + - id: "kilohertz" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kilohertz }} * 1000" + cleanup: + - "kilohertz" + + - id: "convert.kilohertz->megahertz" + inputs: + - id: "kilohertz" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kilohertz }} / 1000" + cleanup: + - "kilohertz" + + - id: "convert.megahertz->kilohertz" + inputs: + - id: "megahertz" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .megahertz }} * 1000" + cleanup: + - "megahertz" + + - id: "convert.megahertz->gigahertz" + inputs: + - id: "megahertz" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .megahertz }} / 1000" + cleanup: + - "megahertz" + + - id: "convert.gigahertz->megahertz" + inputs: + - id: "gigahertz" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .gigahertz }} * 1000" + cleanup: + - "gigahertz" + + - id: "convert.mpg->kpl" + inputs: + - id: "mpg" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .mpg }} * 0.425144" + cleanup: + - "mpg" + + - id: "convert.kpl->mpg" + inputs: + - id: "kpl" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kpl }} / 0.425144" + cleanup: + - "kpl" + + - id: "convert.mpg->lper100km" + inputs: + - id: "mpg" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "235.215 / {{ .mpg }}" + cleanup: + - "mpg" + + - id: "convert.lper100km->mpg" + inputs: + - id: "lper100km" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "235.215 / {{ .lper100km }}" + cleanup: + - "lper100km" + + - id: "convert.psi->kpa" + inputs: + - id: "psi" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .psi }} * 6.89476" + cleanup: + - "psi" + + - id: "convert.kpa->psi" + inputs: + - id: "kpa" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kpa }} / 6.89476" + cleanup: + - "kpa" + + - id: "convert.bar->psi" + inputs: + - id: "bar" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .bar }} * 14.5038" + cleanup: + - "bar" + + - id: "convert.psi->bar" + inputs: + - id: "psi" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .psi }} / 14.5038" + cleanup: + - "psi" + + - id: "convert.bar->kpa" + inputs: + - id: "bar" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .bar }} * 100" + cleanup: + - "bar" + + - id: "convert.kpa->bar" + inputs: + - id: "kpa" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kpa }} / 100" + cleanup: + - "kpa" + + - id: "convert.mmhg->kpa" + inputs: + - id: "mmhg" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .mmhg }} * 0.133322" + cleanup: + - "mmhg" + + - id: "convert.kpa->mmhg" + inputs: + - id: "kpa" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kpa }} / 0.133322" + cleanup: + - "kpa" + + - id: "convert.atmosphere->kpa" + inputs: + - id: "atmosphere" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .atmosphere }} * 101.325" + cleanup: + - "atmosphere" + + - id: "convert.kpa->atmosphere" + inputs: + - id: "kpa" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kpa }} / 101.325" + cleanup: + - "kpa" + + - id: "convert.mph->kph" + inputs: + - id: "mph" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .mph }} * 1.60934" + cleanup: + - "mph" + + - id: "convert.kph->mph" + inputs: + - id: "kph" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kph }} / 1.60934" + cleanup: + - "kph" + + - id: "convert.knots->mph" + inputs: + - id: "knots" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .knots }} * 1.15078" + cleanup: + - "knots" + + - id: "convert.mph->knots" + inputs: + - id: "mph" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .mph }} / 1.15078" + cleanup: + - "mph" + + - id: "convert.knots->kph" + inputs: + - id: "knots" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .knots }} * 1.852" + cleanup: + - "knots" + + - id: "convert.kph->knots" + inputs: + - id: "kph" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kph }} / 1.852" + cleanup: + - "kph" + + - id: "convert.mps->kph" + inputs: + - id: "mps" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .mps }} * 3.6" + cleanup: + - "mps" + + - id: "convert.kph->mps" + inputs: + - id: "kph" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .kph }} / 3.6" + cleanup: + - "kph" + + - id: "convert.mps->mph" + inputs: + - id: "mps" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .mps }} * 2.23694" + cleanup: + - "mps" + + - id: "convert.mph->mps" + inputs: + - id: "mph" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .mph }} / 2.23694" + cleanup: + - "mph" + + - id: "convert.degrees->radians" + inputs: + - id: "degrees" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .degrees }} * 0.0174533" + cleanup: + - "degrees" + + - id: "convert.radians->degrees" + inputs: + - id: "radians" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .radians }} / 0.0174533" + cleanup: + - "radians" + + - id: "convert.degrees->gradians" + inputs: + - id: "degrees" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .degrees }} * 1.11111" + cleanup: + - "degrees" + + - id: "convert.gradians->degrees" + inputs: + - id: "gradians" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .gradians }} / 1.11111" + cleanup: + - "gradians" + + - id: "convert.hours->minutes" + inputs: + - id: "hours" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .hours }} * 60" + cleanup: + - "hours" + + - id: "convert.hours->seconds" + inputs: + - id: "hours" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .hours }} * 3600" + cleanup: + - "hours" + + - id: "convert.minutes->hours" + inputs: + - id: "minutes" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .minutes }} / 60" + cleanup: + - "minutes" + + - id: "convert.minutes->seconds" + inputs: + - id: "minutes" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .minutes }} * 60" + cleanup: + - "minutes" + + - id: "convert.seconds->minutes" + inputs: + - id: "seconds" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .seconds }} / 60" + cleanup: + - "seconds" + + - id: "convert.seconds->hours" + inputs: + - id: "seconds" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .seconds }} / 3600" + cleanup: + - "seconds" + + - id: "convert.days->hours" + inputs: + - id: "days" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .days }} * 24" + cleanup: + - "days" + + - id: "convert.hours->days" + inputs: + - id: "hours" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .hours }} / 24" + cleanup: + - "hours" + + - id: "convert.days->minutes" + inputs: + - id: "days" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .days }} * 24 * 60" + cleanup: + - "days" + + - id: "convert.days->seconds" + inputs: + - id: "days" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .days }} * 24 * 3600" + cleanup: + - "days" + + - id: "convert.weeks->days" + inputs: + - id: "weeks" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .weeks }} * 7" + cleanup: + - "weeks" + + - id: "convert.days->weeks" + inputs: + - id: "days" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .days }} / 7" + cleanup: + - "days" + + - id: "convert.years->days" + inputs: + - id: "years" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .years }} * 365.25" + cleanup: + - "years" + + - id: "convert.days->years" + inputs: + - id: "days" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .days }} / 365.25" + cleanup: + - "days" + + - id: "convert.milliseconds->seconds" + inputs: + - id: "milliseconds" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .milliseconds }} / 1000" + cleanup: + - "milliseconds" + + - id: "convert.seconds->milliseconds" + inputs: + - id: "seconds" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .seconds }} * 1000" + cleanup: + - "seconds" + + - id: "convert.microseconds->milliseconds" + inputs: + - id: "microseconds" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .microseconds }} / 1000" + cleanup: + - "microseconds" + + - id: "convert.milliseconds->microseconds" + inputs: + - id: "milliseconds" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .milliseconds }} * 1000" + cleanup: + - "milliseconds" + + - id: "convert.nanoseconds->microseconds" + inputs: + - id: "nanoseconds" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .nanoseconds }} / 1000" + cleanup: + - "nanoseconds" + + - id: "convert.microseconds->nanoseconds" + inputs: + - id: "microseconds" + required: true + operations: + - uses: "convert._internal" + with: + "equation": "{{ .microseconds }} * 1000" + cleanup: + - "microseconds" diff --git a/recipes/utils/components/crypto.yaml b/recipes/utils/components/crypto.yaml new file mode 100644 index 0000000..285783d --- /dev/null +++ b/recipes/utils/components/crypto.yaml @@ -0,0 +1,87 @@ +components: + - id: "crypto.md5" + inputs: + - id: "data" + operations: + - command: printf "{{ .data }}" | md5 + output_format: "trim" + cleanup: + - "data" + + - id: "crypto.sha1" + inputs: + - id: "data" + operations: + - command: printf "{{ .data }}" | shasum -a 1 | cut -d ' ' -f 1 + output_format: "trim" + cleanup: + - "data" + + - id: "crypto.sha256" + inputs: + - id: "data" + operations: + - command: printf "{{ .data }}" | shasum -a 256 | cut -d ' ' -f 1 + output_format: "trim" + cleanup: + - "data" + + - id: "crypto.sha512" + inputs: + - id: "data" + operations: + - command: printf "{{ .data }}" | shasum -a 512 | cut -d ' ' -f 1 + output_format: "trim" + cleanup: + - "data" + + - id: "crypto.rot13" + inputs: + - id: "data" + operations: + - command: printf "{{ .data }}" | tr 'A-Za-z' 'N-ZA-Mn-za-m' + output_format: "trim" + cleanup: + - "data" + + - id: "crypto.aes.encrypt" + inputs: + - id: "data" + required: true + - id: "passphrase" + required: true + - id: "iterations" + default: 10000 + operations: + - command: | + set +H + openssl enc -aes-256-cbc -a -salt -pbkdf2 -iter {{ .iterations }} -pass pass:"{{ .passphrase }}" << 'EOF' + {{ .data }} + EOF + set -H + output_format: "trim" + cleanup: + - "data" + - "passphrase" + - "iterations" + + - id: "crypto.aes.decrypt" + inputs: + - id: "data" + required: true + - id: "passphrase" + required: true + - id: "iterations" + default: 10000 + operations: + - command: | + set +H + openssl enc -aes-256-cbc -a -d -salt -pbkdf2 -iter {{ .iterations }} -pass pass:"{{ .passphrase }}" << 'EOF' + {{ .data }} + EOF + set -H + output_format: "trim" + cleanup: + - "data" + - "passphrase" + - "iterations" diff --git a/recipes/utils/components/data.yaml b/recipes/utils/components/data.yaml new file mode 100644 index 0000000..908e815 --- /dev/null +++ b/recipes/utils/components/data.yaml @@ -0,0 +1,60 @@ +components: + - id: "data.base64.encode" + inputs: + - id: "data" + required: true + operations: + - command: printf "{{ .data }}" | base64 + output_format: "trim" + cleanup: + - "data" + + - id: "data.base64.decode" + inputs: + - id: "data" + required: true + operations: + - command: printf "{{ .data }}" | base64 -d + output_format: "trim" + cleanup: + - "data" + + - id: "data.hex.encode" + inputs: + - id: "data" + required: true + operations: + - command: printf "{{ .data }}" | xxd -p -c 1000000 + output_format: "trim" + cleanup: + - "data" + + - id: "data.hex.decode" + inputs: + - id: "data" + required: true + operations: + - command: printf "{{ .data }}" | xxd -p -r + output_format: "trim" + cleanup: + - "data" + + - id: "data.binary.encode" + inputs: + - id: "data" + required: true + operations: + - command: echo "{{ .data }}" | perl -ne 'print join("", map {sprintf("%08b", ord($_))} split("", $_))' + output_format: "trim" + cleanup: + - "data" + + - id: "data.binary.decode" + inputs: + - id: "data" + required: true + operations: + - command: echo "{{ .data }}" | perl -ne 's/([01]{8})/chr(oct("0b$1"))/ge; print' + output_format: "trim" + cleanup: + - "data" diff --git a/recipes/utils/components/dir.yaml b/recipes/utils/components/dir.yaml new file mode 100644 index 0000000..286c1ea --- /dev/null +++ b/recipes/utils/components/dir.yaml @@ -0,0 +1,81 @@ +components: + - id: "dir.exists" + inputs: + - id: "dir" + required: true + operations: + - command: '[ -d "{{ .dir }}" ] && echo "true" || echo "false"' + cleanup: + - "dir" + + - id: "dir.create" + inputs: + - id: "dir" + required: true + operations: + - command: mkdir -p {{ .dir }} + cleanup: + - "dir" + + - id: "dir.delete" + inputs: + - id: "dir" + required: true + - id: "force" + default: false + operations: + - command: rmdir {{ .dir }} + condition: .force == "false" + + - command: rm -rf {{ .dir }} + condition: .force == "true" + + - cleanup: + - "dir" + - "force" + + - id: "dir.rename" + inputs: + - id: "old" + required: true + - id: "new" + required: true + operations: + - command: mv {{ .old }} {{ .new }} + cleanup: + - "old" + - "new" + + - id: "dir.sync" + inputs: + - id: "src" + required: true + - id: "dst" + required: true + operations: + - command: | + src="{{ .src }}" + dst="{{ .dst }}" + rsync -a --delete "${src%/}/" "${dst%/}/" + cleanup: + - "src" + - "dst" + + - id: "dir.pwd" + operations: + - command: pwd + output_format: "trim" + + - id: "dir.list" + inputs: + - id: "dir" + operations: + - uses: "dir.pwd" + id: "dir" + condition: .dir == "false" + silent: true + + - command: ls -1 -a + workdir: "{{ .dir }}" + cleanup: + - "dir" diff --git a/recipes/utils/components/file.yaml b/recipes/utils/components/file.yaml new file mode 100644 index 0000000..62e1717 --- /dev/null +++ b/recipes/utils/components/file.yaml @@ -0,0 +1,510 @@ +components: + - id: "file.exists" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - command: '[ -f {{ .file }} ] && echo "true" || echo "false"' + output_format: "trim" + cleanup: + - "file" + + - id: "file.read" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - command: cat "{{ .file }}" + output_format: "trim" + cleanup: + - "file" + + - id: "file.write" + inputs: + - id: "file" + required: true + - id: "contents" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - command: | + cat > {{ .file }} << 'EOF' + {{ .contents }} + EOF + cleanup: + - "file" + - "contents" + + - id: "file.create" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - command: touch {{ .file }} + cleanup: + - "file" + + - id: "file.delete" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - command: rm "{{ .file }}" + cleanup: + - "file" + + - id: "file.empty" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - command: "> {{ .file }}" + cleanup: + - "file" + + - id: "file.copy" + inputs: + - id: "file" + required: true + - id: "copy" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - uses: "user.path.expand" + id: "copy" + with: + path: "{{ .copy }}" + + - command: cp {{ .file }} {{ .copy }} + cleanup: + - "file" + - "copy" + + - id: "file.rename" + inputs: + - id: "old" + required: true + - id: "new" + required: true + operations: + - uses: "user.path.expand" + id: "old" + with: + path: "{{ .old }}" + silent: true + + - uses: "user.path.expand" + id: "new" + with: + path: "{{ .new }}" + silent: true + + - command: mv {{ .old }} {{ .new }} + cleanup: + - "old" + - "new" + + - id: "file.backup" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - uses: "file.parts" + with: + file: "{{ .file }}" + + - id: "backup_filename" + command: echo "{{ .file_parts_path }}/{{ .file_parts_filename }}_bak_$(date +%Y%m%d_%H%M%S).{{ .file_parts_extension }}" + silent: true + + - command: cp {{ .file }} {{ .backup_filename }} + + - command: echo "{{ .backup_filename }}" + cleanup: + - "file" + - "backup_filename" + - "file_parts_path" + - "file_parts_filename" + - "file_parts_extension" + + - id: "file.temp" + inputs: + - id: "contents" + required: true + operations: + - command: | + tempfile=$(mktemp) && cat << 'EOF' > "$tempfile" && echo "$tempfile" + {{ .contents }} + EOF + output_format: "trim" + cleanup: + - "contents" + + - id: "file.encrypt" + inputs: + - id: "file" + required: true + - id: "passphrase" + required: true + - id: "iterations" + default: 10000 + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - command: openssl enc -aes-256-cbc -a -salt -pbkdf2 -iter {{ .iterations }} -pass pass:"{{ .passphrase }}" -in {{ .file }} -out {{ .file }}.enc + + - uses: "file.rename" + with: + old: "{{ .file }}.enc" + new: "{{ .file }}" + + - cleanup: + - "file" + - "passphrase" + - "iterations" + + - id: "file.decrypt" + inputs: + - id: "file" + required: true + - id: "passphrase" + required: true + - id: "iterations" + default: 10000 + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - command: openssl enc -aes-256-cbc -a -d -salt -pbkdf2 -iter {{ .iterations }} -pass pass:"{{ .passphrase }}" -in {{ .file }} -out {{ .file }}.dec + + - uses: "file.rename" + with: + old: "{{ .file }}.dec" + new: "{{ .file }}" + + - cleanup: + - "file" + - "passphrase" + - "iterations" + + - id: "file.fullpath" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - command: readlink -f {{ .file }} + output_format: "trim" + cleanup: + - "file" + + - id: "file.lower" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - uses: "file.parts" + with: + file: "{{ .file }}" + silent: true + + - uses: "string.lower" + id: "filename" + with: + string: "{{ .file_parts_filename }}.{{ .file_parts_extension }}" + silent: true + + - command: echo "{{ .file_parts_path }}/{{ .filename }}" + cleanup: + - "file" + - "filename" + - "file_parts_path" + - "file_parts_filename" + - "file_parts_extension" + + - id: "file.lower.filename" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - uses: "file.parts" + with: + file: "{{ .file }}" + silent: true + + - uses: "string.lower" + id: "filename" + with: + string: "{{ .file_parts_filename }}" + silent: true + + - command: echo "{{ .file_parts_path }}/{{ .filename }}.{{ .file_parts_extension }}" + cleanup: + - "file" + - "filename" + - "file_parts_path" + - "file_parts_filename" + - "file_parts_extension" + + - id: "file.lower.extension" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - uses: "file.parts" + with: + file: "{{ .file }}" + silent: true + + - uses: "string.lower" + id: "extension" + with: + string: "{{ .file_parts_extension }}" + silent: true + + - command: echo "{{ .file_parts_path }}/{{ .file_parts_filename }}.{{ .extension }}" + cleanup: + - "file" + - "extension" + - "file_parts_path" + - "file_parts_filename" + - "file_parts_extension" + + - id: "file.upper" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - uses: "file.parts" + with: + file: "{{ .file }}" + silent: true + + - uses: "string.upper" + id: "filename" + with: + string: "{{ .file_parts_filename }}.{{ .file_parts_extension }}" + silent: true + + - command: echo "{{ .file_parts_path }}/{{ .filename }}" + cleanup: + - "file" + - "filename" + - "file_parts_path" + - "file_parts_filename" + - "file_parts_extension" + + - id: "file.upper.filename" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - uses: "file.parts" + with: + file: "{{ .file }}" + silent: true + + - uses: "string.upper" + id: "filename" + with: + string: "{{ .file_parts_filename }}" + silent: true + + - command: echo "{{ .file_parts_path }}/{{ .filename }}.{{ .file_parts_extension }}" + cleanup: + - "file" + - "filename" + - "file_parts_path" + - "file_parts_filename" + - "file_parts_extension" + + - id: "file.upper.extension" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - uses: "file.parts" + with: + file: "{{ .file }}" + silent: true + + - uses: "string.upper" + id: "extension" + with: + string: "{{ .file_parts_extension }}" + silent: true + + - command: echo "{{ .file_parts_path }}/{{ .file_parts_filename }}.{{ .extension }}" + cleanup: + - "file" + - "extension" + - "file_parts_path" + - "file_parts_filename" + - "file_parts_extension" + + - id: "file.parts" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - uses: "file.filename" + id: "file_parts_filename" + with: + file: "{{ .file }}" + silent: true + + - uses: "file.extension" + id: "file_parts_extension" + with: + file: "{{ .file }}" + silent: true + + - uses: "file.path" + id: "file_parts_path" + with: + file: "{{ .file }}" + silent: true + + - id: "file.filename" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - command: echo "$(basename {{ .file }} | sed 's/\.[^.]*$//')" + output_format: "trim" + + - id: "file.extension" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - command: | + filepath="{{ .file }}" + echo "${filepath##*.}" + output_format: "trim" + + - id: "file.path" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file" + with: + path: "{{ .file }}" + silent: true + + - command: echo $(dirname "{{ .file }}") + output_format: "trim" diff --git a/recipes/utils/components/generate.yaml b/recipes/utils/components/generate.yaml new file mode 100644 index 0000000..3640bdc --- /dev/null +++ b/recipes/utils/components/generate.yaml @@ -0,0 +1,147 @@ +components: + - id: "generate.password" + inputs: + - id: "length" + default: 16 + operations: + - command: LC_ALL=C tr -dc 'A-Za-z0-9!@#$%^&*()-_=+' < /dev/urandom | head -c "{{ .length }}" + output_format: "trim" + cleanup: + - "length" + + - id: "generate.alphanumeric" + inputs: + - id: "length" + default: 16 + operations: + - command: LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "{{ .length }}" + output_format: "trim" + cleanup: + - "length" + + - id: "generate.number" + inputs: + - id: "min" + default: 1 + - id: "max" + default: 1000 + operations: + - command: echo "$((RANDOM % ({{ .max }} - {{ .min }} + 1) + {{ .min }}))" + output_format: "trim" + cleanup: + - "min" + - "max" + + - id: "generate.date" + inputs: + - id: "format" + default: "%Y-%m-%d" + operations: + - command: date +"{{ .format }}" + output_format: "trim" + cleanup: + - "format" + + - id: "generate.time" + inputs: + - id: "format" + default: "%H:%M:%S" + operations: + - uses: "generate.date" + with: + format: "{{ .format }}" + + - cleanup: + - "format" + + - id: "generate.timestamp" + operations: + - command: date +%s + output_format: "trim" + + - id: "generate.hex_color" + operations: + - command: printf "#%06x" $((RANDOM * RANDOM % 16777215)) + output_format: "trim" + + - id: "generate.mac" + operations: + - command: printf "%02x:%02x:%02x:%02x:%02x:%02x" $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) + output_format: "trim" + + - id: "generate.ipv4" + operations: + - command: printf "%d.%d.%d.%d" $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) + output_format: "trim" + + - id: "generate.ipv6" + operations: + - command: printf "%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x" $((RANDOM%65536)) $((RANDOM%65536)) $((RANDOM%65536)) $((RANDOM%65536)) $((RANDOM%65536)) $((RANDOM%65536)) $((RANDOM%65536)) $((RANDOM%65536)) + output_format: "trim" + + - id: "generate.uuid4" + operations: + - command: uuidgen + output_format: "trim" + + - id: "generate.uuid5" + inputs: + - id: "namespace" + default: "url" + - id: "name" + required: true + operations: + - uses: "generate.uuid5.namespace" + id: "namespace_uuid" + with: + namespace: "{{ .namespace }}" + silent: true + + - uses: "python" + id: "uuid5" + with: + code: | + import uuid + + print(uuid.uuid5(uuid.UUID('{{ .namespace_uuid }}'), '{{ .name }}')) + silent: true + + - command: echo "{{ .uuid5 }}" + output_format: "trim" + cleanup: + - "uuid5" + - "namespace" + - "name" + + - id: "generate.uuid5.namespace" + inputs: + - id: "namespace" + default: "url" + operations: + - uses: "string.lower" + id: "namespace" + with: + string: "{{ .namespace }}" + silent: true + + - command: | + case "{{ .namespace }}" in + dns) + echo "6ba7b810-9dad-11d1-80b4-00c04fd430c8" + ;; + url) + echo "6ba7b811-9dad-11d1-80b4-00c04fd430c8" + ;; + oid) + echo "6ba7b812-9dad-11d1-80b4-00c04fd430c8" + ;; + x500) + echo "6ba7b814-9dad-11d1-80b4-00c04fd430c8" + ;; + *) + echo "6ba7b811-9dad-11d1-80b4-00c04fd430c8" + ;; + esac + output_format: "trim" + cleanup: + - "namespace" diff --git a/recipes/utils/components/git.yaml b/recipes/utils/components/git.yaml new file mode 100644 index 0000000..9beef31 --- /dev/null +++ b/recipes/utils/components/git.yaml @@ -0,0 +1,114 @@ +components: + - id: "git.repo.exists" + inputs: + - id: "dir" + operations: + - uses: "dir.pwd" + id: "dir" + condition: .dir == "false" + silent: true + + - uses: "user.path.expand" + id: "dir_to_check" + with: + path: "{{ .dir }}" + silent: true + + - uses: "dir.exists" + id: "exists" + with: + dir: "{{ .dir_to_check }}" + silent: true + + - command: | + cd {{ .dir_to_check }} + git rev-parse --is-inside-work-tree 2>/dev/null || echo "false" + condition: .exists == "true" + output_format: "trim" + + - command: echo "false" + condition: .exists == "false" + output_format: "trim" + cleanup: + - "dir" + - "dir_to_check" + - "exists" + + - id: "git.staged" + operations: + - command: git diff --name-only --staged -z | xargs -0 -I{} readlink -f {} + output_format: "trim" + + - id: "git.unstaged" + operations: + - command: git ls-files --others --modified --exclude-standard -z | xargs -0 -I{} readlink -f {} + output_format: "trim" + + - id: "git.staged+unstaged" + operations: + - uses: "git.staged" + id: "staged" + silent: true + + - uses: "git.unstaged" + id: "unstaged" + silent: true + + - uses: "list.combine" + with: + list_a: "{{ .staged }}" + list_b: "{{ .unstaged }}" + + - id: "git.count.staged" + operations: + - uses: "git.staged" + id: "files" + silent: true + + - uses: "list.length" + id: "count" + with: + list: "{{ .files }}" + silent: true + + - command: echo "{{ .count }}" + output_format: "trim" + cleanup: + - "files" + - "count" + + - id: "git.count.unstaged" + operations: + - uses: "git.unstaged" + id: "files" + silent: true + + - uses: "list.length" + id: "count" + with: + list: "{{ .files }}" + silent: true + + - command: echo "{{ .count }}" + output_format: "trim" + cleanup: + - "files" + - "count" + + - id: "git.count.staged+unstaged" + operations: + - uses: "git.staged+unstaged" + id: "files" + silent: true + + - uses: "list.length" + id: "count" + with: + list: "{{ .files }}" + silent: true + + - command: echo "{{ .count }}" + output_format: "trim" + cleanup: + - "files" + - "count" diff --git a/recipes/utils/components/history.yaml b/recipes/utils/components/history.yaml deleted file mode 100644 index 9f8f32e..0000000 --- a/recipes/utils/components/history.yaml +++ /dev/null @@ -1,32 +0,0 @@ -components: - - id: "get_complete_command_history" - operations: - - command: | - case $SHELL in - */bash) - history_data=$(cat ~/.bash_history) - ;; - */zsh) - history_data=$(cat ~/.zsh_history | awk -F ': [ 0-9 ]*:[0-9]*;' '{print $2}') - ;; - */fish) - history_data=$(cat ~/.local/share/fish/fish_history | grep -o '"cmd": *"[ ^" ]*"' | sed 's/"cmd": *"//;s/"$//') - ;; - *) - history_data="Unknown shell, can't determine history file" - ;; - esac - echo "$history_data" | awk '{print $1}' - raw_command: true - user_shell: true - silent: true - - - id: "get_complete_command_name_usage" - operations: - - uses: "get_complete_command_history" - id: "command_history" - silent: true - - - name: "Get Commands" - command: echo '{{ .command_history }}' | sort | uniq -c | sort -nr | awk '{print $1" "$2}' - silent: true diff --git a/recipes/utils/components/json.yaml b/recipes/utils/components/json.yaml new file mode 100644 index 0000000..c65952a --- /dev/null +++ b/recipes/utils/components/json.yaml @@ -0,0 +1,153 @@ +components: + - id: "json.validate._internal" + inputs: + - id: "json" + required: true + - id: "filter" + required: true + - id: "verbose" + default: "false" + operations: + - command: | + if output=$(jq '{{ .filter }}' << 'EOF' 2>&1 + {{ .json }} + EOF + ); then + echo "true" + else + if [[ "{{ .verbose }}" == "true" ]]; then + echo "$output" + else + echo "false" + fi + fi + output_format: "trim" + cleanup: + - "json" + - "filter" + - "verbose" + + - id: "json.validate._internal.type" + inputs: + - id: "json" + required: true + - id: "type" + required: true + - id: "verbose" + default: "false" + operations: + - uses: "json.validate._internal" + with: + json: "{{ .json }}" + filter: 'if type != "{{ .type }}" then error("Not a valid JSON {{ .type }}") else . end' + verbose: "{{ .verbose }}" + + - id: "json.jq" + inputs: + - id: "json" + required: true + - id: "filter" + required: true + - id: "options" + default: "" + operations: + - command: | + jq {{ .options }} '{{ .filter }}' << 'EOF' + {{ .json }} + EOF + output_format: "trim" + cleanup: + - "json" + - "filter" + - "options" + + - id: "json.validate" + inputs: + - id: "json" + required: true + - id: "verbose" + default: "false" + operations: + - uses: "json.validate._internal" + with: + json: "{{ .json }}" + filter: "." + verbose: "{{ .verbose }}" + + - id: "json.validate.array" + inputs: + - id: "json" + required: true + - id: "verbose" + default: "false" + operations: + - uses: "json.validate._internal.type" + with: + json: "{{ .json }}" + type: "array" + verbose: "{{ .verbose }}" + + - id: "json.validate.object" + inputs: + - id: "json" + required: true + - id: "verbose" + default: "false" + operations: + - uses: "json.validate._internal.type" + with: + json: "{{ .json }}" + type: "object" + verbose: "{{ .verbose }}" + + - id: "json.validate.string" + inputs: + - id: "json" + required: true + - id: "verbose" + default: "false" + operations: + - uses: "json.validate._internal.type" + with: + json: "{{ .json }}" + type: "string" + verbose: "{{ .verbose }}" + + - id: "json.validate.number" + inputs: + - id: "json" + required: true + - id: "verbose" + default: "false" + operations: + - uses: "json.validate._internal.type" + with: + json: "{{ .json }}" + type: "number" + verbose: "{{ .verbose }}" + + - id: "json.validate.boolean" + inputs: + - id: "json" + required: true + - id: "verbose" + default: "false" + operations: + - uses: "json.validate._internal.type" + with: + json: "{{ .json }}" + type: "boolean" + verbose: "{{ .verbose }}" + + - id: "json.validate.null" + inputs: + - id: "json" + required: true + - id: "verbose" + default: "false" + operations: + - uses: "json.validate._internal.type" + with: + json: "{{ .json }}" + type: "null" + verbose: "{{ .verbose }}" diff --git a/recipes/utils/components/lint.yaml b/recipes/utils/components/lint.yaml new file mode 100644 index 0000000..d363084 --- /dev/null +++ b/recipes/utils/components/lint.yaml @@ -0,0 +1,160 @@ +components: + - id: "lint.tabs->spaces" + inputs: + - id: "file" + required: true + - id: "spaces" + default: 4 + operations: + - uses: "user.path.expand" + id: "file_to_convert" + with: + path: "{{ .file }}" + silent: true + + - command: expand -t {{ .spaces }} {{ .file_to_convert }} + id: "converted_file" + silent: true + + - uses: "file.write" + with: + file: "{{ .file_to_convert }}" + contents: "{{ .converted_file }}" + cleanup: + - "file_to_convert" + - "converted_file" + - "spaces" + + - id: "lint.spaces->tabs" + inputs: + - id: "file" + required: true + - id: "spaces" + default: 4 + operations: + - uses: "user.path.expand" + id: "file_to_convert" + with: + path: "{{ .file }}" + silent: true + + - command: unexpand -a -t {{ .spaces }} {{ .file_to_convert }} + id: "converted_file" + silent: true + + - uses: "file.write" + with: + file: "{{ .file_to_convert }}" + contents: "{{ .converted_file }}" + cleanup: + - "file_to_convert" + - "converted_file" + - "spaces" + + - id: "lint.spaces->spaces" + inputs: + - id: "file" + required: true + - id: "from" + default: 2 + - id: "to" + default: 4 + operations: + - uses: "user.path.expand" + id: "file_to_convert" + with: + path: "{{ .file }}" + silent: true + + - command: unexpand -t {{ .from }} {{ .file_to_convert }} | expand -t {{ .to }} + id: "converted_file" + silent: true + + - uses: "file.write" + with: + file: "{{ .file_to_convert }}" + contents: "{{ .converted_file }}" + cleanup: + - "file_to_convert" + - "converted_file" + - "from" + - "to" + + - id: "lint.trailing" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file_to_convert" + with: + path: "{{ .file }}" + silent: true + + - uses: "file.read" + id: "content" + with: + file: "{{ .file_to_convert }}" + silent: true + + - id: "converted" + command: | + cat << 'EOF' | sed 's/[[:space:]]*$//' + {{ .content }} + EOF + silent: true + + - uses: "file.write" + with: + file: "{{ .file_to_convert }}" + contents: "{{ raw .converted }}" + + - cleanup: + - "file_to_convert" + - "content" + - "converted" + + - id: "lint.has.eof.newline" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file_to_check" + with: + path: "{{ .file }}" + silent: true + + - command: | + if [ "$(tail -c1 "{{ .file_to_check }}" 2>/dev/null | hexdump -v -e '1/1 "%02x"')" != "0a" ]; then + echo "false" + else + echo "true" + fi + output_format: "trim" + cleanup: + - "file_to_check" + + - id: "lint.eof" + inputs: + - id: "file" + required: true + operations: + - uses: "user.path.expand" + id: "file_to_convert" + with: + path: "{{ .file }}" + silent: true + + - uses: "lint.has.eof.newline" + id: "has_eof_newline" + with: + file: "{{ .file_to_convert }}" + silent: true + + - command: printf "\n" >> "{{ .file_to_convert }}" + condition: .has_eof_newline == "false" + silent: true + cleanup: + - "file_to_convert" + - "has_eof_newline" diff --git a/recipes/utils/components/list.yaml b/recipes/utils/components/list.yaml new file mode 100644 index 0000000..5d34bd0 --- /dev/null +++ b/recipes/utils/components/list.yaml @@ -0,0 +1,573 @@ +components: + - id: "list.make" + inputs: + - id: "list" + required: true + operations: + - command: | + list="{{ .list }}" + + if [[ "$list" == *$'\n'* ]]; then + if [[ "$list" =~ ^\[.*\]$ ]] || [[ "$list" =~ ^\"\[.*\]\"$ ]]; then + list="${list#\"}" + list="${list%\"}" + echo "$list" | grep -v '^\s*$' | grep -v '^\s*\[\s*$' | grep -v '^\s*\]\s*$' | sed 's/^\s*"\(.*\)",\?\s*$/\1/' | sed 's/^\s*\(.*\),\s*$/\1/' + else + echo "$list" + fi + elif [[ "$list" =~ ^\[.*\]$ ]] || [[ "$list" =~ ^\"\[.*\]\"$ ]]; then + list="${list#\"}" + list="${list%\"}" + list="${list#\[}" + list="${list%\]}" + echo "$list" | tr ',' '\n' | sed 's/^ *//' | sed 's/ *$//' | sed 's/^"\(.*\)"$/\1/' + elif [[ "$list" =~ ^\".*\"$ ]] || [[ "$list" =~ ^\'.*\'$ ]]; then + list="${list#\"}" + list="${list%\"}" + list="${list#\'}" + list="${list%\'}" + echo "$list" + elif [[ "$list" =~ [[:space:]] ]]; then + for item in $list; do + echo "$item" + done + elif [[ "$list" == *","* ]]; then + echo "$list" | tr ',' '\n' | sed 's/^ *//' | sed 's/ *$//' + elif [[ "$list" == *";"* ]]; then + echo "$list" | tr ';' '\n' | sed 's/^ *//' | sed 's/ *$//' + elif [[ "$list" == *":"* ]]; then + echo "$list" | tr ':' '\n' | sed 's/^ *//' | sed 's/ *$//' + else + declare -n arr="$list" 2>/dev/null + if [[ $? -eq 0 ]]; then + printf "%s\n" "${arr[@]}" + else + echo "$list" + fi + fi + output_format: "lines" + cleanup: + - "list" + + - id: "list.length" + inputs: + - id: "list" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: echo "{{ .list }}" | grep -v "^$" | wc -l + output_format: "trim" + cleanup: + - "list" + + - id: "list.empty" + inputs: + - id: "list" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - uses: "list.length" + id: "length" + with: + list: "{{ .list }}" + silent: true + + - command: echo $([ "{{ .length }}" = "0" ] && echo "true" || echo "false") + output_format: "trim" + cleanup: + - "list" + - "length" + + - id: "list.pluck" + inputs: + - id: "list" + required: true + - id: "index" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: echo "{{ .list }}" | awk -v idx="$(( {{ .index }} +1))" 'NR==idx {print}' + output_format: "trim" + cleanup: + - "list" + - "index" + + - id: "list.append" + inputs: + - id: "list" + required: true + - id: "item" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: | + echo "{{ .list }}" + echo "{{ .item }}" + output_format: "trim" + cleanup: + - "list" + - "item" + + - id: "list.prepend" + inputs: + - id: "list" + required: true + - id: "item" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: | + echo "{{ .item }}" + echo "{{ .list }}" + output_format: "trim" + cleanup: + - "list" + - "item" + + - id: "list.insert" + inputs: + - id: "list" + required: true + - id: "item" + required: true + - id: "index" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: | + list="{{ .list }}" + item="{{ .item }}" + index="{{ .index }}" + + awk -v x="${index}" -v i="${item}" ' + BEGIN {line_count = 0} + { + if (line_count == x) { + print i + } + print $0 + line_count++ + } + END { + if (x >= line_count) { + print i + } + }' <<< "${list}" + output_format: "trim" + cleanup: + - "list" + - "item" + - "index" + + - id: "list.remove" + inputs: + - id: "list" + required: true + - id: "index" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: | + list="{{ .list }}" + index="{{ .index }}" + + awk -v x="${index}" ' + BEGIN {line_count = 0} + { + if (line_count != x) { + print $0 + } + line_count++ + }' <<< "${list}" + output_format: "trim" + cleanup: + - "list" + - "index" + + - id: "list.remove.item" + inputs: + - id: "list" + required: true + - id: "item" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: echo "{{ .list }}" | grep -v "^{{ .item }}$" + output_format: "trim" + cleanup: + - "list" + - "item" + + - id: "list.item.index" + inputs: + - id: "list" + required: true + - id: "item" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: | + list="{{ .list }}" + item="{{ .item }}" + + awk -v i="${item}" ' + BEGIN {found = 0} + { + if ($0 == i && found == 0) { + print NR - 1 + found = 1 + exit + } + } + END { + if (found == 0) { + print "-1" + } + }' <<< "${list}" + output_format: "trim" + cleanup: + - "list" + - "item" + + - id: "list.contains" + inputs: + - id: "list" + required: true + - id: "item" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: | + if echo "{{ .list }}" | grep -q "^{{ .item }}$"; then + echo "true" + else + echo "false" + fi + output_format: "trim" + cleanup: + - "list" + - "item" + + - id: "list.not.contains" + inputs: + - id: "list" + required: true + - id: "item" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - uses: "list.contains" + id: "contains" + with: + list: "{{ .list }}" + item: "{{ .item }}" + silent: true + + - command: echo $([ "{{ .contains }}" = "true" ] && echo "false" || echo "true") + output_format: "trim" + cleanup: + - "list" + - "item" + - "contains" + + - id: "list.join" + inputs: + - id: "list" + required: true + - id: "delimiter" + default: "," + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - uses: "string.join" + id: "joined" + with: + string: "{{ .list }}" + delimiter: "{{ .delimiter }}" + silent: true + + - command: echo "{{ .joined }}" + output_format: "trim" + cleanup: + - "list" + - "delimiter" + - "joined" + + - id: "list.reverse" + inputs: + - id: "list" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: sed '1!G;h;$!d' <<< "{{ .list }}" + output_format: "trim" + cleanup: + - "list" + + - id: "list.sort" + inputs: + - id: "list" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: echo "{{ .list }}" | sort + output_format: "trim" + cleanup: + - "list" + + - id: "list.unique" + inputs: + - id: "list" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: echo "{{ .list }}" | sort | uniq + output_format: "trim" + cleanup: + - "list" + + - id: "list.filter" + inputs: + - id: "list" + required: true + - id: "filter" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: echo "{{ .list }}" | grep "{{ .filter }}" + output_format: "trim" + cleanup: + - "list" + - "filter" + + - id: "list.slice" + inputs: + - id: "list" + required: true + - id: "start" + required: true + - id: "end" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: echo "{{ .list }}" | sed -n "$(({{.start}}+1)),$(({{.end}}+1))p" + output_format: "trim" + cleanup: + - "list" + - "start" + - "end" + + - id: "list.chunk" + inputs: + - id: "list" + required: true + - id: "size" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: echo "{{ .list }}" | awk -v size="{{ .size }}" '{if (NR % size == 1) printf "\n"; printf "%s\n", $0}' + output_format: "trim" + cleanup: + - "list" + - "size" + + - id: "list.combine" + inputs: + - id: "list_a" + required: true + - id: "list_b" + required: true + operations: + - uses: "list.make" + id: "list_a" + with: + list: "{{ .list_a }}" + silent: true + + - uses: "list.make" + id: "list_b" + with: + list: "{{ .list_b }}" + silent: true + + - id: "combined" + command: | + echo "{{ .list_a }}" + echo "{{ .list_b }}" + silent: true + + - uses: "list.unique" + id: "unique" + with: + list: "{{ .combined }}" + silent: true + + - command: echo "{{ .unique }}" + output_format: "trim" + cleanup: + - "list_a" + - "list_b" + - "combined" + - "unique" + + - id: "list.shift" + inputs: + - id: "list" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: echo "{{ .list }}" | head -n 1 + id: "item" + silent: true + + - command: echo "{{ .list }}" | tail -n +2 + id: "list" + silent: true + + - command: echo "{{ .item }}" + output_format: "trim" + cleanup: + - "item" + + - id: "list.pop" + inputs: + - id: "list" + required: true + operations: + - uses: "list.make" + id: "list" + with: + list: "{{ .list }}" + silent: true + + - command: echo "{{ .list }}" | tail -n 1 + id: "item" + silent: true + + - command: echo "{{ .list }}" | sed '$d' + id: "list" + silent: true + + - command: echo "{{ .item }}" + output_format: "trim" + cleanup: + - "item" + + - id: "list.overlap" + inputs: + - id: "list_a" + required: true + - id: "list_b" + required: true + operations: + - uses: "list.make" + id: "list_a" + with: + list: "{{ .list_a }}" + silent: true + + - uses: "list.make" + id: "list_b" + with: + list: "{{ .list_b }}" + silent: true + + - transform: '{{ overlap .list_a .list_b }}' + output_format: "trim" + cleanup: + - "list_a" + - "list_b" diff --git a/recipes/utils/components/math.yaml b/recipes/utils/components/math.yaml new file mode 100644 index 0000000..2da7ebd --- /dev/null +++ b/recipes/utils/components/math.yaml @@ -0,0 +1,272 @@ +components: + - id: "math._internal" + inputs: + - id: "equation" + required: true + - id: "decimals" + default: 4 + operations: + - command: | + echo "{{ .equation }}" | bc -l | awk '{ + if ($1 == int($1)) { + printf "%.0f\n", $1 + } else { + printf "%.{{ .decimals }}f\n", $1 + } + }' + output_format: "trim" + cleanup: + - "equation" + - "decimals" + + - id: "math.equation" + inputs: + - id: "equation" + required: true + - id: "decimals" + default: 2 + operations: + - uses: "math._internal" + with: + equation: "{{ .equation }}" + decimals: "{{ .decimals }}" + + - id: "math.const.pi" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "4*a(1)" + decimals: "{{ .decimals }}" + + - id: "math.const.golden_ratio" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "(1 + sqrt(5))/2" + decimals: "{{ .decimals }}" + + - id: "math.const.sqrt2" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "sqrt(2)" + decimals: "{{ .decimals }}" + + - id: "math.const.sqrt3" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "sqrt(3)" + decimals: "{{ .decimals }}" + + - id: "math.const.sqrt5" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "sqrt(5)" + decimals: "{{ .decimals }}" + + - id: "math.const.ln2" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "l(2)" + decimals: "{{ .decimals }}" + + - id: "math.const.ln10" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "l(10)" + decimals: "{{ .decimals }}" + + - id: "math.const.log10_2" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "l(2)/l(10)" + decimals: "{{ .decimals }}" + + - id: "math.const.log10_e" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "1/l(10)" + decimals: "{{ .decimals }}" + + - id: "math.const.log2_e" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "1/l(2)" + decimals: "{{ .decimals }}" + + - id: "math.const.log2_10" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "l(10)/l(2)" + decimals: "{{ .decimals }}" + + - id: "math.const.pi_half" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "4*a(1)/2" + decimals: "{{ .decimals }}" + + - id: "math.const.pi_quarter" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "a(1)" + decimals: "{{ .decimals }}" + + - id: "math.const.tau" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "2*4*a(1)" + decimals: "{{ .decimals }}" + + - id: "math.const.reciprocal_pi" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "1/(4*a(1))" + decimals: "{{ .decimals }}" + + - id: "math.const.two_over_pi" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "2/(4*a(1))" + decimals: "{{ .decimals }}" + + - id: "math.const.two_over_sqrt_pi" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "2/sqrt(4*a(1))" + decimals: "{{ .decimals }}" + + - id: "math.const.euler" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "e(1)" + decimals: "{{ .decimals }}" + + - id: "math.const.euler_mascheroni" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "0.57721566490153286060651209008240243104215933593992" + decimals: "{{ .decimals }}" + + - id: "math.const.catalan" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "0.91596559417721901505460351493238411077414937428167" + decimals: "{{ .decimals }}" + + - id: "math.const.apery" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "1.20205690315959428539973816151144999076498629234049" + decimals: "{{ .decimals }}" + + - id: "math.const.glaisher" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "1.28242712910062263687534256886979172776768892732500" + decimals: "{{ .decimals }}" + + - id: "math.const.conway" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "1.30357726903429639125709911215255189073070250465940" + decimals: "{{ .decimals }}" + + - id: "math.const.khinchin" + inputs: + - id: "decimals" + default: 4 + operations: + - uses: "math._internal" + with: + equation: "2.68545200106530644530971483548179569382038229399446" + decimals: "{{ .decimals }}" diff --git a/recipes/utils/components/mime.yaml b/recipes/utils/components/mime.yaml new file mode 100644 index 0000000..b0dab0b --- /dev/null +++ b/recipes/utils/components/mime.yaml @@ -0,0 +1,158 @@ +components: + - id: "mime.find._internal" + inputs: + - id: "path" + - id: "mime_type" + - id: "mime_subtype" + operations: + - uses: "dir.pwd" + id: "path" + condition: .path == "false" + silent: true + + - command: echo "" + id: "mime_subtype" + output_format: "trim" + condition: .mime_subtype == "false" + silent: true + + - command: | + find {{ .path }} -type f -exec file --mime-type {} \+ | grep "{{ .mime_type }}/{{ .mime_subtype }}" | cut -d: -f1 + cleanup: + - "path" + - "mime_type" + - "mime_subtype" + + - id: "mime.file" + inputs: + - id: "file" + required: true + operations: + - command: file --mime-type -b {{ .file }} + output_format: "trim" + cleanup: + - "file" + + - id: "mime.file.check" + inputs: + - id: "file" + required: true + - id: "type" + required: true + operations: + - uses: "mime.file" + id: "mime_type" + with: + file: "{{ .file }}" + silent: true + + - command: | + if [[ "{{ .type }}" == */* ]]; then + [[ "{{ .mime_type }}" == "{{ .type }}" ]] && echo "true" || echo "false" + else + [[ "{{ .mime_type }}" == "{{ .type }}"/* ]] && echo "true" || echo "false" + fi + output_format: "trim" + cleanup: + - "file" + - "type" + - "mime_type" + + - id: "mime.find.application" + inputs: + - id: "path" + - id: "subtype" + operations: + - uses: "mime.find._internal" + with: + path: "{{ .path }}" + mime_type: "application" + mime_subtype: "{{ .subtype }}" + + - id: "mime.find.audio" + inputs: + - id: "path" + - id: "subtype" + operations: + - uses: "mime.find._internal" + with: + path: "{{ .path }}" + mime_type: "audio" + mime_subtype: "{{ .subtype }}" + + - id: "mime.find.font" + inputs: + - id: "path" + - id: "subtype" + operations: + - uses: "mime.find._internal" + with: + path: "{{ .path }}" + mime_type: "font" + mime_subtype: "{{ .subtype }}" + + - id: "mime.find.image" + inputs: + - id: "path" + - id: "subtype" + operations: + - uses: "mime.find._internal" + with: + path: "{{ .path }}" + mime_type: "image" + mime_subtype: "{{ .subtype }}" + + - id: "mime.find.message" + inputs: + - id: "path" + - id: "subtype" + operations: + - uses: "mime.find._internal" + with: + path: "{{ .path }}" + mime_type: "message" + mime_subtype: "{{ .subtype }}" + + - id: "mime.find.model" + inputs: + - id: "path" + - id: "subtype" + operations: + - uses: "mime.find._internal" + with: + path: "{{ .path }}" + mime_type: "model" + mime_subtype: "{{ .subtype }}" + + - id: "mime.find.multipart" + inputs: + - id: "path" + - id: "subtype" + operations: + - uses: "mime.find._internal" + with: + path: "{{ .path }}" + mime_type: "multipart" + mime_subtype: "{{ .subtype }}" + + - id: "mime.find.text" + inputs: + - id: "path" + - id: "subtype" + operations: + - uses: "mime.find._internal" + with: + path: "{{ .path }}" + mime_type: "text" + mime_subtype: "{{ .subtype }}" + + - id: "mime.find.video" + inputs: + - id: "path" + - id: "subtype" + operations: + - uses: "mime.find._internal" + with: + path: "{{ .path }}" + mime_type: "video" + mime_subtype: "{{ .subtype }}" diff --git a/recipes/utils/components/os.yaml b/recipes/utils/components/os.yaml index adc64fa..6415682 100644 --- a/recipes/utils/components/os.yaml +++ b/recipes/utils/components/os.yaml @@ -1,5 +1,5 @@ components: - - id: "get_os" + - id: "os.get" operations: - command: | case "$(uname -s)" in @@ -18,41 +18,9 @@ components: esac silent: true - - id: "open_app" - inputs: - - id: "app" - operations: - - uses: "get_os" - id: "os" - - - command: | - case "{{ .os }}" in - macos) - open -a "{{ .app }}" - ;; - linux) - if command -v gtk-launch >/dev/null 2>&1; then - gtk-launch "{{ .app }}" - elif command -v flatpak >/dev/null 2>&1 && flatpak list --app | cut -f2 | grep -q -i "^{{ .app }}$"; then - flatpak run "$(flatpak list --app | grep -i "^{{ .app }}$" | cut -f1)" - elif command -v xdg-open >/dev/null 2>&1; then - xdg-open "$(find /usr/share/applications /usr/local/share/applications ~/.local/share/applications -name "*{{ .app }}*.desktop" 2>/dev/null | head -1)" - else - nohup "{{ .app }}" >/dev/null 2>&1 & - fi - ;; - windows) - powershell -command "Start-Process '{{ .app }}'" - ;; - *) - echo "false" - ;; - esac - silent: true - - - id: "get_all_apps" + - id: "os.apps" operations: - - uses: "get_os" + - uses: "os.get" id: "os" - command: | @@ -82,25 +50,27 @@ components: esac silent: true - - id: "get_filtered_apps" + - id: "os.apps.filtered" inputs: - - id: "app_filter" + - id: "filter" operations: - - uses: "get_all_apps" - id: "all_apps" + - uses: "os.apps" + id: "apps" - - command: echo '{{ .all_apps }}' - condition: .all_apps != "false" && .app_filter == "false" + - command: echo "{{ .apps }}" + condition: .apps != "false" && .filter == "false" on_failure: "handle_failure" silent: true - - command: echo '{{ .all_apps }}' | grep -i "{{ .app_filter }}" - condition: .all_apps != "false" && .app_filter != "false" + - command: echo '{{ .apps }}' | grep -i "{{ .filter }}" + condition: .apps != "false" && .filter != "false" on_failure: "handle_failure" silent: true + cleanup: + - "filter" - command: echo "false" - condition: .all_apps == "false" + condition: .apps == "false" on_failure: "handle_failure" silent: true @@ -108,17 +78,17 @@ components: command: echo "false" silent: true - - id: "app_select" + - id: "os.app.select" inputs: - - id: "app_filter" + - id: "filter" operations: - - uses: "get_filtered_apps" + - uses: "os.apps.filtered" with: - app_filter: "{{ .app_filter }}" - id: "all_apps" + filter: "{{ .filter }}" + id: "apps" - - command: echo "{{ .all_apps }}" - condition: '{{ count .all_apps }} == 1' + - command: echo "{{ .apps }}" + condition: '{{ count .apps }} == 1 && {{ .apps }} != "false"' silent: true - command: echo "{{ .app_name }}" @@ -126,10 +96,46 @@ components: - id: "app_name" type: "select" message: "Select an Application" - source_operation: "all_apps" - condition: .all_apps != "false && {{ count .all_apps }} > 1 + source_operation: "apps" + condition: .apps != "false && {{ count .apps }} > 1 silent: true - command: echo "false" - condition: .all_apps == "false" || {{ count .all_apps }} == 0 + condition: .apps == "false" || {{ count .apps }} == 0 + silent: true + cleanup: + - "filter" + + - id: "os.app.open" + inputs: + - id: "app" + operations: + - uses: "os.get" + id: "os" + + - command: | + case "{{ .os }}" in + macos) + open -a "{{ .app }}" + ;; + linux) + if command -v gtk-launch >/dev/null 2>&1; then + gtk-launch "{{ .app }}" + elif command -v flatpak >/dev/null 2>&1 && flatpak list --app | cut -f2 | grep -q -i "^{{ .app }}$"; then + flatpak run "$(flatpak list --app | grep -i "^{{ .app }}$" | cut -f1)" + elif command -v xdg-open >/dev/null 2>&1; then + xdg-open "$(find /usr/share/applications /usr/local/share/applications ~/.local/share/applications -name "*{{ .app }}*.desktop" 2>/dev/null | head -1)" + else + nohup "{{ .app }}" >/dev/null 2>&1 & + fi + ;; + windows) + powershell -command "Start-Process '{{ .app }}'" + ;; + *) + echo "false" + ;; + esac silent: true + cleanup: + - "app" diff --git a/recipes/utils/components/password.yaml b/recipes/utils/components/password.yaml new file mode 100644 index 0000000..0c09ad7 --- /dev/null +++ b/recipes/utils/components/password.yaml @@ -0,0 +1,100 @@ +components: + - id: "password.strength.rating" + inputs: + - id: "password" + required: true + operations: + - uses: "password.strength" + id: "json_results" + with: + password: "{{ .password }}" + silent: true + + - uses: "json.jq" + id: "rating" + with: + json: "{{ .json_results }}" + filter: ".rating" + silent: true + + - transform: '{{ replace .rating "\"" "" }}' + + - id: "password.strength.score" + inputs: + - id: "password" + required: true + operations: + - uses: "password.strength" + id: "json_results" + with: + password: "{{ .password }}" + silent: true + + - uses: "json.jq" + id: "score" + with: + json: "{{ .json_results }}" + filter: ".score" + silent: true + + - transform: '{{ roundTo .score 2 }}' + + - id: "password.strength" + inputs: + - id: "password" + required: true + operations: + - uses: "python" + with: + code: | + import json + import math + import re + from collections import Counter + + def check_password_strength(password): + score = 0 + length = len(password) + + if length == 0: + return json.dumps({"rating": "invalid", "score": 0}) + + score += min(30, length * 2) + + score += sum([ + any(c.islower() for c in password) * 10, + any(c.isupper() for c in password) * 10, + any(c.isdigit() for c in password) * 10, + any(not c.isalnum() for c in password) * 10 + ]) + + char_counts = Counter(password) + entropy = -sum((count/length) * math.log2(count/length) for count in char_counts.values()) + entropy_score = min(30, entropy * 6) + score += entropy_score + + repeats = re.findall(r'(.+?)\1+', password) + if repeats: + score -= min(20, sum(len(r) * 4 for r in repeats)) + + sequential_penalty = 0 + for i in range(length - 2): + if (ord(password[i+1]) - ord(password[i]) == 1 and + ord(password[i+2]) - ord(password[i+1]) == 1): + sequential_penalty += 3 + elif (ord(password[i+1]) - ord(password[i]) == -1 and + ord(password[i+2]) - ord(password[i+1]) == -1): + sequential_penalty += 3 + score -= min(20, sequential_penalty) + + score = max(0, min(100, score)) + + if score < 40: rating = "weak" + elif score < 60: rating = "moderate" + elif score < 80: rating = "strong" + else: rating = "very strong" + + return json.dumps({"rating": rating, "score": score}) + + # Check password strength + print(check_password_strength("{{ .password }}")) diff --git a/recipes/utils/components/python.yaml b/recipes/utils/components/python.yaml new file mode 100644 index 0000000..6360eef --- /dev/null +++ b/recipes/utils/components/python.yaml @@ -0,0 +1,43 @@ +components: + - id: "python.3.installed" + operations: + - command: python --version 2>&1 | grep -q "Python 3" && echo "true" || echo "false" + output_format: "trim" + + - id: "python.2.installed" + operations: + - command: python --version 2>&1 | grep -q "Python 2" && echo "true" || echo "false" + output_format: "trim" + + - id: "python.installed" + inputs: + - id: "version" + default: 3 + operations: + - uses: "python.3.installed" + condition: .version == 3 + cleanup: + - "version" + + - uses: "python.2.installed" + condition: .version == 2 + cleanup: + - "version" + + - id: "python" + inputs: + - id: "code" + required: true + operations: + - uses: "python.installed" + id: "installed" + silent: true + + - command: | + python <<'EOF' + {{ .code }} + EOF + condition: .installed == "true" + cleanup: + - "code" + - "installed" diff --git a/recipes/utils/components/string.yaml b/recipes/utils/components/string.yaml new file mode 100644 index 0000000..262a6d3 --- /dev/null +++ b/recipes/utils/components/string.yaml @@ -0,0 +1,294 @@ +components: + - id: "string.upper" + inputs: + - id: "string" + required: true + operations: + - command: echo {{ .string }} | tr '[:lower:]' '[:upper:]' + output_format: "trim" + cleanup: + - "string" + + - id: "string.lower" + inputs: + - id: "string" + required: true + operations: + - command: echo {{ .string }} | tr '[:upper:]' '[:lower:]' + output_format: "trim" + cleanup: + - "string" + + - id: "string.camel" + inputs: + - id: "string" + required: true + operations: + - command: echo "{{ .string }}" | awk 'BEGIN{RS="[^a-zA-Z0-9]+"} {if (NR==1) {printf tolower($0)} else {printf toupper(substr($0,1,1)) tolower(substr($0,2))}}' + output_format: "trim" + cleanup: + - "string" + + - id: "string.pascal" + inputs: + - id: "string" + required: true + operations: + - command: echo "{{ .string }}" | awk 'BEGIN{RS="[^a-zA-Z0-9]+"} {printf toupper(substr($0,1,1)) tolower(substr($0,2))}' + output_format: "trim" + cleanup: + - "string" + + - id: "string.snake" + inputs: + - id: "string" + required: true + operations: + - command: echo "{{ .string }}" | tr '[:upper:]' '[:lower:]' | sed -e 's/[^[:alnum:]]/_/g' | sed -e 's/__*/_/g' | sed -e 's/^_//' -e 's/_$//' + output_format: "trim" + cleanup: + - "string" + + - id: "string.kebab" + inputs: + - id: "string" + required: true + operations: + - command: echo "{{ .string }}" | tr '[:upper:]' '[:lower:]' | sed -e 's/[^[:alnum:]]/-/g' | sed -e 's/--*/-/g' | sed -e 's/^-//' -e 's/-$//' + output_format: "trim" + cleanup: + - "string" + + - id: "string.slug" + inputs: + - id: "string" + required: true + operations: + - uses: "string.kebab" + with: + string: '{{ .string }}' + + - id: "string.title" + inputs: + - id: "string" + required: true + operations: + - command: echo "{{ .string }}" | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2));}1' + output_format: "trim" + cleanup: + - "string" + + - id: "string.capitalize" + inputs: + - id: "string" + required: true + operations: + - command: echo "{{ .string }}" | awk '{print toupper(substr($0,1,1)) substr($0,2)}' + output_format: "trim" + cleanup: + - "string" + + - id: "string.reverse" + inputs: + - id: "string" + required: true + operations: + - command: echo "{{ .string }}" | rev + output_format: "trim" + cleanup: + - "string" + + - id: "string.count.words" + inputs: + - id: "string" + required: true + operations: + - command: printf "%s" "{{ .string }}" | wc -w + output_format: "trim" + cleanup: + - "string" + + - id: "string.count.chars" + inputs: + - id: "string" + required: true + operations: + - command: printf "%s" "{{ .string }}" | wc -c + output_format: "trim" + cleanup: + - "string" + + - id: "string.count.substring" + inputs: + - id: "string" + required: true + - id: "substring" + required: true + operations: + - command: echo "{{ .string }}" | grep -o "{{ .substring }}" | wc -l + output_format: "trim" + cleanup: + - "string" + - "substring" + + - id: "string.split" + inputs: + - id: "string" + required: true + - id: "delimiter" + default: "_" + description: "Use _ for space" + operations: + - command: echo "{{ .string }}" | awk -F "$(echo '{{ .delimiter }}' | tr '_' ' ')" '{for(i=1;i<=NF;i++) print $i}' + output_format: "trim" + cleanup: + - "string" + - "delimiter" + + - id: "string.join" + inputs: + - id: "string" + required: true + - id: "delimiter" + default: ",_" + description: "Use _ for space" + operations: + - command: echo "{{ .string }}" | tr '\n' '#' | sed 's/#$//' | sed "s/#/$(echo '{{ .delimiter }}' | tr '_' ' ')/g" + output_format: "trim" + cleanup: + - "string" + - "delimiter" + + - id: "string.replace" + inputs: + - id: "string" + required: true + - id: "find" + required: true + - id: "replace" + required: true + description: "Use _ for space" + operations: + - command: echo "{{ .string }}" | sed "s/{{ .find }}/$(echo '{{ .replace }}' | tr '_' ' ')/g" + output_format: "trim" + cleanup: + - "string" + - "find" + - "replace" + + - id: "string.substring" + inputs: + - id: "string" + required: true + - id: "start" + required: true + - id: "length" + required: true + operations: + - command: echo "{{ .string }}" | cut -c "{{ .start }}-$(( {{ .start }} + {{ .length }} - 1 ))" + output_format: "trim" + cleanup: + - "string" + - "start" + - "length" + + - id: "string.urlencode" + inputs: + - id: "string" + required: true + operations: + - uses: "python" + id: "string" + with: + code: | + import sys + import urllib.parse + + print(urllib.parse.quote("{{ .string }}".strip())) + silent: true + + - command: echo "{{ .string }}" + output_format: "trim" + cleanup: + - "string" + + - id: "string.urldecode" + inputs: + - id: "string" + required: true + operations: + - uses: "python" + id: "string" + with: + code: | + import sys + import urllib.parse + + print(urllib.parse.unquote("{{ .string }}".strip())) + silent: true + + - command: echo "{{ .string }}" + output_format: "trim" + cleanup: + - "string" + + - id: "string.truncate" + inputs: + - id: "string" + required: true + - id: "length" + default: 25 + - id: "suffix" + default: "..." + operations: + - command: echo "{{ .string }}" | awk -v len={{ .length }} -v suffix="{{ .suffix }}" 'length($0) > len {print substr($0, 1, len) suffix; next} {print}' + output_format: "trim" + cleanup: + - "string" + - "length" + - "suffix" + + - id: "string.padleft" + inputs: + - id: "string" + required: true + - id: "length" + required: true + - id: "padding" + default: "_" + description: "Use _ for space" + operations: + - command: printf "%s" "{{ .string }}" | awk -v len={{ .length }} -v pad="$(printf "%s" "{{ .padding }}" | tr '_' ' ')" '{while (length($0) < len) {$0=pad $0}; printf "%s", $0}' + cleanup: + - "string" + - "length" + - "padding" + + - id: "string.padright" + inputs: + - id: "string" + required: true + - id: "length" + required: true + - id: "padding" + default: "_" + description: "Use _ for space" + operations: + - command: printf "%s" "{{ .string }}" | awk -v len={{ .length }} -v pad="$(printf "%s" "{{ .padding }}" | tr '_' ' ')" '{while (length($0) < len) {$0=$0 pad}; printf "%s", $0}' + cleanup: + - "string" + - "length" + - "padding" + + - id: "string.regex.match" + inputs: + - id: "string" + required: true + - id: "pattern" + required: true + operations: + - command: echo "{{ .string }}" | grep -o -E "{{ .pattern }}" || echo "" + output_format: "trim" + cleanup: + - "string" + - "pattern" diff --git a/recipes/utils/components/user.yaml b/recipes/utils/components/user.yaml new file mode 100644 index 0000000..cf632a6 --- /dev/null +++ b/recipes/utils/components/user.yaml @@ -0,0 +1,138 @@ +components: + - id: "user.home" + operations: + - command: echo $HOME + output_format: "trim" + + - id: "user.path.expand" + inputs: + - id: "path" + required: true + operations: + - command: | + path="{{ .path }}" + if [[ "$path" == "~"* ]]; then + echo "${path/#~/$HOME}" + else + echo "$path" + fi + cleanup: + - "path" + + - id: "user.shell" + operations: + - command: basename "$SHELL" + output_format: "trim" + + - id: "user.history.file" + operations: + - uses: "user.shell" + id: "shell" + silent: true + + - command: | + shell="{{ .shell }}" + case $shell in + "bash") + echo "~/.bash_history" + ;; + "fish") + echo "~/.local/share/fish/fish_history" + ;; + "zsh") + echo "~/.zsh_history" + ;; + esac + output_format: "trim" + cleanup: + - "shell" + + - id: "user.history.command" + operations: + - uses: "user.history.file" + id: "history_file" + + - uses: "user.shell" + id: "shell" + + - command: | + shell="{{ .shell }}" + case $shell in + "bash") + echo "cat {{ .history_file }}" + ;; + "fish") + echo "cat {{ .history_file }} | grep -o '\"cmd\": *\"[ ^\" ]*\"' | sed 's/\"cmd\": *\"//;s/\"$//'" + ;; + "zsh") + echo "sed -n 's/^: [ 0-9 ]*:[0-9]*;//p' {{ .history_file }}" + ;; + esac + output_format: "trim" + cleanup: + - "history_file" + - "shell" + + - id: "user.history.usage" + operations: + - uses: "user.history.command" + id: "command" + silent: true + + - command: eval "{{ .command }}" | awk '{print $1}' | sort | uniq -c | sort -nr | awk '{print $1" "$2}' + cleanup: + - "command" + + - id: "user.history.sensitive_commands" + operations: + - uses: "user.history.command" + id: "command" + silent: true + + - command: eval "{{ .command }}" | egrep -i "curl\b.*(-E|--cert)\b.*|curl\b.*--pass\b.*|curl\b.*(-U|--proxy-user).*:.*|curl\b.*(-u|--user).*:.*|.*(-H|--header).*(token|auth.*)|wget\b.*--.*password\b.*|http.?://.+:.+@.*" + id: "sensitive_commands" + silent: true + on_failure: ":" + cleanup: + - "command" + + - command: echo "{{ .sensitive_commands }}" + condition: .sensitive_commands != "" && .sensitive_commands != "false" + + - command: echo "false" + condition: .sensitive_commands == "" || .sensitive_commands == "false" + cleanup: + - "sensitive_commands" + + - id: "user.history.checkup" + operations: + - uses: "user.history.sensitive_commands" + id: "sensitive_commands" + silent: true + + - command: | + echo '{{ table + (list (color "red" (style "bold" "Potential Exposures"))) + .sensitive_commands + "rounded" + }}' + condition: .sensitive_commands != "false" + + - command: | + echo '{{ table + (list (color "green" (style "bold" "Potential Exposures"))) + "None found!" + "rounded" + }}' + condition: .sensitive_commands == "false" + + - id: "user.history.sterilize" + operations: + - uses: "user.history.file" + id: "history_file" + silent: true + + - command: grep -v -E "curl\b.*(-E|--cert)\b.*|curl\b.*--pass\b.*|curl\b.*(-U|--proxy-user).*:.*|curl\b.*(-u|--user).*:.*|curl\b.*(-H|--header).*[Aa]uth.*|curl\b.*(-H|--header).*[Tt]oken.*|wget\b.*--.*password\b.*|http.?://.+:.+@.*" {{ .history_file }} > ~/.sterilized_history.tmp && mv ~/.sterilized_history.tmp {{ .history_file }} + silent: true + cleanup: + - "history_file" diff --git a/recipes/utils/json.yaml b/recipes/utils/json.yaml new file mode 100644 index 0000000..d475882 --- /dev/null +++ b/recipes/utils/json.yaml @@ -0,0 +1,62 @@ +recipes: + - name: "json" + description: "A json utility to easily validate and format json" + category: "utils" + help: | + Validates and formats JSON data with optional JQ filtering. + + Usage: + shef utils json # Enter JSON in editor + shef utils json --file=PATH # Format JSON from file + shef utils json [FILTER] # Apply JQ filter (default: ".") + + Invalid JSON will display error messages in red. + vars: + filter: "." + options: "-C" + operations: + - id: "json_file" + command: echo "{{ .file }}" + silent: true + + - id: "jq_filter" + command: | + if [[ "{{ .input }}" != "false" ]]; then + echo "{{ .input }}" + else + echo "{{ .filter }}" + fi + silent: true + + - uses: "file.read" + id: "json_to_format" + with: + file: "{{ .json_file }}" + condition: .json_file != "false" + silent: true + + - prompts: + - name: "JSON Input" + id: "json_to_format" + type: "editor" + message: "Enter JSON" + condition: .json_file == "false" + + - uses: "json.validate" + id: "json_valid" + with: + json: "{{ .json_to_format }}" + silent: true + + - uses: "json.jq" + with: + json: "{{ .json_to_format }}" + filter: "{{ .jq_filter }}" + options: "{{ .options }}" + condition: .json_valid == "true" + + - command: echo "{{ color "red" .json_valid }}" + condition: .json_valid != "true" + + - command: echo "{{ color "red" .json_to_format }}" + condition: .json_valid != "true" diff --git a/recipes/utils/os.yaml b/recipes/utils/os.yaml index 097ab2e..0c99dc7 100644 --- a/recipes/utils/os.yaml +++ b/recipes/utils/os.yaml @@ -2,16 +2,22 @@ recipes: - name: "open" description: "Open an application" category: "os" + help: | + Opens a selected application from the system. + + Usage: + shef os open # Select from all applications + shef os open [FILTER] # Filter application list by name operations: - - uses: "app_select" + - uses: "os.app.select" with: - app_filter: "{{ .input }}" + filter: "{{ .input }}" id: "app" - command: echo {{ color "red" "No applications found" }} condition: .app == "false" - - uses: "open_app" + - uses: "os.app.open" with: app: "{{ .app }}" condition: .app != "false" diff --git a/recipes/utils/terminal.yaml b/recipes/utils/terminal.yaml index b6313cf..253eb80 100644 --- a/recipes/utils/terminal.yaml +++ b/recipes/utils/terminal.yaml @@ -1,23 +1,69 @@ recipes: - - name: "history" - description: "Get your ranked terminal command history" + - name: "usage" + description: "Get your ranked terminal command history usage" category: "terminal" + help: | + Displays your most frequently used terminal commands. + + Usage: + shef terminal usage # Show top 25 commands by usage count operations: - - uses: "get_complete_command_name_usage" - id: "history" + - uses: "user.history.usage" + id: "usage_history" silent: true - - name: "Format 25 Top Commands" + - command: echo "{{ .usage_history }}" | head -n 25 | awk '{print ""$1","$2""}' id: "top_25_commands" - command: echo '{{ .history }}' | head -n 25 | awk '{print ""$1","$2""}' - transform: '{{ (replace .output "\n" "\n,") }}' silent: true - - name: "table" - command: | + - command: | echo '{{ table - (list (style "bold" "Usage") (style "bold" "Command")) + (list "Usage" "Command") .top_25_commands "rounded" (list "right" "left") }}' + + - name: "checkup" + description: "Run a terminal history health checkup" + category: "terminal" + help: | + Scans your terminal history for potential credential exposures. + + Usage: + shef terminal checkup # Scan for sensitive information in history + operations: + - uses: "user.history.checkup" + + - name: "sterilize" + description: "Sterilize terminal history" + category: "terminal" + help: | + Removes credential exposures from your terminal history. + + Usage: + shef terminal sterilize # Remove sensitive information from history + operations: + - uses: "user.history.checkup" + + - condition: .sensitive_commands == "false" + exit: true + cleanup: + - "sensitive_commands" + + - prompts: + - type: "confirm" + id: "confirm" + message: "Sterilize your history?" + default: "true" + help_text: "This will permanently remove all the credential exposures from your history file" + + - command: echo "{{ color "yellow" "Aborted!" }}" + condition: .confirm == "false" + exit: true + + - uses: "user.history.sterilize" + condition: .confirm == "true" + silent: true + + - uses: "user.history.checkup" diff --git a/recipes/utils/time.yaml b/recipes/utils/time.yaml index aea3caa..30c5728 100644 --- a/recipes/utils/time.yaml +++ b/recipes/utils/time.yaml @@ -2,6 +2,11 @@ recipes: - name: "time" description: "A time utility displaying formatted time information" category: "utils" + help: | + Displays current local and UTC time in a formatted table. + + Usage: + shef utils time # Show current time information operations: - name: "Get Local Time" id: "local" @@ -16,9 +21,9 @@ recipes: - name: "Display times within a table" command: | echo '{{ table - (makeHeaders "Local Time" "UTC Time") + (makeHeaders "Local Time" "UTC Time") (list (makeRow (color "green" .local) (color "yellow" .utc)) ) - "rounded" + "rounded" }}'