├── .github └── workflows │ ├── compile-binaries-for-release.yaml │ ├── go-build.yaml │ └── go-test.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── docs └── installation.md ├── go.mod ├── go.sum ├── images ├── conditional.gif ├── hello-world.gif ├── progress.gif └── tasks.gif ├── internal ├── app.go ├── app_test.go ├── command.go ├── component.go ├── condition.go ├── consts.go ├── control_flow_common.go ├── control_flow_for.go ├── control_flow_foreach.go ├── control_flow_while.go ├── debug.go ├── dispatch.go ├── file_system.go ├── help.go ├── linux.go ├── list.go ├── progress.go ├── prompts.go ├── recipe.go ├── style.go ├── sync.go ├── table.go ├── templates.go ├── types.go └── utils.go ├── keys └── shef-binary-gpg-public-key.asc ├── main.go ├── recipes ├── 1password │ ├── components │ │ ├── items.yaml │ │ ├── password.yaml │ │ └── utils.yaml │ ├── op.yaml │ └── password.yaml ├── demo │ ├── arguments.yaml │ ├── background-tasks.yaml │ ├── color.yaml │ ├── components.yaml │ ├── conditional.yaml │ ├── flexible-tables.yaml │ ├── for.yaml │ ├── foreach.yaml │ ├── handlers.yaml │ ├── hello-world.yaml │ ├── monitor.yaml │ ├── operation-order.yaml │ ├── progress-mode.yaml │ ├── progress.yaml │ ├── prompts.yaml │ ├── tables.yaml │ ├── transform.yaml │ └── while.yaml ├── docker │ ├── components │ │ └── container.yaml │ ├── logs.yaml │ ├── prune.yaml │ └── shell.yaml ├── gcp │ ├── components │ │ └── project.yaml │ ├── project.yaml │ └── secret.yaml ├── git │ ├── lint.yaml │ ├── stash.yaml │ ├── update.yaml │ └── version.yaml └── utils │ ├── components │ ├── clipboard.yaml │ ├── convert.yaml │ ├── crypto.yaml │ ├── data.yaml │ ├── dir.yaml │ ├── file.yaml │ ├── generate.yaml │ ├── git.yaml │ ├── json.yaml │ ├── lint.yaml │ ├── list.yaml │ ├── math.yaml │ ├── mime.yaml │ ├── os.yaml │ ├── password.yaml │ ├── python.yaml │ ├── string.yaml │ └── user.yaml │ ├── json.yaml │ ├── os.yaml │ ├── terminal.yaml │ └── time.yaml └── testdata ├── background_mode_recipe.txtar ├── break_exit_recipe.txtar ├── category_filter.txtar ├── command_input_recipe.txtar ├── command_line_vars_recipe.txtar ├── complex_recipe.txtar ├── component_input_recipe.txtar ├── component_input_scope_recipe.txtar ├── component_recipe.txtar ├── condition_types_recipe.txtar ├── count_recipe.txtar ├── debug_mode.txtar ├── direct_recipe_file.txtar ├── duration.txtar ├── error_handling_recipe.txtar ├── filter_cut_recipe.txtar ├── foreach_recipe.txtar ├── help.txtar ├── help_test_recipe.txtar ├── input_recipe.txtar ├── json_output.txtar ├── list_empty.txtar ├── list_with_recipes.txtar ├── math_functions_recipe.txtar ├── multiple_recipe_files.txtar ├── nested_loops_recipe.txtar ├── op_reference_recipe.txtar ├── output_format_recipe.txtar ├── progress_mode_recipe.txtar ├── recipes ├── background_mode_recipe.yaml ├── break_exit_recipe.yaml ├── category_filter_recipe1.yaml ├── category_filter_recipe2.yaml ├── command_input_recipe.yaml ├── command_line_vars_recipe.yaml ├── complex_recipe.yaml ├── component_input_recipe.yaml ├── component_input_scope_recipe.yaml ├── component_recipe.yaml ├── condition_types_recipe.yaml ├── count_recipe.yaml ├── duration_recipe.yaml ├── error_handling_recipe.yaml ├── filter_cut_recipe.yaml ├── foreach_recipe.yaml ├── help_recipe.yaml ├── input_recipe.yaml ├── math_functions_recipe.yaml ├── nested_loops_recipe.yaml ├── op_reference_recipe.yaml ├── output_format_recipe.yaml ├── progress_mode_recipe.yaml ├── silent_recipe.yaml ├── simple_recipe.yaml ├── table_recipe.yaml ├── template_exec_recipe.yaml ├── transform_functions_recipe.yaml ├── vars_recipe.yaml ├── while_recipe.yaml └── workdir_recipe.yaml ├── silent_recipe.txtar ├── simple_recipe.txtar ├── table_recipe.txtar ├── template_exec_recipe.txtar ├── transform_functions_recipe.txtar ├── vars_recipe.txtar ├── version.txtar ├── which_command.txtar ├── while_recipe.txtar └── workdir_recipe.txtar /.github/workflows/compile-binaries-for-release.yaml: -------------------------------------------------------------------------------- 1 | name: Compile Binaries and Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | env: 9 | BINARY_FILE_NAME: shef 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | build-and-release: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v3 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v3 23 | with: 24 | go-version: '1.23.2' 25 | 26 | - name: Install dependencies 27 | run: go mod download 28 | 29 | - name: Build binaries 30 | run: | 31 | platforms=( 32 | "linux/amd64" 33 | "linux/arm64" 34 | "windows/amd64" 35 | "windows/arm64" 36 | "darwin/amd64" 37 | "darwin/arm64" 38 | ) 39 | 40 | for platform in "${platforms[@]}" 41 | do 42 | IFS="/" read -r GOOS GOARCH <<< "$platform" 43 | 44 | executable_name=$BINARY_FILE_NAME 45 | if [ "$GOOS" = "windows" ]; then 46 | executable_name="${BINARY_FILE_NAME}.exe" 47 | fi 48 | 49 | CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} go build -o "${executable_name}" . 50 | 51 | package_dir="${BINARY_FILE_NAME}_${GOOS}_${GOARCH}" 52 | mkdir "$package_dir" 53 | 54 | mv "${executable_name}" "$package_dir/" 55 | 56 | if [ "$GOOS" != "windows" ]; then 57 | chmod +x "$package_dir/${executable_name}" 58 | fi 59 | done 60 | 61 | - name: Archive binaries 62 | run: | 63 | for dir in ${BINARY_FILE_NAME}_* 64 | do 65 | if [[ -d $dir ]]; then 66 | archive_name="${dir}" 67 | if [[ $dir == *"windows"* ]]; then 68 | zip -r "${archive_name}.zip" "$dir" 69 | else 70 | tar czf "${archive_name}.tar.gz" "$dir" 71 | fi 72 | rm -rf "$dir" 73 | fi 74 | done 75 | 76 | - name: Archive recipes directory 77 | run: | 78 | if [ -d "./recipes" ]; then 79 | tar czf "recipes.tar.gz" "./recipes" 80 | else 81 | echo "Warning: recipes directory not found, skipping" 82 | fi 83 | 84 | - name: Generate checksums 85 | run: | 86 | echo "# SHA-256 Checksums for ${BINARY_FILE_NAME} ${GITHUB_REF_NAME}" > checksums.txt 87 | echo "# Generated on $(date)" >> checksums.txt 88 | echo "" >> checksums.txt 89 | 90 | for file in *.tar.gz 91 | do 92 | sha256sum "$file" >> checksums.txt 93 | done 94 | 95 | for file in *.zip 96 | do 97 | sha256sum "$file" >> checksums.txt 98 | done 99 | 100 | for file in *.tar.gz *.zip 101 | do 102 | sha256sum "$file" > "${file}.sha256" 103 | done 104 | 105 | cat checksums.txt 106 | 107 | - name: Import GPG Key 108 | run: | 109 | echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --batch --import 110 | env: 111 | GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} 112 | 113 | - name: Sign Files 114 | run: | 115 | for file in *.tar.gz *.zip checksums.txt 116 | do 117 | gpg --batch --yes --passphrase "${{ secrets.GPG_PASSPHRASE }}" --pinentry-mode loopback --armor --detach-sign "$file" 118 | done 119 | 120 | - name: Create GitHub Release 121 | uses: softprops/action-gh-release@v1 122 | with: 123 | tag_name: ${{ github.ref_name }} 124 | name: ${{ github.ref_name }} 125 | generate_release_notes: true 126 | files: | 127 | *.tar.gz 128 | *.zip 129 | *.asc 130 | checksums.txt 131 | *.sha256 132 | env: 133 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 134 | -------------------------------------------------------------------------------- /.github/workflows/go-build.yaml: -------------------------------------------------------------------------------- 1 | name: Shef Go Build Verify 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | push: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: '1.24' 22 | check-latest: true 23 | cache: true 24 | 25 | - name: Install dependencies 26 | run: | 27 | go mod download 28 | go mod tidy 29 | 30 | - name: Build 31 | run: go build -v ./... 32 | 33 | - name: Verify formatting 34 | run: | 35 | if [ -n "$(gofmt -l .)" ]; then 36 | echo "The following files are not formatted correctly:" 37 | gofmt -l . 38 | exit 1 39 | fi 40 | 41 | - name: Run go vet 42 | run: go vet ./... 43 | -------------------------------------------------------------------------------- /.github/workflows/go-test.yaml: -------------------------------------------------------------------------------- 1 | name: Shef Go Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | push: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | name: Run Tests 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: '1.24' 22 | check-latest: true 23 | cache: true 24 | 25 | - name: Install dependencies 26 | run: | 27 | go mod download 28 | go mod tidy 29 | go get github.com/agiledragon/gomonkey/v2 30 | 31 | - name: Build for tests 32 | run: | 33 | go build -v -o shef 34 | echo "$(pwd)" >> $GITHUB_PATH 35 | 36 | - name: Run tests 37 | run: go test -v ./... 38 | 39 | test-with-coverage: 40 | name: Test with Coverage 41 | runs-on: ubuntu-latest 42 | 43 | steps: 44 | - name: Checkout code 45 | uses: actions/checkout@v4 46 | 47 | - name: Set up Go 48 | uses: actions/setup-go@v5 49 | with: 50 | go-version: '1.24' 51 | check-latest: true 52 | cache: true 53 | 54 | - name: Install dependencies 55 | run: | 56 | go mod download 57 | go mod tidy 58 | go get github.com/agiledragon/gomonkey/v2 59 | 60 | - name: Build for tests 61 | run: | 62 | go build -v -o shef 63 | echo "$(pwd)" >> $GITHUB_PATH 64 | 65 | - name: Run tests with coverage 66 | run: go test -coverprofile=coverage.out ./... 67 | 68 | - name: Display coverage summary 69 | run: go tool cover -func=coverage.out 70 | 71 | - name: Generate coverage report 72 | run: go tool cover -html=coverage.out -o coverage.html 73 | 74 | - name: Upload coverage report 75 | uses: actions/upload-artifact@v4 76 | with: 77 | name: coverage-report 78 | path: coverage.html 79 | retention-days: 7 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Go workspace file 18 | go.work 19 | go.work.sum 20 | 21 | # env file 22 | .env 23 | 24 | # idea 25 | .idea 26 | /shef 27 | /.shef/tools/debug.yaml 28 | 29 | # Garbage 30 | .DS_Store 31 | .AppleDouble 32 | .LSOverride 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | 37 | # AI 38 | /CLAUDE.md 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Eduardo A. Garcia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install install-local update update-local test coverage 2 | 3 | install: 4 | go build -o shef 5 | @echo "Installing Shef to /usr/local/bin (may require sudo password)" 6 | @sudo mv shef /usr/local/bin/ || (echo "Failed to install. Try: sudo make install" && exit 1) 7 | @echo "Installation complete. Run 'shef -h' to verify." 8 | 9 | install-local: 10 | go build -o shef 11 | @mkdir -p $(HOME)/bin 12 | @mv shef $(HOME)/bin/ 13 | @echo "Installed to $(HOME)/bin/shef" 14 | @echo "Make sure $(HOME)/bin is in your PATH" 15 | @echo "Example: export PATH=\"\$$PATH:\$$HOME/bin\"" 16 | 17 | update: 18 | go build -o shef 19 | @echo "Updating Shef in /usr/local/bin (may require sudo password)" 20 | @sudo mv shef /usr/local/bin/ || (echo "Failed to update. Try: sudo make update" && exit 1) 21 | @echo "Update complete. Run 'shef -h' to verify." 22 | 23 | update-local: 24 | go build -o shef 25 | @mkdir -p $(HOME)/bin 26 | @mv shef $(HOME)/bin/ 27 | @echo "Updated to $(HOME)/bin/shef" 28 | @echo "Make sure $(HOME)/bin is in your PATH" 29 | @echo "Example: export PATH=\"\$PATH:\$HOME/bin\"" 30 | 31 | test: 32 | @go test ./internal/... 33 | 34 | test-verbose: 35 | @go test ./internal/... -v > test_output.tmp 36 | @grep -v "WORK=" test_output.tmp | \ 37 | grep -v "PATH=" | \ 38 | grep -v "GOTRACEBACK=" | \ 39 | grep -v "HOME=" | \ 40 | grep -v "TMPDIR=" | \ 41 | grep -v "devnull=" | \ 42 | grep -v "/=/" | \ 43 | grep -v ":=:" | \ 44 | grep -v '\$$=\$$' | \ 45 | grep -v "exe=" | \ 46 | grep -v "^[[:space:]]*>" 47 | @rm test_output.tmp 48 | 49 | test-coverage: 50 | @go test ./internal/... -coverprofile=coverage.out ../... 51 | @go tool cover -html=coverage.out 52 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | ### Homebrew 4 | 5 | ```bash 6 | brew tap eduardoagarcia/tap 7 | brew install shef 8 | 9 | shef -v 10 | ``` 11 | 12 | ### Binary 13 | 14 | For Linux and Windows, the simplest way to install Shef is to download the pre-built binary for your platform from 15 | the [latest release](https://github.com/eduardoagarcia/shef/releases/latest). 16 | 17 | #### Linux / macOS 18 | 19 | ```bash 20 | # Download the appropriate tarball for your platform 21 | curl -L https://github.com/eduardoagarcia/shef/releases/latest/download/shef_[PLATFORM]_[ARCH].tar.gz -o shef.tar.gz 22 | 23 | # Extract the binary 24 | tar -xzf shef.tar.gz 25 | 26 | # Navigate to the extracted directory 27 | cd shef_[PLATFORM]_[ARCH] 28 | 29 | # Make the binary executable 30 | chmod +x shef 31 | 32 | # Move the binary to your PATH 33 | sudo mv shef /usr/local/bin/ 34 | 35 | # Verify installation 36 | shef -v 37 | 38 | # Sync public recipes 39 | shef sync 40 | ``` 41 | 42 | Replace `[PLATFORM]` with `linux` or `darwin` (for macOS) and `[ARCH]` with your architecture (`amd64`, `arm64`). 43 | 44 | #### Windows 45 | 46 | 1. Download the appropriate Windows ZIP file (`shef_windows_amd64.zip` or `shef_windows_arm64.zip`) from 47 | the [releases page](https://github.com/eduardoagarcia/shef/releases/latest) 48 | 2. Extract the archive using Windows Explorer, 7-Zip, WinRAR, or similar tool 49 | 3. Move the extracted executable to a directory in your PATH 50 | 4. Open Command Prompt or PowerShell and run `shef -v` to verify installation 51 | 5. Run `shef sync` to download recipes 52 | 53 | ### Verify Binaries 54 | 55 | > [!IMPORTANT] 56 | > Always verify the integrity of downloaded binaries for security. The release assets are signed with GPG, and you can 57 | > verify them 58 | using [the public key found in the repository](https://raw.githubusercontent.com/eduardoagarcia/shef/main/keys/shef-binary-gpg-public-key.asc). 59 | 60 | #### Linux / macOS 61 | 62 | ```bash 63 | # Import the GPG public key 64 | curl -L https://raw.githubusercontent.com/eduardoagarcia/shef/main/keys/shef-binary-gpg-public-key.asc | gpg --import 65 | 66 | # Download the signature file 67 | curl -L https://github.com/eduardoagarcia/shef/releases/latest/download/shef_[OS]_[ARCH].tar.gz.asc -o shef.tar.gz.asc 68 | 69 | # Verify the tarball 70 | gpg --verify shef.tar.gz.asc shef.tar.gz 71 | ``` 72 | 73 | #### Windows 74 | 75 | ```bash 76 | # Import the GPG public key (requires GPG for Windows) 77 | Invoke-WebRequest -Uri "https://raw.githubusercontent.com/eduardoagarcia/shef/main/keys/shef-binary-gpg-public-key.asc" -OutFile "shef-key.asc" 78 | gpg --import shef-key.asc 79 | 80 | # Download the signature file 81 | Invoke-WebRequest -Uri "https://github.com/eduardoagarcia/shef/releases/latest/download/shef_windows_amd64.zip.asc" -OutFile "shef_windows_amd64.zip.asc" 82 | 83 | # Verify the ZIP file 84 | gpg --verify shef_windows_amd64.zip.asc shef_windows_amd64.zip 85 | ``` 86 | 87 | ### Package Managers 88 | 89 | > [!NOTE] 90 | > Future package manager support: 91 | > - APT (Debian/Ubuntu) 92 | > - YUM/DNF (RHEL/Fedora) 93 | > - Arch User Repository (AUR) 94 | > - Chocolatey/Scoop (Windows) 95 | 96 | ### Manual Installation 97 | 98 | For developers or users who prefer to build from source: 99 | 100 | ```bash 101 | git clone https://github.com/eduardoagarcia/shef.git 102 | cd shef 103 | 104 | # Install (requires sudo for system-wide installation) 105 | make install 106 | 107 | shef -v 108 | ``` 109 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/eduardoagarcia/shef 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.3.7 7 | github.com/agiledragon/gomonkey/v2 v2.13.0 8 | github.com/agnivade/levenshtein v1.2.1 9 | github.com/google/uuid v1.6.0 10 | github.com/jedib0t/go-pretty/v6 v6.6.7 11 | github.com/rogpeppe/go-internal v1.14.1 12 | github.com/schollz/progressbar/v3 v3.18.0 13 | github.com/stretchr/testify v1.10.0 14 | github.com/urfave/cli/v2 v2.27.6 15 | gopkg.in/yaml.v3 v3.0.1 16 | ) 17 | 18 | require ( 19 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 20 | github.com/creack/pty v1.1.24 // indirect 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 23 | github.com/mattn/go-colorable v0.1.2 // indirect 24 | github.com/mattn/go-isatty v0.0.20 // indirect 25 | github.com/mattn/go-runewidth v0.0.16 // indirect 26 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 27 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 28 | github.com/pmezard/go-difflib v1.0.0 // indirect 29 | github.com/rivo/uniseg v0.4.7 // indirect 30 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 31 | github.com/stretchr/objx v0.5.2 // indirect 32 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 33 | golang.org/x/sys v0.31.0 // indirect 34 | golang.org/x/term v0.30.0 // indirect 35 | golang.org/x/text v0.22.0 // indirect 36 | golang.org/x/tools v0.26.0 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /images/conditional.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduardoagarcia/shef/6dfd1017484f3689a860da60257261d70c3f29e0/images/conditional.gif -------------------------------------------------------------------------------- /images/hello-world.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduardoagarcia/shef/6dfd1017484f3689a860da60257261d70c3f29e0/images/hello-world.gif -------------------------------------------------------------------------------- /images/progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduardoagarcia/shef/6dfd1017484f3689a860da60257261d70c3f29e0/images/progress.gif -------------------------------------------------------------------------------- /images/tasks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduardoagarcia/shef/6dfd1017484f3689a860da60257261d70c3f29e0/images/tasks.gif -------------------------------------------------------------------------------- /internal/consts.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | const ( 4 | ExecShell = "sh" 5 | ExitPrompt = "Exit" 6 | GithubRepo = "https://github.com/eduardoagarcia/shef" 7 | PublicRecipesFilename = "recipes.tar.gz" 8 | PublicRecipesFolder = "recipes" 9 | Version = "v0.3.3" 10 | ) 11 | -------------------------------------------------------------------------------- /internal/control_flow_common.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // executeLoopOperations runs all operations for a single iteration. 8 | func executeLoopOperations(operations []Operation, ctx *ExecutionContext, depth int, 9 | executeOp func(Operation, int) (bool, error)) (exit bool, breakLoop bool) { 10 | 11 | for _, subOp := range operations { 12 | if !shouldRunOperation(subOp, ctx) { 13 | continue 14 | } 15 | 16 | shouldExit, err := executeOp(subOp, depth+1) 17 | if err != nil { 18 | return shouldExit, false 19 | } 20 | 21 | if shouldExit || subOp.Exit { 22 | Log(CategoryControlFlow, fmt.Sprintf("Exiting entire recipe due to exit flag in '%s'", subOp.Name)) 23 | return true, false 24 | } 25 | 26 | if subOp.Break { 27 | Log(CategoryControlFlow, fmt.Sprintf("Breaking out of loop due to break flag in '%s'", subOp.Name)) 28 | return false, true 29 | } 30 | } 31 | 32 | return false, false 33 | } 34 | 35 | // setupProgressMode configures progress mode for control flow execution. 36 | func setupProgressMode(ctx *ExecutionContext, useProgressMode bool) (originalMode bool) { 37 | originalMode = ctx.ProgressMode 38 | if useProgressMode { 39 | ctx.ProgressMode = true 40 | } 41 | return originalMode 42 | } 43 | 44 | // cleanupLoopState removes loop variables and sets operation result. 45 | func cleanupLoopState(ctx *ExecutionContext, opID string, varName string) { 46 | delete(ctx.Vars, varName) 47 | delete(ctx.Vars, "iteration") 48 | 49 | if opID != "" { 50 | ctx.OperationResults[opID] = true 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/control_flow_for.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | // ForFlow defines the structure for a for loop control flow 9 | type ForFlow struct { 10 | Type string `yaml:"type"` 11 | Count string `yaml:"count"` 12 | Variable string `yaml:"variable"` 13 | ProgressMode bool `yaml:"progress_mode,omitempty"` 14 | ProgressBar bool `yaml:"progress_bar,omitempty"` 15 | ProgressBarOpts *ProgressBarOptions `yaml:"progress_bar_options,omitempty"` 16 | } 17 | 18 | // GetForFlow extracts for loop configuration from an operation 19 | func (op *Operation) GetForFlow() (*ForFlow, error) { 20 | if op.ControlFlow == nil { 21 | return nil, fmt.Errorf("operation does not have control_flow") 22 | } 23 | 24 | flowMap, ok := op.ControlFlow.(map[string]interface{}) 25 | if !ok { 26 | return nil, fmt.Errorf("invalid control_flow structure") 27 | } 28 | 29 | typeVal, ok := flowMap["type"].(string) 30 | if !ok || typeVal != "for" { 31 | return nil, fmt.Errorf("not a for control flow") 32 | } 33 | 34 | countVal, ok := flowMap["count"] 35 | if !ok { 36 | return nil, fmt.Errorf("for requires a 'count' field") 37 | } 38 | count := fmt.Sprintf("%v", countVal) 39 | 40 | variable, ok := flowMap["variable"].(string) 41 | if !ok || variable == "" { 42 | variable = "i" 43 | } 44 | 45 | progressMode, _ := flowMap["progress_mode"].(bool) 46 | progressBar, _ := flowMap["progress_bar"].(bool) 47 | 48 | var progressBarOpts *ProgressBarOptions 49 | if optsVal, ok := flowMap["progress_bar_options"]; ok { 50 | if optsMap, ok := optsVal.(map[string]interface{}); ok { 51 | progressBarOpts = ParseProgressBarOptions(optsMap) 52 | } 53 | } 54 | 55 | return &ForFlow{ 56 | Type: "for", 57 | Count: count, 58 | Variable: variable, 59 | ProgressMode: progressMode, 60 | ProgressBar: progressBar, 61 | ProgressBarOpts: progressBarOpts, 62 | }, nil 63 | } 64 | 65 | // ExecuteFor runs a for loop with the given parameters 66 | func ExecuteFor(op Operation, forFlow *ForFlow, ctx *ExecutionContext, depth int, executeOp func(Operation, int) (bool, error)) (bool, error) { 67 | loopCtx := ctx.pushLoopContext("for", depth) 68 | defer ctx.popLoopContext() 69 | 70 | originalMode := setupProgressMode(ctx, forFlow.ProgressMode) 71 | defer func() { 72 | ctx.ProgressMode = originalMode 73 | if forFlow.ProgressMode && !forFlow.ProgressBar { 74 | fmt.Println() 75 | } 76 | }() 77 | 78 | count, err := getIterationCount(forFlow, ctx) 79 | if err != nil { 80 | return false, err 81 | } 82 | 83 | Log(CategoryLoop, fmt.Sprintf("For loop with %d iterations", count)) 84 | 85 | var progressBar *ProgressBar 86 | if forFlow.ProgressBar { 87 | description := "" 88 | if forFlow.ProgressBarOpts != nil && forFlow.ProgressBarOpts.Description != "" { 89 | description = forFlow.ProgressBarOpts.Description 90 | } 91 | progressBar = CreateProgressBar(count, description, forFlow.ProgressBarOpts) 92 | } 93 | 94 | for i := 0; i < count; i++ { 95 | ctx.updateLoopDuration() 96 | ctx.Vars[forFlow.Variable] = i 97 | ctx.Vars["iteration"] = i + 1 98 | 99 | LogLoopIteration("for", i+1, count, map[string]interface{}{ 100 | "variable": forFlow.Variable, 101 | "value": i, 102 | "duration": formatDuration(loopCtx.Duration), 103 | }) 104 | 105 | if progressBar != nil && forFlow.ProgressBarOpts != nil && forFlow.ProgressBarOpts.MessageTemplate != "" { 106 | rendered, err := renderTemplate(forFlow.ProgressBarOpts.MessageTemplate, ctx.templateVars()) 107 | if err == nil { 108 | progressBar.Update(rendered) 109 | } 110 | } 111 | 112 | exit, breakLoop := executeLoopOperations(op.Operations, ctx, depth, executeOp) 113 | 114 | if progressBar != nil { 115 | progressBar.Increment() 116 | } 117 | 118 | if exit { 119 | if progressBar != nil { 120 | progressBar.Complete() 121 | } 122 | return true, nil 123 | } 124 | if breakLoop { 125 | break 126 | } 127 | } 128 | 129 | if progressBar != nil { 130 | progressBar.Complete() 131 | } 132 | 133 | ctx.updateLoopDuration() 134 | cleanupLoopState(ctx, op.ID, forFlow.Variable) 135 | 136 | return false, nil 137 | } 138 | 139 | // getIterationCount resolves the number of iterations for a for loop 140 | func getIterationCount(forFlow *ForFlow, ctx *ExecutionContext) (int, error) { 141 | countStr, err := renderTemplate(forFlow.Count, ctx.templateVars()) 142 | if err != nil { 143 | return 0, fmt.Errorf("failed to render count template: %w", err) 144 | } 145 | 146 | count, err := strconv.Atoi(countStr) 147 | if err != nil { 148 | return 0, fmt.Errorf("invalid count value after rendering: %s", countStr) 149 | } 150 | 151 | return count, nil 152 | } 153 | -------------------------------------------------------------------------------- /internal/control_flow_foreach.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ForEachFlow defines the structure for a foreach loop control flow 8 | type ForEachFlow struct { 9 | Type string `yaml:"type"` 10 | Collection string `yaml:"collection"` 11 | As string `yaml:"as"` 12 | ProgressMode bool `yaml:"progress_mode,omitempty"` 13 | ProgressBar bool `yaml:"progress_bar,omitempty"` 14 | ProgressBarOpts *ProgressBarOptions `yaml:"progress_bar_options,omitempty"` 15 | } 16 | 17 | // GetType returns the control flow type 18 | func (f *ForEachFlow) GetType() string { 19 | return f.Type 20 | } 21 | 22 | // GetForEachFlow extracts foreach loop configuration from an operation 23 | func (op *Operation) GetForEachFlow() (*ForEachFlow, error) { 24 | if op.ControlFlow == nil { 25 | return nil, fmt.Errorf("operation does not have control_flow") 26 | } 27 | 28 | flowMap, ok := op.ControlFlow.(map[string]interface{}) 29 | if !ok { 30 | return nil, fmt.Errorf("invalid control_flow structure") 31 | } 32 | 33 | typeVal, ok := flowMap["type"].(string) 34 | if !ok || typeVal != "foreach" { 35 | return nil, fmt.Errorf("not a foreach control flow") 36 | } 37 | 38 | collection, ok := flowMap["collection"].(string) 39 | if !ok { 40 | return nil, fmt.Errorf("foreach requires a 'collection' field") 41 | } 42 | 43 | as, ok := flowMap["as"].(string) 44 | if !ok { 45 | return nil, fmt.Errorf("foreach requires an 'as' field") 46 | } 47 | 48 | progressMode, _ := flowMap["progress_mode"].(bool) 49 | progressBar, _ := flowMap["progress_bar"].(bool) 50 | 51 | var progressBarOpts *ProgressBarOptions 52 | if optsVal, ok := flowMap["progress_bar_options"]; ok { 53 | if optsMap, ok := optsVal.(map[string]interface{}); ok { 54 | progressBarOpts = ParseProgressBarOptions(optsMap) 55 | } 56 | } 57 | 58 | return &ForEachFlow{ 59 | Type: "foreach", 60 | Collection: collection, 61 | As: as, 62 | ProgressMode: progressMode, 63 | ProgressBar: progressBar, 64 | ProgressBarOpts: progressBarOpts, 65 | }, nil 66 | } 67 | 68 | // ExecuteForEach runs a foreach loop with the given parameters 69 | func ExecuteForEach(op Operation, forEach *ForEachFlow, ctx *ExecutionContext, depth int, executeOp func(Operation, int) (bool, error)) (bool, error) { 70 | loopCtx := ctx.pushLoopContext("foreach", depth) 71 | defer ctx.popLoopContext() 72 | 73 | originalMode := setupProgressMode(ctx, forEach.ProgressMode) 74 | defer func() { 75 | ctx.ProgressMode = originalMode 76 | if forEach.ProgressMode && !forEach.ProgressBar { 77 | fmt.Println() 78 | } 79 | }() 80 | 81 | collectionExpr, err := renderTemplate(forEach.Collection, ctx.templateVars()) 82 | if err != nil { 83 | return false, fmt.Errorf("failed to render collection template: %w", err) 84 | } 85 | 86 | items := parseOptionsFromOutput(collectionExpr) 87 | 88 | Log(CategoryLoop, fmt.Sprintf("Foreach loop over %d items", len(items))) 89 | 90 | var progressBar *ProgressBar 91 | if forEach.ProgressBar { 92 | description := "" 93 | if forEach.ProgressBarOpts != nil && forEach.ProgressBarOpts.Description != "" { 94 | description = forEach.ProgressBarOpts.Description 95 | } 96 | progressBar = CreateProgressBar(len(items), description, forEach.ProgressBarOpts) 97 | } 98 | 99 | for idx, item := range items { 100 | ctx.updateLoopDuration() 101 | ctx.Vars[forEach.As] = item 102 | ctx.Vars["iteration"] = idx + 1 103 | 104 | LogLoopIteration("foreach", idx+1, len(items), map[string]interface{}{ 105 | "variable": forEach.As, 106 | "value": item, 107 | "duration": formatDuration(loopCtx.Duration), 108 | }) 109 | 110 | if progressBar != nil && forEach.ProgressBarOpts != nil && forEach.ProgressBarOpts.MessageTemplate != "" { 111 | rendered, err := renderTemplate(forEach.ProgressBarOpts.MessageTemplate, ctx.templateVars()) 112 | if err == nil { 113 | progressBar.Update(rendered) 114 | } 115 | } 116 | 117 | exit, breakLoop := executeLoopOperations(op.Operations, ctx, depth, executeOp) 118 | 119 | if progressBar != nil { 120 | progressBar.Increment() 121 | } 122 | 123 | if exit { 124 | if progressBar != nil { 125 | progressBar.Complete() 126 | } 127 | return true, nil 128 | } 129 | if breakLoop { 130 | break 131 | } 132 | } 133 | 134 | if progressBar != nil { 135 | progressBar.Complete() 136 | } 137 | 138 | ctx.updateLoopDuration() 139 | cleanupLoopState(ctx, op.ID, forEach.As) 140 | 141 | return false, nil 142 | } 143 | -------------------------------------------------------------------------------- /internal/control_flow_while.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // WhileFlow defines the structure for a while loop control flow 8 | type WhileFlow struct { 9 | Type string `yaml:"type"` 10 | Condition string `yaml:"condition"` 11 | ProgressMode bool `yaml:"progress_mode,omitempty"` 12 | } 13 | 14 | // GetType returns the control flow type 15 | func (w *WhileFlow) GetType() string { 16 | return w.Type 17 | } 18 | 19 | // GetWhileFlow extracts while loop configuration from an operation 20 | func (op *Operation) GetWhileFlow() (*WhileFlow, error) { 21 | if op.ControlFlow == nil { 22 | return nil, fmt.Errorf("operation does not have control_flow") 23 | } 24 | 25 | flowMap, ok := op.ControlFlow.(map[string]interface{}) 26 | if !ok { 27 | return nil, fmt.Errorf("invalid control_flow structure") 28 | } 29 | 30 | typeVal, ok := flowMap["type"].(string) 31 | if !ok || typeVal != "while" { 32 | return nil, fmt.Errorf("not a while control flow") 33 | } 34 | 35 | condition, ok := flowMap["condition"].(string) 36 | if !ok { 37 | return nil, fmt.Errorf("while requires a 'condition' field") 38 | } 39 | 40 | progressMode, _ := flowMap["progress_mode"].(bool) 41 | 42 | return &WhileFlow{ 43 | Type: "while", 44 | Condition: condition, 45 | ProgressMode: progressMode, 46 | }, nil 47 | } 48 | 49 | // ExecuteWhile runs a while loop with the given parameters 50 | func ExecuteWhile(op Operation, whileFlow *WhileFlow, ctx *ExecutionContext, depth int, executeOp func(Operation, int) (bool, error)) (bool, error) { 51 | loopCtx := ctx.pushLoopContext("while", depth) 52 | defer ctx.popLoopContext() 53 | 54 | originalMode := setupProgressMode(ctx, whileFlow.ProgressMode) 55 | defer func() { 56 | ctx.ProgressMode = originalMode 57 | if whileFlow.ProgressMode { 58 | fmt.Println() 59 | } 60 | }() 61 | 62 | iterations := 0 63 | 64 | for { 65 | ctx.updateLoopDuration() 66 | 67 | shouldContinue, err := evaluateWhileCondition(whileFlow.Condition, ctx) 68 | if err != nil { 69 | return false, err 70 | } 71 | 72 | if !shouldContinue { 73 | break 74 | } 75 | 76 | iterations++ 77 | ctx.Vars["iteration"] = iterations 78 | 79 | LogLoopIteration("while", iterations, -1, map[string]interface{}{ 80 | "condition": whileFlow.Condition, 81 | "duration": formatDuration(loopCtx.Duration), 82 | }) 83 | 84 | exit, breakLoop := executeLoopOperations(op.Operations, ctx, depth, executeOp) 85 | if exit { 86 | return true, nil 87 | } 88 | if breakLoop { 89 | break 90 | } 91 | } 92 | 93 | ctx.updateLoopDuration() 94 | cleanupLoopState(ctx, op.ID, "") 95 | 96 | return false, nil 97 | } 98 | 99 | // evaluateWhileCondition renders and evaluates the while loop condition 100 | func evaluateWhileCondition(condition string, ctx *ExecutionContext) (bool, error) { 101 | renderedCondition, err := renderTemplate(condition, ctx.templateVars()) 102 | if err != nil { 103 | return false, fmt.Errorf("failed to render while condition template: %w", err) 104 | } 105 | 106 | conditionResult, err := evaluateCondition(renderedCondition, ctx) 107 | if err != nil { 108 | return false, fmt.Errorf("failed to evaluate while condition '%s': %w", renderedCondition, err) 109 | } 110 | 111 | return conditionResult, nil 112 | } 113 | -------------------------------------------------------------------------------- /internal/help.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // displayRecipeHelp renders formatted help information for a given recipe 9 | func displayRecipeHelp(recipe *Recipe) { 10 | displayNameSection(recipe) 11 | 12 | if recipe.Category != "" { 13 | displayCategorySection(recipe) 14 | } 15 | 16 | if recipe.Author != "" { 17 | displayAuthorSection(recipe) 18 | } 19 | 20 | displayUsageSection(recipe) 21 | displayOverviewSection(recipe) 22 | 23 | fmt.Println("") 24 | } 25 | 26 | // displayNameSection formats and displays the recipe name and description 27 | func displayNameSection(recipe *Recipe) { 28 | name := strings.ToLower(recipe.Name) 29 | description := strings.ToLower(recipe.Description) 30 | 31 | fmt.Printf("%s:\n %s - %s\n", "NAME", name, description) 32 | } 33 | 34 | // displayCategorySection formats and displays the recipe category 35 | func displayCategorySection(recipe *Recipe) { 36 | category := strings.ToLower(recipe.Category) 37 | fmt.Printf("\n%s:\n %s\n", "CATEGORY", category) 38 | } 39 | 40 | // displayAuthorSection formats and displays the recipe author 41 | func displayAuthorSection(recipe *Recipe) { 42 | fmt.Printf("\n%s:\n %s\n", "AUTHOR", recipe.Author) 43 | } 44 | 45 | // displayUsageSection formats and displays usage examples for the recipe 46 | func displayUsageSection(recipe *Recipe) { 47 | name := strings.ToLower(recipe.Name) 48 | 49 | fmt.Printf("\n%s:\n shef %s [input] [options]\n", "USAGE", name) 50 | 51 | if recipe.Category != "" { 52 | category := strings.ToLower(recipe.Category) 53 | fmt.Printf(" shef %s %s [input] [options]\n", category, name) 54 | } 55 | } 56 | 57 | // displayOverviewSection formats and displays the recipe's detailed help information 58 | func displayOverviewSection(recipe *Recipe) { 59 | fmt.Printf("\n%s:\n", "OVERVIEW") 60 | 61 | if recipe.Help != "" { 62 | indentedText := indentText(recipe.Help, 4) 63 | fmt.Printf("%s\n", indentedText) 64 | } else { 65 | fmt.Printf(" %s\n", "No detailed help available for this recipe.") 66 | } 67 | } 68 | 69 | // indentText indents each non-empty line in the given text by a specified number of spaces 70 | func indentText(text string, spaces int) string { 71 | indent := strings.Repeat(" ", spaces) 72 | lines := strings.Split(text, "\n") 73 | 74 | for i, line := range lines { 75 | if line != "" { 76 | lines[i] = indent + line 77 | } 78 | } 79 | 80 | return strings.Join(lines, "\n") 81 | } 82 | -------------------------------------------------------------------------------- /internal/linux.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | ) 8 | 9 | // addXDGRecipeSources adds recipe files from XDG directories to the sources list 10 | func addXDGRecipeSources(sources []string, userDir, publicRepo bool, findYamlFiles func(string) ([]string, error)) []string { 11 | if userDir { 12 | sources = appendXDGUserRecipes(sources, findYamlFiles) 13 | } 14 | 15 | if publicRepo { 16 | sources = appendXDGPublicRecipes(sources, findYamlFiles) 17 | } 18 | 19 | return sources 20 | } 21 | 22 | // appendXDGUserRecipes adds user recipe files from XDG_CONFIG_HOME/shef/user 23 | func appendXDGUserRecipes(sources []string, findYamlFiles func(string) ([]string, error)) []string { 24 | xdgUserRoot := filepath.Join(getXDGConfigHome(), "shef", "user") 25 | return appendRecipesIfDirExists(sources, xdgUserRoot, findYamlFiles) 26 | } 27 | 28 | // appendXDGPublicRecipes adds public recipe files from XDG_DATA_HOME/shef/public 29 | func appendXDGPublicRecipes(sources []string, findYamlFiles func(string) ([]string, error)) []string { 30 | xdgPublicRoot := filepath.Join(getXDGDataHome(), "shef", "public") 31 | return appendRecipesIfDirExists(sources, xdgPublicRoot, findYamlFiles) 32 | } 33 | 34 | // appendRecipesIfDirExists adds recipe files if the directory exists 35 | func appendRecipesIfDirExists(sources []string, dirPath string, findYamlFiles func(string) ([]string, error)) []string { 36 | if _, err := os.Stat(dirPath); err == nil { 37 | if files, err := findYamlFiles(dirPath); err == nil { 38 | return append(sources, files...) 39 | } 40 | } 41 | return sources 42 | } 43 | 44 | // getXDGConfigHome returns the XDG_CONFIG_HOME directory path 45 | func getXDGConfigHome() string { 46 | configHome := os.Getenv("XDG_CONFIG_HOME") 47 | if configHome == "" { 48 | homeDir, err := os.UserHomeDir() 49 | if err != nil { 50 | return "" 51 | } 52 | return filepath.Join(homeDir, ".config") 53 | } 54 | return configHome 55 | } 56 | 57 | // getXDGDataHome returns the XDG_DATA_HOME directory path 58 | func getXDGDataHome() string { 59 | dataHome := os.Getenv("XDG_DATA_HOME") 60 | if dataHome == "" { 61 | homeDir, err := os.UserHomeDir() 62 | if err != nil { 63 | return "" 64 | } 65 | return filepath.Join(homeDir, ".local", "share") 66 | } 67 | return dataHome 68 | } 69 | 70 | // isLinux determines if the current operating system is Linux 71 | func isLinux() bool { 72 | return runtime.GOOS == "linux" 73 | } 74 | -------------------------------------------------------------------------------- /internal/list.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | // recipeInfo represents recipe data for JSON output 13 | type recipeInfo struct { 14 | Name string `json:"name"` 15 | Description string `json:"description,omitempty"` 16 | Category string `json:"category,omitempty"` 17 | Author string `json:"author,omitempty"` 18 | } 19 | 20 | // handleListCommand processes the list command and displays recipes 21 | func handleListCommand(c *cli.Context, args []string, sourcePriority []string) error { 22 | category := determineCategory(c, args) 23 | sourceFlags := determineSourceFlags(c) 24 | 25 | recipes := collectRecipes(sourcePriority, sourceFlags, category) 26 | 27 | if category == "" { 28 | recipes = filterDemoRecipes(recipes) 29 | } 30 | 31 | if len(recipes) == 0 { 32 | return handleEmptyResults(c) 33 | } 34 | 35 | if c.Bool("json") { 36 | return outputRecipesAsJSON(recipes) 37 | } 38 | 39 | listRecipes(recipes) 40 | return nil 41 | } 42 | 43 | // determineCategory extracts the category from flags or arguments 44 | func determineCategory(c *cli.Context, args []string) string { 45 | category := c.String("category") 46 | if category == "" && len(args) >= 1 { 47 | category = args[0] 48 | } 49 | return category 50 | } 51 | 52 | // determineSourceFlags checks which recipe sources should be used 53 | func determineSourceFlags(c *cli.Context) map[string]bool { 54 | useLocal := c.Bool("local") || c.Bool("L") 55 | useUser := c.Bool("user") || c.Bool("U") 56 | usePublic := c.Bool("public") || c.Bool("P") 57 | 58 | // If no flags specified, use all sources 59 | if !useLocal && !useUser && !usePublic { 60 | useLocal, useUser, usePublic = true, true, true 61 | } 62 | 63 | return map[string]bool{ 64 | "local": useLocal, 65 | "user": useUser, 66 | "public": usePublic, 67 | } 68 | } 69 | 70 | // collectRecipes gathers recipes from all specified sources 71 | func collectRecipes(sourcePriority []string, sourceFlags map[string]bool, category string) []Recipe { 72 | var allRecipes []Recipe 73 | recipeMap := make(map[string]bool) 74 | 75 | for _, source := range sourcePriority { 76 | if !sourceFlags[source] { 77 | continue 78 | } 79 | 80 | sources, _ := findRecipeSourcesByType(source == "local", source == "user", source == "public") 81 | recipes, _ := loadRecipes(sources, category) 82 | 83 | for _, r := range recipes { 84 | if !recipeMap[r.Name] { 85 | allRecipes = append(allRecipes, r) 86 | recipeMap[r.Name] = true 87 | } 88 | } 89 | } 90 | 91 | return allRecipes 92 | } 93 | 94 | // filterDemoRecipes removes recipes with the "demo" category 95 | func filterDemoRecipes(recipes []Recipe) []Recipe { 96 | var filtered []Recipe 97 | for _, recipe := range recipes { 98 | if recipe.Category != "demo" { 99 | filtered = append(filtered, recipe) 100 | } 101 | } 102 | return filtered 103 | } 104 | 105 | // handleEmptyResults returns appropriate response when no recipes are found 106 | func handleEmptyResults(c *cli.Context) error { 107 | if c.Bool("json") { 108 | fmt.Println("[]") 109 | } else { 110 | fmt.Println("No recipes found.") 111 | } 112 | return nil 113 | } 114 | 115 | // listRecipes displays recipes grouped by category in a formatted text output 116 | func listRecipes(recipes []Recipe) { 117 | if len(recipes) == 0 { 118 | fmt.Println(FormatText("No recipes found.", ColorYellow, StyleNone)) 119 | return 120 | } 121 | 122 | fmt.Println("\nAvailable recipes:") 123 | 124 | categories := groupRecipesByCategory(recipes) 125 | categoryNames := getSortedCategoryNames(categories) 126 | 127 | for _, category := range categoryNames { 128 | printCategoryHeader(category) 129 | printCategoryRecipes(categories[category]) 130 | } 131 | 132 | fmt.Printf("\n\n") 133 | } 134 | 135 | // groupRecipesByCategory organizes recipes into a map keyed by category 136 | func groupRecipesByCategory(recipes []Recipe) map[string][]Recipe { 137 | categories := make(map[string][]Recipe) 138 | for _, recipe := range recipes { 139 | cat := recipe.Category 140 | if cat == "" { 141 | cat = "uncategorized" 142 | } 143 | categories[cat] = append(categories[cat], recipe) 144 | } 145 | return categories 146 | } 147 | 148 | // getSortedCategoryNames returns category names in alphabetical order 149 | func getSortedCategoryNames(categories map[string][]Recipe) []string { 150 | var names []string 151 | for category := range categories { 152 | names = append(names, category) 153 | } 154 | sort.Strings(names) 155 | return names 156 | } 157 | 158 | // printCategoryHeader displays the formatted category name 159 | func printCategoryHeader(category string) { 160 | fmt.Printf( 161 | "\n %s%s%s\n", 162 | FormatText("[", ColorNone, StyleDim), 163 | FormatText(strings.ToLower(category), ColorMagenta, StyleNone), 164 | FormatText("]", ColorNone, StyleDim), 165 | ) 166 | } 167 | 168 | // printCategoryRecipes displays all recipes within a category 169 | func printCategoryRecipes(recipes []Recipe) { 170 | sort.Slice(recipes, func(i, j int) bool { 171 | return recipes[i].Name < recipes[j].Name 172 | }) 173 | 174 | for _, recipe := range recipes { 175 | fmt.Printf( 176 | " %s %s: %s\n", 177 | FormatText("•", ColorNone, StyleDim), 178 | FormatText(strings.ToLower(recipe.Name), ColorGreen, StyleBold), 179 | strings.ToLower(recipe.Description), 180 | ) 181 | } 182 | } 183 | 184 | // outputRecipesAsJSON formats and outputs recipes as JSON 185 | func outputRecipesAsJSON(recipes []Recipe) error { 186 | result := make([]recipeInfo, len(recipes)) 187 | for i, r := range recipes { 188 | result[i] = recipeInfo{ 189 | Name: r.Name, 190 | Description: r.Description, 191 | Category: r.Category, 192 | Author: r.Author, 193 | } 194 | } 195 | 196 | jsonBytes, err := json.MarshalIndent(result, "", " ") 197 | if err != nil { 198 | return err 199 | } 200 | 201 | fmt.Println(string(jsonBytes)) 202 | return nil 203 | } 204 | -------------------------------------------------------------------------------- /internal/progress.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/schollz/progressbar/v3" 8 | ) 9 | 10 | // defaultProgressTheme defines the default visual style for progress bars 11 | var defaultProgressTheme = Theme{ 12 | Saucer: "[green]=[reset]", 13 | SaucerHead: "[green]>[reset]", 14 | SaucerPadding: " ", 15 | BarStart: "[", 16 | BarEnd: "]", 17 | } 18 | 19 | type ProgressBarOptions struct { 20 | Width int `yaml:"width,omitempty"` 21 | Description string `yaml:"description,omitempty"` 22 | Theme *Theme `yaml:"theme,omitempty"` 23 | ShowCount bool `yaml:"show_count,omitempty"` 24 | ShowPercentage bool `yaml:"show_percentage,omitempty"` 25 | ShowElapsedTime bool `yaml:"show_elapsed_time,omitempty"` 26 | ShowIterationSpeed bool `yaml:"show_iteration_speed,omitempty"` 27 | RefreshRate float64 `yaml:"refresh_rate,omitempty"` 28 | MessageTemplate string `yaml:"message_template,omitempty"` 29 | } 30 | 31 | type Theme struct { 32 | Saucer string `yaml:"saucer,omitempty"` 33 | SaucerHead string `yaml:"saucer_head,omitempty"` 34 | SaucerPadding string `yaml:"saucer_padding,omitempty"` 35 | BarStart string `yaml:"bar_start,omitempty"` 36 | BarEnd string `yaml:"bar_end,omitempty"` 37 | } 38 | 39 | type ProgressBar struct { 40 | bar *progressbar.ProgressBar 41 | } 42 | 43 | // CreateProgressBar creates a new progress bar with the given total, operation name, and options 44 | func CreateProgressBar(total int, opName string, opts *ProgressBarOptions) *ProgressBar { 45 | if opts == nil { 46 | opts = &ProgressBarOptions{ 47 | ShowCount: true, 48 | ShowPercentage: true, 49 | ShowElapsedTime: true, 50 | Theme: &Theme{}, 51 | } 52 | *opts.Theme = defaultProgressTheme 53 | } 54 | 55 | options := []progressbar.Option{progressbar.OptionEnableColorCodes(true)} 56 | 57 | if opts.Width > 0 { 58 | options = append(options, progressbar.OptionSetWidth(opts.Width)) 59 | } 60 | 61 | if opts.Description != "" { 62 | options = append(options, progressbar.OptionSetDescription(opts.Description)) 63 | } else { 64 | options = append(options, progressbar.OptionSetDescription(opName)) 65 | } 66 | 67 | if opts.Theme != nil { 68 | theme := progressbar.Theme{ 69 | Saucer: defaultProgressTheme.Saucer, 70 | SaucerHead: defaultProgressTheme.SaucerHead, 71 | SaucerPadding: defaultProgressTheme.SaucerPadding, 72 | BarStart: defaultProgressTheme.BarStart, 73 | BarEnd: defaultProgressTheme.BarEnd, 74 | } 75 | 76 | if opts.Theme.Saucer != "" { 77 | theme.Saucer = opts.Theme.Saucer 78 | } 79 | if opts.Theme.SaucerHead != "" { 80 | theme.SaucerHead = opts.Theme.SaucerHead 81 | } 82 | if opts.Theme.SaucerPadding != "" { 83 | theme.SaucerPadding = opts.Theme.SaucerPadding 84 | } 85 | if opts.Theme.BarStart != "" { 86 | theme.BarStart = opts.Theme.BarStart 87 | } 88 | if opts.Theme.BarEnd != "" { 89 | theme.BarEnd = opts.Theme.BarEnd 90 | } 91 | 92 | options = append(options, progressbar.OptionSetTheme(theme)) 93 | } 94 | 95 | if opts.ShowCount { 96 | options = append(options, progressbar.OptionShowCount()) 97 | } 98 | if opts.ShowPercentage { 99 | options = append(options, progressbar.OptionShowBytes(false)) 100 | } 101 | if opts.ShowElapsedTime { 102 | options = append(options, progressbar.OptionSetElapsedTime(true)) 103 | } 104 | if opts.ShowIterationSpeed { 105 | options = append(options, progressbar.OptionShowIts()) 106 | } 107 | if opts.RefreshRate > 0 { 108 | options = append(options, progressbar.OptionSetRenderBlankState(true)) 109 | options = append(options, progressbar.OptionThrottle(time.Duration(float64(time.Second)*opts.RefreshRate))) 110 | } 111 | 112 | return &ProgressBar{bar: progressbar.NewOptions(total, options...)} 113 | } 114 | 115 | // Increment adds 1 to the progress bar 116 | func (p *ProgressBar) Increment() { 117 | if err := p.bar.Add(1); err != nil { 118 | return 119 | } 120 | } 121 | 122 | // Complete marks the progress bar as finished 123 | func (p *ProgressBar) Complete() { 124 | if err := p.bar.Finish(); err != nil { 125 | return 126 | } 127 | fmt.Println() 128 | } 129 | 130 | // Update changes the description of the progress bar 131 | func (p *ProgressBar) Update(message string) { 132 | p.bar.Describe(message) 133 | } 134 | 135 | // ParseProgressBarOptions converts a map of interface{} to a ProgressBarOptions struct 136 | func ParseProgressBarOptions(optsMap map[string]interface{}) *ProgressBarOptions { 137 | opts := &ProgressBarOptions{ 138 | ShowCount: true, 139 | ShowPercentage: true, 140 | ShowElapsedTime: true, 141 | Theme: &Theme{}, 142 | } 143 | *opts.Theme = defaultProgressTheme 144 | 145 | if val, ok := optsMap["message_template"].(string); ok { 146 | opts.MessageTemplate = val 147 | } 148 | if val, ok := optsMap["width"].(int); ok { 149 | opts.Width = val 150 | } 151 | if val, ok := optsMap["description"].(string); ok { 152 | opts.Description = val 153 | } 154 | if val, ok := optsMap["show_count"].(bool); ok { 155 | opts.ShowCount = val 156 | } 157 | if val, ok := optsMap["show_percentage"].(bool); ok { 158 | opts.ShowPercentage = val 159 | } 160 | if val, ok := optsMap["show_elapsed_time"].(bool); ok { 161 | opts.ShowElapsedTime = val 162 | } 163 | if val, ok := optsMap["show_iteration_speed"].(bool); ok { 164 | opts.ShowIterationSpeed = val 165 | } 166 | if val, ok := optsMap["refresh_rate"].(float64); ok { 167 | opts.RefreshRate = val 168 | } 169 | 170 | if themeMap, ok := optsMap["theme"].(map[string]interface{}); ok { 171 | if val, ok := themeMap["saucer"].(string); ok { 172 | opts.Theme.Saucer = val 173 | } 174 | if val, ok := themeMap["saucer_head"].(string); ok { 175 | opts.Theme.SaucerHead = val 176 | } 177 | if val, ok := themeMap["saucer_padding"].(string); ok { 178 | opts.Theme.SaucerPadding = val 179 | } 180 | if val, ok := themeMap["bar_start"].(string); ok { 181 | opts.Theme.BarStart = val 182 | } 183 | if val, ok := themeMap["bar_end"].(string); ok { 184 | opts.Theme.BarEnd = val 185 | } 186 | } 187 | 188 | return opts 189 | } 190 | -------------------------------------------------------------------------------- /internal/style.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | // Color represents terminal text colors 9 | type Color string 10 | 11 | const ( 12 | ColorNone Color = "" 13 | ColorBlack Color = "black" 14 | ColorRed Color = "red" 15 | ColorGreen Color = "green" 16 | ColorYellow Color = "yellow" 17 | ColorBlue Color = "blue" 18 | ColorMagenta Color = "magenta" 19 | ColorCyan Color = "cyan" 20 | ColorWhite Color = "white" 21 | BgColorBlack Color = "bg-black" 22 | BgColorRed Color = "bg-red" 23 | BgColorGreen Color = "bg-green" 24 | BgColorYellow Color = "bg-yellow" 25 | BgColorBlue Color = "bg-blue" 26 | BgColorMagenta Color = "bg-magenta" 27 | BgColorCyan Color = "bg-cyan" 28 | BgColorWhite Color = "bg-white" 29 | ) 30 | 31 | // colorCodes maps color names to ANSI escape sequences 32 | var colorCodes = map[string]string{ 33 | "black": "\033[30m", 34 | "red": "\033[31m", 35 | "green": "\033[32m", 36 | "yellow": "\033[33m", 37 | "blue": "\033[34m", 38 | "magenta": "\033[35m", 39 | "cyan": "\033[36m", 40 | "white": "\033[37m", 41 | "bg-black": "\033[40m", 42 | "bg-red": "\033[41m", 43 | "bg-green": "\033[42m", 44 | "bg-yellow": "\033[43m", 45 | "bg-blue": "\033[44m", 46 | "bg-magenta": "\033[45m", 47 | "bg-cyan": "\033[46m", 48 | "bg-white": "\033[47m", 49 | "reset": "\033[0m", 50 | } 51 | 52 | // Style represents terminal text styling options 53 | type Style string 54 | 55 | const ( 56 | StyleNone Style = "" 57 | StyleBold Style = "bold" 58 | StyleDim Style = "dim" 59 | StyleItalic Style = "italic" 60 | StyleUnderline Style = "underline" 61 | ) 62 | 63 | // styleCodes maps style names to ANSI escape sequences 64 | var styleCodes = map[string]string{ 65 | "bold": "\033[1m", 66 | "dim": "\033[2m", 67 | "italic": "\033[3m", 68 | "underline": "\033[4m", 69 | "reset": "\033[0m", 70 | } 71 | 72 | // FormatText applies color and style formatting to text for terminal output 73 | func FormatText(text string, color Color, style Style) string { 74 | if os.Getenv("NO_COLOR") != "" { 75 | return text 76 | } 77 | 78 | if color == ColorNone && style == StyleNone { 79 | return text 80 | } 81 | 82 | if color != ColorNone && style != StyleNone { 83 | return applyColorAndStyle(text, color, style) 84 | } 85 | 86 | if color != ColorNone { 87 | return applyColor(text, color) 88 | } 89 | 90 | return applyStyle(text, style) 91 | } 92 | 93 | // applyColor adds color formatting to text 94 | func applyColor(text string, color Color) string { 95 | if code, ok := colorCodes[string(color)]; ok { 96 | return code + text + colorCodes["reset"] 97 | } 98 | return text 99 | } 100 | 101 | // applyStyle adds style formatting to text 102 | func applyStyle(text string, style Style) string { 103 | if code, ok := styleCodes[string(style)]; ok { 104 | return code + text + styleCodes["reset"] 105 | } 106 | return text 107 | } 108 | 109 | // applyColorAndStyle adds both color and style formatting to text 110 | func applyColorAndStyle(text string, color Color, style Style) string { 111 | result := applyColor(text, color) 112 | 113 | pos := strings.Index(result, text) 114 | if pos == -1 { 115 | return result 116 | } 117 | 118 | styleCode, ok := styleCodes[string(style)] 119 | if !ok { 120 | return result 121 | } 122 | 123 | result = result[:pos] + styleCode + result[pos:] 124 | 125 | resetPos := strings.LastIndex(result, colorCodes["reset"]) 126 | if resetPos != -1 { 127 | result = result[:resetPos] + styleCodes["reset"] + result[resetPos:] 128 | } 129 | 130 | return result 131 | } 132 | -------------------------------------------------------------------------------- /keys/shef-binary-gpg-public-key.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQINBGfQStoBEADneqQqtrzznLJfgmSDpU01ba5oZ8JsPWNFQ6lQZC+n5bHRUPaC 4 | 8pgX/dJMHdLI5CgSHBvGPcAIydOfMjkO0+bg/9PVOMS3EQOVMILjGueoGPTETG3w 5 | emB9BcCdwXEXV3aH0Y7FW1XUCuc3oHa0nK03T2QaMcqe2X+087RD5jdeyXM3bmDd 6 | RZJhfEi8OD1LtMnRpk/E9tWNEnMlN01YEEqYcgbyAHFb+Zp8OxLpOuxB3QrkDZiv 7 | NvLt55wUZKoz43OY81BnnPqXA5H3burAxxX/A41zoB9Ezhd6wv3ETqhh5IxFflS4 8 | vgnnr/x75E5Uw1hY185lYXQpiaIPh/H59983KzqS+lkxp+MwquIRLsNbytil5V4J 9 | mURJcZFnv3RDIe8ubt1FYzZXx+t+ZPWftxqc358pHQutKHJd5I2tLDvvqqLy0ui+ 10 | Q7Y5HzAOXvajnWsfFkOWmatcOuJ1YIzBb8NrG8eUIVT9hT+Tv/7UsKek1gmFRLev 11 | B6F6SDd+fX9i5uMaGwSJORJXFptuyE49ErDrVoUF3beGqPjlu4ssgZ8k5QYtxmkE 12 | ZlnaFeOuIspQLer3BdGNZ/3R2AGLIuEQyn2pRX31+U+POQHgbMgxGMIZoBQUBUjA 13 | cUoNj6JHWkBVccZRURgh3FaHycDNQQYwIYSHC2HZAbVK/fLhZIjj+TtKywARAQAB 14 | tDNFZHVhcmRvIEEuIEdhcmNpYSAoU2hlZikgPDI0ZWcxNCtzaGVmcGdwQGdtYWls 15 | LmNvbT6JAlEEEwEIADsWIQRv97eHg1F7YQL7tgNJfSlsRRD5WwUCZ9BK2gIbAwUL 16 | CQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRBJfSlsRRD5W9GhEACiAAczCYdX 17 | LoKijtsLECO7uY5Xft2paTWERcgU1EIIFQGnpDKH7EtUbyDnh0/7dbopopAyZzdR 18 | 8JP+CEyTRVArwyoG7p8HsVv3ujPFpBha5BMZ59LFPJx72yNtMUwEiPtUf7gastPT 19 | dBELO0eiC4CUNu59KAAdusduc88NWDzAhTESCp8WGAgiV5GFoet6wtlANWD9YnJb 20 | /SjkdNmeZIH+JxCPFu/8V14d0p3SEi7c00/7KVDvncTJjXfo8hGdNBAoENYDq1dL 21 | rNysVo87KEV+59tWXGGCJzuh7ztR0swcrvRDnj/IQpNbLxVfFqi5QKz9DdcJGcO4 22 | /9e5GhlyBM9YeYg+SbbUmvIiovV1L7aI9KWgTJX66/7Dqh+hZH1kTJi57QH1TMAI 23 | 8zk9s8ULun4K7Tl6n+Y79GXGqEExOFHbIAwxwhTbu33bl4ulDvO2d6KOCBSc3jEF 24 | MCoJKSPOSvbaiZPtIf3y4jEv3YVekix+I0xqc77cyasuE5Zusnw9pzRop5S7h8NM 25 | WR6kBbZCZ26b9zunHbhMxGokI/QfsKkogNtEdQk2AD1R1abhUmjbcWv+LSY6MeF0 26 | rozK3sWqJuJRoUsZ94cQFH422eHWfxIgWEhptdO06HSOvpyVrXuT4EYgfGHrzRyq 27 | l9Seoi+1GiDZV1w8b1Re0WAJ+oSc9mvZkLkCDQRn0EraARAAySIe2tUfkKomin5P 28 | iTiZ1lP572QpGUixHQRilq1dz3TvXtkOnghYF1MPutb97Cvxg14W+N1T8zCXdLBx 29 | cqVQzkQ4zOJicFh1mmL8UVbwAlTJ6ttEnEaCkOoIJemO9ssiw6rpHfF3W4S4AMgi 30 | dkhRFwKCPUOU9RxAexJwe4yW9Gxl0j+HguvaCtbobEYEgK07rEuXYHzX/LwVkYAt 31 | tH9btBgPShCGT8ikHzt2tDXDW2bibPljUsymkyJqcqbct1cY7/skx8QC9+Z/ghHy 32 | yqqQq8v5Tv/7nglcUa1fJDMQL6WcMzBxTGMKOdPhavKrWi84tVDaIPx72zQh0MV4 33 | T1dTJXRGr9Cr5L7QKf5bZBTlBMPwZMfdvXaxcrjF5dXoSTJ4auhFWGx70iwgwpzF 34 | MNrSTRgLQGpSOPq1QfWA6cuT2YcrpO1mpfKC57zEmPqTqcm55Fxxj++dJRxJ+lb8 35 | 7hwakSttz87Pw8at6ZCOnf66JFrbjzHeekBZJihorfrPk1h/EcJuaMjoYimDXhbE 36 | UWCdPFPjI7c+fnD7Vjw3JC10zuztvAHGgOIuUVpoQzmYrDAVB5j6i+sjWdjVu8jj 37 | BdzpMMBajD0mh8hyXcORE07VIJA5wMtkUa0+pTspG+RmL52Mh7BaLJTxZFuWcrVN 38 | 3euaoAOBP2wEF/cR6UJ5geW4IisAEQEAAYkCNgQYAQgAIBYhBG/3t4eDUXthAvu2 39 | A0l9KWxFEPlbBQJn0EraAhsMAAoJEEl9KWxFEPlbN+8P+wVa+zMWL3M0ebFuoz8H 40 | AiLxr9hwzozingSoepIEDL4O5yMnLBRXwvyH4HAmCP8mi01J9oCAJHokbi8hVfoq 41 | N6bcrMIxoMnf3Tl66ARpc/S5MjlPtLZbQiRZAaeps+RnZ6Xp1pjybMgs5hNmtRmx 42 | 0Bo0uAcILegtLIBssuH2GnhMQB+lkvkDoiANUOMKpgwf3xXSyAeIvDxDFwbIWdSI 43 | JLcdpt6WmtLsQoMuGNWRZ4VZAvmDukXYWo2KB86H5BiYJSW1rYESoet64p6z8hw/ 44 | FMHFP/ydQWp0gsF34eIM/DiYY2XiDIDIEzqLYRP5sqXFnY5YhXlTpSQaOzpzLI7h 45 | A8T+sVy9I6SIWLJs2dknWaMTKspsMVGe4LPd6aOg9D6Nx6fxseXwI33cthSA+CHm 46 | uq+MLzvdsdE/2h8aifYYyetZPWI8+EguonpgPXmIq+91rVXBQEBHZuEydCCaOsph 47 | P8R8CZZFwZGKY1U65EcyC5bvrhLYtPkBxaHE/AdIYDH2uWaTGuZ6yquYsFhko9Md 48 | UPCeRHKI2wdNp2KkTm4hb94HTRxfDGeRg6F2w07oZJCl130Tp2Xfeg6qecOC6L/Z 49 | OafVsjvNJNde++ASIIyH0Yj3osPHCaul0BOLAUF3GD7+H3JyoLkS1GIiBbtcbvIo 50 | MQobDlScWNiU7udC38+QZrAw 51 | =GsMH 52 | -----END PGP PUBLIC KEY BLOCK----- 53 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/eduardoagarcia/shef/internal" 8 | ) 9 | 10 | func init() { 11 | rand.New(rand.NewSource(time.Now().UnixNano())) 12 | } 13 | 14 | func main() { 15 | internal.Run() 16 | } 17 | -------------------------------------------------------------------------------- /recipes/1password/components/items.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "op.get.item.names" 3 | inputs: 4 | - id: "filter" 5 | operations: 6 | - command: op item list --format=json | jq -r '.[] | "\(.title)"' | sort -f 7 | silent: true 8 | condition: .filter == "false" 9 | 10 | - command: op item list --format=json | jq -r '.[] | "\(.title)"' | sort -f | grep -i "{{ .filter }}" 11 | silent: true 12 | condition: .filter != "false" 13 | cleanup: 14 | - "filter" 15 | 16 | - id: "op.item.select" 17 | inputs: 18 | - id: "filter" 19 | operations: 20 | - uses: "op.get.item.names" 21 | with: 22 | filter: "{{ .filter }}" 23 | id: "items" 24 | 25 | - unset: 26 | - "filter" 27 | 28 | - command: echo "{{ .item }}" 29 | silent: true 30 | prompts: 31 | - id: "item" 32 | type: "select" 33 | message: "Select a 1Password Item" 34 | source_operation: "items" 35 | -------------------------------------------------------------------------------- /recipes/1password/components/password.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "op.password" 3 | inputs: 4 | - id: "item" 5 | operations: 6 | - command: op item get "{{ .item }}" --fields label=password --reveal 7 | cleanup: 8 | - "item" 9 | -------------------------------------------------------------------------------- /recipes/1password/components/utils.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "op.lock" 3 | operations: 4 | - command: op signout --all 5 | silent: true 6 | -------------------------------------------------------------------------------- /recipes/1password/op.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "open" 3 | description: "Open 1Password" 4 | category: "op" 5 | help: | 6 | Opens the 1Password application. 7 | 8 | Usage: 9 | shef op open # Launch 1Password 10 | operations: 11 | - uses: "os.apps.filtered" 12 | with: 13 | filter: "1Password" 14 | id: "op_app" 15 | 16 | - uses: "os.app.open" 17 | with: 18 | app: "{{ .op_app }}" 19 | 20 | - name: "lock" 21 | description: "Lock 1Password" 22 | category: "op" 23 | help: | 24 | Locks the 1Password application. 25 | 26 | Usage: 27 | shef op lock # Lock 1Password 28 | operations: 29 | - uses: "op.lock" 30 | -------------------------------------------------------------------------------- /recipes/1password/password.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "password" 3 | description: "Get the password from an item in 1Password" 4 | category: "op" 5 | help: | 6 | Retrieves a password from a 1Password item and copies it to clipboard. 7 | 8 | Usage: 9 | shef op password # Select from all items 10 | shef op password [FILTER] # Filter items by name 11 | operations: 12 | - id: "item" 13 | uses: "op.item.select" 14 | with: 15 | filter: "{{ .input }}" 16 | 17 | - id: "password" 18 | uses: "op.password" 19 | with: 20 | item: "{{ .item }}" 21 | silent: true 22 | 23 | - uses: "op.lock" 24 | 25 | - uses: "clipboard.copy" 26 | with: 27 | value: "{{ .password }}" 28 | 29 | - cleanup: 30 | - "password" 31 | 32 | - name: "otp" 33 | description: "Get the one time password from an item in 1Password" 34 | category: "op" 35 | help: | 36 | Retrieves a one-time password (TOTP) from a 1Password item and copies it to clipboard. 37 | 38 | Usage: 39 | shef op otp # Select from all items with OTP 40 | shef op otp [FILTER] # Filter items by name 41 | operations: 42 | - id: "item" 43 | uses: "op.item.select" 44 | with: 45 | filter: "{{ .input }}" 46 | 47 | - id: "password" 48 | command: op item get "{{ .item }}" --format json | jq '.fields[] | select(.type == "OTP") | .totp' 49 | silent: true 50 | cleanup: 51 | - "item" 52 | 53 | - uses: "op.lock" 54 | 55 | - uses: "clipboard.copy" 56 | with: 57 | value: "{{ .password }}" 58 | 59 | - cleanup: 60 | - "password" 61 | 62 | - name: "item" 63 | description: "Get an item in 1Password" 64 | category: "op" 65 | help: | 66 | Displays detailed information for a selected 1Password item. 67 | 68 | Usage: 69 | shef op item # Select from all items 70 | shef op item [FILTER] # Filter items by name 71 | operations: 72 | - id: "item" 73 | uses: "op.item.select" 74 | with: 75 | filter: "{{ .input }}" 76 | 77 | - id: "item" 78 | command: op item get "{{ .item }}" 79 | cleanup: 80 | - "item" 81 | 82 | - uses: "op.lock" 83 | -------------------------------------------------------------------------------- /recipes/demo/arguments.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "arguments" 3 | description: "A simple demo using arguments and flags" 4 | category: "demo" 5 | help: | 6 | Demonstrates how to use command-line arguments and flags in recipes. 7 | 8 | Usage: 9 | shef demo arguments [INPUT] # Pass a positional argument 10 | shef demo arguments -f # Use a flag (sets f to true) 11 | shef demo arguments --name=VALUE # Set a named variable 12 | shef demo arguments [INPUT] -f # Combine positional and flag arguments 13 | operations: 14 | - name: "Display Arguments" 15 | command: | 16 | echo "Input: {{ .input }}" 17 | echo "Short Flag f: {{ .f }}" 18 | echo "Long Flag name: {{ .name }}" 19 | 20 | - name: "Check if -f Flag is Set" 21 | command: echo "The -f flag was set!" 22 | condition: .f != false 23 | 24 | - name: "Check if -f Flag is Not Set" 25 | command: echo "The -f flag was NOT set." 26 | condition: .f == false 27 | 28 | - name: "Print Name" 29 | command: echo "Hello, {{ .name }}!" 30 | condition: .name != false 31 | -------------------------------------------------------------------------------- /recipes/demo/background-tasks.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "background-tasks" 3 | description: "A simple demo showing the background execution of concurrent tasks" 4 | category: "demo" 5 | help: | 6 | Demonstrates running multiple tasks in parallel with background execution mode. 7 | 8 | Usage: 9 | shef demo background-tasks # Select fruit tasks to run concurrently 10 | 11 | Shows real-time status monitoring and accessing results from background operations. 12 | operations: 13 | - name: "Choose Fruit" 14 | id: "fruit_choice" 15 | command: echo {{ .fruit }} 16 | silent: true 17 | prompts: 18 | - name: "Fruit Select" 19 | id: "fruit" 20 | type: "multiselect" 21 | message: "Choose one or more fruits:" 22 | options: 23 | - "Apple" 24 | - "Orange" 25 | - "Lemon" 26 | - "Kiwi" 27 | descriptions: 28 | "Apple": "Run the Apple Task 🍎" 29 | "Orange": "Run the Orange Task 🍊" 30 | "Lemon": "Run the Lemon Task 🍋" 31 | "Kiwi": "Run the Kiwi Task 🥝" 32 | 33 | - name: "Apple Operation" 34 | id: "apple" 35 | command: sleep 5 && echo "The apple task finished! 🍎" 36 | silent: true 37 | condition: '{{ contains .fruit_choice "Apple" }}' 38 | execution_mode: "background" 39 | 40 | - name: "Orange Operation" 41 | id: "orange" 42 | command: sleep 2 && echo "The orange task finished! 🍊" 43 | silent: true 44 | condition: '{{ contains .fruit_choice "Orange" }}' 45 | execution_mode: "background" 46 | 47 | - name: "Lemon Operation" 48 | id: "lemon" 49 | command: sleep 7 && echo "The lemon task finished! 🍋" 50 | silent: true 51 | condition: '{{ contains .fruit_choice "Lemon" }}' 52 | execution_mode: "background" 53 | 54 | - name: "Kiwi Operation" 55 | id: "kiwi" 56 | command: sleep 4 && echo "The kiwi task finished! 🥝" 57 | silent: true 58 | condition: '{{ contains .fruit_choice "Kiwi" }}' 59 | execution_mode: "background" 60 | 61 | - name: "Wait Loop" 62 | control_flow: 63 | type: "while" 64 | condition: .allTasksComplete != "true" 65 | progress_mode: true 66 | operations: 67 | - name: "Delay" 68 | command: sleep 0.05 69 | 70 | - name: "Fruit Status" 71 | control_flow: 72 | type: "foreach" 73 | collection: "apple\norange\nlemon\nkiwi" 74 | as: "fruit" 75 | operations: 76 | - name: "{{ .fruit }} Status" 77 | id: "{{ .fruit }}_status" 78 | command: echo "[{{ taskStatusMessage .fruit (color "green" (printf "✅ %s" .fruit)) (color "yellow" (printf "%s" .fruit)) "" (style "dim" (printf "%s" .fruit)) }}]" 79 | silent: true 80 | 81 | - name: "Status Update" 82 | command: echo {{ printf "%s %s %s %s '(%s)'" .apple_status .orange_status .lemon_status .kiwi_status (color "magenta" .duration_ms_fmt) }} 83 | 84 | - name: "Success" 85 | command: echo {{ color "green" "All background tasks are complete!" }} 86 | -------------------------------------------------------------------------------- /recipes/demo/color.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "color" 3 | description: "A simple demo of colors and styles" 4 | category: "demo" 5 | help: | 6 | Demonstrates using colors and text styles in recipe outputs. 7 | 8 | Usage: 9 | shef demo color # See examples of colors and styles 10 | 11 | Shows text formatting with green, red, blue, bold, italic, underline, and more. 12 | operations: 13 | - name: "Basic color example" 14 | command: echo "{{ color "green" "This text is green" }} and {{ color "red" "this is red" }}" 15 | 16 | - name: "Style examples" 17 | command: | 18 | echo {{ style "bold" "This text is bold" }} 19 | echo {{ style "italic" "This text is italic" }} 20 | echo {{ style "underline" "This text is underlined" }} 21 | echo {{ style "dim" "This text is dimmed" }} 22 | 23 | - name: "Combined colors and styles" 24 | command: | 25 | echo {{ style "bold" (color "blue" "This is bold blue text") }} 26 | echo {{ color "black" (color "bg-yellow" "Black text on yellow background") }} 27 | 28 | - name: "Multiple styles" 29 | command: echo {{ style "bold" (style "underline" "This is bold and underlined") }} 30 | 31 | - name: "Color in command output" 32 | command: echo "Running {{ color "cyan" "important ls -la" }} command..." && ls -la 33 | 34 | - name: "Color in conditionals" 35 | id: "status_check" 36 | command: echo "status:ok" 37 | transform: | 38 | {{ if contains .output "ok" }} 39 | {{ color "green" (style "bold" "✓ Status check passed") }} 40 | {{ else }} 41 | {{ color "red" (style "bold" "✗ Status check failed") }} 42 | {{ end }} 43 | -------------------------------------------------------------------------------- /recipes/demo/components.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "components" 3 | description: "A simple demo of reusable components" 4 | category: "demo" 5 | help: | 6 | Demonstrates reusable components for modular recipe design. 7 | 8 | Usage: 9 | shef demo components # Run a system health check using components 10 | 11 | Shows nested components and how to reference component outputs. 12 | operations: 13 | - name: "Run System Checkup" 14 | uses: "health-check" 15 | 16 | - name: "Generate System Report" 17 | command: | 18 | echo "==== SYSTEM INFO ====" 19 | echo "Hostname: {{ .hostname }}" 20 | echo "User: {{ .user }}" 21 | echo "Time: {{ .datetime }}" 22 | echo "Disk Usage: {{ .disk_space }}" 23 | 24 | components: 25 | - id: "system-info" 26 | name: "System Information" 27 | description: "Collects basic system information" 28 | operations: 29 | - name: "Get Hostname" 30 | id: "hostname" 31 | command: "hostname" 32 | silent: true 33 | 34 | - name: "Get Current User" 35 | id: "user" 36 | command: "whoami" 37 | silent: true 38 | 39 | - name: "Get Date and Time" 40 | id: "datetime" 41 | command: "date" 42 | silent: true 43 | 44 | - id: "health-check" 45 | name: "System Health Check" 46 | description: "Performs basic system health checks" 47 | operations: 48 | - name: "Collect System Information" 49 | uses: "system-info" 50 | 51 | - name: "Check Disk Space" 52 | id: "disk_space" 53 | command: "df -h | grep -E '/$' | awk '{print $5}'" 54 | silent: true 55 | -------------------------------------------------------------------------------- /recipes/demo/conditional.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "conditional" 3 | description: "A simple demo of conditional operations using direct prompt values" 4 | category: "demo" 5 | help: | 6 | Demonstrates conditional operations that run based on user input selections. 7 | 8 | Usage: 9 | shef demo conditional # Select a fruit to see conditional execution 10 | operations: 11 | - name: "Choose Fruit" 12 | id: "choose" 13 | command: 'echo "You selected: {{ .fruit }}"' 14 | prompts: 15 | - name: "Fruit Select" 16 | id: "fruit" 17 | type: "select" 18 | message: "Choose a fruit:" 19 | options: 20 | - "Apples" 21 | - "Oranges" 22 | 23 | - name: "Apple Operation" 24 | id: "apple" 25 | command: echo "This is the apple operation! 🍎" 26 | condition: .fruit == "Apples" 27 | 28 | - name: "Orange Operation" 29 | id: "orange" 30 | command: echo "This is the orange operation! 🍊" 31 | condition: .fruit == "Oranges" 32 | -------------------------------------------------------------------------------- /recipes/demo/flexible-tables.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "tables-flexible" 3 | description: "A demo of flexible table input formats" 4 | category: "demo" 5 | help: | 6 | Demonstrates flexible ways to format input for tables. 7 | 8 | Usage: 9 | shef demo tables-flexible # View examples of different table input formats 10 | 11 | Shows newline-separated headers, comma-separated rows, array input, and dynamic data. 12 | operations: 13 | - name: "Newline-Separated Headers" 14 | command: | 15 | echo "Table with newline-separated headers:" 16 | echo '{{ table 17 | "Name\nAge\nCity" 18 | (list 19 | (makeRow "John Doe" "34" "New York") 20 | (makeRow "Jane Smith" "28" "San Francisco") 21 | ) 22 | "rounded" 23 | }}' 24 | 25 | - name: "Comma-Separated Rows" 26 | command: | 27 | echo "Table with comma-separated row strings:" 28 | echo '{{ table 29 | (makeHeaders "Product" "Price" "Stock") 30 | "Apple,$1.25,125\nOrange,$0.90,83\nBanana,$0.50,42" 31 | "double" 32 | }}' 33 | 34 | - name: "Array Input" 35 | command: | 36 | echo "Table with array input:" 37 | echo '{{ table 38 | "[Name, Age, Role]" 39 | "[[John, 34, Developer], [Jane, 28, Designer], [Bob, 45, Manager]]" 40 | "light" 41 | }}' 42 | 43 | - name: "Mixed Input Formats" 44 | command: | 45 | echo "Table with mixed input formats:" 46 | echo '{{ table 47 | "Language,First Released,Paradigm" 48 | (list 49 | "Go,2009,Concurrent" 50 | "Python,1991,Multi-paradigm" 51 | "Rust,2010,Multi-paradigm" 52 | ) 53 | "rounded" 54 | }}' 55 | 56 | - name: "Generate Dynamic Data" 57 | id: "dynamic_data" 58 | command: echo '[[Service, Status, Port], [Web Server, Running, 8080], [Database, Stopped, 5432], [Cache, Running, 6379]]' 59 | silent: true 60 | 61 | - name: "Dynamic Data Table" 62 | command: | 63 | echo "Table with dynamic input data:" 64 | echo '{{ table "[Column A, Column B, Column C]" .dynamic_data "bold" }}' 65 | -------------------------------------------------------------------------------- /recipes/demo/for.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "for" 3 | description: "A simple demo of the for loop control flow" 4 | category: "demo" 5 | help: | 6 | Demonstrates the 'for' loop control flow with a fixed number of iterations. 7 | 8 | Usage: 9 | shef demo for # Run a loop with 5 iterations 10 | operations: 11 | - name: "Run a For Loop" 12 | control_flow: 13 | type: "for" 14 | count: 5 15 | variable: "i" 16 | operations: 17 | - name: "Print Iteration" 18 | command: echo "Running iteration {{ color "yellow" .iteration }}" 19 | id: "last_iteration" 20 | 21 | - name: "Show Completion" 22 | command: 'echo "{{ color "green" "For loop completed!" }} Last iteration ran: {{ .last_iteration }}"' 23 | -------------------------------------------------------------------------------- /recipes/demo/foreach.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "foreach" 3 | description: "A simple demo of the foreach control flow" 4 | category: "demo" 5 | help: | 6 | Demonstrates the 'foreach' loop that iterates through a collection of items. 7 | 8 | Usage: 9 | shef demo foreach # Process a collection of fruit items 10 | operations: 11 | - name: "Process Each Fruit" 12 | control_flow: 13 | type: "foreach" 14 | collection: "🍎 Apple\n🍌 Banana\n🍒 Cherry\n🍊 Orange" 15 | as: "fruit" 16 | operations: 17 | - name: "Process Fruit" 18 | command: echo "Processing {{ .fruit }}" 19 | 20 | - name: "Show Completion" 21 | command: echo {{ color "green" "All fruits processed!" }} 22 | -------------------------------------------------------------------------------- /recipes/demo/handlers.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "handlers" 3 | description: "A simple demo of error and success handlers" 4 | category: "demo" 5 | help: | 6 | Demonstrates success and error handlers for operations. 7 | 8 | Usage: 9 | shef demo handlers [PATH] # Test if a directory exists 10 | shef demo handlers /tmp # Example with existing directory 11 | shef demo handlers /nonexistent # Example with non-existing directory 12 | operations: 13 | - name: "Help" 14 | command: echo {{ color "magenta" "Please provide a valid or invalid directory argument to test" }} 15 | condition: .input == "false" 16 | 17 | - name: "Test if directory exists" 18 | command: cd {{ .input }} 19 | on_failure: "handle_error" 20 | on_success: "handle_success" 21 | condition: .input != "false" 22 | 23 | - name: "Handle error" 24 | id: "handle_error" 25 | command: echo {{ color "red" (printf "Directory %s does NOT exist!" .input) }} 26 | 27 | - name: "Handle success" 28 | id: "handle_success" 29 | command: echo {{ color "green" (printf "Directory %s exists!" .input) }} 30 | -------------------------------------------------------------------------------- /recipes/demo/hello-world.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "hello-world" 3 | description: "A simple hello world recipe" 4 | category: "demo" 5 | help: | 6 | A simple introductory recipe demonstrating the basics of Shef. 7 | 8 | Usage: 9 | shef demo hello-world # Get a personalized greeting 10 | 11 | Shows basic prompts, variable usage, and colorized output. 12 | operations: 13 | - name: "Greet User" 14 | command: | 15 | echo "Hello, {{ color "green" .name }}!" 16 | echo "Current time: $(date)" 17 | echo "Welcome to Shef, the shell recipe tool." 18 | prompts: 19 | - name: "Name Input" 20 | id: "name" 21 | type: "input" 22 | message: "What is your name?" 23 | default: "World" 24 | -------------------------------------------------------------------------------- /recipes/demo/monitor.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "monitor" 3 | description: "Monitor a service until it returns a success status code" 4 | category: "demo" 5 | help: | 6 | Demonstrates service monitoring with a while loop until success status is received. 7 | 8 | Usage: 9 | shef demo monitor # Monitor simulated service health 10 | 11 | The simulated service returns success (200) after 3 polling attempts. 12 | operations: 13 | - name: "Initialize Empty Status Code" 14 | id: "status_code" 15 | command: echo "" 16 | silent: true 17 | 18 | - name: "Health Check" 19 | control_flow: 20 | type: "while" 21 | condition: .status_code != "200" || .status_code == "" 22 | operations: 23 | - name: "Check Service Status" 24 | id: "status_code" 25 | command: | 26 | # simulate a status change after three polling attempts 27 | if [ {{ .iteration }} -gt 3 ]; then 28 | curl -s -o /dev/null -w "%{http_code}" https://httpbin.org/status/200 29 | else 30 | curl -s -o /dev/null -w "%{http_code}" https://httpbin.org/status/500 31 | fi 32 | silent: true 33 | 34 | - name: "Display Current Error Status" 35 | command: echo {{ color "red" "Service unavailable! Status code:" }} {{ style "bold" (color "red" .status_code) }} 36 | condition: .status_code != "200" 37 | 38 | - name: "Throttle" 39 | command: sleep 1 40 | silent: true 41 | condition: .status_code != "200" 42 | 43 | - name: "Success Message" 44 | command: echo {{ color "green" "Service available! Status code:" }} {{ style "bold" (color "green" .status_code) }} 45 | -------------------------------------------------------------------------------- /recipes/demo/operation-order.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "operation-order" 3 | description: "A simple demo to show the order of operations in a recipe" 4 | category: "demo" 5 | help: | 6 | Demonstrates the execution order of operations in recipes. 7 | 8 | Usage: 9 | shef demo operation-order -f # Run with flag to see full operation flow 10 | 11 | Shows conditions, prompts, control flow, commands, transformations, and handlers. 12 | operations: 13 | - name: "Hello" 14 | command: echo "Executing command [old text] {{ .item }}" 15 | condition: .f == "true" 16 | transform: '{{ replace .output "old" "new" }}' 17 | on_success: "success_op" 18 | prompts: 19 | - name: "Item Select" 20 | id: "item" 21 | type: "select" 22 | message: "Choose an option:" 23 | options: 24 | - "Item 1" 25 | - "Item 2" 26 | control_flow: 27 | type: "for" 28 | count: 10 29 | variable: "i" 30 | operations: 31 | - name: "Control Flow Operations" 32 | command: echo "Iteration " {{ .i }} 33 | 34 | - name: "Control Flow Exit" 35 | command: echo "Exiting for loop control flow!" 36 | condition: .i == 5 37 | break: true 38 | 39 | - name: "Success" 40 | id: "success_op" 41 | command: echo "Success operation!" 42 | -------------------------------------------------------------------------------- /recipes/demo/progress-mode.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "progress-mode" 3 | description: "A simple demo to show progress mode in control flows" 4 | category: "demo" 5 | help: | 6 | Demonstrates progress_mode for in-place updates during loop execution. 7 | 8 | Usage: 9 | shef demo progress-mode # See different loop types with inline progress updates 10 | 11 | Shows for, foreach, and while loops with dynamic in-place status updates. 12 | operations: 13 | - name: "For Progress Mode" 14 | control_flow: 15 | type: "for" 16 | count: 500 17 | variable: "i" 18 | progress_mode: true 19 | operations: 20 | - name: "Print Iteration" 21 | command: echo "Running iteration... {{ color "green" .iteration }}" 22 | 23 | - name: "Foreach Progress Mode" 24 | control_flow: 25 | type: "foreach" 26 | collection: "Item 1\nItem 2\nItem 3\nItem 4\nItem 5" 27 | as: "item" 28 | progress_mode: true 29 | operations: 30 | - name: "Print Item" 31 | command: echo {{ color "magenta" (printf "Processing... '%s'" .item) }} 32 | 33 | - name: "While Progress Mode" 34 | control_flow: 35 | type: "while" 36 | condition: .duration_s < 5 37 | progress_mode: true 38 | operations: 39 | - name: "Print Duration" 40 | command: echo {{ color "yellow" (printf "Build is running... '%s'" .duration_ms_fmt) }} 41 | -------------------------------------------------------------------------------- /recipes/demo/progress.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "progress" 3 | description: "A simple demo to show progress bars, variables, and workdir" 4 | category: "demo" 5 | help: | 6 | Demonstrates progress bars, variables, and working directories. 7 | 8 | Usage: 9 | shef demo progress # See progress bars during file operations 10 | 11 | Shows customizable progress bars, different themes, and using workdir. 12 | vars: 13 | "tmp_dir": "/tmp" 14 | "shef_dir": "shef_progress_demo" 15 | "files_to_create": 50 16 | workdir: "/tmp/shef_progress_demo" 17 | operations: 18 | - name: "Start Progress Bar Demo Prompt" 19 | prompts: 20 | - name: "Confirm" 21 | id: "confirm" 22 | type: "confirm" 23 | message: "Create {{ .files_to_create }} temporary files?" 24 | default: "true" 25 | 26 | - name: "Exit Check" 27 | condition: .confirm == "false" 28 | exit: true 29 | 30 | - name: "Create Temp Files" 31 | control_flow: 32 | type: "for" 33 | count: '{{ .files_to_create }}' 34 | variable: "i" 35 | progress_bar: true 36 | progress_bar_options: 37 | message_template: "Creating {{ .files_to_create }} temporary files" 38 | operations: 39 | - name: "Create files with random names" 40 | command: touch "$(uuidgen).txt" 41 | 42 | - name: "Delay" 43 | command: sleep 0.1 44 | 45 | - name: "Output Temp File Count" 46 | command: ls -1 {{ .workdir }} | wc -l 47 | transform: '{{ color "yellow" (printf "Files created: %s" (trim .output)) }}' 48 | 49 | - name: "Get File List" 50 | id: "file_list" 51 | command: ls -1 {{ .workdir }} 52 | silent: true 53 | 54 | - name: "Process Each Temporary File" 55 | condition: '{{ count .file_list }} > 0' 56 | control_flow: 57 | type: "foreach" 58 | collection: "{{ .file_list }}" 59 | as: "file" 60 | progress_bar: true 61 | progress_bar_options: 62 | message_template: "Deleting {{ .files_to_create }} temporary files" 63 | theme: 64 | saucer: "[red]=[reset]" 65 | saucer_head: "[red]>[reset]" 66 | operations: 67 | - name: "Remove File" 68 | command: rm {{ .file }} 69 | 70 | - name: "Delay" 71 | command: sleep 0.05 72 | 73 | - name: "Output Remaining Temp File Count" 74 | command: ls -1 {{ .workdir }} | wc -l 75 | transform: '{{ color "yellow" (printf "Files remaining: %s" (trim .output)) }}' 76 | 77 | - name: "Cleanup" 78 | command: | 79 | # Bypass workdir here 80 | cd {{ .tmp_dir }} 81 | rm -rf {{ .shef_dir }} 82 | echo {{ color "green" (printf "✅ %d temporary files deleted!" .files_to_create) }} 83 | echo {{ color "green" "✅ Temporary directory removed!" }} 84 | echo {{ color "green" "✅ Cleanup complete!" }} 85 | -------------------------------------------------------------------------------- /recipes/demo/tables.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "tables" 3 | description: "A demo of rendering tables" 4 | category: "demo" 5 | help: | 6 | Demonstrates table rendering with various styles and formatting options. 7 | 8 | Usage: 9 | shef demo tables # View examples of different table styles 10 | 11 | Shows rounded, double, light, and ASCII styles, column alignment, and JSON configuration. 12 | operations: 13 | - name: "Basic Table" 14 | command: | 15 | echo "Basic Table Demo:" 16 | echo '{{ table 17 | (makeHeaders "Name" "Age" "City") 18 | (list 19 | (makeRow "John Doe" "34" "New York") 20 | (makeRow "Jane Smith" "28" "San Francisco") 21 | (makeRow "Bob Johnson" "42" "Chicago") 22 | ) 23 | "rounded" 24 | }}' 25 | 26 | - name: "Double Border Table" 27 | command: | 28 | echo "Table with Double Border Style:" 29 | echo '{{ table 30 | (makeHeaders "Product" "Price" "Stock") 31 | (list 32 | (makeRow "Apple" "$1.25" "125") 33 | (makeRow "Orange" "$0.90" "83") 34 | (makeRow "Banana" "$0.50" "42") 35 | ) 36 | "double" 37 | }}' 38 | 39 | - name: "Light Border Table" 40 | command: | 41 | echo "Table with Light Border Style:" 42 | echo '{{ table 43 | (makeHeaders "Product" "Price" "Stock") 44 | (list 45 | (makeRow "Apple" "$1.25" "125") 46 | (makeRow "Orange" "$0.90" "83") 47 | (makeRow "Banana" "$0.50" "42") 48 | ) 49 | "light" 50 | }}' 51 | 52 | - name: "ASCII Style Table" 53 | command: | 54 | echo "ASCII Style Table:" 55 | echo '{{ table 56 | (makeHeaders "Language" "First Released" "Paradigm") 57 | (list 58 | (makeRow "Go" "2009" "Concurrent") 59 | (makeRow "Python" "1991" "Multi-paradigm") 60 | (makeRow "Rust" "2010" "Multi-paradigm") 61 | ) 62 | "ascii" 63 | }}' 64 | 65 | - name: "Aligned Table Example" 66 | command: | 67 | echo "Table with Column Alignment:" 68 | echo '{{ table 69 | (makeHeaders "Product" "Price" "Percentage") 70 | (list 71 | (makeRow "Widget A" "$10.00" "32.4%") 72 | (makeRow "Widget B" "$15.00" "29.1%") 73 | (makeRow "Widget C" "$8.50" "38.5%") 74 | ) 75 | "rounded" 76 | (list "left" "right" "center") 77 | }}' 78 | 79 | - name: "JSON Configured Table" 80 | command: | 81 | echo "JSON Configured Table:" 82 | echo '{{ tableJSON `{ 83 | "headers": ["Project", "Stars", "Language", "License"], 84 | "rows": [ 85 | ["VS Code", "150k+", "TypeScript", "MIT"], 86 | ["React", "200k+", "JavaScript", "MIT"], 87 | ["TensorFlow", "170k+", "C++/Python", "Apache 2.0"], 88 | ["Kubernetes", "95k+", "Go", "Apache 2.0"] 89 | ], 90 | "style": "rounded" 91 | }` }}' 92 | 93 | - name: "JSON Configured Table with Alignment" 94 | command: | 95 | echo "JSON Table with Alignment and Footer:" 96 | echo '{{ tableJSON `{ 97 | "headers": ["Item", "Quantity", "Unit Price", "Total"], 98 | "rows": [ 99 | ["Widget A", "5", "$10.00", "$50.00"], 100 | ["Widget B", "3", "$15.00", "$45.00"], 101 | ["Widget C", "7", "$8.50", "$59.50"] 102 | ], 103 | "align": ["left", "center", "right", "right"], 104 | "footers": ["", "15", "", "$154.50"], 105 | "style": "double" 106 | }` }}' 107 | -------------------------------------------------------------------------------- /recipes/demo/transform.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "transform" 3 | description: "A simple demo of data transformation and pipeline flow" 4 | category: "demo" 5 | help: | 6 | Demonstrates data transformation and pipeline flow between operations. 7 | 8 | Usage: 9 | shef demo transform # See data filtering and formatting in action 10 | operations: 11 | - name: "Generate a Simple List" 12 | id: "generate" 13 | command: | 14 | echo "apple 15 | banana 16 | cherry 17 | dragonfruit 18 | eggplant" 19 | 20 | - name: "Filter 'a' Items" 21 | id: "filter_a" 22 | command: cat 23 | transform: '{{ filter .generate "a" }}' 24 | silent: true 25 | 26 | - name: "Display 'a' Results" 27 | id: "display_a" 28 | command: echo "Items containing 'a':\n{{ color "green" .filter_a }}" 29 | 30 | - name: "Filter 'her' Items" 31 | id: "filter_her" 32 | command: cat 33 | transform: '{{ filter .generate "her" }}' 34 | silent: true 35 | 36 | - name: "Display 'her' Results" 37 | id: "display_her" 38 | command: echo "Items containing 'her':\n{{ color "yellow" .filter_her }}" 39 | -------------------------------------------------------------------------------- /recipes/demo/while.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "while" 3 | description: "A simple demo of the while loop control flow" 4 | category: "demo" 5 | help: | 6 | Demonstrates the 'while' loop control flow with a time-based condition. 7 | 8 | Usage: 9 | shef demo while # Run a loop that continues for 3 seconds 10 | operations: 11 | - name: "Loop While Status is Running" 12 | control_flow: 13 | type: "while" 14 | condition: .duration_s < 3 15 | progress_mode: true 16 | operations: 17 | - name: "Status Update" 18 | command: echo {{ color "yellow" (printf "Performing work... '%s'" .duration_ms_fmt) }} 19 | 20 | - name: "Show Completion" 21 | command: echo {{ color "green" "While loop finished - work is complete!" }} 22 | -------------------------------------------------------------------------------- /recipes/docker/components/container.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "docker.container.list" 3 | inputs: 4 | - id: "filter" 5 | description: "Filter the container results. Must be a string list: 'container-1,container-2,container-3'" 6 | default: "false" 7 | operations: 8 | - id: "docker_list_containers" 9 | command: docker ps --format "{{ `{{ .Names }}` }}" 10 | silent: true 11 | 12 | - uses: "list.overlap" 13 | id: "docker_list_containers" 14 | with: 15 | "list_a": "{{ .docker_list_containers }}" 16 | "list_b": "{{ .filter }}" 17 | condition: .filter != "false" 18 | silent: true 19 | cleanup: 20 | - "filter" 21 | 22 | - command: echo "{{ .docker_list_containers }}" 23 | 24 | - id: "docker.container.select" 25 | inputs: 26 | - id: "filter" 27 | default: "false" 28 | operations: 29 | - uses: "docker.container.list" 30 | id: "containers" 31 | with: 32 | filter: "{{ .filter }}" 33 | silent: true 34 | 35 | - id: "docker_selected_container" 36 | command: echo "{{ .docker_container }}" 37 | silent: true 38 | prompts: 39 | - id: "docker_container" 40 | type: "select" 41 | message: "Select a container" 42 | source_operation: "containers" 43 | 44 | - id: "docker.container.multiselect" 45 | inputs: 46 | - id: "filter" 47 | default: "false" 48 | operations: 49 | - uses: "docker.container.list" 50 | id: "containers" 51 | with: 52 | filter: "{{ .filter }}" 53 | silent: true 54 | 55 | - id: "docker_multiselect_containers" 56 | command: echo {{ .docker_multiselect_containers }} 57 | silent: true 58 | prompts: 59 | - id: "docker_multiselect_containers" 60 | type: "multiselect" 61 | message: "Select one or more containers" 62 | source_operation: "containers" 63 | 64 | - id: "docker.which.shell" 65 | inputs: 66 | - id: "container" 67 | operations: 68 | - command: | 69 | if docker exec {{ .container }} which bash >/dev/null 2>&1; then 70 | echo "bash" 71 | elif docker exec {{ .container }} which zsh >/dev/null 2>&1; then 72 | echo "zsh" 73 | elif docker exec {{ .container }} which dash >/dev/null 2>&1; then 74 | echo "dash" 75 | elif docker exec {{ .container }} which ksh >/dev/null 2>&1; then 76 | echo "ksh" 77 | elif docker exec {{ .container }} which tcsh >/dev/null 2>&1; then 78 | echo "tcsh" 79 | elif docker exec {{ .container }} which fish >/dev/null 2>&1; then 80 | echo "fish" 81 | elif docker exec {{ .container }} which ash >/dev/null 2>&1; then 82 | echo "ash" 83 | elif docker exec {{ .container }} which sh >/dev/null 2>&1; then 84 | echo "sh" 85 | else 86 | exit 1 87 | fi 88 | on_failure: "handle_shell_error" 89 | cleanup: 90 | - "container" 91 | 92 | - id: "handle_shell_error" 93 | command: echo {{ color "red" "No common shell found in container." }} 94 | exit: true 95 | -------------------------------------------------------------------------------- /recipes/docker/logs.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "logs" 3 | description: "Stream logs from a Docker container with advanced filtering" 4 | category: "docker" 5 | help: | 6 | Streams logs from a selected Docker container with filtering options. 7 | 8 | Usage: 9 | shef docker logs [TEXT] # Stream logs with optional text highlighting 10 | shef docker logs -f [TEXT] # Filter to only show matching entries 11 | shef docker logs --lines=N # Set context lines (default: 5) 12 | shef docker logs --since=TIME # Show logs since time (e.g., "1h", "72h", "2025-01-01") 13 | 14 | Press Ctrl+C to stop log streaming. 15 | vars: 16 | "filter_string": "" 17 | "filter_base": "2>&1 | grep --color=always -i" 18 | "filter_lines": 5 19 | "since": "" 20 | operations: 21 | - uses: "docker.container.select" 22 | id: "container" 23 | silent: true 24 | 25 | - name: "Since" 26 | id: "since" 27 | command: echo '--since="{{ .since }}"' 28 | condition: .since != "false" 29 | silent: true 30 | 31 | - name: "Filter Lines" 32 | id: "filter_lines" 33 | command: echo {{ .lines }} 34 | condition: .lines != "false" 35 | silent: true 36 | 37 | - name: "Filter" 38 | id: "filter_string" 39 | command: echo '{{ .filter_base }} -B{{ .filter_lines }} -A{{ .filter_lines }} "{{ .input }}"' 40 | condition: .f == "true" && .input != "false" 41 | silent: true 42 | 43 | - name: "Highlight" 44 | id: "filter_string" 45 | command: echo '{{ .filter_base }} -E "{{ .input }}|$"' 46 | condition: .f != "true" && .input != "false" 47 | silent: true 48 | 49 | - name: "Stream Logs" 50 | command: docker logs --follow {{ .since }} {{ .container }} {{ .filter_string }} 51 | execution_mode: "stream" 52 | -------------------------------------------------------------------------------- /recipes/docker/prune.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "prune" 3 | description: "Prune unused Docker resources" 4 | category: "docker" 5 | help: | 6 | Cleans up unused Docker resources to free disk space. 7 | 8 | Usage: 9 | shef docker prune # Select resources to prune 10 | 11 | Options include dangling/unused images, stopped containers, volumes, networks, and build cache. 12 | Use caution when pruning volumes as this permanently deletes data. 13 | vars: 14 | "initial_usage": "" 15 | "final_usage": "" 16 | operations: 17 | - name: "Initial Disk Usage Message" 18 | command: echo "{{ color "magenta" "Collecting Docker disk usage information..." }}" 19 | 20 | - name: "Initial Disk Usage" 21 | command: docker system df 22 | transform: '{{ color "yellow" .output }}' 23 | 24 | - name: "Select Resources to Clean Up" 25 | silent: true 26 | prompts: 27 | - name: "Resources" 28 | id: "prune_resources" 29 | type: "multiselect" 30 | message: "Select which Docker resources to prune:" 31 | options: 32 | - "1" 33 | - "2" 34 | - "3" 35 | - "4" 36 | - "5" 37 | - "6" 38 | descriptions: 39 | "1": "Dangling images (untagged images not used by containers)" 40 | "2": "All unused images (not used by any container)" 41 | "3": "Stopped containers" 42 | "4": "Unused volumes (not used by containers - data will be permanently deleted)" 43 | "5": "Unused networks (not connected to containers)" 44 | "6": "Build cache (temporary files from image builds)" 45 | 46 | - name: "Clean Dangling Images" 47 | command: | 48 | echo {{ color "magenta" "• Pruning dangling images..." }} 49 | docker image prune -f 50 | condition: '{{ contains .prune_resources "1" }}' 51 | 52 | - name: "Clean All Unused Images" 53 | command: | 54 | echo {{ color "magenta" "• Pruning all unused images..." }} 55 | docker image prune -a -f 56 | condition: '{{ contains .prune_resources "2" }}' 57 | 58 | - name: "Clean Stopped Containers" 59 | command: | 60 | echo {{ color "magenta" "• Pruning stopped containers..." }} 61 | docker container prune -f 62 | condition: '{{ contains .prune_resources "3" }}' 63 | 64 | - name: "Clean Unused Volumes" 65 | command: | 66 | echo {{ color "magenta" "• Pruning unused volumes..." }} 67 | docker volume prune -f 68 | condition: '{{ contains .prune_resources "4" }}' 69 | 70 | - name: "Clean Unused Networks" 71 | command: | 72 | echo {{ color "magenta" "• Pruning unused networks..." }} 73 | docker network prune -f 74 | condition: '{{ contains .prune_resources "5" }}' 75 | 76 | - name: "Clean Build Cache" 77 | command: | 78 | echo {{ color "magenta" "• Pruning build cache..." }} 79 | docker builder prune -f 80 | condition: '{{ contains .prune_resources "6" }}' 81 | 82 | - command: echo {{ color "magenta" "• Complete!" }} 83 | 84 | - name: "Final Disk Usage Message" 85 | command: echo {{ color "magenta" "Collecting Docker disk usage information..." }} 86 | 87 | - name: "Final Disk Usage" 88 | command: docker system df 89 | transform: '{{ color "green" .output }}' 90 | -------------------------------------------------------------------------------- /recipes/docker/shell.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "shell" 3 | description: "Shell into a Docker container" 4 | category: "docker" 5 | help: | 6 | Opens an interactive shell session into a running Docker container. 7 | 8 | Usage: 9 | shef docker shell # Select container and open interactive shell 10 | 11 | Exit the shell session with the 'exit' command. 12 | operations: 13 | - uses: "docker.container.select" 14 | id: "docker_container" 15 | silent: true 16 | 17 | - uses: "docker.which.shell" 18 | id: "docker_shell" 19 | with: 20 | container: "{{ .docker_container }}" 21 | silent: true 22 | 23 | - name: "Execute Shell" 24 | command: docker exec -it {{ .docker_container }} {{ .docker_shell }} 25 | execution_mode: "interactive" 26 | -------------------------------------------------------------------------------- /recipes/gcp/components/project.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "gcp.project.current" 3 | operations: 4 | - command: gcloud config get-value project 5 | 6 | - id: "gcp.project.select" 7 | operations: 8 | - uses: "gcp.project.current" 9 | id: "current_project" 10 | silent: true 11 | 12 | - id: "projects_list" 13 | command: gcloud projects list --filter="NOT project_id:sys-*" --format="value(project_id,name)" | awk '{print $1 "=" $2}' 14 | silent: true 15 | 16 | - id: "selected_project" 17 | command: echo "{{ .gcp_project_prompt }}" 18 | silent: true 19 | prompts: 20 | - id: "gcp_project_prompt" 21 | type: "select" 22 | message: "Select a GCP project" 23 | source_operation: "projects_list" 24 | default: "{{ .current_project }}" 25 | 26 | - command: gcloud config set project {{ .selected_project }} 27 | silent: true 28 | 29 | - command: echo "{{ .selected_project }}" 30 | -------------------------------------------------------------------------------- /recipes/gcp/project.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "project" 3 | description: "Select a GCP project" 4 | category: "gcp" 5 | help: | 6 | Selects or displays the current Google Cloud Platform project. 7 | 8 | Usage: 9 | shef gcp project # Select a GCP project 10 | shef gcp project -w # Show current project without changing 11 | operations: 12 | - name: "GCP Project Select" 13 | id: "project" 14 | uses: "gcp.project.select" 15 | condition: .w != "true" && .which != "true" 16 | silent: true 17 | on_success: "exit" 18 | 19 | - name: "GCP Current Project" 20 | id: "project" 21 | uses: "gcp.project.current" 22 | condition: .w == "true" || .which == "true" 23 | silent: true 24 | on_success: "exit" 25 | 26 | - name: "Show Selected Project" 27 | id: "exit" 28 | command: echo "Selected project:" {{ style "bold" (color "green" .project) }} 29 | exit: true 30 | -------------------------------------------------------------------------------- /recipes/gcp/secret.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "secret" 3 | description: "Fetch a GCP secret and copy its value to the clipboard" 4 | category: "gcp" 5 | help: | 6 | Fetches a Google Cloud Platform secret and copies its value to the clipboard. 7 | 8 | Usage: 9 | shef gcp secret # Select project and secret to copy 10 | shef gcp secret -f # Skip project selection prompt 11 | operations: 12 | - name: "GCP Project Select" 13 | id: "gcp.project.current" 14 | uses: "gcp.project.select" 15 | condition: .f != "true" && .force != "true" 16 | silent: true 17 | 18 | - name: "List GCP Secrets" 19 | id: "list_secrets" 20 | command: gcloud secrets list --format="value(name)" 21 | silent: true 22 | 23 | - name: "Select Secret" 24 | id: "selected_secret" 25 | command: echo {{ .secret_name }} 26 | silent: true 27 | prompts: 28 | - name: "Secret Select" 29 | id: "secret_name" 30 | type: "select" 31 | message: "Select a GCP secret" 32 | source_operation: "list_secrets" 33 | 34 | - name: "Fetch Secret Value" 35 | id: "secret_value" 36 | command: gcloud secrets versions access latest --secret={{ .selected_secret }} 37 | transform: "{{ .output | trim }}" 38 | silent: true 39 | 40 | - uses: "clipboard.copy" 41 | with: 42 | value: "{{ .secret_value }}" 43 | -------------------------------------------------------------------------------- /recipes/git/lint.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "lint" 3 | description: "Run basic lint for all staged and unstaged files" 4 | category: "git" 5 | help: | 6 | Lints git staged and unstaged files for common issues. 7 | 8 | Usage: 9 | shef git lint # Check files for trailing whitespace and missing EOF newlines 10 | operations: 11 | - uses: "git.staged+unstaged" 12 | id: "files" 13 | silent: true 14 | 15 | - command: echo "{{ color "yellow" "No files to lint" }}" 16 | condition: .files == "" 17 | exit: true 18 | 19 | - control_flow: 20 | type: "foreach" 21 | collection: "{{ .files }}" 22 | as: "file_to_lint" 23 | progress_mode: true 24 | operations: 25 | - command: 'echo "Linting: {{ style "dim" .file_to_lint }}"' 26 | 27 | - uses: "lint.trailing" 28 | with: 29 | file: "{{ .file_to_lint }}" 30 | 31 | - uses: "lint.eof" 32 | with: 33 | file: "{{ .file_to_lint }}" 34 | 35 | - command: echo "{{ color "green" "Complete!" }}" 36 | -------------------------------------------------------------------------------- /recipes/git/stash.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "stash" 3 | description: "Easily list, save, apply, and drop git stashes" 4 | category: "git" 5 | help: | 6 | Manages Git stashes with an interactive interface for listing, saving, applying, and dropping. 7 | 8 | Usage: 9 | shef git stash # Select from available actions (List, Save, Apply, Drop) 10 | 11 | Stash names are prefixed with the current branch name for better organization. 12 | operations: 13 | - name: "Stash Action" 14 | id: "action" 15 | prompts: 16 | - name: "Stash Action" 17 | id: "stash_action" 18 | type: "select" 19 | message: "Which stash action do you wish to run?" 20 | options: 21 | - "List" 22 | - "Save" 23 | - "Apply" 24 | - "Drop" 25 | default: "Save" 26 | 27 | - name: "Get Current Branch" 28 | id: "current_branch" 29 | command: git rev-parse --abbrev-ref HEAD 30 | silent: true 31 | 32 | - name: "Git Changes" 33 | id: "git_changes" 34 | command: git status --porcelain 35 | silent: true 36 | 37 | # Save Stash 38 | - name: "Handle No Changes to Stash" 39 | command: echo {{ color "yellow" "No changes to stash." }} 40 | condition: .git_changes == "" && .stash_action == "Save" 41 | exit: true 42 | 43 | - name: "Show git status" 44 | command: git status --porcelain 45 | execution_mode: "stream" 46 | condition: .stash_action == "Save" 47 | 48 | - name: "Get New Stash Name" 49 | prompts: 50 | - name: "Stash Name" 51 | id: "stash_name" 52 | type: "input" 53 | message: "Enter stash name" 54 | condition: .stash_action == "Save" 55 | 56 | - name: "Save New Stash" 57 | command: 'git stash save -u "({{ .current_branch }}): {{ .stash_name }}"' 58 | silent: true 59 | condition: .stash_action == "Save" && stash_name != "" 60 | on_success: "save_success" 61 | on_failure: "stash_error" 62 | 63 | - name: "Save Success" 64 | id: "save_success" 65 | command: echo {{ color "green" "Stash saved!" }} 66 | exit: true 67 | 68 | # List, Apply, and Drop Stashes 69 | - name: "Get Indexes with Messages" 70 | id: "index_messages" 71 | command: | 72 | git stash list | sed -E 's/(stash@\{[0-9]+\}): On ([^:]+): (.*)/\1||||\3/g' 73 | silent: true 74 | 75 | - name: "Get Messages" 76 | id: "messages" 77 | command: | 78 | git stash list | sed -E 's/(stash@\{[0-9]+\}): On ([^:]+): (.*)/\3/g' 79 | silent: true 80 | 81 | - name: "Handle No Stashes" 82 | command: echo {{ color "yellow" "No stashes found." }} 83 | condition: .index_messages == "" 84 | exit: true 85 | 86 | # List Stashes 87 | - name: "List Stashes" 88 | command: echo "{{ .messages }}" 89 | condition: .stash_action == "List" 90 | exit: true 91 | 92 | # Apply and Drop stash 93 | - name: "Select Stash" 94 | prompts: 95 | - name: "Selected Message" 96 | id: "selected_message" 97 | type: "select" 98 | message: "Select a stash" 99 | source_operation: "messages" 100 | 101 | - name: "Filter Messages" 102 | id: "filter" 103 | transform: '{{ filter .index_messages .selected_message }}' 104 | silent: true 105 | 106 | - name: "Cut Index" 107 | id: "index" 108 | transform: '{{ cut .filter "||||" 0 }}' 109 | silent: true 110 | 111 | # Apply Stash 112 | - name: "Apply Stash" 113 | command: git stash apply {{ .index }} 114 | silent: true 115 | condition: .stash_action == "Apply" 116 | on_success: "apply_success" 117 | on_failure: "stash_error" 118 | 119 | - name: "Apply Success" 120 | id: "apply_success" 121 | command: echo {{ color "green" "Stash applied!" }} 122 | exit: true 123 | 124 | # Drop Stash 125 | - name: "Confirm Drop Stash" 126 | condition: .stash_action == "Drop" 127 | prompts: 128 | - name: "Confirm Drop" 129 | id: "confirm_drop" 130 | type: "confirm" 131 | message: "Drop this stash?" 132 | default: "false" 133 | 134 | - name: "Drop Stash" 135 | command: git stash drop {{ .index }} 136 | silent: true 137 | condition: .stash_action == "Drop" && .confirm_drop == "true" 138 | on_success: "drop_success" 139 | on_failure: "stash_error" 140 | 141 | - name: "Drop Success" 142 | id: "drop_success" 143 | command: echo {{ color "green" "Stash dropped!" }} 144 | condition: .confirm_drop == "true" 145 | exit: true 146 | 147 | - name: "Stash Error" 148 | id: "stash_error" 149 | command: echo {{ color "red" "There was a stash error. Please try again." }} 150 | exit: true 151 | -------------------------------------------------------------------------------- /recipes/git/update.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "update" 3 | description: "Update all git repositories within the current directory" 4 | category: "git" 5 | help: | 6 | Updates all Git repositories in the current directory with 'git pull'. 7 | 8 | Usage: 9 | shef git update # Pull updates for all repositories in current directory 10 | 11 | Only processes immediate subdirectories (depth=1), skipping non-git directories. 12 | operations: 13 | - name: "Find Subdirectories" 14 | id: "find_dirs" 15 | command: find . -maxdepth 1 -type d -not -path "." | sort -f 16 | silent: true 17 | transform: "{{ .output | trim | split '\n' }}" 18 | 19 | - name: "Process Directories" 20 | id: "process_dirs" 21 | control_flow: 22 | type: "foreach" 23 | collection: "{{ .find_dirs }}" 24 | as: "dir" 25 | operations: 26 | - name: "Check If Git Repository" 27 | id: "check_git_repo" 28 | command: ls -d "{{ .dir }}/.git" 2>/dev/null || echo "" 29 | output_format: trim 30 | silent: true 31 | transform: "{{ if .output }}is_git_repo{{ else }}not_git_repo{{ end }}" 32 | 33 | - name: "Echo Repository" 34 | id: "echo_repo" 35 | command: echo {{ color "yellow" "Updating" }} {{ color "magenta" .dir }} 36 | output_format: trim 37 | condition: .check_git_repo == "is_git_repo" 38 | 39 | - name: "Update Repository" 40 | id: "update_repo" 41 | command: git -C {{ .dir }} pull 42 | execution_mode: "stream" 43 | condition: .check_git_repo == "is_git_repo" 44 | 45 | - name: "Skip Repository" 46 | id: "skip_repo" 47 | command: echo {{ style "dim" (printf "Skipping %s because it is not a git repository" .dir) }} 48 | output_format: trim 49 | condition: .check_git_repo != "is_git_repo" 50 | 51 | - name: "Display Summary" 52 | command: | 53 | echo "" 54 | echo {{ color "green" "Complete!" }} 55 | -------------------------------------------------------------------------------- /recipes/git/version.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "version" 3 | description: "Create and push version tags to Git" 4 | category: "git" 5 | help: | 6 | Creates and pushes version tags to Git using semantic versioning (MAJOR.MINOR.PATCH). 7 | 8 | Usage: 9 | shef git version # Create and optionally push a new version tag 10 | 11 | Allows major (1.0.0 → 2.0.0), minor (1.0.0 → 1.1.0), or patch (1.0.0 → 1.0.1) increments. 12 | operations: 13 | - name: "Check Current Branch" 14 | id: "branch" 15 | command: | 16 | git rev-parse --abbrev-ref HEAD 17 | git fetch --tags 18 | git fetch --all 19 | silent: true 20 | 21 | - name: "Find Latest Tag" 22 | id: "latest_tag" 23 | command: git tag -l "v*.*.*" --sort=-v:refname | head -n1 || echo "" 24 | silent: true 25 | transform: | 26 | {{ $tag := .output | trim }} 27 | {{ if eq $tag "" }} 28 | {{ "NO_EXISTING_TAGS" }} 29 | {{ else }} 30 | {{ $tag }} 31 | {{ end }} 32 | 33 | - name: "Existing Tags Message" 34 | id: "existing_tags_message" 35 | command: echo {{ color "magenta" "The current version:" }} {{ style "bold" (color "yellow" .latest_tag) }} 36 | condition: .latest_tag != "NO_EXISTING_TAGS" 37 | 38 | - name: "No Existing Tags Message" 39 | id: "no_existing_tags_message" 40 | command: echo {{ color "yellow" "No existing version tags found." }} 41 | condition: .latest_tag == "NO_EXISTING_TAGS" 42 | 43 | - name: "Choose Version" 44 | prompts: 45 | - name: "Version" 46 | id: "version" 47 | type: "select" 48 | message: "{{ if eq .latest_tag `NO_EXISTING_TAGS` }}Select the initial version{{ else }}Choose version{{ end }}" 49 | options: 50 | - "major" 51 | - "minor" 52 | - "patch" 53 | default: "{{ if eq .latest_tag `NO_EXISTING_TAGS` }}minor{{ else }}patch{{ end }}" 54 | 55 | - name: "Calculate New Version" 56 | id: "new_version" 57 | silent: true 58 | command: | 59 | if [[ "{{ .latest_tag }}" == "NO_EXISTING_TAGS" ]]; then 60 | # First version case 61 | if [[ "{{ .version }}" == "custom" ]]; then 62 | printf "{{ .custom_version }}" 63 | elif [[ "{{ .version }}" == "major" ]]; then 64 | printf "v1.0.0" 65 | elif [[ "{{ .version }}" == "minor" ]]; then 66 | printf "v0.1.0" 67 | else 68 | printf "v0.0.1" 69 | fi 70 | else 71 | # Existing version case 72 | TAG="{{ .latest_tag }}" 73 | # Remove the 'v' prefix 74 | VERSION="${TAG#v}" 75 | # Split into parts 76 | IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" 77 | 78 | if [[ "{{ .version }}" == "custom" ]]; then 79 | printf "{{ .custom_version }}" 80 | elif [[ "{{ .version }}" == "major" ]]; then 81 | MAJOR=$((MAJOR + 1)) 82 | MINOR=0 83 | PATCH=0 84 | printf "v$MAJOR.$MINOR.$PATCH" 85 | elif [[ "{{ .version }}" == "minor" ]]; then 86 | MINOR=$((MINOR + 1)) 87 | PATCH=0 88 | printf "v$MAJOR.$MINOR.$PATCH" 89 | else 90 | # Patch is the default 91 | PATCH=$((PATCH + 1)) 92 | printf "v$MAJOR.$MINOR.$PATCH" 93 | fi 94 | fi 95 | 96 | - name: "Display New Version" 97 | id: "display_new_version" 98 | command: echo {{ color "magenta" "The new version:" }} {{ style "bold" (color "yellow" .new_version) }} 99 | condition: .latest_tag != "NO_EXISTING_TAGS" 100 | 101 | - name: "Display Initial Version" 102 | id: "display_initial_version" 103 | command: echo {{ color "magenta" "Initial version:" }} {{ style "bold" (color "yellow" .new_version) }} 104 | condition: .latest_tag == "NO_EXISTING_TAGS" 105 | 106 | - name: "Prepare Tag" 107 | id: "tag_prep" 108 | prompts: 109 | - name: "Tag Message" 110 | id: "tag_message" 111 | type: "input" 112 | message: "Please provide a release message [optional]" 113 | default: "{{ if eq .latest_tag `NO_EXISTING_TAGS` }}Initial release {{ .new_version }}{{ else }}Release {{ .new_version }}{{ end }}" 114 | - name: "Confirm Create" 115 | id: "confirm_create" 116 | type: "confirm" 117 | message: "Create tag {{ .new_version }} locally?" 118 | default: "true" 119 | 120 | - name: "Create Tag" 121 | id: "create_tag" 122 | command: git tag -a {{ .new_version }} -m "{{ .tag_message }}" 123 | condition: .confirm_create == "true" 124 | 125 | - name: "Confirm Push" 126 | id: "confirm_push_op" 127 | condition: create_tag.success 128 | prompts: 129 | - name: "Confirm Push" 130 | id: "confirm_push" 131 | type: "confirm" 132 | message: "Push tag {{ .new_version }} to origin?" 133 | default: "true" 134 | 135 | - name: "Push Tag Message" 136 | id: "push_tag_message" 137 | command: echo {{ color "magenta" (printf "Pushing the new tag %s to origin..." .new_version) }} 138 | condition: create_tag.success && .confirm_push == "true" 139 | 140 | - name: "Push Tag" 141 | id: "push_tag" 142 | command: git push origin {{ .new_version }} 143 | condition: create_tag.success && .confirm_push == "true" 144 | 145 | - name: "Show Result" 146 | command: | 147 | {{ if eq .confirm_push "true" }} 148 | echo {{ color "green" "Successfully created and pushed" }} {{ style "bold" (color "yellow" .new_version) }} {{ color "green" "to origin" }} 149 | {{ else }} 150 | echo {{ color "green" "Successfully created" }} {{ style "bold" (color "yellow" .new_version) }} 151 | {{ end }} 152 | condition: create_tag.success 153 | -------------------------------------------------------------------------------- /recipes/utils/components/clipboard.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "clipboard.copy" 3 | inputs: 4 | - id: "value" 5 | operations: 6 | - id: "clipboard_cmd" 7 | command: | 8 | if [[ "$OSTYPE" == "darwin"* && -x "$(command -v pbcopy)" ]]; then 9 | echo "pbcopy" 10 | elif [[ "$OSTYPE" == "linux-gnu"* && -x "$(command -v xclip)" ]]; then 11 | echo "xclip -selection clipboard" 12 | elif [[ "$OSTYPE" == "linux-gnu"* && -x "$(command -v xsel)" ]]; then 13 | echo "xsel --clipboard" 14 | elif [[ ("$OSTYPE" == "msys" || "$OSTYPE" == "win32") && -x "$(command -v clip)" ]]; then 15 | echo "clip" 16 | else 17 | echo "none" 18 | fi 19 | silent: true 20 | 21 | - command: printf "%s" "{{ .value }}" | {{ .clipboard_cmd }} 22 | condition: .clipboard_cmd != "none" 23 | cleanup: 24 | - "value" 25 | 26 | - command: echo {{ color "green" "Copied to the clipboard!" }} 27 | condition: .clipboard_cmd != "none" 28 | 29 | - command: echo {{ color "red" "Unable to copy to the clipboard, no clipboard utility available." }} 30 | condition: .clipboard_cmd == "none" 31 | -------------------------------------------------------------------------------- /recipes/utils/components/crypto.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "crypto.md5" 3 | inputs: 4 | - id: "data" 5 | operations: 6 | - command: printf "{{ .data }}" | md5 7 | output_format: "trim" 8 | cleanup: 9 | - "data" 10 | 11 | - id: "crypto.sha1" 12 | inputs: 13 | - id: "data" 14 | operations: 15 | - command: printf "{{ .data }}" | shasum -a 1 | cut -d ' ' -f 1 16 | output_format: "trim" 17 | cleanup: 18 | - "data" 19 | 20 | - id: "crypto.sha256" 21 | inputs: 22 | - id: "data" 23 | operations: 24 | - command: printf "{{ .data }}" | shasum -a 256 | cut -d ' ' -f 1 25 | output_format: "trim" 26 | cleanup: 27 | - "data" 28 | 29 | - id: "crypto.sha512" 30 | inputs: 31 | - id: "data" 32 | operations: 33 | - command: printf "{{ .data }}" | shasum -a 512 | cut -d ' ' -f 1 34 | output_format: "trim" 35 | cleanup: 36 | - "data" 37 | 38 | - id: "crypto.rot13" 39 | inputs: 40 | - id: "data" 41 | operations: 42 | - command: printf "{{ .data }}" | tr 'A-Za-z' 'N-ZA-Mn-za-m' 43 | output_format: "trim" 44 | cleanup: 45 | - "data" 46 | 47 | - id: "crypto.aes.encrypt" 48 | inputs: 49 | - id: "data" 50 | required: true 51 | - id: "passphrase" 52 | required: true 53 | - id: "iterations" 54 | default: 10000 55 | operations: 56 | - command: | 57 | set +H 58 | openssl enc -aes-256-cbc -a -salt -pbkdf2 -iter {{ .iterations }} -pass pass:"{{ .passphrase }}" << 'EOF' 59 | {{ .data }} 60 | EOF 61 | set -H 62 | output_format: "trim" 63 | cleanup: 64 | - "data" 65 | - "passphrase" 66 | - "iterations" 67 | 68 | - id: "crypto.aes.decrypt" 69 | inputs: 70 | - id: "data" 71 | required: true 72 | - id: "passphrase" 73 | required: true 74 | - id: "iterations" 75 | default: 10000 76 | operations: 77 | - command: | 78 | set +H 79 | openssl enc -aes-256-cbc -a -d -salt -pbkdf2 -iter {{ .iterations }} -pass pass:"{{ .passphrase }}" << 'EOF' 80 | {{ .data }} 81 | EOF 82 | set -H 83 | output_format: "trim" 84 | cleanup: 85 | - "data" 86 | - "passphrase" 87 | - "iterations" 88 | -------------------------------------------------------------------------------- /recipes/utils/components/data.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "data.base64.encode" 3 | inputs: 4 | - id: "data" 5 | required: true 6 | operations: 7 | - command: printf "{{ .data }}" | base64 8 | output_format: "trim" 9 | cleanup: 10 | - "data" 11 | 12 | - id: "data.base64.decode" 13 | inputs: 14 | - id: "data" 15 | required: true 16 | operations: 17 | - command: printf "{{ .data }}" | base64 -d 18 | output_format: "trim" 19 | cleanup: 20 | - "data" 21 | 22 | - id: "data.hex.encode" 23 | inputs: 24 | - id: "data" 25 | required: true 26 | operations: 27 | - command: printf "{{ .data }}" | xxd -p -c 1000000 28 | output_format: "trim" 29 | cleanup: 30 | - "data" 31 | 32 | - id: "data.hex.decode" 33 | inputs: 34 | - id: "data" 35 | required: true 36 | operations: 37 | - command: printf "{{ .data }}" | xxd -p -r 38 | output_format: "trim" 39 | cleanup: 40 | - "data" 41 | 42 | - id: "data.binary.encode" 43 | inputs: 44 | - id: "data" 45 | required: true 46 | operations: 47 | - command: echo "{{ .data }}" | perl -ne 'print join("", map {sprintf("%08b", ord($_))} split("", $_))' 48 | output_format: "trim" 49 | cleanup: 50 | - "data" 51 | 52 | - id: "data.binary.decode" 53 | inputs: 54 | - id: "data" 55 | required: true 56 | operations: 57 | - command: echo "{{ .data }}" | perl -ne 's/([01]{8})/chr(oct("0b$1"))/ge; print' 58 | output_format: "trim" 59 | cleanup: 60 | - "data" 61 | -------------------------------------------------------------------------------- /recipes/utils/components/dir.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "dir.exists" 3 | inputs: 4 | - id: "dir" 5 | required: true 6 | operations: 7 | - command: '[ -d "{{ .dir }}" ] && echo "true" || echo "false"' 8 | cleanup: 9 | - "dir" 10 | 11 | - id: "dir.create" 12 | inputs: 13 | - id: "dir" 14 | required: true 15 | operations: 16 | - command: mkdir -p {{ .dir }} 17 | cleanup: 18 | - "dir" 19 | 20 | - id: "dir.delete" 21 | inputs: 22 | - id: "dir" 23 | required: true 24 | - id: "force" 25 | default: false 26 | operations: 27 | - command: rmdir {{ .dir }} 28 | condition: .force == "false" 29 | 30 | - command: rm -rf {{ .dir }} 31 | condition: .force == "true" 32 | 33 | - cleanup: 34 | - "dir" 35 | - "force" 36 | 37 | - id: "dir.rename" 38 | inputs: 39 | - id: "old" 40 | required: true 41 | - id: "new" 42 | required: true 43 | operations: 44 | - command: mv {{ .old }} {{ .new }} 45 | cleanup: 46 | - "old" 47 | - "new" 48 | 49 | - id: "dir.sync" 50 | inputs: 51 | - id: "src" 52 | required: true 53 | - id: "dst" 54 | required: true 55 | operations: 56 | - command: | 57 | src="{{ .src }}" 58 | dst="{{ .dst }}" 59 | rsync -a --delete "${src%/}/" "${dst%/}/" 60 | cleanup: 61 | - "src" 62 | - "dst" 63 | 64 | - id: "dir.pwd" 65 | operations: 66 | - command: pwd 67 | output_format: "trim" 68 | 69 | - id: "dir.list" 70 | inputs: 71 | - id: "dir" 72 | operations: 73 | - uses: "dir.pwd" 74 | id: "dir" 75 | condition: .dir == "false" 76 | silent: true 77 | 78 | - command: ls -1 -a 79 | workdir: "{{ .dir }}" 80 | cleanup: 81 | - "dir" 82 | -------------------------------------------------------------------------------- /recipes/utils/components/generate.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "generate.password" 3 | inputs: 4 | - id: "length" 5 | default: 16 6 | operations: 7 | - command: LC_ALL=C tr -dc 'A-Za-z0-9!@#$%^&*()-_=+' < /dev/urandom | head -c "{{ .length }}" 8 | output_format: "trim" 9 | cleanup: 10 | - "length" 11 | 12 | - id: "generate.alphanumeric" 13 | inputs: 14 | - id: "length" 15 | default: 16 16 | operations: 17 | - command: LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "{{ .length }}" 18 | output_format: "trim" 19 | cleanup: 20 | - "length" 21 | 22 | - id: "generate.number" 23 | inputs: 24 | - id: "min" 25 | default: 1 26 | - id: "max" 27 | default: 1000 28 | operations: 29 | - command: echo "$((RANDOM % ({{ .max }} - {{ .min }} + 1) + {{ .min }}))" 30 | output_format: "trim" 31 | cleanup: 32 | - "min" 33 | - "max" 34 | 35 | - id: "generate.date" 36 | inputs: 37 | - id: "format" 38 | default: "%Y-%m-%d" 39 | operations: 40 | - command: date +"{{ .format }}" 41 | output_format: "trim" 42 | cleanup: 43 | - "format" 44 | 45 | - id: "generate.time" 46 | inputs: 47 | - id: "format" 48 | default: "%H:%M:%S" 49 | operations: 50 | - uses: "generate.date" 51 | with: 52 | format: "{{ .format }}" 53 | 54 | - cleanup: 55 | - "format" 56 | 57 | - id: "generate.timestamp" 58 | operations: 59 | - command: date +%s 60 | output_format: "trim" 61 | 62 | - id: "generate.hex_color" 63 | operations: 64 | - command: printf "#%06x" $((RANDOM * RANDOM % 16777215)) 65 | output_format: "trim" 66 | 67 | - id: "generate.mac" 68 | operations: 69 | - command: printf "%02x:%02x:%02x:%02x:%02x:%02x" $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) 70 | output_format: "trim" 71 | 72 | - id: "generate.ipv4" 73 | operations: 74 | - command: printf "%d.%d.%d.%d" $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) 75 | output_format: "trim" 76 | 77 | - id: "generate.ipv6" 78 | operations: 79 | - 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)) 80 | output_format: "trim" 81 | 82 | - id: "generate.uuid4" 83 | operations: 84 | - command: uuidgen 85 | output_format: "trim" 86 | 87 | - id: "generate.uuid5" 88 | inputs: 89 | - id: "namespace" 90 | default: "url" 91 | - id: "name" 92 | required: true 93 | operations: 94 | - uses: "generate.uuid5.namespace" 95 | id: "namespace_uuid" 96 | with: 97 | namespace: "{{ .namespace }}" 98 | silent: true 99 | 100 | - uses: "python" 101 | id: "uuid5" 102 | with: 103 | code: | 104 | import uuid 105 | 106 | print(uuid.uuid5(uuid.UUID('{{ .namespace_uuid }}'), '{{ .name }}')) 107 | silent: true 108 | 109 | - command: echo "{{ .uuid5 }}" 110 | output_format: "trim" 111 | cleanup: 112 | - "uuid5" 113 | - "namespace" 114 | - "name" 115 | 116 | - id: "generate.uuid5.namespace" 117 | inputs: 118 | - id: "namespace" 119 | default: "url" 120 | operations: 121 | - uses: "string.lower" 122 | id: "namespace" 123 | with: 124 | string: "{{ .namespace }}" 125 | silent: true 126 | 127 | - command: | 128 | case "{{ .namespace }}" in 129 | dns) 130 | echo "6ba7b810-9dad-11d1-80b4-00c04fd430c8" 131 | ;; 132 | url) 133 | echo "6ba7b811-9dad-11d1-80b4-00c04fd430c8" 134 | ;; 135 | oid) 136 | echo "6ba7b812-9dad-11d1-80b4-00c04fd430c8" 137 | ;; 138 | x500) 139 | echo "6ba7b814-9dad-11d1-80b4-00c04fd430c8" 140 | ;; 141 | *) 142 | echo "6ba7b811-9dad-11d1-80b4-00c04fd430c8" 143 | ;; 144 | esac 145 | output_format: "trim" 146 | cleanup: 147 | - "namespace" 148 | -------------------------------------------------------------------------------- /recipes/utils/components/git.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "git.repo.exists" 3 | inputs: 4 | - id: "dir" 5 | operations: 6 | - uses: "dir.pwd" 7 | id: "dir" 8 | condition: .dir == "false" 9 | silent: true 10 | 11 | - uses: "user.path.expand" 12 | id: "dir_to_check" 13 | with: 14 | path: "{{ .dir }}" 15 | silent: true 16 | 17 | - uses: "dir.exists" 18 | id: "exists" 19 | with: 20 | dir: "{{ .dir_to_check }}" 21 | silent: true 22 | 23 | - command: | 24 | cd {{ .dir_to_check }} 25 | git rev-parse --is-inside-work-tree 2>/dev/null || echo "false" 26 | condition: .exists == "true" 27 | output_format: "trim" 28 | 29 | - command: echo "false" 30 | condition: .exists == "false" 31 | output_format: "trim" 32 | cleanup: 33 | - "dir" 34 | - "dir_to_check" 35 | - "exists" 36 | 37 | - id: "git.staged" 38 | operations: 39 | - command: git diff --name-only --staged -z | xargs -0 -I{} readlink -f {} 40 | output_format: "trim" 41 | 42 | - id: "git.unstaged" 43 | operations: 44 | - command: git ls-files --others --modified --exclude-standard -z | xargs -0 -I{} readlink -f {} 45 | output_format: "trim" 46 | 47 | - id: "git.staged+unstaged" 48 | operations: 49 | - uses: "git.staged" 50 | id: "staged" 51 | silent: true 52 | 53 | - uses: "git.unstaged" 54 | id: "unstaged" 55 | silent: true 56 | 57 | - uses: "list.combine" 58 | with: 59 | list_a: "{{ .staged }}" 60 | list_b: "{{ .unstaged }}" 61 | 62 | - id: "git.count.staged" 63 | operations: 64 | - uses: "git.staged" 65 | id: "files" 66 | silent: true 67 | 68 | - uses: "list.length" 69 | id: "count" 70 | with: 71 | list: "{{ .files }}" 72 | silent: true 73 | 74 | - command: echo "{{ .count }}" 75 | output_format: "trim" 76 | cleanup: 77 | - "files" 78 | - "count" 79 | 80 | - id: "git.count.unstaged" 81 | operations: 82 | - uses: "git.unstaged" 83 | id: "files" 84 | silent: true 85 | 86 | - uses: "list.length" 87 | id: "count" 88 | with: 89 | list: "{{ .files }}" 90 | silent: true 91 | 92 | - command: echo "{{ .count }}" 93 | output_format: "trim" 94 | cleanup: 95 | - "files" 96 | - "count" 97 | 98 | - id: "git.count.staged+unstaged" 99 | operations: 100 | - uses: "git.staged+unstaged" 101 | id: "files" 102 | silent: true 103 | 104 | - uses: "list.length" 105 | id: "count" 106 | with: 107 | list: "{{ .files }}" 108 | silent: true 109 | 110 | - command: echo "{{ .count }}" 111 | output_format: "trim" 112 | cleanup: 113 | - "files" 114 | - "count" 115 | -------------------------------------------------------------------------------- /recipes/utils/components/json.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "json.validate._internal" 3 | inputs: 4 | - id: "json" 5 | required: true 6 | - id: "filter" 7 | required: true 8 | - id: "verbose" 9 | default: "false" 10 | operations: 11 | - command: | 12 | if output=$(jq '{{ .filter }}' << 'EOF' 2>&1 13 | {{ .json }} 14 | EOF 15 | ); then 16 | echo "true" 17 | else 18 | if [[ "{{ .verbose }}" == "true" ]]; then 19 | echo "$output" 20 | else 21 | echo "false" 22 | fi 23 | fi 24 | output_format: "trim" 25 | cleanup: 26 | - "json" 27 | - "filter" 28 | - "verbose" 29 | 30 | - id: "json.validate._internal.type" 31 | inputs: 32 | - id: "json" 33 | required: true 34 | - id: "type" 35 | required: true 36 | - id: "verbose" 37 | default: "false" 38 | operations: 39 | - uses: "json.validate._internal" 40 | with: 41 | json: "{{ .json }}" 42 | filter: 'if type != "{{ .type }}" then error("Not a valid JSON {{ .type }}") else . end' 43 | verbose: "{{ .verbose }}" 44 | 45 | - id: "json.jq" 46 | inputs: 47 | - id: "json" 48 | required: true 49 | - id: "filter" 50 | required: true 51 | - id: "options" 52 | default: "" 53 | operations: 54 | - command: | 55 | jq {{ .options }} '{{ .filter }}' << 'EOF' 56 | {{ .json }} 57 | EOF 58 | output_format: "trim" 59 | cleanup: 60 | - "json" 61 | - "filter" 62 | - "options" 63 | 64 | - id: "json.validate" 65 | inputs: 66 | - id: "json" 67 | required: true 68 | - id: "verbose" 69 | default: "false" 70 | operations: 71 | - uses: "json.validate._internal" 72 | with: 73 | json: "{{ .json }}" 74 | filter: "." 75 | verbose: "{{ .verbose }}" 76 | 77 | - id: "json.validate.array" 78 | inputs: 79 | - id: "json" 80 | required: true 81 | - id: "verbose" 82 | default: "false" 83 | operations: 84 | - uses: "json.validate._internal.type" 85 | with: 86 | json: "{{ .json }}" 87 | type: "array" 88 | verbose: "{{ .verbose }}" 89 | 90 | - id: "json.validate.object" 91 | inputs: 92 | - id: "json" 93 | required: true 94 | - id: "verbose" 95 | default: "false" 96 | operations: 97 | - uses: "json.validate._internal.type" 98 | with: 99 | json: "{{ .json }}" 100 | type: "object" 101 | verbose: "{{ .verbose }}" 102 | 103 | - id: "json.validate.string" 104 | inputs: 105 | - id: "json" 106 | required: true 107 | - id: "verbose" 108 | default: "false" 109 | operations: 110 | - uses: "json.validate._internal.type" 111 | with: 112 | json: "{{ .json }}" 113 | type: "string" 114 | verbose: "{{ .verbose }}" 115 | 116 | - id: "json.validate.number" 117 | inputs: 118 | - id: "json" 119 | required: true 120 | - id: "verbose" 121 | default: "false" 122 | operations: 123 | - uses: "json.validate._internal.type" 124 | with: 125 | json: "{{ .json }}" 126 | type: "number" 127 | verbose: "{{ .verbose }}" 128 | 129 | - id: "json.validate.boolean" 130 | inputs: 131 | - id: "json" 132 | required: true 133 | - id: "verbose" 134 | default: "false" 135 | operations: 136 | - uses: "json.validate._internal.type" 137 | with: 138 | json: "{{ .json }}" 139 | type: "boolean" 140 | verbose: "{{ .verbose }}" 141 | 142 | - id: "json.validate.null" 143 | inputs: 144 | - id: "json" 145 | required: true 146 | - id: "verbose" 147 | default: "false" 148 | operations: 149 | - uses: "json.validate._internal.type" 150 | with: 151 | json: "{{ .json }}" 152 | type: "null" 153 | verbose: "{{ .verbose }}" 154 | -------------------------------------------------------------------------------- /recipes/utils/components/lint.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "lint.tabs->spaces" 3 | inputs: 4 | - id: "file" 5 | required: true 6 | - id: "spaces" 7 | default: 4 8 | operations: 9 | - uses: "user.path.expand" 10 | id: "file_to_convert" 11 | with: 12 | path: "{{ .file }}" 13 | silent: true 14 | 15 | - command: expand -t {{ .spaces }} {{ .file_to_convert }} 16 | id: "converted_file" 17 | silent: true 18 | 19 | - uses: "file.write" 20 | with: 21 | file: "{{ .file_to_convert }}" 22 | contents: "{{ .converted_file }}" 23 | cleanup: 24 | - "file_to_convert" 25 | - "converted_file" 26 | - "spaces" 27 | 28 | - id: "lint.spaces->tabs" 29 | inputs: 30 | - id: "file" 31 | required: true 32 | - id: "spaces" 33 | default: 4 34 | operations: 35 | - uses: "user.path.expand" 36 | id: "file_to_convert" 37 | with: 38 | path: "{{ .file }}" 39 | silent: true 40 | 41 | - command: unexpand -a -t {{ .spaces }} {{ .file_to_convert }} 42 | id: "converted_file" 43 | silent: true 44 | 45 | - uses: "file.write" 46 | with: 47 | file: "{{ .file_to_convert }}" 48 | contents: "{{ .converted_file }}" 49 | cleanup: 50 | - "file_to_convert" 51 | - "converted_file" 52 | - "spaces" 53 | 54 | - id: "lint.spaces->spaces" 55 | inputs: 56 | - id: "file" 57 | required: true 58 | - id: "from" 59 | default: 2 60 | - id: "to" 61 | default: 4 62 | operations: 63 | - uses: "user.path.expand" 64 | id: "file_to_convert" 65 | with: 66 | path: "{{ .file }}" 67 | silent: true 68 | 69 | - command: unexpand -t {{ .from }} {{ .file_to_convert }} | expand -t {{ .to }} 70 | id: "converted_file" 71 | silent: true 72 | 73 | - uses: "file.write" 74 | with: 75 | file: "{{ .file_to_convert }}" 76 | contents: "{{ .converted_file }}" 77 | cleanup: 78 | - "file_to_convert" 79 | - "converted_file" 80 | - "from" 81 | - "to" 82 | 83 | - id: "lint.trailing" 84 | inputs: 85 | - id: "file" 86 | required: true 87 | operations: 88 | - uses: "user.path.expand" 89 | id: "file_to_convert" 90 | with: 91 | path: "{{ .file }}" 92 | silent: true 93 | 94 | - uses: "file.read" 95 | id: "content" 96 | with: 97 | file: "{{ .file_to_convert }}" 98 | silent: true 99 | 100 | - id: "converted" 101 | command: | 102 | cat << 'EOF' | sed 's/[[:space:]]*$//' 103 | {{ .content }} 104 | EOF 105 | silent: true 106 | 107 | - uses: "file.write" 108 | with: 109 | file: "{{ .file_to_convert }}" 110 | contents: "{{ raw .converted }}" 111 | 112 | - cleanup: 113 | - "file_to_convert" 114 | - "content" 115 | - "converted" 116 | 117 | - id: "lint.has.eof.newline" 118 | inputs: 119 | - id: "file" 120 | required: true 121 | operations: 122 | - uses: "user.path.expand" 123 | id: "file_to_check" 124 | with: 125 | path: "{{ .file }}" 126 | silent: true 127 | 128 | - command: | 129 | if [ "$(tail -c1 "{{ .file_to_check }}" 2>/dev/null | hexdump -v -e '1/1 "%02x"')" != "0a" ]; then 130 | echo "false" 131 | else 132 | echo "true" 133 | fi 134 | output_format: "trim" 135 | cleanup: 136 | - "file_to_check" 137 | 138 | - id: "lint.eof" 139 | inputs: 140 | - id: "file" 141 | required: true 142 | operations: 143 | - uses: "user.path.expand" 144 | id: "file_to_convert" 145 | with: 146 | path: "{{ .file }}" 147 | silent: true 148 | 149 | - uses: "lint.has.eof.newline" 150 | id: "has_eof_newline" 151 | with: 152 | file: "{{ .file_to_convert }}" 153 | silent: true 154 | 155 | - command: printf "\n" >> "{{ .file_to_convert }}" 156 | condition: .has_eof_newline == "false" 157 | silent: true 158 | cleanup: 159 | - "file_to_convert" 160 | - "has_eof_newline" 161 | -------------------------------------------------------------------------------- /recipes/utils/components/mime.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "mime.find._internal" 3 | inputs: 4 | - id: "path" 5 | - id: "mime_type" 6 | - id: "mime_subtype" 7 | operations: 8 | - uses: "dir.pwd" 9 | id: "path" 10 | condition: .path == "false" 11 | silent: true 12 | 13 | - command: echo "" 14 | id: "mime_subtype" 15 | output_format: "trim" 16 | condition: .mime_subtype == "false" 17 | silent: true 18 | 19 | - command: | 20 | find {{ .path }} -type f -exec file --mime-type {} \+ | grep "{{ .mime_type }}/{{ .mime_subtype }}" | cut -d: -f1 21 | cleanup: 22 | - "path" 23 | - "mime_type" 24 | - "mime_subtype" 25 | 26 | - id: "mime.file" 27 | inputs: 28 | - id: "file" 29 | required: true 30 | operations: 31 | - command: file --mime-type -b {{ .file }} 32 | output_format: "trim" 33 | cleanup: 34 | - "file" 35 | 36 | - id: "mime.file.check" 37 | inputs: 38 | - id: "file" 39 | required: true 40 | - id: "type" 41 | required: true 42 | operations: 43 | - uses: "mime.file" 44 | id: "mime_type" 45 | with: 46 | file: "{{ .file }}" 47 | silent: true 48 | 49 | - command: | 50 | if [[ "{{ .type }}" == */* ]]; then 51 | [[ "{{ .mime_type }}" == "{{ .type }}" ]] && echo "true" || echo "false" 52 | else 53 | [[ "{{ .mime_type }}" == "{{ .type }}"/* ]] && echo "true" || echo "false" 54 | fi 55 | output_format: "trim" 56 | cleanup: 57 | - "file" 58 | - "type" 59 | - "mime_type" 60 | 61 | - id: "mime.find.application" 62 | inputs: 63 | - id: "path" 64 | - id: "subtype" 65 | operations: 66 | - uses: "mime.find._internal" 67 | with: 68 | path: "{{ .path }}" 69 | mime_type: "application" 70 | mime_subtype: "{{ .subtype }}" 71 | 72 | - id: "mime.find.audio" 73 | inputs: 74 | - id: "path" 75 | - id: "subtype" 76 | operations: 77 | - uses: "mime.find._internal" 78 | with: 79 | path: "{{ .path }}" 80 | mime_type: "audio" 81 | mime_subtype: "{{ .subtype }}" 82 | 83 | - id: "mime.find.font" 84 | inputs: 85 | - id: "path" 86 | - id: "subtype" 87 | operations: 88 | - uses: "mime.find._internal" 89 | with: 90 | path: "{{ .path }}" 91 | mime_type: "font" 92 | mime_subtype: "{{ .subtype }}" 93 | 94 | - id: "mime.find.image" 95 | inputs: 96 | - id: "path" 97 | - id: "subtype" 98 | operations: 99 | - uses: "mime.find._internal" 100 | with: 101 | path: "{{ .path }}" 102 | mime_type: "image" 103 | mime_subtype: "{{ .subtype }}" 104 | 105 | - id: "mime.find.message" 106 | inputs: 107 | - id: "path" 108 | - id: "subtype" 109 | operations: 110 | - uses: "mime.find._internal" 111 | with: 112 | path: "{{ .path }}" 113 | mime_type: "message" 114 | mime_subtype: "{{ .subtype }}" 115 | 116 | - id: "mime.find.model" 117 | inputs: 118 | - id: "path" 119 | - id: "subtype" 120 | operations: 121 | - uses: "mime.find._internal" 122 | with: 123 | path: "{{ .path }}" 124 | mime_type: "model" 125 | mime_subtype: "{{ .subtype }}" 126 | 127 | - id: "mime.find.multipart" 128 | inputs: 129 | - id: "path" 130 | - id: "subtype" 131 | operations: 132 | - uses: "mime.find._internal" 133 | with: 134 | path: "{{ .path }}" 135 | mime_type: "multipart" 136 | mime_subtype: "{{ .subtype }}" 137 | 138 | - id: "mime.find.text" 139 | inputs: 140 | - id: "path" 141 | - id: "subtype" 142 | operations: 143 | - uses: "mime.find._internal" 144 | with: 145 | path: "{{ .path }}" 146 | mime_type: "text" 147 | mime_subtype: "{{ .subtype }}" 148 | 149 | - id: "mime.find.video" 150 | inputs: 151 | - id: "path" 152 | - id: "subtype" 153 | operations: 154 | - uses: "mime.find._internal" 155 | with: 156 | path: "{{ .path }}" 157 | mime_type: "video" 158 | mime_subtype: "{{ .subtype }}" 159 | -------------------------------------------------------------------------------- /recipes/utils/components/os.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "os.get" 3 | operations: 4 | - command: | 5 | case "$(uname -s)" in 6 | Darwin) 7 | echo "macos" 8 | ;; 9 | Linux) 10 | echo "linux" 11 | ;; 12 | MINGW*|MSYS*|CYGWIN*) 13 | echo "windows" 14 | ;; 15 | *) 16 | echo "unknown" 17 | ;; 18 | esac 19 | silent: true 20 | 21 | - id: "os.apps" 22 | operations: 23 | - uses: "os.get" 24 | id: "os" 25 | 26 | - command: | 27 | case "{{ .os }}" in 28 | macos) 29 | ls /Applications | sed 's/\.app$//' | sort -f 30 | ;; 31 | linux) 32 | if [ -d "/usr/share/applications" ] || [ -d "/usr/local/share/applications" ] || [ -d ~/.local/share/applications ]; then 33 | find /usr/share/applications /usr/local/share/applications ~/.local/share/applications -name "*.desktop" 2>/dev/null | xargs -I{} grep -l "^Type=Application" {} | xargs -I{} grep -l "^NoDisplay=false" {} 2>/dev/null | xargs -I{} basename {} .desktop | sort -f 34 | elif command -v flatpak >/dev/null 2>&1; then 35 | flatpak list --app | cut -f2 | sort -f 36 | elif command -v snap >/dev/null 2>&1; then 37 | snap list | tail -n +2 | awk '{print $1}' | sort -f 38 | elif [ -d "/opt" ]; then 39 | find /opt -maxdepth 1 -type d | tail -n +2 | xargs -I{} basename {} | sort -f 40 | else 41 | echo "false" 42 | fi 43 | ;; 44 | windows) 45 | powershell -command "Get-StartApps | ForEach-Object { $_.Name } | Sort-Object" 46 | ;; 47 | *) 48 | echo "false" 49 | ;; 50 | esac 51 | silent: true 52 | 53 | - id: "os.apps.filtered" 54 | inputs: 55 | - id: "filter" 56 | operations: 57 | - uses: "os.apps" 58 | id: "apps" 59 | 60 | - command: echo "{{ .apps }}" 61 | condition: .apps != "false" && .filter == "false" 62 | on_failure: "handle_failure" 63 | silent: true 64 | 65 | - command: echo '{{ .apps }}' | grep -i "{{ .filter }}" 66 | condition: .apps != "false" && .filter != "false" 67 | on_failure: "handle_failure" 68 | silent: true 69 | cleanup: 70 | - "filter" 71 | 72 | - command: echo "false" 73 | condition: .apps == "false" 74 | on_failure: "handle_failure" 75 | silent: true 76 | 77 | - id: "handle_failure" 78 | command: echo "false" 79 | silent: true 80 | 81 | - id: "os.app.select" 82 | inputs: 83 | - id: "filter" 84 | operations: 85 | - uses: "os.apps.filtered" 86 | with: 87 | filter: "{{ .filter }}" 88 | id: "apps" 89 | 90 | - command: echo "{{ .apps }}" 91 | condition: '{{ count .apps }} == 1 && {{ .apps }} != "false"' 92 | silent: true 93 | 94 | - command: echo "{{ .app_name }}" 95 | prompts: 96 | - id: "app_name" 97 | type: "select" 98 | message: "Select an Application" 99 | source_operation: "apps" 100 | condition: .apps != "false && {{ count .apps }} > 1 101 | silent: true 102 | 103 | - command: echo "false" 104 | condition: .apps == "false" || {{ count .apps }} == 0 105 | silent: true 106 | cleanup: 107 | - "filter" 108 | 109 | - id: "os.app.open" 110 | inputs: 111 | - id: "app" 112 | operations: 113 | - uses: "os.get" 114 | id: "os" 115 | 116 | - command: | 117 | case "{{ .os }}" in 118 | macos) 119 | open -a "{{ .app }}" 120 | ;; 121 | linux) 122 | if command -v gtk-launch >/dev/null 2>&1; then 123 | gtk-launch "{{ .app }}" 124 | elif command -v flatpak >/dev/null 2>&1 && flatpak list --app | cut -f2 | grep -q -i "^{{ .app }}$"; then 125 | flatpak run "$(flatpak list --app | grep -i "^{{ .app }}$" | cut -f1)" 126 | elif command -v xdg-open >/dev/null 2>&1; then 127 | xdg-open "$(find /usr/share/applications /usr/local/share/applications ~/.local/share/applications -name "*{{ .app }}*.desktop" 2>/dev/null | head -1)" 128 | else 129 | nohup "{{ .app }}" >/dev/null 2>&1 & 130 | fi 131 | ;; 132 | windows) 133 | powershell -command "Start-Process '{{ .app }}'" 134 | ;; 135 | *) 136 | echo "false" 137 | ;; 138 | esac 139 | silent: true 140 | cleanup: 141 | - "app" 142 | -------------------------------------------------------------------------------- /recipes/utils/components/password.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "password.strength.rating" 3 | inputs: 4 | - id: "password" 5 | required: true 6 | operations: 7 | - uses: "password.strength" 8 | id: "json_results" 9 | with: 10 | password: "{{ .password }}" 11 | silent: true 12 | 13 | - uses: "json.jq" 14 | id: "rating" 15 | with: 16 | json: "{{ .json_results }}" 17 | filter: ".rating" 18 | silent: true 19 | 20 | - transform: '{{ replace .rating "\"" "" }}' 21 | 22 | - id: "password.strength.score" 23 | inputs: 24 | - id: "password" 25 | required: true 26 | operations: 27 | - uses: "password.strength" 28 | id: "json_results" 29 | with: 30 | password: "{{ .password }}" 31 | silent: true 32 | 33 | - uses: "json.jq" 34 | id: "score" 35 | with: 36 | json: "{{ .json_results }}" 37 | filter: ".score" 38 | silent: true 39 | 40 | - transform: '{{ roundTo .score 2 }}' 41 | 42 | - id: "password.strength" 43 | inputs: 44 | - id: "password" 45 | required: true 46 | operations: 47 | - uses: "python" 48 | with: 49 | code: | 50 | import json 51 | import math 52 | import re 53 | from collections import Counter 54 | 55 | def check_password_strength(password): 56 | score = 0 57 | length = len(password) 58 | 59 | if length == 0: 60 | return json.dumps({"rating": "invalid", "score": 0}) 61 | 62 | score += min(30, length * 2) 63 | 64 | score += sum([ 65 | any(c.islower() for c in password) * 10, 66 | any(c.isupper() for c in password) * 10, 67 | any(c.isdigit() for c in password) * 10, 68 | any(not c.isalnum() for c in password) * 10 69 | ]) 70 | 71 | char_counts = Counter(password) 72 | entropy = -sum((count/length) * math.log2(count/length) for count in char_counts.values()) 73 | entropy_score = min(30, entropy * 6) 74 | score += entropy_score 75 | 76 | repeats = re.findall(r'(.+?)\1+', password) 77 | if repeats: 78 | score -= min(20, sum(len(r) * 4 for r in repeats)) 79 | 80 | sequential_penalty = 0 81 | for i in range(length - 2): 82 | if (ord(password[i+1]) - ord(password[i]) == 1 and 83 | ord(password[i+2]) - ord(password[i+1]) == 1): 84 | sequential_penalty += 3 85 | elif (ord(password[i+1]) - ord(password[i]) == -1 and 86 | ord(password[i+2]) - ord(password[i+1]) == -1): 87 | sequential_penalty += 3 88 | score -= min(20, sequential_penalty) 89 | 90 | score = max(0, min(100, score)) 91 | 92 | if score < 40: rating = "weak" 93 | elif score < 60: rating = "moderate" 94 | elif score < 80: rating = "strong" 95 | else: rating = "very strong" 96 | 97 | return json.dumps({"rating": rating, "score": score}) 98 | 99 | # Check password strength 100 | print(check_password_strength("{{ .password }}")) 101 | -------------------------------------------------------------------------------- /recipes/utils/components/python.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "python.3.installed" 3 | operations: 4 | - command: python --version 2>&1 | grep -q "Python 3" && echo "true" || echo "false" 5 | output_format: "trim" 6 | 7 | - id: "python.2.installed" 8 | operations: 9 | - command: python --version 2>&1 | grep -q "Python 2" && echo "true" || echo "false" 10 | output_format: "trim" 11 | 12 | - id: "python.installed" 13 | inputs: 14 | - id: "version" 15 | default: 3 16 | operations: 17 | - uses: "python.3.installed" 18 | condition: .version == 3 19 | cleanup: 20 | - "version" 21 | 22 | - uses: "python.2.installed" 23 | condition: .version == 2 24 | cleanup: 25 | - "version" 26 | 27 | - id: "python" 28 | inputs: 29 | - id: "code" 30 | required: true 31 | operations: 32 | - uses: "python.installed" 33 | id: "installed" 34 | silent: true 35 | 36 | - command: | 37 | python <<'EOF' 38 | {{ .code }} 39 | EOF 40 | condition: .installed == "true" 41 | cleanup: 42 | - "code" 43 | - "installed" 44 | -------------------------------------------------------------------------------- /recipes/utils/components/user.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "user.home" 3 | operations: 4 | - command: echo $HOME 5 | output_format: "trim" 6 | 7 | - id: "user.path.expand" 8 | inputs: 9 | - id: "path" 10 | required: true 11 | operations: 12 | - command: | 13 | path="{{ .path }}" 14 | if [[ "$path" == "~"* ]]; then 15 | echo "${path/#~/$HOME}" 16 | else 17 | echo "$path" 18 | fi 19 | cleanup: 20 | - "path" 21 | 22 | - id: "user.shell" 23 | operations: 24 | - command: basename "$SHELL" 25 | output_format: "trim" 26 | 27 | - id: "user.history.file" 28 | operations: 29 | - uses: "user.shell" 30 | id: "shell" 31 | silent: true 32 | 33 | - command: | 34 | shell="{{ .shell }}" 35 | case $shell in 36 | "bash") 37 | echo "~/.bash_history" 38 | ;; 39 | "fish") 40 | echo "~/.local/share/fish/fish_history" 41 | ;; 42 | "zsh") 43 | echo "~/.zsh_history" 44 | ;; 45 | esac 46 | output_format: "trim" 47 | cleanup: 48 | - "shell" 49 | 50 | - id: "user.history.command" 51 | operations: 52 | - uses: "user.history.file" 53 | id: "history_file" 54 | 55 | - uses: "user.shell" 56 | id: "shell" 57 | 58 | - command: | 59 | shell="{{ .shell }}" 60 | case $shell in 61 | "bash") 62 | echo "cat {{ .history_file }}" 63 | ;; 64 | "fish") 65 | echo "cat {{ .history_file }} | grep -o '\"cmd\": *\"[ ^\" ]*\"' | sed 's/\"cmd\": *\"//;s/\"$//'" 66 | ;; 67 | "zsh") 68 | echo "sed -n 's/^: [ 0-9 ]*:[0-9]*;//p' {{ .history_file }}" 69 | ;; 70 | esac 71 | output_format: "trim" 72 | cleanup: 73 | - "history_file" 74 | - "shell" 75 | 76 | - id: "user.history.usage" 77 | operations: 78 | - uses: "user.history.command" 79 | id: "command" 80 | silent: true 81 | 82 | - command: eval "{{ .command }}" | awk '{print $1}' | sort | uniq -c | sort -nr | awk '{print $1" "$2}' 83 | cleanup: 84 | - "command" 85 | 86 | - id: "user.history.sensitive_commands" 87 | operations: 88 | - uses: "user.history.command" 89 | id: "command" 90 | silent: true 91 | 92 | - 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.?://.+:.+@.*" 93 | id: "sensitive_commands" 94 | silent: true 95 | on_failure: ":" 96 | cleanup: 97 | - "command" 98 | 99 | - command: echo "{{ .sensitive_commands }}" 100 | condition: .sensitive_commands != "" && .sensitive_commands != "false" 101 | 102 | - command: echo "false" 103 | condition: .sensitive_commands == "" || .sensitive_commands == "false" 104 | cleanup: 105 | - "sensitive_commands" 106 | 107 | - id: "user.history.checkup" 108 | operations: 109 | - uses: "user.history.sensitive_commands" 110 | id: "sensitive_commands" 111 | silent: true 112 | 113 | - command: | 114 | echo '{{ table 115 | (list (color "red" (style "bold" "Potential Exposures"))) 116 | .sensitive_commands 117 | "rounded" 118 | }}' 119 | condition: .sensitive_commands != "false" 120 | 121 | - command: | 122 | echo '{{ table 123 | (list (color "green" (style "bold" "Potential Exposures"))) 124 | "None found!" 125 | "rounded" 126 | }}' 127 | condition: .sensitive_commands == "false" 128 | 129 | - id: "user.history.sterilize" 130 | operations: 131 | - uses: "user.history.file" 132 | id: "history_file" 133 | silent: true 134 | 135 | - 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 }} 136 | silent: true 137 | cleanup: 138 | - "history_file" 139 | -------------------------------------------------------------------------------- /recipes/utils/json.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "json" 3 | description: "A json utility to easily validate and format json" 4 | category: "utils" 5 | help: | 6 | Validates and formats JSON data with optional JQ filtering. 7 | 8 | Usage: 9 | shef utils json # Enter JSON in editor 10 | shef utils json --file=PATH # Format JSON from file 11 | shef utils json [FILTER] # Apply JQ filter (default: ".") 12 | 13 | Invalid JSON will display error messages in red. 14 | vars: 15 | filter: "." 16 | options: "-C" 17 | operations: 18 | - id: "json_file" 19 | command: echo "{{ .file }}" 20 | silent: true 21 | 22 | - id: "jq_filter" 23 | command: | 24 | if [[ "{{ .input }}" != "false" ]]; then 25 | echo "{{ .input }}" 26 | else 27 | echo "{{ .filter }}" 28 | fi 29 | silent: true 30 | 31 | - uses: "file.read" 32 | id: "json_to_format" 33 | with: 34 | file: "{{ .json_file }}" 35 | condition: .json_file != "false" 36 | silent: true 37 | 38 | - prompts: 39 | - name: "JSON Input" 40 | id: "json_to_format" 41 | type: "editor" 42 | message: "Enter JSON" 43 | condition: .json_file == "false" 44 | 45 | - uses: "json.validate" 46 | id: "json_valid" 47 | with: 48 | json: "{{ .json_to_format }}" 49 | silent: true 50 | 51 | - uses: "json.jq" 52 | with: 53 | json: "{{ .json_to_format }}" 54 | filter: "{{ .jq_filter }}" 55 | options: "{{ .options }}" 56 | condition: .json_valid == "true" 57 | 58 | - command: echo "{{ color "red" .json_valid }}" 59 | condition: .json_valid != "true" 60 | 61 | - command: echo "{{ color "red" .json_to_format }}" 62 | condition: .json_valid != "true" 63 | -------------------------------------------------------------------------------- /recipes/utils/os.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "open" 3 | description: "Open an application" 4 | category: "os" 5 | help: | 6 | Opens a selected application from the system. 7 | 8 | Usage: 9 | shef os open # Select from all applications 10 | shef os open [FILTER] # Filter application list by name 11 | operations: 12 | - uses: "os.app.select" 13 | with: 14 | filter: "{{ .input }}" 15 | id: "app" 16 | 17 | - command: echo {{ color "red" "No applications found" }} 18 | condition: .app == "false" 19 | 20 | - uses: "os.app.open" 21 | with: 22 | app: "{{ .app }}" 23 | condition: .app != "false" 24 | -------------------------------------------------------------------------------- /recipes/utils/terminal.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "usage" 3 | description: "Get your ranked terminal command history usage" 4 | category: "terminal" 5 | help: | 6 | Displays your most frequently used terminal commands. 7 | 8 | Usage: 9 | shef terminal usage # Show top 25 commands by usage count 10 | operations: 11 | - uses: "user.history.usage" 12 | id: "usage_history" 13 | silent: true 14 | 15 | - command: echo "{{ .usage_history }}" | head -n 25 | awk '{print ""$1","$2""}' 16 | id: "top_25_commands" 17 | silent: true 18 | 19 | - command: | 20 | echo '{{ table 21 | (list "Usage" "Command") 22 | .top_25_commands 23 | "rounded" 24 | (list "right" "left") 25 | }}' 26 | 27 | - name: "checkup" 28 | description: "Run a terminal history health checkup" 29 | category: "terminal" 30 | help: | 31 | Scans your terminal history for potential credential exposures. 32 | 33 | Usage: 34 | shef terminal checkup # Scan for sensitive information in history 35 | operations: 36 | - uses: "user.history.checkup" 37 | 38 | - name: "sterilize" 39 | description: "Sterilize terminal history" 40 | category: "terminal" 41 | help: | 42 | Removes credential exposures from your terminal history. 43 | 44 | Usage: 45 | shef terminal sterilize # Remove sensitive information from history 46 | operations: 47 | - uses: "user.history.checkup" 48 | 49 | - condition: .sensitive_commands == "false" 50 | exit: true 51 | cleanup: 52 | - "sensitive_commands" 53 | 54 | - prompts: 55 | - type: "confirm" 56 | id: "confirm" 57 | message: "Sterilize your history?" 58 | default: "true" 59 | help_text: "This will permanently remove all the credential exposures from your history file" 60 | 61 | - command: echo "{{ color "yellow" "Aborted!" }}" 62 | condition: .confirm == "false" 63 | exit: true 64 | 65 | - uses: "user.history.sterilize" 66 | condition: .confirm == "true" 67 | silent: true 68 | 69 | - uses: "user.history.checkup" 70 | -------------------------------------------------------------------------------- /recipes/utils/time.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "time" 3 | description: "A time utility displaying formatted time information" 4 | category: "utils" 5 | help: | 6 | Displays current local and UTC time in a formatted table. 7 | 8 | Usage: 9 | shef utils time # Show current time information 10 | operations: 11 | - name: "Get Local Time" 12 | id: "local" 13 | command: date "+%Y-%m-%d %H:%M:%S" 14 | silent: true 15 | 16 | - name: "Get UTC Time" 17 | id: "utc" 18 | command: date -u "+%Y-%m-%d %H:%M:%S" 19 | silent: true 20 | 21 | - name: "Display times within a table" 22 | command: | 23 | echo '{{ table 24 | (makeHeaders "Local Time" "UTC Time") 25 | (list 26 | (makeRow (color "green" .local) (color "yellow" .utc)) 27 | ) 28 | "rounded" 29 | }}' 30 | -------------------------------------------------------------------------------- /testdata/background_mode_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp background_mode_recipe.yaml .shef/ 13 | 14 | # Test running background mode recipe 15 | exec shef background_mode_recipe 16 | 17 | # Validate test 18 | stdout 'Background tasks started' 19 | stdout 'Task 1 status: pending' 20 | stdout 'Task 2 status: pending' 21 | stdout 'Waiting for task 1...' 22 | stdout 'Task 1 is now complete' 23 | stdout 'Task 1 output: Background task 1 completed' 24 | stdout 'Task 2 status: pending' 25 | stdout 'Waiting for task 2...' 26 | stdout 'All background tasks completed' 27 | stdout 'Task 1 output: Background task 1 completed' 28 | stdout 'Task 2 output: Background task 2 completed' 29 | -------------------------------------------------------------------------------- /testdata/break_exit_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp break_exit_recipe.yaml .shef/ 13 | 14 | # Test running the break exit recipe 15 | exec shef break_exit_recipe 16 | 17 | # Validate test 18 | stdout 'First operation' 19 | stdout 'Loop iteration 0' 20 | stdout 'Loop iteration 1' 21 | stdout 'Loop iteration 2' 22 | stdout 'Breaking out of loop' 23 | stdout 'After loop' 24 | stdout 'Exiting now' 25 | ! stdout 'This should not be executed' 26 | -------------------------------------------------------------------------------- /testdata/category_filter.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe files for testing 12 | cp category_filter_recipe1.yaml .shef/ 13 | cp category_filter_recipe2.yaml .shef/ 14 | 15 | # Test listing all recipes 16 | exec shef list 17 | 18 | # Validate test 19 | stdout '\[cat_a\]' 20 | stdout 'cat_a_recipe: a recipe in category a' 21 | stdout '\[cat_b\]' 22 | stdout 'cat_b_recipe: a recipe in category b' 23 | 24 | # Test filtering by category 25 | exec shef list cat_a 26 | 27 | # Validate test 28 | stdout '\[cat_a\]' 29 | stdout 'cat_a_recipe: a recipe in category a' 30 | ! stdout '\[cat_b\]' 31 | ! stdout 'cat_b_recipe' 32 | 33 | # Test running recipe with category prefix 34 | exec shef cat_a cat_a_recipe 35 | 36 | # Validate test 37 | stdout 'Category A recipe' 38 | -------------------------------------------------------------------------------- /testdata/command_input_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp command_input_recipe.yaml .shef/ 13 | 14 | # Test running the command input recipe 15 | exec shef command_input_recipe 16 | 17 | # Validate test 18 | stdout 'line1' 19 | stdout 'line2' 20 | stdout 'line3' 21 | stdout 'Found: line2' 22 | -------------------------------------------------------------------------------- /testdata/command_line_vars_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp command_line_vars_recipe.yaml .shef/ 13 | 14 | # Test running with command line variables 15 | exec shef command_line_vars_recipe --name=John --value=42 -f 16 | 17 | # Validate test 18 | stdout 'name = John' 19 | stdout 'value = 42' 20 | stdout 'f = true' 21 | -------------------------------------------------------------------------------- /testdata/complex_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp complex_recipe.yaml .shef/ 13 | 14 | # Test running the complex recipe 15 | exec shef complex_recipe 16 | 17 | # Validate test 18 | stdout 'Line 1' 19 | stdout 'Line 2' 20 | stdout 'Line 3' 21 | stdout 'Line 4' 22 | stdout 'Line 5' 23 | stdout 'Iteration 1' 24 | stdout 'Iteration 2' 25 | stdout 'Iteration 3' 26 | stdout 'File has more than 3 lines' 27 | stdout 'HELLO, world!' 28 | -------------------------------------------------------------------------------- /testdata/component_input_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy component input test file 12 | cp component_input_recipe.yaml .shef/ 13 | 14 | # Run the component input test recipe 15 | exec shef component_input_test_recipe 16 | 17 | # Validate test 18 | stdout 'Message Hello with default prefix' 19 | stdout 'Custom Hello with custom prefix' 20 | -------------------------------------------------------------------------------- /testdata/component_input_scope_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy component input test file 12 | cp component_input_scope_recipe.yaml .shef/ 13 | 14 | # Run the component input test recipe 15 | exec shef component_input_scope 16 | 17 | # Validate test 18 | stdout 'false' 19 | stdout 'foo' 20 | stdout 'bar' 21 | stdout 'quix' 22 | stdout 'quix' 23 | stdout 'quix' 24 | -------------------------------------------------------------------------------- /testdata/component_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy component test file 12 | cp component_recipe.yaml .shef/ 13 | 14 | # Test running the component test recipe 15 | exec shef component_test_recipe 16 | 17 | # Validate test 18 | stdout 'First output: Hello from component' 19 | stdout 'Second output: World from component' 20 | stdout 'Component ID output: World from component' 21 | -------------------------------------------------------------------------------- /testdata/condition_types_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp condition_types_recipe.yaml .shef/ 13 | 14 | # Test running the condition types recipe 15 | exec shef condition_types_recipe 16 | 17 | # Validate test 18 | stdout 'true' 19 | stdout 'Variable equality condition passed' 20 | stdout 'Variable inequality condition passed' 21 | stdout '5 is greater than 3' 22 | stdout '3 is less than 5' 23 | stdout '5 is greater than or equal to 5' 24 | stdout '5 is less than or equal to 5' 25 | stdout 'AND condition passed' 26 | stdout 'OR condition passed' 27 | stdout 'NOT condition passed' 28 | ! stdout 'This should be skipped' 29 | -------------------------------------------------------------------------------- /testdata/count_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp count_recipe.yaml .shef/ 13 | 14 | # Test running count test recipe 15 | exec shef count_test_recipe 16 | 17 | # Validate test 18 | stdout 'Line 1' 19 | stdout 'Line 2' 20 | stdout 'Line 3' 21 | stdout 'Line 4' 22 | stdout 'Line count 4' 23 | stdout 'Array count 3' 24 | stdout 'CSV count 3' 25 | stdout 'Empty count 0' 26 | -------------------------------------------------------------------------------- /testdata/debug_mode.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp simple_recipe.yaml .shef/ 13 | 14 | # Test debugging output 15 | exec shef --debug simple_recipe 16 | 17 | # Validate test 18 | stdout 'RECIPE: Running recipe: simple_recipe' 19 | stdout 'COMMAND: echo "Test successful"' 20 | stdout 'OUTPUT: Test successful' 21 | -------------------------------------------------------------------------------- /testdata/direct_recipe_file.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create temporary recipe file 9 | cp simple_recipe.yaml recipe_to_run.yaml 10 | 11 | # Test running recipe directly from file 12 | exec shef --recipe-file recipe_to_run.yaml 13 | 14 | # Validate test 15 | stdout 'Test successful' 16 | -------------------------------------------------------------------------------- /testdata/duration.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp duration_recipe.yaml .shef/ 13 | 14 | # Test running the duration test recipe 15 | exec shef duration_test 16 | 17 | # Validate that all iterations ran 18 | stdout 'Iteration 1/3:' 19 | stdout 'Iteration 2/3:' 20 | stdout 'Iteration 3/3:' 21 | 22 | # Validate that each duration variable appears 23 | stdout 'duration_ms:' 24 | stdout 'duration_s:' 25 | stdout 'duration:' 26 | stdout 'duration_ms_fmt:' 27 | 28 | # Validate that variables are accessible after loop completion 29 | stdout 'After loop completion:' 30 | stdout 'Successfully accessed all duration variables!' 31 | -------------------------------------------------------------------------------- /testdata/error_handling_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp error_handling_recipe.yaml .shef/ 13 | 14 | # Test running the error handling recipe 15 | exec shef error_handling_recipe 16 | 17 | # Validate test 18 | stdout 'This operation succeeds' 19 | stdout 'Success handler executed' 20 | stdout 'Failure handler executed' 21 | stdout 'Operation results verified' 22 | -------------------------------------------------------------------------------- /testdata/filter_cut_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp filter_cut_recipe.yaml .shef/ 13 | 14 | # Test running the filter cut recipe 15 | exec shef filter_cut_recipe 16 | 17 | # Validate test 18 | stdout 'name:John:25' 19 | stdout 'name:Jane:30' 20 | stdout 'name:Bob:22' 21 | stdout 'name:Jane:30' 22 | stdout 'Names: John' 23 | stdout 'Jane' 24 | stdout 'Bob' 25 | -------------------------------------------------------------------------------- /testdata/foreach_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp foreach_recipe.yaml .shef/ 13 | 14 | # Test running the recipe 15 | exec shef foreach_recipe 16 | 17 | # Validate test 18 | stdout 'apple' 19 | stdout 'banana' 20 | stdout 'cherry' 21 | stdout 'Processing apple' 22 | stdout 'Processing banana' 23 | stdout 'Processing cherry' 24 | -------------------------------------------------------------------------------- /testdata/help.txtar: -------------------------------------------------------------------------------- 1 | # Setup test 2 | env NO_COLOR=1 3 | 4 | # Test help 5 | exec shef --help 6 | 7 | # Validate test 8 | stdout 'Shef is a powerful CLI tool' 9 | stdout 'USAGE:' 10 | stdout 'COMMANDS:' 11 | -------------------------------------------------------------------------------- /testdata/help_test_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp $WORK/help_recipe.yaml .shef/ 13 | 14 | # Test displaying help with -h flag 15 | exec shef help_test_recipe -h 16 | stdout 'NAME:' 17 | stdout ' help_test_recipe - a recipe to test help functionality' 18 | stdout 'CATEGORY:' 19 | stdout ' test' 20 | stdout 'USAGE:' 21 | stdout ' shef help_test_recipe \[input\] \[options\]' 22 | stdout ' shef test help_test_recipe \[input\] \[options\]' 23 | stdout 'OVERVIEW:' 24 | stdout ' This is detailed help text for testing the help functionality.' 25 | 26 | # Test displaying help with --help flag 27 | exec shef help_test_recipe --help 28 | stdout 'NAME:' 29 | stdout ' help_test_recipe - a recipe to test help functionality' 30 | stdout 'OVERVIEW:' 31 | stdout ' This is detailed help text for testing the help functionality.' 32 | 33 | # Test normal execution without help flag 34 | exec shef help_test_recipe 35 | stdout 'Help test recipe executed successfully' 36 | ! stdout 'This is detailed help text' 37 | -------------------------------------------------------------------------------- /testdata/input_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp input_recipe.yaml .shef/ 13 | 14 | # Test running the recipe with input 15 | exec shef test input 'Hello World' 16 | 17 | # Validate test 18 | stdout 'input = Hello World' 19 | -------------------------------------------------------------------------------- /testdata/json_output.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe files for testing 12 | cp simple_recipe.yaml .shef/ 13 | 14 | # Test JSON output 15 | exec shef list --json 16 | 17 | # Validate test 18 | stdout '{' 19 | stdout '"name": "simple_recipe"' 20 | stdout '"description": "A simple test recipe"' 21 | stdout '"category": "test"' 22 | -------------------------------------------------------------------------------- /testdata/list_empty.txtar: -------------------------------------------------------------------------------- 1 | # Clear any existing recipes 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Test listing recipes when none are available 9 | exec shef list 10 | 11 | # Validate test 12 | stdout 'No recipes found.' 13 | -------------------------------------------------------------------------------- /testdata/list_with_recipes.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp simple_recipe.yaml .shef/ 13 | 14 | # Test listing recipes 15 | exec shef list 16 | 17 | # Validate test 18 | stdout '\[test\]' 19 | stdout 'simple_recipe: a simple test recipe' 20 | -------------------------------------------------------------------------------- /testdata/math_functions_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp math_functions_recipe.yaml .shef/ 13 | 14 | # Test running the math functions recipe 15 | exec shef math_functions_recipe 16 | 17 | # Validate test 18 | stdout '10.7,3.2' 19 | stdout '1' 20 | stdout '4' 21 | stdout '4' 22 | stdout '3' 23 | stdout '4.5' 24 | stdout '7' 25 | stdout '10' 26 | stdout '5' 27 | stdout '10.1' 28 | stdout '5.5' 29 | stdout '8' 30 | stdout '4' 31 | stdout '1' 32 | stdout '2' 33 | stdout '25' 34 | stdout '33.3%' 35 | stdout '3.14' 36 | stdout '3.142' 37 | stdout '3.32' 38 | -------------------------------------------------------------------------------- /testdata/multiple_recipe_files.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directories 9 | mkdir -p .shef 10 | mkdir -p $HOME/.shef/user 11 | 12 | # Copy recipe files for testing 13 | cp simple_recipe.yaml .shef/ 14 | cp input_recipe.yaml $HOME/.shef/user/ 15 | 16 | # Test listing recipes from multiple sources 17 | exec shef list 18 | 19 | # Validate test 20 | stdout '\[test\]' 21 | stdout 'simple_recipe: a simple test recipe' 22 | stdout 'input: a recipe that uses input' 23 | 24 | # Test running recipe from local directory 25 | exec shef simple_recipe 26 | 27 | # Validate test 28 | stdout 'Test successful' 29 | 30 | # Test running recipe from user directory 31 | exec shef input 'Testing from user dir' 32 | 33 | # Validate test 34 | stdout 'input = Testing from user dir' 35 | -------------------------------------------------------------------------------- /testdata/nested_loops_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp nested_loops_recipe.yaml .shef/ 13 | 14 | # Test running the nested loops recipe 15 | exec shef nested_loops_recipe 16 | 17 | # Validate test 18 | stdout 'Outer 0, Inner 0' 19 | stdout 'Outer 0, Inner 1' 20 | stdout 'Outer 1, Inner 0' 21 | stdout 'Outer 1, Inner 1' 22 | stdout 'Outer 2, Inner 0' 23 | stdout 'Outer 2, Inner 1' 24 | -------------------------------------------------------------------------------- /testdata/op_reference_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp op_reference_recipe.yaml .shef/ 13 | 14 | # Test running the operation reference recipe 15 | exec shef op_reference_recipe 16 | 17 | # Validate test 18 | stdout 'Hello, World!' 19 | stdout 'Previous output Hello, World!' 20 | stdout 'Transformed output Goodbye, World!' 21 | -------------------------------------------------------------------------------- /testdata/output_format_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp output_format_recipe.yaml .shef/ 13 | 14 | # Test running the output format recipe 15 | exec shef output_format_recipe 16 | 17 | # Validate test 18 | stdout 'Raw output: Line1' 19 | stdout 'Line2' 20 | stdout 'Trimmed output: Trimmed' 21 | stdout 'Lines output: Line1' 22 | stdout 'Line2' 23 | stdout 'Line3' 24 | -------------------------------------------------------------------------------- /testdata/progress_mode_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp progress_mode_recipe.yaml .shef/ 13 | 14 | # Test running progress mode recipe 15 | exec shef progress_mode_recipe 16 | 17 | # Validate test 18 | stdout 'For loop iteration 2' 19 | stdout '---' 20 | stdout 'Processing Item 3' 21 | stdout '---' 22 | stdout 'While loop iteration 2' 23 | stdout 'All progress mode tests completed successfully' 24 | ! stdout 'For loop iteration 1\nFor loop iteration 2' 25 | ! stdout 'Processing Item 1\nProcessing Item 2' 26 | ! stdout 'While loop iteration 1\nWhile loop iteration 2' 27 | -------------------------------------------------------------------------------- /testdata/recipes/background_mode_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "background_mode_recipe" 3 | description: "A recipe that tests background execution mode" 4 | category: "test" 5 | operations: 6 | - name: "Start Background Task 1" 7 | id: "bg_task1" 8 | command: "sleep 2 && echo 'Background task 1 completed'" 9 | execution_mode: "background" 10 | 11 | - name: "Start Background Task 2" 12 | id: "bg_task2" 13 | command: "sleep 4 && echo 'Background task 2 completed'" 14 | execution_mode: "background" 15 | 16 | - name: "Check Initial Status" 17 | command: | 18 | echo "Background tasks started" 19 | echo 'Task 1 status: {{ bgTaskStatus "bg_task1" }}' 20 | echo 'Task 2 status: {{ bgTaskStatus "bg_task2" }}' 21 | 22 | - name: "Wait For Task 1" 23 | control_flow: 24 | type: "while" 25 | condition: '{{ not (bgTaskComplete "bg_task1") }}' 26 | operations: 27 | - name: "Check Status" 28 | command: | 29 | echo "Waiting for task 1..." 30 | sleep 1 31 | 32 | - name: "Task 1 Complete" 33 | command: | 34 | echo "Task 1 is now complete" 35 | echo "Task 1 output: {{ .bg_task1 }}" 36 | echo 'Task 2 status: {{ bgTaskStatus "bg_task2" }}' 37 | 38 | - name: "Wait For Task 2" 39 | control_flow: 40 | type: "while" 41 | condition: '{{ not (bgTaskComplete "bg_task2") }}' 42 | operations: 43 | - name: "Check Status" 44 | command: | 45 | echo "Waiting for task 2..." 46 | sleep 1 47 | 48 | - name: "All Tasks Complete" 49 | command: | 50 | echo "All background tasks completed" 51 | echo "Task 1 output: {{ .bg_task1 }}" 52 | echo "Task 2 output: {{ .bg_task2 }}" 53 | -------------------------------------------------------------------------------- /testdata/recipes/break_exit_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "break_exit_recipe" 3 | description: "A recipe that tests break and exit flags" 4 | category: "test" 5 | operations: 6 | - name: "First operation" 7 | command: echo "First operation" 8 | 9 | - name: "For loop with break" 10 | control_flow: 11 | type: "for" 12 | count: 5 13 | variable: "i" 14 | operations: 15 | - name: "Loop operation" 16 | command: echo "Loop iteration {{ .i }}" 17 | 18 | - name: "Conditional break" 19 | condition: .i == 2 20 | command: echo "Breaking out of loop" 21 | break: true 22 | 23 | - name: "Operation after loop" 24 | command: echo "After loop" 25 | 26 | - name: "Exit operation" 27 | command: echo "Exiting now" 28 | exit: true 29 | 30 | - name: "Operation after exit" 31 | command: echo "This should not be executed" 32 | -------------------------------------------------------------------------------- /testdata/recipes/category_filter_recipe1.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "cat_a_recipe" 3 | description: "A recipe in category A" 4 | category: "cat_a" 5 | operations: 6 | - name: "Cat A operation" 7 | command: echo "Category A recipe" -------------------------------------------------------------------------------- /testdata/recipes/category_filter_recipe2.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "cat_b_recipe" 3 | description: "A recipe in category B" 4 | category: "cat_b" 5 | operations: 6 | - name: "Cat B operation" 7 | command: echo "Category B recipe" -------------------------------------------------------------------------------- /testdata/recipes/command_input_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "command_input_recipe" 3 | description: "A recipe that tests commands with input" 4 | category: "test" 5 | operations: 6 | - name: "Generate data" 7 | id: "input_data" 8 | command: echo "line1\nline2\nline3" 9 | 10 | - name: "Command with input" 11 | command: grep "line2" 12 | transform: "Found: {{ trim .input }}" 13 | -------------------------------------------------------------------------------- /testdata/recipes/command_line_vars_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "command_line_vars_recipe" 3 | description: "A recipe that uses command line variables" 4 | category: "test" 5 | operations: 6 | - name: "Show command line variables" 7 | command: | 8 | echo "name = {{ .name }}" 9 | echo "value = {{ .value }}" 10 | echo "f = {{ .f }}" 11 | -------------------------------------------------------------------------------- /testdata/recipes/complex_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "complex_recipe" 3 | description: "A complex recipe with multiple operations, for loop, transforms, etc." 4 | category: "test" 5 | operations: 6 | - name: "Write content to a file" 7 | id: "write_file" 8 | command: echo "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" > test_file.txt 9 | 10 | - name: "Show file content" 11 | id: "show_file" 12 | command: cat test_file.txt 13 | 14 | - name: "Count lines in file" 15 | id: "count_lines" 16 | command: wc -l < test_file.txt 17 | transform: "{{ trim .input }}" 18 | 19 | - name: "For loop example" 20 | control_flow: 21 | type: "for" 22 | count: 4 23 | variable: "i" 24 | operations: 25 | - name: "Process iteration" 26 | command: echo "Iteration {{ .i }}" 27 | 28 | - name: "Conditional operation" 29 | condition: .count_lines > 3 30 | command: echo "File has more than 3 lines" 31 | 32 | - name: "Transform output" 33 | id: "transform_output" 34 | command: echo "hello, world!" 35 | transform: '{{ replace .input "hello" "HELLO" }}' 36 | -------------------------------------------------------------------------------- /testdata/recipes/component_input_recipe.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "parameterized_component" 3 | name: "Parameterized Echo Component" 4 | description: "A component that accepts input parameters" 5 | inputs: 6 | - id: "message" 7 | name: "Message to Echo" 8 | description: "The message that will be echoed" 9 | required: true 10 | - id: "prefix" 11 | name: "Message Prefix" 12 | description: "Optional prefix for the message" 13 | default: "Message" 14 | operations: 15 | - name: "Echo Parameterized Message" 16 | id: "parameterized_output" 17 | command: echo "{{ .prefix }} {{ .message }}" 18 | 19 | recipes: 20 | - name: "component_input_test_recipe" 21 | description: "Tests component input functionality" 22 | category: "test" 23 | operations: 24 | - name: "Use Parameterized Component with Default Prefix" 25 | uses: "parameterized_component" 26 | with: 27 | message: "Hello with default prefix" 28 | 29 | - name: "Use Parameterized Component with Custom Prefix" 30 | uses: "parameterized_component" 31 | with: 32 | message: "Hello with custom prefix" 33 | prefix: "Custom" 34 | -------------------------------------------------------------------------------- /testdata/recipes/component_input_scope_recipe.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - name: "Component Input Test" 3 | id: "input_test" 4 | inputs: 5 | - name: "Name Input" 6 | id: "name" 7 | operations: 8 | - name: "Component First" 9 | command: echo {{ .name }} 10 | 11 | - name: "Name" 12 | id: "name" 13 | command: echo "quix" 14 | 15 | - name: "Component Last" 16 | command: echo {{ .name }} 17 | 18 | recipes: 19 | - name: "component_input_scope" 20 | description: "A test to ensure scope of a recipe" 21 | category: "test" 22 | operations: 23 | - name: "Recipe First" 24 | command: echo {{ .name }} 25 | 26 | - name: "Name" 27 | id: "name" 28 | command: echo "foo" 29 | 30 | - name: "Test Component Inputs" 31 | uses: "input_test" 32 | with: 33 | name: "bar" 34 | 35 | - name: "Recipe Last" 36 | command: echo {{ .name }} 37 | -------------------------------------------------------------------------------- /testdata/recipes/component_recipe.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | - id: "echo_component" 3 | name: "Echo Component" 4 | description: "A simple component that echoes text" 5 | operations: 6 | - name: "Echo Hello" 7 | id: "hello_output" 8 | command: "echo 'Hello from component'" 9 | 10 | - name: "Echo World" 11 | id: "world_output" 12 | command: "echo 'World from component'" 13 | 14 | recipes: 15 | - name: "component_test_recipe" 16 | description: "A recipe that tests component functionality" 17 | category: "test" 18 | operations: 19 | - name: "Use Echo Component" 20 | uses: "echo_component" 21 | 22 | - name: "Show Component Outputs" 23 | command: | 24 | echo "First output: {{ .hello_output }}" 25 | echo "Second output: {{ .world_output }}" 26 | 27 | - name: "Direct Component Usage" 28 | uses: "echo_component" 29 | id: "my_component" 30 | 31 | - name: "Show Direct Usage Output" 32 | command: | 33 | echo "Component ID output: {{ .my_component }}" 34 | -------------------------------------------------------------------------------- /testdata/recipes/condition_types_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "condition_types_recipe" 3 | description: "A recipe that tests different condition types" 4 | category: "test" 5 | operations: 6 | - name: "Set variables" 7 | id: "test_var" 8 | command: echo "true" 9 | 10 | - name: "Variable equality (true)" 11 | condition: .test_var == "true" 12 | command: echo "Variable equality condition passed" 13 | 14 | - name: "Variable inequality (true)" 15 | condition: .test_var != "false" 16 | command: echo "Variable inequality condition passed" 17 | 18 | - name: "Numeric comparison (greater than)" 19 | condition: 5 > 3 20 | command: echo "5 is greater than 3" 21 | 22 | - name: "Numeric comparison (less than)" 23 | condition: 3 < 5 24 | command: echo "3 is less than 5" 25 | 26 | - name: "Numeric comparison (greater than or equal)" 27 | condition: 5 >= 5 28 | command: echo "5 is greater than or equal to 5" 29 | 30 | - name: "Numeric comparison (less than or equal)" 31 | condition: 5 <= 5 32 | command: echo "5 is less than or equal to 5" 33 | 34 | - name: "AND condition (true)" 35 | condition: .test_var == "true" && 5 > 3 36 | command: echo "AND condition passed" 37 | 38 | - name: "OR condition (true)" 39 | condition: .test_var == "false" || 5 > 3 40 | command: echo "OR condition passed" 41 | 42 | - name: "NOT condition (true)" 43 | condition: '!(.test_var == "false")' 44 | command: echo "NOT condition passed" 45 | 46 | - name: "Skip this (false condition)" 47 | condition: .test_var == "false" && 5 > 3 48 | command: echo "This should be skipped" 49 | -------------------------------------------------------------------------------- /testdata/recipes/count_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "count_test_recipe" 3 | description: "A recipe that tests the count function" 4 | category: "test" 5 | vars: 6 | array_var: [ "item1", "item2", "item3" ] 7 | operations: 8 | - name: "Generate Lines" 9 | id: "lines" 10 | command: | 11 | echo "Line 1 12 | Line 2 13 | Line 3 14 | Line 4" 15 | 16 | - name: "Count Lines" 17 | command: echo "Line count {{ count .lines }}" 18 | 19 | - name: "Count Predefined Array" 20 | command: echo "Array count {{ count .array_var }}" 21 | 22 | - name: "Generate CSV" 23 | id: "csv_data" 24 | command: echo "item1,item2,item3" 25 | 26 | - name: "Count CSV Items" 27 | command: echo "CSV count {{ count (split .csv_data ",") }}" 28 | 29 | - name: "Empty String" 30 | id: "empty" 31 | command: echo "" 32 | 33 | - name: "Count Empty" 34 | command: echo "Empty count {{ count .empty }}" 35 | -------------------------------------------------------------------------------- /testdata/recipes/duration_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "duration_test" 3 | description: "Tests duration tracking in loops" 4 | category: "test" 5 | operations: 6 | - name: "For loop with duration tracking" 7 | id: "duration_loop" 8 | control_flow: 9 | type: "for" 10 | count: "3" 11 | variable: "i" 12 | operations: 13 | - name: "Add a delay" 14 | command: "sleep 1" 15 | 16 | - name: "Output iteration with durations" 17 | command: | 18 | echo "Iteration {{ .iteration }}/3:" 19 | echo " duration_ms: {{ .duration_ms }}" 20 | echo " duration_s: {{ .duration_s }}" 21 | echo " duration: {{ .duration }}" 22 | echo " duration_ms_fmt: {{ .duration_ms_fmt }}" 23 | 24 | - name: "Verify variables still accessible after loop" 25 | command: | 26 | echo "After loop completion:" 27 | echo " duration_ms: {{ .duration_ms }}" 28 | echo " duration_s: {{ .duration_s }}" 29 | echo " duration: {{ .duration }}" 30 | echo " duration_ms_fmt: {{ .duration_ms_fmt }}" 31 | echo "Successfully accessed all duration variables!" 32 | -------------------------------------------------------------------------------- /testdata/recipes/error_handling_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "error_handling_recipe" 3 | description: "A recipe that tests error handling and handlers" 4 | category: "test" 5 | operations: 6 | - name: "Success operation" 7 | id: "success_op" 8 | command: echo "This operation succeeds" 9 | on_success: "success_handler" 10 | 11 | - name: "Failure operation" 12 | id: "failure_op" 13 | command: "invalid_command" 14 | on_failure: "failure_handler" 15 | 16 | - name: "Success handler" 17 | id: "success_handler" 18 | command: echo "Success handler executed" 19 | 20 | - name: "Failure handler" 21 | id: "failure_handler" 22 | command: echo "Failure handler executed" 23 | 24 | - name: "Check operation results" 25 | condition: success_op.success && failure_op.failure 26 | command: echo "Operation results verified" 27 | -------------------------------------------------------------------------------- /testdata/recipes/filter_cut_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "filter_cut_recipe" 3 | description: "A recipe that tests filter/grep and cut functions" 4 | category: "test" 5 | operations: 6 | - name: "Generate sample data" 7 | id: "sample_data" 8 | command: echo -e "name:John:25\nname:Jane:30\nname:Bob:22" 9 | 10 | - name: "Filter lines with 'Jane'" 11 | command: echo "{{ .sample_data }}" 12 | transform: '{{ filter .input "Jane" }}' 13 | 14 | - name: "Cut fields" 15 | command: echo "{{ .sample_data }}" 16 | transform: 'Names: {{ cut .input ":" 1 }}' 17 | -------------------------------------------------------------------------------- /testdata/recipes/foreach_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "foreach_recipe" 3 | description: "A recipe that uses forEach" 4 | category: "test" 5 | operations: 6 | - name: "Generate list" 7 | id: "generate_list" 8 | command: echo "apple\nbanana\ncherry" 9 | 10 | - name: "Process list" 11 | control_flow: 12 | type: "foreach" 13 | collection: "{{ .generate_list }}" 14 | as: "fruit" 15 | operations: 16 | - name: "Process fruit" 17 | command: echo "Processing {{.fruit}}" 18 | -------------------------------------------------------------------------------- /testdata/recipes/help_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "help_test_recipe" 3 | description: "A recipe to test help functionality" 4 | category: "test" 5 | help: | 6 | This is detailed help text for testing the help functionality. 7 | 8 | It includes multiple lines and examples: 9 | - Example 1: shef help_test_recipe input.txt 10 | - Example 2: shef help_test_recipe --option=value 11 | 12 | The help text should be properly formatted and indented. 13 | operations: 14 | - name: "Simple operation" 15 | command: echo "Help test recipe executed successfully" 16 | -------------------------------------------------------------------------------- /testdata/recipes/input_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "input" 3 | description: "A recipe that uses input" 4 | category: "test" 5 | operations: 6 | - name: "Echo input" 7 | id: "echo_input" 8 | command: echo "input = {{ .input }}" 9 | -------------------------------------------------------------------------------- /testdata/recipes/math_functions_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "math_functions_recipe" 3 | description: "A recipe that tests all new math functions" 4 | category: "test" 5 | operations: 6 | - name: "Create test values" 7 | id: "test_values" 8 | command: echo "10.7,3.2" 9 | 10 | - name: "Mod function" 11 | transform: '{{ mod 10 3 }}' 12 | 13 | - name: "Round function" 14 | transform: '{{ round 3.7 }}' 15 | 16 | - name: "Ceil function" 17 | transform: '{{ ceil 3.2 }}' 18 | 19 | - name: "Floor function" 20 | transform: '{{ floor 3.7 }}' 21 | 22 | - name: "Abs function (float)" 23 | transform: '{{ abs -4.5 }}' 24 | 25 | - name: "Abs function (int)" 26 | transform: '{{ absInt -7 }}' 27 | 28 | - name: "Max function" 29 | transform: '{{ max 5 10 }}' 30 | 31 | - name: "Min function" 32 | transform: '{{ min 5 10 }}' 33 | 34 | - name: "Max function (float)" 35 | transform: '{{ max 5.5 10.1 }}' 36 | 37 | - name: "Min function (float)" 38 | transform: '{{ min 5.5 10.1 }}' 39 | 40 | - name: "Pow function" 41 | transform: '{{ pow 2 3 }}' 42 | 43 | - name: "Sqrt function" 44 | transform: '{{ sqrt 16 }}' 45 | 46 | - name: "Log function" 47 | transform: '{{ round (log 2.718) }}' 48 | 49 | - name: "Log10 function" 50 | transform: '{{ log10 100 }}' 51 | 52 | - name: "Percent function" 53 | transform: '{{ percent 25 100 }}' 54 | 55 | - name: "FormatPercent function" 56 | transform: '{{ formatPercent 33.333 1 }}' 57 | 58 | - name: "RoundTo function" 59 | transform: '{{ roundTo 3.14159 2 }}' 60 | 61 | - name: "FormatNumber function" 62 | transform: '{{ formatNumber "%.3f" 3.14159 }}' 63 | 64 | - name: "Combined math operations" 65 | transform: '{{ roundTo (sqrt (add (pow 2 3) 3)) 2 }}' 66 | -------------------------------------------------------------------------------- /testdata/recipes/nested_loops_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "nested_loops_recipe" 3 | description: "A recipe with nested loops" 4 | category: "test" 5 | operations: 6 | - name: "Outer for loop" 7 | control_flow: 8 | type: "for" 9 | count: 3 10 | variable: "i" 11 | operations: 12 | - name: "Inner for loop" 13 | control_flow: 14 | type: "for" 15 | count: 2 16 | variable: "j" 17 | operations: 18 | - name: "Show indices" 19 | command: echo "Outer {{ .i }}, Inner {{ .j }}" 20 | -------------------------------------------------------------------------------- /testdata/recipes/op_reference_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "op_reference_recipe" 3 | description: "A recipe that references outputs from other operations" 4 | category: "test" 5 | operations: 6 | - name: "Generate data" 7 | id: "data_source" 8 | command: echo "Hello, World!" 9 | 10 | - name: "Reference previous output" 11 | command: echo "Previous output {{ .data_source }}" 12 | 13 | - name: "Transform previous output" 14 | id: "transformed" 15 | command: echo "{{ .data_source }}" 16 | transform: '{{ replace .input "Hello" "Goodbye" }}' 17 | 18 | - name: "Reference transformed output" 19 | command: echo "Transformed output {{ .transformed }}" 20 | -------------------------------------------------------------------------------- /testdata/recipes/output_format_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "output_format_recipe" 3 | description: "A recipe that tests different output formats" 4 | category: "test" 5 | operations: 6 | - name: "Raw output" 7 | id: "raw_output" 8 | command: echo "Line1\nLine2\n" 9 | output_format: "raw" 10 | silent: true 11 | 12 | - name: "Trimmed output" 13 | id: "trimmed_output" 14 | command: echo " Trimmed \n" 15 | output_format: "trim" 16 | silent: true 17 | 18 | - name: "Lines output" 19 | id: "lines_output" 20 | command: echo "Line1\n\n Line2 \n\nLine3" 21 | output_format: "lines" 22 | silent: true 23 | 24 | - name: "Show all outputs" 25 | command: | 26 | echo "Raw output: {{ .raw_output }}" 27 | echo "Trimmed output: {{ .trimmed_output }}" 28 | echo "Lines output: {{ .lines_output }}" 29 | -------------------------------------------------------------------------------- /testdata/recipes/progress_mode_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "progress_mode_recipe" 3 | description: "A recipe that tests progress modes" 4 | category: "test" 5 | operations: 6 | - name: "For Progress Mode" 7 | control_flow: 8 | type: "for" 9 | count: 3 10 | variable: "i" 11 | progress_mode: true 12 | operations: 13 | - name: "Print Iteration" 14 | command: echo "For loop iteration {{ .i }}" 15 | 16 | - name: "Separator 1" 17 | command: echo "---" 18 | 19 | - name: "Foreach Progress Mode" 20 | control_flow: 21 | type: "foreach" 22 | collection: "Item 1\nItem 2\nItem 3" 23 | as: "item" 24 | progress_mode: true 25 | operations: 26 | - name: "Print Item" 27 | command: echo "Processing {{ .item }}" 28 | 29 | - name: "Separator 2" 30 | command: echo "---" 31 | 32 | - name: "While Progress Mode" 33 | control_flow: 34 | type: "while" 35 | condition: .iteration < 3 36 | progress_mode: true 37 | operations: 38 | - name: "Print Counter" 39 | command: echo "While loop iteration {{ .iteration }}" 40 | 41 | - name: "Test Complete" 42 | command: echo "All progress mode tests completed successfully" 43 | -------------------------------------------------------------------------------- /testdata/recipes/silent_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "silent_recipe" 3 | description: "A recipe that tests silent operations" 4 | category: "test" 5 | operations: 6 | - name: "Normal operation" 7 | id: "normal_op" 8 | command: echo "This output is visible" 9 | 10 | - name: "Silent operation" 11 | id: "silent_op" 12 | command: echo "This output is suppressed" 13 | silent: true 14 | -------------------------------------------------------------------------------- /testdata/recipes/simple_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "simple_recipe" 3 | description: "A simple test recipe" 4 | category: "test" 5 | operations: 6 | - name: "Echo Test" 7 | command: echo "Test successful" 8 | -------------------------------------------------------------------------------- /testdata/recipes/table_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "table_test_recipe" 3 | description: "A recipe that tests various table rendering functions" 4 | category: "test" 5 | operations: 6 | - name: "Basic Table Test" 7 | id: "basic_table" 8 | command: | 9 | echo '{{ table 10 | (makeHeaders "Name" "Age" "City") 11 | (list 12 | (makeRow "John" "34" "Chicago") 13 | (makeRow "Jane" "28" "Seattle") 14 | ) 15 | "rounded" 16 | }}' 17 | 18 | - name: "Style Constants Test" 19 | id: "style_constants" 20 | command: | 21 | echo '{{ table 22 | (makeHeaders "Product" "Price") 23 | (list 24 | (makeRow "Apple" "$1.25") 25 | (makeRow "Orange" "$0.90") 26 | ) 27 | (tableStyleDouble) 28 | }}' 29 | 30 | - name: "Column Alignment Test" 31 | id: "aligned_table" 32 | command: | 33 | echo '{{ table 34 | (makeHeaders "Product" "Price" "Percentage") 35 | (list 36 | (makeRow "Widget A" "$10.00" "32.4%") 37 | (makeRow "Widget B" "$15.00" "29.1%") 38 | ) 39 | "rounded" 40 | (list "left" "right" "center") 41 | }}' 42 | 43 | - name: "JSON Table Test" 44 | id: "json_table" 45 | command: | 46 | echo '{{ tableJSON `{ 47 | "headers": ["Date", "Value"], 48 | "rows": [ 49 | ["2023-01-01", "$100"], 50 | ["2023-01-02", "$150"] 51 | ], 52 | "align": ["left", "right"], 53 | "footers": ["Total", "$250"], 54 | "style": "light" 55 | }` }}' 56 | -------------------------------------------------------------------------------- /testdata/recipes/template_exec_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "template_exec_recipe" 3 | description: "A recipe that tests the exec template function" 4 | category: "test" 5 | operations: 6 | - name: "Execute shell command in template" 7 | command: echo "Current directory {{ exec "pwd" | trim }}" 8 | 9 | - name: "Process template command output" 10 | command: echo "{{ exec "echo Hello | tr a-z A-Z" | trim }}" 11 | -------------------------------------------------------------------------------- /testdata/recipes/transform_functions_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "transform_functions_recipe" 3 | description: "A recipe that tests various transform functions" 4 | category: "test" 5 | operations: 6 | - name: "Create sample data" 7 | id: "sample_data" 8 | command: echo "apple,banana,cherry" 9 | 10 | - name: "Split function" 11 | command: echo {{ .sample_data }} 12 | transform: '{{ split .output "," }}' 13 | 14 | - name: "Replace function" 15 | command: echo "Hello, World!" 16 | transform: '{{ replace .output "Hello" "Goodbye" }}' 17 | 18 | - name: "Trim function" 19 | command: echo " Spaces around " 20 | transform: '{{ trim .output }}' 21 | 22 | - name: "Math functions Add" 23 | transform: '{{ add 2 3 }}' 24 | 25 | - name: "Math functions Sub" 26 | transform: '{{ sub 5 2 }}' 27 | 28 | - name: "Math functions Mul" 29 | transform: '{{ mul 3 4 }}' 30 | 31 | - name: "Math functions Div" 32 | transform: '{{ div 10 2 }}' 33 | -------------------------------------------------------------------------------- /testdata/recipes/vars_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "vars_test_recipe" 3 | description: "A recipe that tests pre-defined variables" 4 | category: "test" 5 | vars: 6 | string_var: "hello world" 7 | number_var: 42 8 | boolean_var: true 9 | operations: 10 | - name: "Check String Variable" 11 | command: echo "String variable {{ .string_var }}" 12 | 13 | - name: "Check Number Variable" 14 | command: echo "Number variable {{ .number_var }}" 15 | 16 | - name: "Check Boolean Variable" 17 | command: echo "Boolean variable {{ .boolean_var }}" 18 | 19 | - name: "Use Variables In Expression" 20 | command: echo "Number plus one {{ add .number_var 1 }}" 21 | -------------------------------------------------------------------------------- /testdata/recipes/while_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "while_recipe" 3 | description: "A recipe that uses a while loop" 4 | category: "test" 5 | operations: 6 | - name: "While loop example" 7 | control_flow: 8 | type: "while" 9 | condition: .complete == "false" 10 | operations: 11 | - name: "Process iteration" 12 | command: echo "Counter value {{ .iteration }}" 13 | 14 | - name: "Status Check" 15 | id: "complete" 16 | command: echo "{{ if ge .iteration 3 }}true{{ else }}false{{ end }}" 17 | silent: true 18 | -------------------------------------------------------------------------------- /testdata/recipes/workdir_recipe.yaml: -------------------------------------------------------------------------------- 1 | recipes: 2 | - name: "workdir_test_recipe" 3 | description: "A recipe that tests working directory setting" 4 | category: "test" 5 | workdir: "./test_workdir" 6 | operations: 7 | - name: "Check Directory Creation" 8 | command: echo "Working directory created automatically" 9 | 10 | - name: "Create Files" 11 | command: | 12 | echo "Creating files in working directory" 13 | touch file1.txt file2.txt 14 | echo "success" > success.txt 15 | 16 | - name: "List Files" 17 | command: ls -1 18 | 19 | - name: "Read File" 20 | command: cat success.txt 21 | -------------------------------------------------------------------------------- /testdata/silent_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp silent_recipe.yaml .shef/ 13 | 14 | # Test running the silent recipe 15 | exec shef silent_recipe 16 | 17 | # Validate test 18 | stdout 'This output is visible' 19 | ! stdout 'This output is suppressed' 20 | -------------------------------------------------------------------------------- /testdata/simple_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp simple_recipe.yaml .shef/ 13 | 14 | # Test running the simple recipe 15 | exec shef simple_recipe 16 | 17 | # Validate test 18 | stdout 'Test successful' 19 | -------------------------------------------------------------------------------- /testdata/table_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp table_recipe.yaml .shef/ 13 | 14 | # Test running the table test recipe 15 | exec shef table_test_recipe 16 | 17 | # Validate test 18 | stdout '| NAME | AGE | CITY' 19 | stdout '| John | 34 | Chicago' 20 | stdout '| Jane | 28 | Seattle' 21 | stdout '| PRODUCT | PRICE' 22 | stdout '| Apple | $1.25' 23 | stdout '| Orange | $0.90' 24 | stdout '| PRODUCT | PRICE | PERCENTAGE' 25 | stdout '| Widget A | $10.00 | 32.4%' 26 | stdout '| Widget B | $15.00 | 29.1%' 27 | stdout '| DATE | VALUE' 28 | stdout '| 2023-01-01 | $100' 29 | stdout '| 2023-01-02 | $150' 30 | stdout '| Total | $250' 31 | -------------------------------------------------------------------------------- /testdata/template_exec_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp template_exec_recipe.yaml .shef/ 13 | 14 | # Test running the template exec recipe 15 | exec shef template_exec_recipe 16 | 17 | # Validate test 18 | stdout 'Current directory' 19 | stdout 'HELLO' 20 | -------------------------------------------------------------------------------- /testdata/transform_functions_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp transform_functions_recipe.yaml .shef/ 13 | 14 | # Test running the transform functions recipe 15 | exec shef transform_functions_recipe 16 | 17 | # Validate test 18 | stdout 'apple,banana,cherry' 19 | stdout 'apple banana cherry' 20 | stdout 'Goodbye, World!' 21 | stdout 'Spaces around' 22 | stdout '5' 23 | stdout '3' 24 | stdout '12' 25 | stdout '5' 26 | -------------------------------------------------------------------------------- /testdata/vars_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp vars_recipe.yaml .shef/ 13 | 14 | # Test running vars test recipe 15 | exec shef vars_test_recipe 16 | 17 | # Validate test 18 | stdout 'String variable hello world' 19 | stdout 'Number variable 42' 20 | stdout 'Boolean variable true' 21 | stdout 'Number plus one 43' 22 | -------------------------------------------------------------------------------- /testdata/version.txtar: -------------------------------------------------------------------------------- 1 | # Set up test 2 | env NO_COLOR=1 3 | 4 | # Test version output 5 | exec shef --version 6 | 7 | # Validate test 8 | stdout 'shef version v' 9 | -------------------------------------------------------------------------------- /testdata/which_command.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp simple_recipe.yaml .shef/ 13 | 14 | # Test which command 15 | exec shef which simple_recipe 16 | 17 | # Validate test 18 | stdout '.shef/simple_recipe.yaml' 19 | -------------------------------------------------------------------------------- /testdata/while_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | 8 | # Create recipe directory 9 | mkdir -p .shef 10 | 11 | # Copy recipe file for testing 12 | cp while_recipe.yaml .shef/ 13 | 14 | # Test running the while loop recipe 15 | exec shef while_recipe 16 | 17 | # Validate test 18 | stdout 'Counter value 1' 19 | stdout 'Counter value 2' 20 | stdout 'Counter value 3' 21 | -------------------------------------------------------------------------------- /testdata/workdir_recipe.txtar: -------------------------------------------------------------------------------- 1 | # Set up home directory 2 | env HOME=$WORK/home 3 | env NO_COLOR=1 4 | mkdir -p $HOME 5 | rm -rf $HOME/.shef 6 | rm -rf .shef 7 | rm -rf ./test_workdir 8 | 9 | # Create recipe directory 10 | mkdir -p .shef 11 | 12 | # Copy recipe file for testing 13 | cp workdir_recipe.yaml .shef/ 14 | 15 | # Test running workdir test recipe 16 | exec shef workdir_test_recipe 17 | 18 | # Validate test 19 | stdout 'Working directory created automatically' 20 | stdout 'Creating files in working directory' 21 | stdout 'file1.txt' 22 | stdout 'file2.txt' 23 | stdout 'success.txt' 24 | stdout 'success' 25 | --------------------------------------------------------------------------------